n8n-nodes-redactor 5.2.0 → 6.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.
@@ -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,104 @@ 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 coveringPending(pending, cursor, pos) {
615
+ while (cursor.i < pending.length && pending[cursor.i].end <= pos)
616
+ cursor.i++;
617
+ for (let k = cursor.i; k < pending.length && pending[k].start <= pos; k++) {
618
+ if (pending[k].end > pos)
619
+ return pending[k];
620
+ }
621
+ return undefined;
622
+ }
623
+ function claimSegment(pass, seg, pending, cursor, into) {
624
+ let pos = seg.src;
625
+ while (pos < seg.end) {
626
+ const p = coveringPending(pending, cursor, pos);
627
+ if (p) {
628
+ const end = Math.min(seg.end, p.end);
629
+ into.push({ text: redactMatch(pass, pass.source.slice(pos, end), p.decision), locked: true, src: pos, end });
630
+ pos = end;
631
+ continue;
632
+ }
633
+ const next = cursor.i < pending.length ? Math.max(pos + 1, pending[cursor.i].start) : seg.end;
634
+ const end = Math.min(seg.end, next);
635
+ into.push(freeSlice(seg, pos, end));
636
+ pos = end;
637
+ }
638
+ }
639
+ function claimPending(pass, pending) {
640
+ var _a;
641
+ if (pending.length === 0)
642
+ return;
643
+ pending.sort((a, b) => a.start - b.start);
644
+ const next = [];
645
+ const cursor = { i: 0 };
646
+ for (const seg of pass.segs) {
647
+ if (seg.locked || seg.end <= ((_a = pending[cursor.i]) === null || _a === void 0 ? void 0 : _a.start) || cursor.i >= pending.length)
648
+ next.push(seg);
649
+ else
650
+ claimSegment(pass, seg, pending, cursor, next);
651
+ }
652
+ pass.segs = next;
653
+ }
654
+ function claimOne(pass, cursor, pending, start, end, decide) {
655
+ const text = pass.source.slice(start, end);
656
+ const free = cursor.freeAt(start, end) !== undefined;
657
+ const decision = decide(text, start);
658
+ if (decision && free)
659
+ cursor.lock(start, end, redactMatch(pass, text, decision));
660
+ else if (decision)
661
+ pending.push({ start, end, decision });
662
+ return free;
663
+ }
664
+ function replaceFree(pass, re, decide) {
587
665
  const global = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
588
666
  const cursor = new SegmentCursor(pass.segs);
667
+ const pending = [];
589
668
  let m;
590
669
  while ((m = global.exec(pass.source)) !== null) {
591
670
  const start = m.index;
592
- const end = start + m[0].length;
593
- if (m[0] === '' || !cursor.freeAt(start, end)) {
671
+ if (m[0] === '' || !claimOne(pass, cursor, pending, start, start + m[0].length, decide))
594
672
  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
673
  }
601
674
  pass.segs = cursor.finish();
675
+ claimPending(pass, pending);
602
676
  }
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;
677
+ function replaceSpans(pass, spans, decide) {
678
+ const ordered = [...spans].sort((a, b) => a.start - b.start || b.end - a.end);
679
+ const cursor = new SegmentCursor(pass.segs);
680
+ const pending = [];
681
+ let reach = -1;
682
+ for (const span of ordered) {
683
+ if (span.end <= reach)
684
+ continue;
685
+ claimOne(pass, cursor, pending, span.start, span.end, decide);
686
+ reach = span.end;
687
+ }
688
+ pass.segs = cursor.finish();
689
+ claimPending(pass, pending);
607
690
  }
608
691
  function denyListRegex(entry) {
609
692
  if (entry.type === 'regex')
610
693
  return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
611
694
  return new RegExp(escapeRe(entry.value), 'gi');
612
695
  }
696
+ const DENY_DECISION = {
697
+ pattern: { label: 'DENY_LIST', category: 'other' },
698
+ detail: { patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE },
699
+ };
613
700
  function denyListStage(pass) {
614
701
  var _a;
615
702
  for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
@@ -617,61 +704,58 @@ function denyListStage(pass) {
617
704
  assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
618
705
  try {
619
706
  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
- }));
707
+ if (re)
708
+ replaceFree(pass, re, () => DENY_DECISION);
627
709
  }
628
710
  catch { }
629
711
  }
630
712
  }
631
- function builtInMatch(pass, pattern, match, offset) {
632
- if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
633
- return null;
713
+ function builtInDecision(pass, pattern, match, offset) {
634
714
  const validatorPassed = pattern.validate ? pattern.validate(match) : true;
635
715
  if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
636
716
  return null;
637
717
  const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
638
718
  if (confidence < pass.threshold)
639
719
  return null;
640
- return redactMatch(pass, match, pattern, {
641
- patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
642
- });
720
+ return { pattern, detail: { patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence } };
643
721
  }
644
722
  function builtInStage(pass) {
645
723
  const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
646
724
  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));
