n8n-nodes-redactor 5.2.0 → 6.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.
@@ -43,6 +43,16 @@ const RECOMMENDED_PATTERNS = [
43
43
  'vin',
44
44
  'uuid', 'imei', 'iccid',
45
45
  ];
46
+ function versionRopaSummary(ropa, byLevel, typeVersion) {
47
+ const summary = ropa.processingSummary;
48
+ if (typeVersion >= 2) {
49
+ summary.hitsBySensitivity = { ...byLevel };
50
+ }
51
+ else {
52
+ summary.hitsByPiiCategory = summary.hitsByCategory;
53
+ summary.hitsByCategory = { ...byLevel };
54
+ }
55
+ }
46
56
  class Redactor {
47
57
  constructor() {
48
58
  this.description = {
@@ -50,7 +60,8 @@ class Redactor {
50
60
  name: 'redactor',
51
61
  icon: { light: 'file:redactor.svg', dark: 'file:redactor.dark.svg' },
52
62
  group: ['transform'],
53
- version: 1,
63
+ version: [1, 2],
64
+ defaultVersion: 2,
54
65
  subtitle: '={{$parameter["operation"]}}',
55
66
  usableAsTool: true,
56
67
  description: 'Redact and remove sensitive data, PII, personal information, emails, phone numbers, addresses, IBAN, credit cards, names before sending to LLM. Anonymize, mask, sanitize, scrub customer data. Restore original values after. Helps GDPR, HIPAA and CCPA workflows. Data privacy, data protection, data masking, data anonymization.',
@@ -793,7 +804,8 @@ class Redactor {
793
804
  for (let i = 0; i < items.length; i++) {
794
805
  const hits = [];
795
806
  (0, engine_1.redactValue)(items[i].json, ctx, tempVault, tempSessionId, hits, i);
796
- allHits.push(...hits);
807
+ for (const hit of hits)
808
+ allHits.push(hit);
797
809
  }
798
810
  tempVault.deleteSession(tempSessionId);
799
811
  if (operation === 'detect') {
@@ -867,12 +879,14 @@ class Redactor {
867
879
  dedup: true,
868
880
  fieldRules: [],
869
881
  fieldMode: 'all',
882
+ scanNumbers: true,
870
883
  };
871
884
  const allHits = [];
872
885
  for (let i = 0; i < items.length; i++) {
873
886
  const hits = [];
874
887
  (0, engine_1.redactValue)(items[i].json, ctx, tempVault, tempSessionId, hits, i);
875
- allHits.push(...hits);
888
+ for (const hit of hits)
889
+ allHits.push(hit);
876
890
  }
877
891
  tempVault.deleteSession(tempSessionId);
878
892
  const classification = (0, classification_1.classifyData)(allHits);
@@ -1041,6 +1055,7 @@ class Redactor {
1041
1055
  keyOrigins: new WeakMap(),
1042
1056
  inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1043
1057
  lockedSpans: new WeakMap(),
1058
+ keyLocks: new WeakMap(),
1044
1059
  priorTokens: (0, engine_1.priorTokenMatcher)((_e = vault.getSession(sessionId)) === null || _e === void 0 ? void 0 : _e.entries),
1045
1060
  };
1046
1061
  const allHits = [];
@@ -1057,7 +1072,8 @@ class Redactor {
1057
1072
  }
1058
1073
  }
1059
1074
  returnData.push({ json: redacted, pairedItem: { item: i } });
1060
- allHits.push(...hits);
1075
+ for (const hit of hits)
1076
+ allHits.push(hit);
1061
1077
  }
1062
1078
  vault.save(sessionId);
1063
1079
  let injectionResult = null;
@@ -1093,6 +1109,7 @@ class Redactor {
1093
1109
  const d = this.getNodeParameter('ropa', 0, {});
1094
1110
  const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
1095
1111
  report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_f = d.controllerName) !== null && _f !== void 0 ? _f : ''), contactEmail: String((_g = d.controllerContact) !== null && _g !== void 0 ? _g : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_h = d.legalBasis) !== null && _h !== void 0 ? _h : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1112
+ versionRopaSummary(report.ropa, (0, classification_1.classifyData)(allHits).hitCountByLevel, this.getNode().typeVersion);
1096
1113
  }
1097
1114
  return [returnData, [{ json: report, pairedItem: allPaired }]];
1098
1115
  }
@@ -22,6 +22,7 @@ const LEVEL_NUMERIC = {
22
22
  RESTRICTED: 3,
23
23
  };
24
24
  function classifyData(hits) {
25
+ var _a;
25
26
  if (hits.length === 0) {
26
27
  return {
27
28
  level: 'PUBLIC',
@@ -40,12 +41,14 @@ function classifyData(hits) {
40
41
  CONFIDENTIAL: 0,
41
42
  RESTRICTED: 0,
42
43
  };
43
- const categoryCounts = {};
44
+ const categoryCounts = new Map();
44
45
  let totalConfidence = 0;
45
46
  for (const hit of hits) {
46
- const catLevel = CATEGORY_SENSITIVITY[hit.category] || 'INTERNAL';
47
+ const catLevel = Object.prototype.hasOwnProperty.call(CATEGORY_SENSITIVITY, hit.category)
48
+ ? CATEGORY_SENSITIVITY[hit.category]
49
+ : 'INTERNAL';
47
50
  hitCountByLevel[catLevel]++;
48
- categoryCounts[hit.category] = (categoryCounts[hit.category] || 0) + 1;
51
+ categoryCounts.set(hit.category, ((_a = categoryCounts.get(hit.category)) !== null && _a !== void 0 ? _a : 0) + 1);
49
52
  totalConfidence += hit.confidence || 0.65;
50
53
  }
51
54
  let level = 'PUBLIC';
@@ -64,10 +67,10 @@ function classifyData(hits) {
64
67
  level = 'CONFIDENTIAL';
65
68
  escalated = true;
66
69
  }
67
- const topCategory = Object.entries(categoryCounts)
68
- .sort((a, b) => b[1] - a[1])[0][0];
70
+ const ranked = [...categoryCounts.entries()].sort((a, b) => b[1] - a[1]);
71
+ const topCategory = ranked[0][0];
69
72
  const reasonParts = [];
70
- for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
73
+ for (const [cat, count] of ranked) {
71
74
  reasonParts.push(`${count} ${cat}`);
72
75
  }
73
76
  let reason = `Contains: ${reasonParts.join(', ')}.`;
@@ -43,6 +43,7 @@ exports.buildReport = buildReport;
43
43
  const crypto = __importStar(require("crypto"));
44
44
  const patterns_1 = require("./patterns");
45
45
  const regexSafety_1 = require("./regexSafety");
46
+ const types_1 = require("./types");
46
47
  const context_1 = require("./context");
47
48
  const names_1 = require("./names");
48
49
  const pseudonymize_1 = require("./pseudonymize");
@@ -77,7 +78,6 @@ function generateToken(label, index) {
77
78
  function setOwn(obj, key, value) {
78
79
  Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
79
80
  }
80
- const TOKEN_RE = /^\[[A-Z][A-Z0-9_]*_\d+\]$/;
81
81
  function maskValue(value, label) {
82
82
  if (value.length <= 2)
83
83
  return '*'.repeat(value.length);
@@ -136,13 +136,15 @@ function applyRedaction(match, pattern, ctx, vault, sessionId) {
136
136
  const irreversible = IRREVERSIBLE[ctx.mode];
137
137
  return irreversible ? irreversible(match, pattern.label) : match;
138
138
  }
139
+ const nextIndex = new WeakMap();
139
140
  function freshToken(label, vault, sessionId, reserved) {
140
- var _a, _b;
141
+ var _a, _b, _c;
141
142
  const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
142
- let index = Object.keys(entries).length;
143
+ let index = (_c = nextIndex.get(entries)) !== null && _c !== void 0 ? _c : Object.keys(entries).length;
143
144
  let token = generateToken(label, index);
144
145
  while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
145
146
  token = generateToken(label, ++index);
147
+ nextIndex.set(entries, index + 1);
146
148
  return token;
147
149
  }
148
150
  const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
@@ -455,7 +457,7 @@ function numericMapping(out, text, ctx, vault, sessionId) {
455
457
  return token;
456
458
  }
457
459
  function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
458
- const text = String(value);
460
+ const text = Object.is(value, -0) ? '-0' : String(value);
459
461
  const numCtx = { ...ctx, dedup: false, numeric: true };
460
462
  let out = null;
461
463
  if (!ctx.skipSemantic)
@@ -472,17 +474,12 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
472
474
  ctx.lastLocked = [[0, mapped.length]];
473
475
  return mapped;
474
476
  }
475
- function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
477
+ function distinctKey(out, original, clashes, ctx, vault, sessionId, reserved) {
476
478
  var _a;
477
- const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
478
- if (out === key)
479
- return key;
480
- const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
481
- if (!clashes(out))
482
- return out;
483
479
  if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
484
- const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...sourceKeys, ...Object.keys(taken)]));
485
- vault.addEntry(sessionId, { token, original: key, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
480
+ const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...reserved]));
481
+ vault.addEntry(sessionId, { token, original, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
482
+ ctx.lastLocked = [[0, token.length]];
486
483
  return token;
487
484
  }
488
485
  let suffixed = out;
@@ -490,6 +487,24 @@ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, key
490
487
  suffixed = `${out}_${n}`;
491
488
  return suffixed;
492
489
  }
490
+ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
491
+ const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
492
+ if (out === key)
493
+ return key;
494
+ const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
495
+ if (!clashes(out))
496
+ return out;
497
+ return distinctKey(out, key, clashes, ctx, vault, sessionId, [...sourceKeys, ...Object.keys(taken)]);
498
+ }
499
+ function rememberKeyLock(ctx, container, key) {
500
+ var _a;
501
+ if (ctx.lastLocked && ctx.keyLocks) {
502
+ const byKey = (_a = ctx.keyLocks.get(container)) !== null && _a !== void 0 ? _a : new Map();
503
+ byKey.set(key, ctx.lastLocked);
504
+ ctx.keyLocks.set(container, byKey);
505
+ }
506
+ ctx.lastLocked = undefined;
507
+ }
493
508
  function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
494
509
  var _a;
495
510
  const entries = Object.entries(value);
@@ -500,9 +515,11 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
500
515
  for (const [k, v] of entries) {
501
516
  const fieldPath = path ? `${path}.${k}` : k;
502
517
  const keyHits = [];
518
+ ctx.lastLocked = undefined;
503
519
  const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
504
520
  ? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
505
- : k;
521
+ : protectLiterals(k, ctx, vault, sessionId, hits, itemIndex);
522
+ rememberKeyLock(ctx, obj, outKey);
506
523
  origins.set(outKey, k);
507
524
  const childDisplay = display ? `${display}.${outKey}` : outKey;
508
525
  for (const h of keyHits)
@@ -516,7 +533,7 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
516
533
  const processed = shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules);
517
534
  if (typeof value === 'string') {
518
535
  if (!processed)
519
- return value;
536
+ return protectLiterals(value, ctx, vault, sessionId, hits, itemIndex);
520
537
  if (!ctx.skipSemantic) {
521
538
  const semantic = redactSemantic(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
522
539
  if (semantic !== null)
@@ -557,19 +574,18 @@ class SegmentCursor {
557
574
  this.cur = segs[0];
558
575
  }
559
576
  freeAt(start, end) {
560
- while (this.cur && (this.cur.locked || this.cur.src + this.cur.text.length <= start))
577
+ while (this.cur && (this.cur.locked || this.cur.end <= start))
561
578
  this.take();
562
579
  const cur = this.cur;
563
- return cur && cur.src <= start && end <= cur.src + cur.text.length ? cur : undefined;
580
+ return cur && cur.src <= start && end <= cur.end ? cur : undefined;
564
581
  }
565
582
  lock(start, end, replacement) {
566
583
  const cur = this.cur;
567
584
  if (start > cur.src)
568
- this.next.push({ text: cur.text.slice(0, start - cur.src), locked: false, src: cur.src });
569
- this.next.push({ text: replacement, locked: true, src: -1 });
570
- const tail = end - cur.src;
571
- if (tail < cur.text.length)
572
- this.cur = { text: cur.text.slice(tail), locked: false, src: end };
585
+ this.next.push(freeSlice(cur, cur.src, start));
586
+ this.next.push({ text: replacement, locked: true, src: start, end });
587
+ if (end < cur.end)
588
+ this.cur = freeSlice(cur, end, cur.end);
573
589
  else
574
590
  this.cur = this.segs[++this.i];
575
591
  }
@@ -583,33 +599,98 @@ class SegmentCursor {
583
599
  this.cur = this.segs[++this.i];
584
600
  }
585
601
  }
586
- function replaceFree(pass, re, fn) {
602
+ function freeSlice(seg, start, end) {
603
+ return { text: seg.text.slice(start - seg.src, end - seg.src), locked: false, src: start, end };
604
+ }
605
+ function redactMatch(pass, match, decision) {
606
+ if (decision.store)
607
+ return decision.store(match);
608
+ const replacement = applyRedaction(match, decision.pattern, pass.ctx, pass.vault, pass.sessionId);
609
+ if (decision.detail) {
610
+ pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...decision.detail });
611
+ }
612
+ return replacement;
613
+ }
614
+ function segmentAfter(segs, pos) {
615
+ let lo = 0;
616
+ let hi = segs.length;
617
+ while (lo < hi) {
618
+ const mid = (lo + hi) >> 1;
619
+ if (segs[mid].end <= pos)
620
+ lo = mid + 1;
621
+ else
622
+ hi = mid;
623
+ }
624
+ return lo;
625
+ }
626
+ function claimPending(pass, pending) {
627
+ for (const p of pending) {
628
+ let i = segmentAfter(pass.segs, p.start);
629
+ while (i < pass.segs.length && pass.segs[i].src < p.end) {
630
+ const seg = pass.segs[i];
631
+ if (seg.locked) {
632
+ i++;
633
+ continue;
634
+ }
635
+ const start = Math.max(seg.src, p.start);
636
+ const end = Math.min(seg.end, p.end);
637
+ const parts = [];
638
+ if (start > seg.src)
639
+ parts.push(freeSlice(seg, seg.src, start));
640
+ parts.push({ text: redactMatch(pass, pass.source.slice(start, end), p.decision), locked: true, src: start, end });
641
+ if (end < seg.end)
642
+ parts.push(freeSlice(seg, end, seg.end));
643
+ pass.segs.splice(i, 1, ...parts);
644
+ i += parts.length;
645
+ }
646
+ }
647
+ }
648
+ function claimOne(pass, cursor, pending, start, end, decide) {
649
+ const text = pass.source.slice(start, end);
650
+ const free = cursor.freeAt(start, end) !== undefined;
651
+ const decision = decide(text, start);
652
+ if (decision && free)
653
+ cursor.lock(start, end, redactMatch(pass, text, decision));
654
+ else if (decision)
655
+ pending.push({ start, end, decision });
656
+ return free;
657
+ }
658
+ function replaceFree(pass, re, decide) {
587
659
  const global = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
588
660
  const cursor = new SegmentCursor(pass.segs);
661
+ const pending = [];
589
662
  let m;
590
663
  while ((m = global.exec(pass.source)) !== null) {
591
664
  const start = m.index;
592
- const end = start + m[0].length;
593
- if (m[0] === '' || !cursor.freeAt(start, end)) {
665
+ if (m[0] === '' || !claimOne(pass, cursor, pending, start, start + m[0].length, decide))
594
666
  global.lastIndex = start + 1;
595
- continue;
596
- }
597
- const replacement = fn(m[0], start);
598
- if (replacement !== null)
599
- cursor.lock(start, end, replacement);
600
667
  }
601
668
  pass.segs = cursor.finish();
669
+ claimPending(pass, pending);
602
670
  }
603
- function redactMatch(pass, match, pattern, detail) {
604
- const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
605
- pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
606
- return replacement;
671
+ function replaceSpans(pass, spans, decide) {
672
+ const ordered = [...spans].sort((a, b) => a.start - b.start || b.end - a.end);
673
+ const cursor = new SegmentCursor(pass.segs);
674
+ const pending = [];
675
+ let reach = -1;
676
+ for (const span of ordered) {
677
+ if (span.end <= reach)
678
+ continue;
679
+ claimOne(pass, cursor, pending, span.start, span.end, decide);
680
+ reach = span.end;
681
+ }
682
+ pass.segs = cursor.finish();
683
+ claimPending(pass, pending);
607
684
  }
608
685
  function denyListRegex(entry) {
609
686
  if (entry.type === 'regex')
610
687
  return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
611
688
  return new RegExp(escapeRe(entry.value), 'gi');
612
689
  }
690
+ const DENY_DECISION = {
691
+ pattern: { label: 'DENY_LIST', category: 'other' },
692
+ detail: { patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE },
693
+ };
613
694
  function denyListStage(pass) {
614
695
  var _a;
615
696
  for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
@@ -617,61 +698,57 @@ function denyListStage(pass) {
617
698
  assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
618
699
  try {
619
700
  const re = denyListRegex(entry);
620
- if (!re)
621
- continue;
622
- replaceFree(pass, re, (match) => TOKEN_RE.test(match)
623
- ? null
624
- : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
625
- patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
626
- }));
701
+ if (re)
702
+ replaceFree(pass, re, () => DENY_DECISION);
627
703
  }
628
704
  catch { }
629
705
  }
630
706
  }
631
- function builtInMatch(pass, pattern, match, offset) {
632
- if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
633
- return null;
707
+ function builtInDecision(pass, pattern, match, offset) {
634
708
  const validatorPassed = pattern.validate ? pattern.validate(match) : true;
635
709
  if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
636
710
  return null;
637
711
  const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
638
712
  if (confidence < pass.threshold)
639
713
  return null;
640
- return redactMatch(pass, match, pattern, {
641
- patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
642
- });
714
+ return { pattern, detail: { patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence } };
643
715
  }
644
716
  function builtInStage(pass) {
645
717
  const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
646
718
  for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
647
- replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
719
+ if (pattern.anchor && !pass.source.includes(pattern.anchor))
720
+ continue;
721
+ const decide = (match, offset) => builtInDecision(pass, pattern, match, offset);
722
+ if (pattern.scan)
723
+ replaceSpans(pass, pattern.scan(pass.source), decide);
724
+ else
725
+ replaceFree(pass, pattern.regex, decide);
648
726
  }
649
727
  }
650
- function sourceNames(pass) {
651
- const names = new Set((0, names_1.detectNamesInText)(pass.source).map((nm) => nm.name));
652
- for (const seg of pass.segs)
653
- if (!seg.locked)
654
- for (const nm of (0, names_1.detectNamesInText)(seg.text))
655
- names.add(nm.name);
656
- return [...names].sort((x, y) => y.length - x.length);
728
+ function nameSpans(pass) {
729
+ const spans = (0, names_1.detectNamesInText)(pass.source).map((nm) => ({ start: nm.start, end: nm.end }));
730
+ for (const seg of pass.segs) {
731
+ if (seg.locked)
732
+ continue;
733
+ for (const nm of (0, names_1.detectNamesInText)(seg.text))
734
+ spans.push({ start: seg.src + nm.start, end: seg.src + nm.end });
735
+ }
736
+ return spans;
657
737
  }
658
738
  function nameStage(pass) {
659
739
  if (pass.ctx.skipSemantic)
660
740
  return;
661
- for (const name of sourceNames(pass)) {
662
- if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
663
- continue;
664
- let replacement;
665
- replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match, start) => {
666
- const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
667
- if (confidence < pass.threshold)
668
- return null;
669
- replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
670
- patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
671
- }));
672
- return replacement;
673
- });
674
- }
741
+ replaceSpans(pass, nameSpans(pass), (match, start) => {
742
+ if (isAllowListed(match, pass.ctx.allowList))
743
+ return null;
744
+ const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
745
+ if (confidence < pass.threshold)
746
+ return null;
747
+ return {
748
+ pattern: { label: 'PERSON_NAME', category: 'identity' },
749
+ detail: { patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence },
750
+ };
751
+ });
675
752
  }