725
+ if (pattern.anchor && !pass.source.includes(pattern.anchor))
726
+ continue;
727
+ const decide = (match, offset) => builtInDecision(pass, pattern, match, offset);
728
+ if (pattern.scan)
729
+ replaceSpans(pass, pattern.scan(pass.source), decide);
730
+ else
731
+ replaceFree(pass, pattern.regex, decide);
648
732
  }
649
733
  }
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);
734
+ function nameSpans(pass) {
735
+ const spans = (0, names_1.detectNamesInText)(pass.source).map((nm) => ({ start: nm.start, end: nm.end }));
736
+ for (const seg of pass.segs) {
737
+ if (seg.locked)
738
+ continue;
739
+ for (const nm of (0, names_1.detectNamesInText)(seg.text))
740
+ spans.push({ start: seg.src + nm.start, end: seg.src + nm.end });
741
+ }
742
+ return spans;
657
743
  }
658
744
  function nameStage(pass) {
659
- if (pass.ctx.skipSemantic)
745
+ var _a;
746
+ if ((_a = pass.ctx.skipNames) !== null && _a !== void 0 ? _a : pass.ctx.skipSemantic)
660
747
  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
- }
748
+ replaceSpans(pass, nameSpans(pass), (match, start) => {
749
+ if (isAllowListed(match, pass.ctx.allowList))
750
+ return null;
751
+ const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
752
+ if (confidence < pass.threshold)
753
+ return null;
754
+ return {
755
+ pattern: { label: 'PERSON_NAME', category: 'identity' },
756
+ detail: { patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence },
757
+ };
758
+ });
675
759
  }
676
760
  function customStage(pass) {
677
761
  if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
@@ -679,15 +763,15 @@ function customStage(pass) {
679
763
  for (const cp of pass.ctx.customPatterns) {
680
764
  if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
681
765
  continue;
682
- const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
766
+ const category = (0, types_1.isPiiCategory)(cp.category) ? cp.category : cp.category ? 'other' : 'identity';
767
+ const pattern = { label: cp.label.toUpperCase(), category };
768
+ const decision = {
769
+ pattern,
770
+ detail: { patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category, confidence: context_1.CUSTOM_PATTERN_CONFIDENCE },
771
+ };
683
772
  assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
684
773
  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
- }));
774
+ replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => (isAllowListed(match, pass.ctx.allowList) ? null : decision));
691
775
  }
692
776
  catch { }
693
777
  }
@@ -722,16 +806,31 @@ function literalStage(pass) {
722
806
  const { ctx, vault, sessionId } = pass;
723
807
  if (!ctx.priorTokens || (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize'))
724
808
  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
- });
809
+ const decision = {
810
+ pattern: { label: 'LITERAL', category: 'other' },
811
+ store: (match) => {
812
+ const token = freshToken('LITERAL', vault, sessionId, ctx.reservedTokens);
813
+ vault.addEntry(sessionId, { token, original: match, patternLabel: 'LITERAL', category: 'other', createdAt: new Date().toISOString() });
814
+ return token;
815
+ },
816
+ };
817
+ replaceFree(pass, ctx.priorTokens, () => decision);
818
+ }
819
+ function protectLiterals(text, ctx, vault, sessionId, hits, itemIndex) {
820
+ if (!ctx.priorTokens)
821
+ return text;
822
+ const pass = {
823
+ source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: 0,
824
+ ctx, vault, sessionId, hits, itemIndex, fieldPath: '',
825
+ };
826
+ literalStage(pass);
827
+ recordLocked(ctx, pass.segs);
828
+ return pass.segs.map((seg) => seg.text).join('');
730
829
  }
731
830
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
732
831
  var _a;
733
832
  const pass = {
734
- source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
833
+ source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
735
834
  ctx, vault, sessionId, hits, itemIndex, fieldPath,
736
835
  };
737
836
  literalStage(pass);
@@ -781,44 +880,104 @@ function toUtf16Spans(text, entities) {
781
880
  .filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
782
881
  .map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
783
882
  }
784
- function resolveOverlaps(spans) {
785
- const ranked = [...spans].sort((a, b) => b.score - a.score || (b.end - b.start) - (a.end - a.start) || a.start - b.start);
883
+ function resolveCluster(cluster) {
884
+ const ranked = [...cluster].sort((a, b) => b.score - a.score || (b.end - b.start) - (a.end - a.start) || a.start - b.start);
786
885
  const kept = [];
787
886
  for (const span of ranked) {
788
887
  if (!kept.some((k) => span.start < k.end && k.start < span.end))
789
888
  kept.push(span);
790
889
  }
791
- return kept.sort((a, b) => b.start - a.start);
890
+ return kept;
891
+ }
892
+ function resolveOverlaps(spans) {
893
+ const sorted = [...spans].sort((a, b) => a.start - b.start);
894
+ const out = [];
895
+ let cluster = [];
896
+ let reach = -1;
897
+ for (const span of sorted) {
898
+ if (span.start >= reach && cluster.length > 0) {
899
+ out.push(...resolveCluster(cluster));
900
+ cluster = [];
901
+ }
902
+ cluster.push(span);
903
+ reach = Math.max(reach, span.end);
904
+ }
905
+ out.push(...resolveCluster(cluster));
906
+ return out.sort((a, b) => a.start - b.start);
907
+ }
908
+ function subtractLocked(spans, locked) {
909
+ const sortedLocks = [...locked].sort((a, b) => a[0] - b[0]);
910
+ const out = [];
911
+ let k = 0;
912
+ for (const span of spans) {
913
+ while (k < sortedLocks.length && sortedLocks[k][1] <= span.start)
914
+ k++;
915
+ let pos = span.start;
916
+ for (let j = k; j < sortedLocks.length && sortedLocks[j][0] < span.end; j++) {
917
+ if (sortedLocks[j][0] > pos)
918
+ out.push({ ...span, start: pos, end: sortedLocks[j][0] });
919
+ pos = Math.max(pos, sortedLocks[j][1]);
920
+ }
921
+ if (pos < span.end)
922
+ out.push({ ...span, start: pos, end: span.end });
923
+ }
924
+ return out;
792
925
  }
793
926
  function presidioSpans(value, entities, ctx, locked) {
794
927
  var _a;
795
928
  const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
796
- const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
797
- return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
929
+ return subtractLocked(resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold)), locked);
798
930
  }
799
931
  async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
800
- if (value.trim().length === 0 || TOKEN_RE.test(value))
932
+ if (value.trim().length === 0)
801
933
  return value;
802
934
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
803
- const spans = presidioSpans(value, entities, ctx, locked);
804
- let text = value;
805
- for (const span of spans) {
935
+ const parts = [];
936
+ let last = 0;
937
+ for (const span of presidioSpans(value, entities, ctx, locked)) {
806
938
  const matchedText = value.slice(span.start, span.end);
807
- if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
808
- continue;
809
939
  if (isAllowListed(matchedText, ctx.allowList))
810
940
  continue;
811
941
  const label = (0, presidio_1.getRedactorLabel)(span.entityType);
812
942
  const category = (0, presidio_1.getRedactorCategory)(span.entityType);
813
943
  const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
814
- text = text.slice(0, span.start) + replacement + text.slice(span.end);
944
+ parts.push(value.slice(last, span.start), replacement);
945
+ last = span.end;
815
946
  hits.push({
816
947
  token: replacement, original: '***',
817
948
  patternName: `presidio_${span.entityType}`, patternLabel: label,
818
949
  category, field: path, itemIndex, confidence: span.score,
819
950
  });
820
951
  }
821
- return text;
952
+ parts.push(value.slice(last));
953
+ return parts.join('');
954
+ }
955
+ const IDENTIFIER_KEY = /^[\p{L}_$][\p{L}\p{N}_$-]*$/u;
956
+ async function presidioKey(key, original, source, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
957
+ var _a, _b, _c;
958
+ if (IDENTIFIER_KEY.test(key) || !shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
959
+ return key;
960
+ const locked = (_c = (_b = (_a = ctx.keyLocks) === null || _a === void 0 ? void 0 : _a.get(source)) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
961
+ let out = key;
962
+ try {
963
+ out = await presidioRedactString(key, ctx, vault, sessionId, hits, itemIndex, path, locked);
964
+ }
965
+ catch {
966
+ return key;
967
+ }
968
+ const others = new Set(Object.keys(source).filter((k) => k !== key));
969
+ const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || others.has(k);
970
+ if (out === key || !clashes(out))
971
+ return out;
972
+ return distinctKey(out, original, clashes, ctx, vault, sessionId, [...others, ...Object.keys(taken)]);
973
+ }
974
+ function renamePaths(hits, from, to) {
975
+ for (const h of hits) {
976
+ if (h.field === from)
977
+ h.field = to;
978
+ else if (h.field.startsWith(`${from}.`) || h.field.startsWith(`${from}[`))
979
+ h.field = to + h.field.slice(from.length);
980
+ }
822
981
  }
823
982
  async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
824
983
  var _a, _b, _c, _d, _e, _f;
@@ -848,7 +1007,15 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
848
1007
  const child = path ? `${path}.${k}` : k;
849
1008
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
850
1009
  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));
1010
+ const keyHits = [];
1011
+ const source = value;
1012
+ const key = await presidioKey(k, original, source, out, ctx, vault, sessionId, keyHits, itemIndex, child, childPolicy);
1013
+ const finalChild = path ? `${path}.${key}` : key;
1014
+ if (key !== k)
1015
+ renamePaths(hits, child, finalChild);
1016
+ for (const h of keyHits)
1017
+ hits.push({ ...h, field: finalChild });
1018
+ setOwn(out, key, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, finalChild, childPolicy, lock));
852
1019
  }
853
1020
  return out;
854
1021
  }