676
753
  function customStage(pass) {
677
754
  if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
@@ -679,15 +756,15 @@ function customStage(pass) {
679
756
  for (const cp of pass.ctx.customPatterns) {
680
757
  if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
681
758
  continue;
682
- const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
759
+ const category = (0, types_1.isPiiCategory)(cp.category) ? cp.category : cp.category ? 'other' : 'identity';
760
+ const pattern = { label: cp.label.toUpperCase(), category };
761
+ const decision = {
762
+ pattern,
763
+ detail: { patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category, confidence: context_1.CUSTOM_PATTERN_CONFIDENCE },
764
+ };
683
765
  assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
684
766
  try {
685
- replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
686
- ? null
687
- : redactMatch(pass, match, pattern, {
688
- patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
689
- confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
690
- }));
767
+ replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => (isAllowListed(match, pass.ctx.allowList) ? null : decision));
691
768
  }
692
769
  catch { }
693
770
  }
@@ -722,16 +799,31 @@ function literalStage(pass) {
722
799
  const { ctx, vault, sessionId } = pass;
723
800
  if (!ctx.priorTokens || (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize'))
724
801
  return;
725
- replaceFree(pass, ctx.priorTokens, (match) => {
726
- const token = freshToken('LITERAL', vault, sessionId, ctx.reservedTokens);
727
- vault.addEntry(sessionId, { token, original: match, patternLabel: 'LITERAL', category: 'other', createdAt: new Date().toISOString() });
728
- return token;
729
- });
802
+ const decision = {
803
+ pattern: { label: 'LITERAL', category: 'other' },
804
+ store: (match) => {
805
+ const token = freshToken('LITERAL', vault, sessionId, ctx.reservedTokens);
806
+ vault.addEntry(sessionId, { token, original: match, patternLabel: 'LITERAL', category: 'other', createdAt: new Date().toISOString() });
807
+ return token;
808
+ },
809
+ };
810
+ replaceFree(pass, ctx.priorTokens, () => decision);
811
+ }
812
+ function protectLiterals(text, ctx, vault, sessionId, hits, itemIndex) {
813
+ if (!ctx.priorTokens)
814
+ return text;
815
+ const pass = {
816
+ source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: 0,
817
+ ctx, vault, sessionId, hits, itemIndex, fieldPath: '',
818
+ };
819
+ literalStage(pass);
820
+ recordLocked(ctx, pass.segs);
821
+ return pass.segs.map((seg) => seg.text).join('');
730
822
  }
731
823
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
732
824
  var _a;
733
825
  const pass = {
734
- source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
826
+ source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
735
827
  ctx, vault, sessionId, hits, itemIndex, fieldPath,
736
828
  };
737
829
  literalStage(pass);
@@ -797,15 +889,13 @@ function presidioSpans(value, entities, ctx, locked) {
797
889
  return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
798
890
  }
799
891
  async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
800
- if (value.trim().length === 0 || TOKEN_RE.test(value))
892
+ if (value.trim().length === 0)
801
893
  return value;
802
894
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
803
895
  const spans = presidioSpans(value, entities, ctx, locked);
804
896
  let text = value;
805
897
  for (const span of spans) {
806
898
  const matchedText = value.slice(span.start, span.end);
807
- if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
808
- continue;
809
899
  if (isAllowListed(matchedText, ctx.allowList))
810
900
  continue;
811
901
  const label = (0, presidio_1.getRedactorLabel)(span.entityType);
@@ -820,6 +910,24 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
820
910
  }
821
911
  return text;
822
912
  }
913
+ const IDENTIFIER_KEY = /^[\p{L}_$][\p{L}\p{N}_$-]*$/u;
914
+ async function presidioKey(key, original, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
915
+ var _a, _b, _c;
916
+ if (IDENTIFIER_KEY.test(key) || !shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
917
+ return key;
918
+ const locked = (_c = (_b = (_a = ctx.keyLocks) === null || _a === void 0 ? void 0 : _a.get(taken)) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
919
+ let out = key;
920
+ try {
921
+ out = await presidioRedactString(key, ctx, vault, sessionId, hits, itemIndex, path, locked);
922
+ }
923
+ catch {
924
+ return key;
925
+ }
926
+ const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k);
927
+ if (out === key || !clashes(out))
928
+ return out;
929
+ return distinctKey(out, original, clashes, ctx, vault, sessionId, Object.keys(taken));
930
+ }
823
931
  async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
824
932
  var _a, _b, _c, _d, _e, _f;
825
933
  if (typeof value === 'string') {
@@ -848,7 +956,8 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
848
956
  const child = path ? `${path}.${k}` : k;
849
957
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
850
958
  const lock = (_f = (_e = ctx.lockedSpans) === null || _e === void 0 ? void 0 : _e.get(value)) === null || _f === void 0 ? void 0 : _f.get(k);
851
- setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
959
+ const key = await presidioKey(k, original, out, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
960
+ setOwn(out, key, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
852
961
  }
853
962
  return out;
854
963
  }
@@ -307,72 +307,49 @@ function detectPromptInjection(text) {
307
307
  message,
308
308
  };
309
309
  }
310
- function scanForInjection(data, currentPath = '') {
311
- let worstResult = {
312
- detected: false,
313
- score: 0,
314
- action: 'CLEAN',
315
- matchedPatterns: [],
316
- encodedPayloadsFound: false,
317
- zeroWidthCharsFound: false,
318
- message: 'No prompt injection detected.',
319
- };
320
- for (const [key, value] of Object.entries(data)) {
321
- const fieldPath = currentPath ? `${currentPath}.${key}` : key;
322
- if (typeof value === 'string' && value.trim().length > 0) {
323
- const result = detectPromptInjection(value);
324
- if (result.score > worstResult.score) {
325
- worstResult = {
326
- ...result,
327
- matchedPatterns: result.matchedPatterns.map((m) => ({
328
- ...m,
329
- pattern: `[${fieldPath}] ${m.pattern}`,
330
- })),
331
- };
332
- }
333
- else if (result.detected) {
334
- worstResult.matchedPatterns.push(...result.matchedPatterns.map((m) => ({
335
- ...m,
336
- pattern: `[${fieldPath}] ${m.pattern}`,
337
- })));
338
- worstResult.score = Math.min(1.0, worstResult.score + result.score * 0.3);
339
- worstResult.detected = true;
340
- if (result.encodedPayloadsFound)
341
- worstResult.encodedPayloadsFound = true;
342
- if (result.zeroWidthCharsFound)
343
- worstResult.zeroWidthCharsFound = true;
344
- }
345
- }
346
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
347
- const nested = scanForInjection(value, fieldPath);
348
- if (nested.score > worstResult.score) {
349
- worstResult = nested;
350
- }
351
- else if (nested.detected) {
352
- worstResult.matchedPatterns.push(...nested.matchedPatterns);
353
- worstResult.score = Math.min(1.0, worstResult.score + nested.score * 0.3);
354
- worstResult.detected = true;
355
- }
356
- }
357
- if (Array.isArray(value)) {
358
- for (let i = 0; i < value.length; i++) {
359
- const item = value[i];
360
- if (item !== null && typeof item === 'object') {
361
- const nested = scanForInjection(item, `${fieldPath}[${i}]`);
362
- if (nested.detected) {
363
- worstResult.matchedPatterns.push(...nested.matchedPatterns);
364
- worstResult.score = Math.min(1.0, worstResult.score + nested.score * 0.3);
365
- worstResult.detected = true;
366
- }
367
- }
368
- }
310
+ function collectStrings(value, path, into) {
311
+ if (typeof value === 'string') {
312
+ if (value.trim().length > 0)
313
+ into.push({ path, text: value });
314
+ }
315
+ else if (Array.isArray(value)) {
316
+ value.forEach((item, i) => collectStrings(item, `${path}[${i}]`, into));
317
+ }
318
+ else if (value !== null && typeof value === 'object') {
319
+ for (const [key, item] of Object.entries(value)) {
320
+ collectStrings(item, path ? `${path}.${key}` : key, into);
369
321
  }
370
322
  }
371
- if (worstResult.score >= 0.7)
372
- worstResult.action = 'BLOCK';
373
- else if (worstResult.score >= 0.3)
374
- worstResult.action = 'FLAG';
375
- else if (worstResult.detected)
376
- worstResult.action = 'LOG_ONLY';
377
- return worstResult;
323
+ }
324
+ function actionFor(score, detected) {
325
+ if (score >= 0.7)
326
+ return 'BLOCK';
327
+ if (score >= 0.3)
328
+ return 'FLAG';
329
+ return detected ? 'LOG_ONLY' : 'CLEAN';
330
+ }
331
+ function scanForInjection(data, currentPath = '') {
332
+ const fields = [];
333
+ collectStrings(data, currentPath, fields);
334
+ const found = fields
335
+ .map((f) => ({ path: f.path, result: detectPromptInjection(f.text) }))
336
+ .filter((f) => f.result.detected)
337
+ .sort((a, b) => b.result.score - a.result.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
338
+ if (found.length === 0) {
339
+ return {
340
+ detected: false, score: 0, action: 'CLEAN', matchedPatterns: [],
341
+ encodedPayloadsFound: false, zeroWidthCharsFound: false, message: 'No prompt injection detected.',
342
+ };
343
+ }
344
+ const score = Math.min(1.0, found.slice(1).reduce((sum, f) => sum + f.result.score * 0.3, found[0].result.score));
345
+ const matchedPatterns = found.flatMap((f) => f.result.matchedPatterns.map((m) => ({ ...m, pattern: `[${f.path}] ${m.pattern}` })));
346
+ return {
347
+ ...found[0].result,
348
+ detected: true,
349
+ score,
350
+ action: actionFor(score, true),
351
+ matchedPatterns,
352
+ encodedPayloadsFound: found.some((f) => f.result.encodedPayloadsFound),
353
+ zeroWidthCharsFound: found.some((f) => f.result.zeroWidthCharsFound),
354
+ };
378
355
  }
@@ -1609,29 +1609,42 @@ const NOT_NAMES = new Set([
1609
1609
  'True', 'False', 'Null', 'None', 'Default', 'Admin', 'System', 'Test', 'Debug', 'Config',
1610
1610
  'Company', 'Corporation', 'Inc', 'Ltd', 'GmbH', 'LLC', 'Group', 'Team', 'Department', 'Division',
1611
1611
  'Total', 'Amount', 'Price', 'Cost', 'Rate', 'Tax', 'Fee', 'Balance',
1612
- 'Solangi', 'Ghaffar', 'Shaikh', 'Syed', 'Qureshi', 'Mirza', 'Abbasi', 'Awan', 'Bhutto', 'Butt', 'Chaudhry', 'Durrani', 'Gilani', 'Hashmi', 'Iqbal', 'Jatoi', 'Leghari', 'Mengal', 'Niazi', 'Raisani', 'Talpur', 'Zardari',
1613
1612
  ]);
1613
+ for (const surname of ['Solangi', 'Ghaffar', 'Shaikh', 'Syed', 'Qureshi', 'Mirza', 'Abbasi', 'Awan', 'Bhutto', 'Butt', 'Chaudhry', 'Durrani', 'Gilani', 'Hashmi', 'Iqbal', 'Jatoi', 'Leghari', 'Mengal', 'Niazi', 'Raisani', 'Talpur', 'Zardari']) {
1614
+ exports.LAST_NAMES.add(surname);
1615
+ }
1614
1616
  function detectNamesInText(text) {
1615
1617
  const results = [];
1616
1618
  const capitalWordSeq = /\b([A-ZÀ-ÿ][a-zà-ÿ]{1,20}(?:\s+[A-ZÀ-ÿ][a-zà-ÿ]{1,20}){1,3})\b/g;
1617
1619
  let match;
1618
1620
  while ((match = capitalWordSeq.exec(text)) !== null) {
1619
- const fullMatch = match[1];
1620
- const words = fullMatch.split(/\s+/);
1621
- if (words.some((w) => NOT_NAMES.has(w)))
1622
- continue;
1623
- const hasFirstName = words.some((w) => exports.FIRST_NAMES.has(w));
1624
- const hasLastName = words.some((w) => exports.LAST_NAMES.has(w));
1625
- if (hasFirstName || hasLastName) {
1626
- results.push({
1627
- name: fullMatch,
1628
- start: match.index,
1629
- end: match.index + fullMatch.length,
1630
- });
1631
- }
1621
+ for (const run of nameRuns(match[1], match.index))
1622
+ results.push(run);
1632
1623
  }
1633
1624
  return results;
1634
1625
  }
1626
+ function nameRuns(sequence, offset) {
1627
+ var _a;
1628
+ const runs = [];
1629
+ let words = [];
1630
+ const flush = () => {
1631
+ const known = words.some((w) => exports.FIRST_NAMES.has(w.word) || exports.LAST_NAMES.has(w.word));
1632
+ if (words.length >= 2 && known) {
1633
+ const start = offset + words[0].at;
1634
+ const last = words[words.length - 1];
1635
+ runs.push({ name: sequence.slice(words[0].at, last.at + last.word.length), start, end: offset + last.at + last.word.length });
1636
+ }
1637
+ words = [];
1638
+ };
1639
+ for (const m of sequence.matchAll(/\S+/g)) {
1640
+ if (NOT_NAMES.has(m[0]))
1641
+ flush();
1642
+ else
1643
+ words.push({ word: m[0], at: (_a = m.index) !== null && _a !== void 0 ? _a : 0 });
1644
+ }
1645
+ flush();
1646
+ return runs;
1647
+ }
1635
1648
  exports.NAME_CONTEXT_WORDS = [
1636
1649
  'spoke with', 'contact', 'customer', 'client', 'patient', 'employee',
1637
1650
  'user', 'member', 'subscriber', 'applicant', 'candidate', 'tenant',
@@ -19,6 +19,38 @@ function luhnCheck(num) {
19
19
  }
20
20
  return sum % 10 === 0;
21
21
  }
22
+ const EMAIL_LOCAL = /[A-Za-z0-9._%+'!#$&*/=?^`{|}~-]/;
23
+ const EMAIL_DOMAIN = /[\p{L}\p{N}.-]/u;
24
+ const EMAIL_DOMAIN_SHAPE = /^[\p{L}\p{N}-]+(?:[.][\p{L}\p{N}-]+)*[.]\p{L}{2,}$/u;
25
+ function emailLocalStart(text, at) {
26
+ if (text[at - 1] === '"') {
27
+ const open = text.lastIndexOf('"', at - 2);
28
+ const quoted = open >= 0 ? text.slice(open + 1, at - 1) : '';
29
+ return quoted.length > 0 && !/[\r\n"]/.test(quoted) ? open : -1;
30
+ }
31
+ let start = at;
32
+ while (start > 0 && EMAIL_LOCAL.test(text[start - 1]))
33
+ start--;
34
+ return start < at ? start : -1;
35
+ }
36
+ function emailDomainEnd(text, at) {
37
+ let end = at + 1;
38
+ while (end < text.length && EMAIL_DOMAIN.test(text[end]))
39
+ end++;
40
+ while (end > at + 1 && /[.-]/.test(text[end - 1]))
41
+ end--;
42
+ return EMAIL_DOMAIN_SHAPE.test(text.slice(at + 1, end)) ? end : -1;
43
+ }
44
+ function scanEmails(text) {
45
+ const spans = [];
46
+ for (let at = text.indexOf('@'); at >= 0; at = text.indexOf('@', at + 1)) {
47
+ const start = emailLocalStart(text, at);
48
+ const end = start < 0 ? -1 : emailDomainEnd(text, at);
49
+ if (end > 0)
50
+ spans.push({ start, end });
51
+ }
52
+ return spans;
53
+ }
22
54
  function ibanCheck(iban) {
23
55
  const cleaned = iban.replace(/\s/g, '').toUpperCase();
24
56
  if (cleaned.length < 15 || cleaned.length > 34)
@@ -35,8 +67,10 @@ exports.PII_PATTERNS = [
35
67
  name: 'email',
36
68
  label: 'EMAIL',
37
69
  category: 'contact',
38
- regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
39
- description: 'Email addresses',
70
+ regex: /[a-zA-Z0-9._%+'-]+@[\p{L}\p{N}.-]+[.]\p{L}{2,}/gu,
71
+ anchor: '@',
72
+ scan: scanEmails,
73
+ description: 'Email addresses, including quoted local parts and international domains',
40
74
  },
41
75
  {
42
76
  name: 'phone',
@@ -56,7 +90,7 @@ exports.PII_PATTERNS = [
56
90
  name: 'phoneDE',
57
91
  label: 'PHONE_DE',
58
92
  category: 'contact',
59
- regex: /(?:0|\+49)\s?\d{2,4}[\s/]\d{3,8}(?:[\s-]\d{1,5})?/g,
93
+ regex: /(?:0|\+49)\s?\d{2,4}[\s/]\d{3,8}(?:[\s-]\d{1,5})?|\+49\d{9,12}\b/g,
60
94
  description: 'German phone numbers (089 1234 5678, +49 30 12345678)',
61
95
  },
62
96
  {
@@ -277,9 +311,9 @@ exports.PII_PATTERNS = [
277
311
  name: 'creditCard',
278
312
  label: 'CREDIT_CARD',
279
313
  category: 'financial',
280
- regex: /\b(?:\d{4}[\s-]?){3}\d{4}\b/g,
314
+ regex: /\b(?:\d[\s-]?){12,18}\d\b/g,
281
315
  validate: (match) => luhnCheck(match),
282
- description: 'Credit/debit card numbers (Luhn validated)',
316
+ description: 'Credit/debit card numbers, 13 to 19 digits (Luhn validated)',
283
317
  },
284
318
  {
285
319
  name: 'iban',
@@ -825,8 +859,8 @@ exports.PII_PATTERNS = [
825
859
  name: 'pemPrivateKey',
826
860
  label: 'PRIVATE_KEY',
827
861
  category: 'enterprise',
828
- regex: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g,
829
- description: 'PEM private key headers',
862
+ regex: /-----BEGIN (RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----(?:[\s\S]*?-----END \1?PRIVATE KEY-----|[A-Za-z0-9+/=\s]*)/g,
863
+ description: 'PEM private keys, the whole block through the END line',
830
864
  },
831
865
  {
832
866
  name: 'sshPublicKey',
@@ -1448,8 +1482,8 @@ exports.PII_PATTERNS = [
1448
1482
  name: 'pgpPrivateKey',
1449
1483
  label: 'PGP_KEY',
1450
1484
  category: 'enterprise',
1451
- regex: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
1452
- description: 'PGP private key block headers',
1485
+ regex: /-----BEGIN PGP PRIVATE KEY BLOCK-----(?:[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----|[A-Za-z0-9+/=:\s]*)/g,
1486
+ description: 'PGP private key blocks, the whole block through the END line',
1453
1487
  },
1454
1488
  {
1455
1489
  name: 'envSecrets',
@@ -120,7 +120,7 @@ function parseTenantProfile(input, knownPatterns) {
120
120
  }
121
121
  if (!isObject(raw))
122
122
  fail('must be a JSON object');
123
- const unknown = Object.keys(raw).filter((k) => !(k in PARSERS));
123
+ const unknown = Object.keys(raw).filter((k) => !Object.prototype.hasOwnProperty.call(PARSERS, k));
124
124
  if (unknown.length)
125
125
  fail(`unknown field(s) ${unknown.join(', ')}`);
126
126
  const profile = {};
@@ -1,6 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateRopaRecord = generateRopaRecord;
4
+ function retentionDescription(vaultTtlMinutes, auditLogEnabled) {
5
+ const vault = vaultTtlMinutes > 0
6
+ ? `Vault sessions expire after ${vaultTtlMinutes} minutes.`
7
+ : 'Vault sessions do not expire automatically. Purge them when they are no longer needed.';
8
+ const audit = auditLogEnabled
9
+ ? 'Audit entries go to the Report output. Their retention depends on where the workflow stores them.'
10
+ : 'No audit log is written.';
11
+ return `${vault} ${audit}`;
12
+ }
13
+ function countByCategory(hits) {
14
+ var _a;
15
+ const counts = new Map();
16
+ for (const hit of hits)
17
+ counts.set(hit.category, ((_a = counts.get(hit.category)) !== null && _a !== void 0 ? _a : 0) + 1);
18
+ return Object.fromEntries(counts);
19
+ }
4
20
  function generateRopaRecord(hits, classification, itemCount, patternsUsed, redactionMode, vaultTtlMinutes, encryptionEnabled, auditLogEnabled, controller, processingPurposes, legalBasis, dataSubjectCategories, recipientCategories, thirdCountryTransfers) {
5
21
  const categoryMap = {};
6
22
  for (const hit of hits) {
@@ -30,8 +46,8 @@ function generateRopaRecord(hits, classification, itemCount, patternsUsed, redac
30
46
  personalDataCategories,
31
47
  retentionPolicy: {
32
48
  vaultTtlMinutes,
33
- auditLogRetentionDays: 2555,
34
- description: `Vault sessions auto-expire after ${vaultTtlMinutes} minutes. Audit logs retained for 7 years.`,
49
+ auditLogRetentionDays: null,
50
+ description: retentionDescription(vaultTtlMinutes, auditLogEnabled),
35
51
  },
36
52
  securityMeasures: {
37
53
  encryptionAtRest: encryptionEnabled,
@@ -46,7 +62,7 @@ function generateRopaRecord(hits, classification, itemCount, patternsUsed, redac
46
62
  redactionMode,
47
63
  patternsUsed,
48
64
  classificationLevel: classification.level,
49
- hitsByCategory: classification.hitCountByLevel,
65
+ hitsByCategory: countByCategory(hits),
50
66
  },
51
67
  };
52
68
  }
@@ -6,6 +6,7 @@ exports.assertUserRegexes = assertUserRegexes;
6
6
  exports.sanitizeInjection = sanitizeInjection;
7
7
  const n8n_workflow_1 = require("n8n-workflow");
8
8
  const regexSafety_1 = require("./regexSafety");
9
+ const types_1 = require("./types");
9
10
  const injection_1 = require("./injection");
10
11
  const VAULT_CREDENTIAL = 'redactorVaultApi';
11
12
  async function resolvePassphrase(fns) {
@@ -58,6 +59,13 @@ function workflowNamespace(fns) {
58
59
  }
59
60
  return 'wf:unsaved';
60
61
  }
62
+ function assertCategory(fns, cp) {
63
+ if (fns.getNode().typeVersion < 2 || !cp.category || (0, types_1.isPiiCategory)(cp.category))
64
+ return;
65
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `Custom pattern "${cp.label}" has an unknown category "${cp.category}"`, {
66
+ description: `Use one of ${types_1.PII_CATEGORIES.join(', ')}.`,
67
+ });
68
+ }
61
69
  function assertUserRegexes(fns, customPatterns, lists) {
62
70
  const refuse = (what, problem) => {
63
71
  throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `${what} regex ${problem}`, {
@@ -67,6 +75,7 @@ function assertUserRegexes(fns, customPatterns, lists) {
67
75
  for (const cp of customPatterns) {
68
76
  if (!cp.label || !cp.regex)
69
77
  continue;
78
+ assertCategory(fns, cp);
70
79
  const problem = (0, regexSafety_1.checkRegex)(cp.regex);
71
80
  if (problem)
72
81
  refuse(`Custom pattern "${cp.label}"`, problem);
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPiiCategory = exports.PII_CATEGORIES = void 0;
4
+ exports.PII_CATEGORIES = [
5
+ 'identity', 'financial', 'contact', 'network', 'location', 'temporal',
6
+ 'medical', 'crypto', 'enterprise', 'vehicle', 'biometric', 'other',
7
+ ];
8
+ const isPiiCategory = (value) => typeof value === 'string' && exports.PII_CATEGORIES.includes(value);
9
+ exports.isPiiCategory = isPiiCategory;
@@ -167,6 +167,7 @@ class WorkflowVault {
167
167
  constructor(store, passphrase = '') {
168
168
  this.store = store;
169
169
  this.passphrase = passphrase;
170
+ this.originals = new WeakMap();
170
171
  this.cache = new Map();
171
172
  }
172
173
  get sessions() {
@@ -240,12 +241,27 @@ class WorkflowVault {
240
241
  return;
241
242
  assertNoConflict(session, entry);
242
243
  session.entries[entry.token] = entry;
244
+ const index = this.originals.get(session);
245
+ const first = index === null || index === void 0 ? void 0 : index.get(entry.original);
246
+ if (index && (!first || first.token === entry.token))
247
+ index.set(entry.original, entry);
243
248
  }
244
249
  findByOriginal(sessionId, original) {
245
250
  const session = this.cache.get(sessionId);
246
251
  if (!session)
247
252
  return undefined;
248
- return Object.values(session.entries).find((e) => e.original === original);
253
+ return this.originalIndex(session).get(original);
254
+ }
255
+ originalIndex(session) {
256
+ let index = this.originals.get(session);
257
+ if (!index) {
258
+ index = new Map();
259
+ for (const entry of Object.values(session.entries))
260
+ if (!index.has(entry.original))
261
+ index.set(entry.original, entry);
262
+ this.originals.set(session, index);
263
+ }
264
+ return index;
249
265
  }
250
266
  deleteSession(sessionId) {
251
267
  this.cache.delete(sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "5.2.0",
3
+ "version": "6.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",