n8n-nodes-redactor 6.0.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.
@@ -798,6 +798,7 @@ class Redactor {
798
798
  fieldRules: [],
799
799
  fieldMode: 'all',
800
800
  skipSemantic: operation === 'verify',
801
+ skipNames: operation === 'verify' && this.getNode().typeVersion < 2,
801
802
  scanNumbers: true,
802
803
  };
803
804
  const allHits = [];
@@ -1032,7 +1033,8 @@ class Redactor {
1032
1033
  { name: 'Deny list', entries: denyList },
1033
1034
  ]);
1034
1035
  try {
1035
- vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1036
+ const session = vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1037
+ sessionTtl = session.ttl / 60000;
1036
1038
  }
1037
1039
  catch (error) {
1038
1040
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
@@ -611,39 +611,45 @@ function redactMatch(pass, match, decision) {
611
611
  }
612
612
  return replacement;
613
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;
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];
623
620
  }
624
- return lo;
621
+ return undefined;
625
622
  }
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);
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) {
636
628
  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;
629
+ into.push({ text: redactMatch(pass, pass.source.slice(pos, end), p.decision), locked: true, src: pos, end });
630
+ pos = end;
631
+ continue;
645
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);
646
651
  }
652
+ pass.segs = next;
647
653
  }
648
654
  function claimOne(pass, cursor, pending, start, end, decide) {
649
655
  const text = pass.source.slice(start, end);
@@ -736,7 +742,8 @@ function nameSpans(pass) {
736
742
  return spans;
737
743
  }
738
744
  function nameStage(pass) {
739
- if (pass.ctx.skipSemantic)
745
+ var _a;
746
+ if ((_a = pass.ctx.skipNames) !== null && _a !== void 0 ? _a : pass.ctx.skipSemantic)
740
747
  return;
741
748
  replaceSpans(pass, nameSpans(pass), (match, start) => {
742
749
  if (isAllowListed(match, pass.ctx.allowList))
@@ -873,49 +880,84 @@ function toUtf16Spans(text, entities) {
873
880
  .filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
874
881
  .map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
875
882
  }
876
- function resolveOverlaps(spans) {
877
- 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);
878
885
  const kept = [];
879
886
  for (const span of ranked) {
880
887
  if (!kept.some((k) => span.start < k.end && k.start < span.end))
881
888
  kept.push(span);
882
889
  }
883
- 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;
884
925
  }
885
926
  function presidioSpans(value, entities, ctx, locked) {
886
927
  var _a;
887
928
  const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
888
- const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
889
- 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);
890
930
  }
891
931
  async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
892
932
  if (value.trim().length === 0)
893
933
  return value;
894
934
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
895
- const spans = presidioSpans(value, entities, ctx, locked);
896
- let text = value;
897
- for (const span of spans) {
935
+ const parts = [];
936
+ let last = 0;
937
+ for (const span of presidioSpans(value, entities, ctx, locked)) {
898
938
  const matchedText = value.slice(span.start, span.end);
899
939
  if (isAllowListed(matchedText, ctx.allowList))
900
940
  continue;
901
941
  const label = (0, presidio_1.getRedactorLabel)(span.entityType);
902
942
  const category = (0, presidio_1.getRedactorCategory)(span.entityType);
903
943
  const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
904
- text = text.slice(0, span.start) + replacement + text.slice(span.end);
944
+ parts.push(value.slice(last, span.start), replacement);
945
+ last = span.end;
905
946
  hits.push({
906
947
  token: replacement, original: '***',
907
948
  patternName: `presidio_${span.entityType}`, patternLabel: label,
908
949
  category, field: path, itemIndex, confidence: span.score,
909
950
  });
910
951
  }
911
- return text;
952
+ parts.push(value.slice(last));
953
+ return parts.join('');
912
954
  }
913
955
  const IDENTIFIER_KEY = /^[\p{L}_$][\p{L}\p{N}_$-]*$/u;
914
- async function presidioKey(key, original, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
956
+ async function presidioKey(key, original, source, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
915
957
  var _a, _b, _c;
916
958
  if (IDENTIFIER_KEY.test(key) || !shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
917
959
  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 : [];
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 : [];
919
961
  let out = key;
920
962
  try {
921
963
  out = await presidioRedactString(key, ctx, vault, sessionId, hits, itemIndex, path, locked);
@@ -923,10 +965,19 @@ async function presidioKey(key, original, taken, ctx, vault, sessionId, hits, it
923
965
  catch {
924
966
  return key;
925
967
  }
926
- const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k);
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);
927
970
  if (out === key || !clashes(out))
928
971
  return out;
929
- return distinctKey(out, original, clashes, ctx, vault, sessionId, Object.keys(taken));
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
+ }
930
981
  }
931
982
  async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
932
983
  var _a, _b, _c, _d, _e, _f;
@@ -956,8 +1007,15 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
956
1007
  const child = path ? `${path}.${k}` : k;
957
1008
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
958
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);
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));
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));
961
1019
  }
962
1020
  return out;
963
1021
  }
@@ -206,12 +206,18 @@ exports.INJECTION_DESCRIPTIONS = [
206
206
  'Invisible Unicode characters detected',
207
207
  'Base64-encoded injection payload detected',
208
208
  ];
209
- const ZERO_WIDTH_RE = /(?:\u200B|\u200C|\u200D|\uFEFF|\u00AD|\u2060|\u2061|\u2062|\u2063|\u2064|\u2066|\u2067|\u2068|\u2069|\u206A|\u206B|\u206C|\u206D|\u206E|\u206F)/g;
209
+ const ZERO_WIDTH_RE = /(?:\u200B|\u200C|\u200D|\uFEFF|\u00AD|\u2060|\u2061|\u2062|\u2063|\u2064|\u2066|\u2067|\u2068|\u2069|\u206A|\u206B|\u206C|\u206D|\u206E|\u206F)/;
210
210
  const BASE64_SUSPICIOUS_RE = /[A-Za-z0-9+/]{40,}={0,2}/g;
211
- const INJECTION_SEEDS = [
212
- 'ignore', 'override', 'system', 'prompt', 'instruction', 'jailbreak',
213
- 'developer mode', 'DAN', 'bypass', 'unrestricted',
214
- ];
211
+ const INJECTION_SEED_RE = /ignore|override|system|prompt|instruction|jailbreak|developer mode|\bdan\b|bypass|unrestricted/i;
212
+ function messageFor(action, score, count) {
213
+ if (count === 0)
214
+ return 'No prompt injection detected.';
215
+ if (action === 'BLOCK')
216
+ return `BLOCKED: High-confidence prompt injection detected (score: ${score}). ${count} suspicious pattern(s) found.`;
217
+ if (action === 'FLAG')
218
+ return `FLAGGED: Potential prompt injection detected (score: ${score}). Review recommended.`;
219
+ return `Low-confidence match (score: ${score}). Likely safe but logged for review.`;
220
+ }
215
221
  function detectPromptInjection(text) {
216
222
  if (!text || text.trim().length === 0) {
217
223
  return {
@@ -257,8 +263,7 @@ function detectPromptInjection(text) {
257
263
  for (const b64 of base64Matches.slice(0, 5)) {
258
264
  try {
259
265
  const decoded = Buffer.from(b64, 'base64').toString('utf-8');
260
- const decodedLower = decoded.toLowerCase();
261
- if (INJECTION_SEEDS.some((seed) => decodedLower.includes(seed))) {
266
+ if (INJECTION_SEED_RE.test(decoded)) {
262
267
  encodedPayloadsFound = true;
263
268
  totalScore += 0.35;
264
269
  matches.push({
@@ -284,19 +289,7 @@ function detectPromptInjection(text) {
284
289
  action = 'FLAG';
285
290
  else if (matches.length > 0)
286
291
  action = 'LOG_ONLY';
287
- let message;
288
- if (!matches.length) {
289
- message = 'No prompt injection detected.';
290
- }
291
- else if (action === 'BLOCK') {
292
- message = `BLOCKED: High-confidence prompt injection detected (score: ${totalScore}). ${matches.length} suspicious pattern(s) found.`;
293
- }
294
- else if (action === 'FLAG') {
295
- message = `FLAGGED: Potential prompt injection detected (score: ${totalScore}). Review recommended.`;
296
- }
297
- else {
298
- message = `Low-confidence match (score: ${totalScore}). Likely safe but logged for review.`;
299
- }
292
+ const message = messageFor(action, totalScore, matches.length);
300
293
  return {
301
294
  detected: matches.length > 0,
302
295
  score: totalScore,
@@ -316,11 +309,22 @@ function collectStrings(value, path, into) {
316
309
  value.forEach((item, i) => collectStrings(item, `${path}[${i}]`, into));
317
310
  }
318
311
  else if (value !== null && typeof value === 'object') {
319
- for (const [key, item] of Object.entries(value)) {
320
- collectStrings(item, path ? `${path}.${key}` : key, into);
321
- }
312
+ collectEntries(value, path, into);
322
313
  }
323
314
  }
315
+ function collectEntries(value, path, into) {
316
+ const keyPath = path ? `${path} (key)` : '(key)';
317
+ for (const [key, item] of Object.entries(value)) {
318
+ if (key.trim().length > 0)
319
+ into.push({ path: keyPath, text: key, key: true });
320
+ collectStrings(item, path ? `${path}.${key}` : key, into);
321
+ }
322
+ }
323
+ function withheld(result, key) {
324
+ if (!key)
325
+ return result;
326
+ return { ...result, matchedPatterns: result.matchedPatterns.map((m) => ({ ...m, matchedText: '(key text withheld)' })) };
327
+ }
324
328
  function actionFor(score, detected) {
325
329
  if (score >= 0.7)
326
330
  return 'BLOCK';
@@ -332,7 +336,7 @@ function scanForInjection(data, currentPath = '') {
332
336
  const fields = [];
333
337
  collectStrings(data, currentPath, fields);
334
338
  const found = fields
335
- .map((f) => ({ path: f.path, result: detectPromptInjection(f.text) }))
339
+ .map((f) => ({ path: f.path, result: withheld(detectPromptInjection(f.text), f.key) }))
336
340
  .filter((f) => f.result.detected)
337
341
  .sort((a, b) => b.result.score - a.result.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
338
342
  if (found.length === 0) {
@@ -341,13 +345,15 @@ function scanForInjection(data, currentPath = '') {
341
345
  encodedPayloadsFound: false, zeroWidthCharsFound: false, message: 'No prompt injection detected.',
342
346
  };
343
347
  }
344
- const score = Math.min(1.0, found.slice(1).reduce((sum, f) => sum + f.result.score * 0.3, found[0].result.score));
348
+ const raw = found.slice(1).reduce((sum, f) => sum + f.result.score * 0.3, found[0].result.score);
349
+ const score = Math.round(Math.min(1.0, raw) * 100) / 100;
345
350
  const matchedPatterns = found.flatMap((f) => f.result.matchedPatterns.map((m) => ({ ...m, pattern: `[${f.path}] ${m.pattern}` })));
346
351
  return {
347
352
  ...found[0].result,
348
353
  detected: true,
349
354
  score,
350
355
  action: actionFor(score, true),
356
+ message: messageFor(actionFor(score, true), score, matchedPatterns.length),
351
357
  matchedPatterns,
352
358
  encodedPayloadsFound: found.some((f) => f.result.encodedPayloadsFound),
353
359
  zeroWidthCharsFound: found.some((f) => f.result.zeroWidthCharsFound),
@@ -1613,37 +1613,54 @@ const NOT_NAMES = new Set([
1613
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
1614
  exports.LAST_NAMES.add(surname);
1615
1615
  }
1616
+ const NAME_WORD = String.raw `\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*`;
1617
+ const CAPITAL_RUN = new RegExp(String.raw `(?<![\p{L}\p{N}'’-])${NAME_WORD}(?:\s+${NAME_WORD})*(?![\p{L}\p{N}])`, 'gu');
1618
+ const MAX_NAME_WORDS = 4;
1619
+ function isKnownName(word) {
1620
+ const parts = word.split('-').flatMap((p) => [p, p.replace(/^(?:O['’]|Mc|Mac)(?=\p{Lu})/u, '')]);
1621
+ return parts.some((p) => exports.FIRST_NAMES.has(p) || exports.LAST_NAMES.has(p));
1622
+ }
1616
1623
  function detectNamesInText(text) {
1617
1624
  const results = [];
1618
- const capitalWordSeq = /\b([A-ZÀ-ÿ][a-zà-ÿ]{1,20}(?:\s+[A-ZÀ-ÿ][a-zà-ÿ]{1,20}){1,3})\b/g;
1619
- let match;
1620
- while ((match = capitalWordSeq.exec(text)) !== null) {
1621
- for (const run of nameRuns(match[1], match.index))
1622
- results.push(run);
1625
+ for (const match of text.matchAll(CAPITAL_RUN)) {
1626
+ if (!/\p{Ll}/u.test(match[0]))
1627
+ continue;
1628
+ const words = [...match[0].matchAll(/\S+/g)].map((w) => { var _a, _b; return ({ word: w[0], at: ((_a = match.index) !== null && _a !== void 0 ? _a : 0) + ((_b = w.index) !== null && _b !== void 0 ? _b : 0) }); });
1629
+ for (const segment of nameSegments(words))
1630
+ results.push(...chunkSegment(text, segment));
1623
1631
  }
1624
1632
  return results;
1625
1633
  }
1626
- function nameRuns(sequence, offset) {
1627
- var _a;
1628
- const runs = [];
1629
- let words = [];
1634
+ function nameSegments(words) {
1635
+ const segments = [];
1636
+ let current = [];
1630
1637
  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
+ const anchored = current.some((w) => isKnownName(w.word) && !NOT_NAMES.has(w.word));
1639
+ if (anchored)
1640
+ segments.push(current);
1641
+ current = [];
1638
1642
  };
1639
- for (const m of sequence.matchAll(/\S+/g)) {
1640
- if (NOT_NAMES.has(m[0]))
1643
+ for (const w of words) {
1644
+ if (NOT_NAMES.has(w.word) && !isKnownName(w.word))
1641
1645
  flush();
1642
1646
  else
1643
- words.push({ word: m[0], at: (_a = m.index) !== null && _a !== void 0 ? _a : 0 });
1647
+ current.push(w);
1644
1648
  }
1645
1649
  flush();
1646
- return runs;
1650
+ return segments;
1651
+ }
1652
+ function chunkSegment(text, segment) {
1653
+ const spans = [];
1654
+ for (let i = 0; i < segment.length; i += MAX_NAME_WORDS) {
1655
+ const chunk = segment.slice(i, i + MAX_NAME_WORDS);
1656
+ if (chunk.length < 2 || !chunk.some((w) => isKnownName(w.word)))
1657
+ continue;
1658
+ const last = chunk[chunk.length - 1];
1659
+ const start = chunk[0].at;
1660
+ const end = last.at + last.word.length;
1661
+ spans.push({ name: text.slice(start, end), start, end });
1662
+ }
1663
+ return spans;
1647
1664
  }
1648
1665
  exports.NAME_CONTEXT_WORDS = [
1649
1666
  'spoke with', 'contact', 'customer', 'client', 'patient', 'employee',
@@ -21,12 +21,35 @@ function luhnCheck(num) {
21
21
  }
22
22
  const EMAIL_LOCAL = /[A-Za-z0-9._%+'!#$&*/=?^`{|}~-]/;
23
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;
24
+ const EMAIL_DOMAIN_SHAPE = /^[\p{L}\p{N}-]+(?:[.][\p{L}\p{N}-]+)*[.](?:\p{L}{2,}|xn--[a-z0-9-]{2,59})$/iu;
25
+ const QUOTED_LOCAL_MAX = 128;
26
+ function openingQuote(text, close) {
27
+ for (let j = close - 1; j >= Math.max(0, close - QUOTED_LOCAL_MAX); j--) {
28
+ if (text[j] === '\r' || text[j] === '\n')
29
+ return -1;
30
+ if (text[j] !== '"')
31
+ continue;
32
+ let slashes = 0;
33
+ while (text[j - 1 - slashes] === '\\')
34
+ slashes++;
35
+ if (slashes % 2 === 0)
36
+ return j;
37
+ }
38
+ return -1;
39
+ }
40
+ function addressLiteralEnd(text, at) {
41
+ const close = text.indexOf(']', at + 2);
42
+ if (close < 0 || close - at > 64)
43
+ return -1;
44
+ const inner = text.slice(at + 2, close);
45
+ const v4 = /^\d{1,3}(?:[.]\d{1,3}){3}$/.test(inner) && inner.split('.').every((n) => Number(n) <= 255);
46
+ const v6 = /^IPv6:/i.test(inner) && isIpv6(inner.slice(5));
47
+ return v4 || v6 ? close + 1 : -1;
48
+ }
25
49
  function emailLocalStart(text, at) {
26
50
  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;
51
+ const open = openingQuote(text, at - 1);
52
+ return open >= 0 && open < at - 2 ? open : -1;
30
53
  }
31
54
  let start = at;
32
55
  while (start > 0 && EMAIL_LOCAL.test(text[start - 1]))
@@ -34,6 +57,8 @@ function emailLocalStart(text, at) {
34
57
  return start < at ? start : -1;
35
58
  }
36
59
  function emailDomainEnd(text, at) {
60
+ if (text[at + 1] === '[')
61
+ return addressLiteralEnd(text, at);
37
62
  let end = at + 1;
38
63
  while (end < text.length && EMAIL_DOMAIN.test(text[end]))
39
64
  end++;
@@ -62,6 +87,142 @@ function ibanCheck(iban) {
62
87
  .reduce((acc, chunk) => (parseInt(acc + chunk, 10) % 97).toString(), '');
63
88
  return parseInt(remainder, 10) === 1;
64
89
  }
90
+ function firstAtLeast(sorted, min) {
91
+ let lo = 0;
92
+ let hi = sorted.length;
93
+ while (lo < hi) {
94
+ const mid = (lo + hi) >> 1;
95
+ if (sorted[mid] < min)
96
+ lo = mid + 1;
97
+ else
98
+ hi = mid;
99
+ }
100
+ return lo < sorted.length ? lo : -1;
101
+ }
102
+ function bodyEnd(text, from, limit) {
103
+ let end = from;
104
+ while (end < limit && /[A-Za-z0-9+/=:\s]/.test(text[end]))
105
+ end++;
106
+ return end;
107
+ }
108
+ function scanBlocks(text, header, endLine) {
109
+ const spans = [];
110
+ const starts = [...text.matchAll(header)].map((m) => { var _a; return ({ at: (_a = m.index) !== null && _a !== void 0 ? _a : 0, len: m[0].length }); });
111
+ const ends = [...text.matchAll(endLine)].map((m) => { var _a; return ({ at: (_a = m.index) !== null && _a !== void 0 ? _a : 0, len: m[0].length }); });
112
+ const endAt = ends.map((e) => e.at);
113
+ let reach = 0;
114
+ starts.forEach((s, i) => {
115
+ if (s.at < reach)
116
+ return;
117
+ const nextStart = i + 1 < starts.length ? starts[i + 1].at : text.length;
118
+ const k = firstAtLeast(endAt, s.at + s.len);
119
+ const end = k >= 0 && endAt[k] < nextStart ? endAt[k] + ends[k].len : bodyEnd(text, s.at + s.len, nextStart);
120
+ spans.push({ start: s.at, end });
121
+ reach = end;
122
+ });
123
+ return spans;
124
+ }
125
+ const scanPemKeys = (text) => scanBlocks(text, /-----BEGIN (?:RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g, /-----END (?:RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g);
126
+ const scanPgpKeys = (text) => scanBlocks(text, /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, /-----END PGP PRIVATE KEY BLOCK-----/g);
127
+ const scanEduEmails = (text) => scanEmails(text).filter((s) => /[.]edu$/i.test(text.slice(s.start, s.end)));
128
+ const IBAN_LENGTHS = {
129
+ AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, BI: 27, BR: 29, BY: 28, CH: 21, CR: 22,
130
+ CY: 28, CZ: 24, DE: 22, DJ: 27, DK: 18, DO: 28, EE: 20, EG: 29, ES: 24, FI: 18, FK: 18, FO: 18, FR: 27, GB: 22,
131
+ GE: 22, GI: 23, GL: 18, GR: 27, GT: 28, HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23, IS: 26, IT: 27, JO: 30, KW: 30,
132
+ KZ: 20, LB: 28, LC: 32, LI: 21, LT: 20, LU: 20, LV: 21, LY: 25, MC: 27, MD: 24, ME: 22, MK: 19, MN: 20, MR: 27,
133
+ MT: 31, MU: 30, NI: 28, NL: 18, NO: 15, OM: 23, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29, RO: 24, RS: 22, RU: 33,
134
+ SA: 24, SC: 31, SD: 18, SE: 24, SI: 19, SK: 24, SM: 27, SO: 23, ST: 25, SV: 28, TL: 23, TN: 24, TR: 26, UA: 29,
135
+ VA: 22, VG: 24, XK: 20, YE: 30,
136
+ };
137
+ const ALNUM = /[A-Za-z0-9]/;
138
+ const groupGap = (text, at, compact) => { var _a; return text[at] === ' ' && compact % 4 === 0 && ALNUM.test((_a = text[at + 1]) !== null && _a !== void 0 ? _a : ''); };
139
+ function ibanEnd(text, start, length) {
140
+ var _a;
141
+ let compact = 0;
142
+ let end = start;
143
+ while (end < text.length && compact < length) {
144
+ if (/[A-Z0-9]/.test(text[end]))
145
+ compact++;
146
+ else if (!groupGap(text, end, compact))
147
+ break;
148
+ end++;
149
+ }
150
+ return compact === length && !ALNUM.test((_a = text[end]) !== null && _a !== void 0 ? _a : '') ? end : -1;
151
+ }
152
+ function scanIbans(text) {
153
+ var _a;
154
+ const spans = [];
155
+ for (const m of text.matchAll(/(?<![A-Za-z0-9])([A-Z]{2})\d{2}/g)) {
156
+ const length = IBAN_LENGTHS[m[1]];
157
+ const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
158
+ const end = length ? ibanEnd(text, start, length) : -1;
159
+ if (end > 0)
160
+ spans.push({ start, end });
161
+ }
162
+ return spans;
163
+ }
164
+ function longestCard(groups, i) {
165
+ let best = -1;
166
+ let digits = '';
167
+ for (let j = i; j < groups.length && digits.length + groups[j].digits.length <= 19; j++) {
168
+ digits += groups[j].digits;
169
+ if (digits.length >= 13 && luhnCheck(digits))
170
+ best = j;
171
+ }
172
+ return best;
173
+ }
174
+ function cardsInRun(groups, spans) {
175
+ let i = 0;
176
+ while (i < groups.length) {
177
+ const best = longestCard(groups, i);
178
+ if (best >= 0)
179
+ spans.push({ start: groups[i].start, end: groups[best].start + groups[best].digits.length });
180
+ i = best >= 0 ? best + 1 : i + 1;
181
+ }
182
+ }
183
+ function scanCards(text) {
184
+ var _a;
185
+ const spans = [];
186
+ for (const run of text.matchAll(/(?<![0-9A-Za-z_])\d+(?:[\s-]\d+)*(?![0-9A-Za-z_])/g)) {
187
+ const base = (_a = run.index) !== null && _a !== void 0 ? _a : 0;
188
+ cardsInRun([...run[0].matchAll(/\d+/g)].map((g) => { var _a; return ({ start: base + ((_a = g.index) !== null && _a !== void 0 ? _a : 0), digits: g[0] }); }), spans);
189
+ }
190
+ return spans;
191
+ }
192
+ function isIpv6(candidate) {
193
+ var _a;
194
+ const halves = candidate.split('::');
195
+ if (halves.length > 2)
196
+ return false;
197
+ const parts = (h) => (h === '' ? [] : h.split(':'));
198
+ const head = parts(halves[0]);
199
+ const tail = halves.length === 2 ? parts(halves[1]) : [];
200
+ const all = [...head, ...tail];
201
+ const last = (_a = all[all.length - 1]) !== null && _a !== void 0 ? _a : '';
202
+ const v4 = /^\d{1,3}(?:[.]\d{1,3}){3}$/.test(last) && last.split('.').every((n) => Number(n) <= 255);
203
+ const groups = all.length + (v4 ? 1 : 0);
204
+ if (!all.slice(0, v4 ? -1 : undefined).every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g)))
205
+ return false;
206
+ return halves.length === 2 ? groups <= 7 : groups === 8;
207
+ }
208
+ function trimIpv6(s) {
209
+ let out = s;
210
+ while (out.endsWith('.') || (out.endsWith(':') && !out.endsWith('::')))
211
+ out = out.slice(0, -1);
212
+ return out;
213
+ }
214
+ const looksLikeIpv6 = (s) => s.split(':').length >= 3 && /[0-9A-Fa-f]/.test(s) && isIpv6(s);
215
+ function scanIpv6(text) {
216
+ var _a;
217
+ const spans = [];
218
+ for (const m of text.matchAll(/(?<![0-9A-Za-z:.])[0-9A-Fa-f:.]*:[0-9A-Fa-f:.]*/g)) {
219
+ const s = trimIpv6(m[0]);
220
+ const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
221
+ if (looksLikeIpv6(s))
222
+ spans.push({ start, end: start + s.length });
223
+ }
224
+ return spans;
225
+ }
65
226
  exports.PII_PATTERNS = [
66
227
  {
67
228
  name: 'email',
@@ -83,7 +244,7 @@ exports.PII_PATTERNS = [
83
244
  name: 'phoneUK',
84
245
  label: 'PHONE_UK',
85
246
  category: 'contact',
86
- regex: /\b(?:0|\+44)\s?\d{2,4}\s?\d{3,4}\s?\d{3,4}\b/g,
247
+ regex: /(?<![\w+])(?:0|\+44)\s?\d{2,4}\s?\d{3,4}\s?\d{3,4}\b/g,
87
248
  description: 'UK phone numbers',
88
249
  },
89
250
  {
@@ -111,21 +272,21 @@ exports.PII_PATTERNS = [
111
272
  name: 'phoneFR',
112
273
  label: 'PHONE_FR',
113
274
  category: 'contact',
114
- regex: /\b(?:0|\+33)\s?\d[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}\b/g,
275
+ regex: /(?<![\w+])(?:0|\+33)\s?\d[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}\b/g,
115
276
  description: 'French phone numbers (06 12 34 56 78)',
116
277
  },
117
278
  {
118
279
  name: 'phoneNL',
119
280
  label: 'PHONE_NL',
120
281
  category: 'contact',
121
- regex: /\b(?:0|\+31)\s?\d{1,3}[\s-]\d{3,4}[\s-]\d{2,4}\b/g,
282
+ regex: /(?<![\w+])(?:0|\+31)\s?\d{1,3}[\s-]\d{3,4}[\s-]\d{2,4}\b/g,
122
283
  description: 'Dutch phone numbers',
123
284
  },
124
285
  {
125
286
  name: 'personName',
126
287
  label: 'PERSON',
127
288
  category: 'identity',
128
- regex: /\b(?:Mr\.?|Mrs\.?|Ms\.?|Miss|Dr\.?|Prof\.?|Sr\.?|Jr\.?|Herr|Frau|Monsieur|Madame|Señor|Señora)\s+[A-Z][a-zA-ZÀ-ÿ]+(?:\s+[A-Z][a-zA-ZÀ-ÿ]+){0,3}\b/g,
289
+ regex: /(?<![\p{L}\p{N}])(?:Mr\.?|Mrs\.?|Ms\.?|Miss|Dr\.?|Prof\.?|Sr\.?|Jr\.?|Herr|Frau|Monsieur|Madame|Señor|Señora)\s+\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*(?:\s+\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*){0,3}(?![\p{L}\p{N}])/gu,
129
290
  description: 'Person names with title prefixes (Mr., Mrs., Dr., etc.)',
130
291
  },
131
292
  {
@@ -312,6 +473,7 @@ exports.PII_PATTERNS = [
312
473
  label: 'CREDIT_CARD',
313
474
  category: 'financial',
314
475
  regex: /\b(?:\d[\s-]?){12,18}\d\b/g,
476
+ scan: scanCards,
315
477
  validate: (match) => luhnCheck(match),
316
478
  description: 'Credit/debit card numbers, 13 to 19 digits (Luhn validated)',
317
479
  },
@@ -320,6 +482,7 @@ exports.PII_PATTERNS = [
320
482
  label: 'IBAN',
321
483
  category: 'financial',
322
484
  regex: /\b[A-Z]{2}\d{2}[\s]?[\dA-Z]{4}[\s]?(?:[\dA-Z]{4}[\s]?){2,7}[\dA-Z]{1,4}\b/g,
485
+ scan: scanIbans,
323
486
  validate: (match) => ibanCheck(match),
324
487
  description: 'International Bank Account Numbers (IBAN checksum validated)',
325
488
  },
@@ -356,7 +519,8 @@ exports.PII_PATTERNS = [
356
519
  label: 'IPV6_ADDRESS',
357
520
  category: 'network',
358
521
  regex: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g,
359
- description: 'IPv6 addresses',
522
+ scan: scanIpv6,
523
+ description: 'IPv6 addresses, full, compressed (RFC 5952) and with an IPv4 tail',
360
524
  },
361
525
  {
362
526
  name: 'macAddress',
@@ -460,7 +624,7 @@ exports.PII_PATTERNS = [
460
624
  name: 'addressDE',
461
625
  label: 'ADDRESS_DE',
462
626
  category: 'location',
463
- regex: /(?:[A-ZÀ-ÿa-zà-ÿ]+(?:straße|strasse|str\.|weg|gasse|platz|allee|ring|damm|ufer|chaussee))\s+\d{1,5}[a-z]?(?:\s*[,/]\s*\d{5}\s+[A-ZÀ-ÿ][a-zà-ÿ]+(?:\s+[a-zà-ÿ]+\.?\s+[A-ZÀ-ÿ][a-zà-ÿ]+)?)?/gi,
627
+ regex: /(?<![A-ZÀ-ÿa-zà-ÿ])(?:[A-ZÀ-ÿa-zà-ÿ]{1,60}(?:straße|strasse|str\.|weg|gasse|platz|allee|ring|damm|ufer|chaussee))\s+\d{1,5}[a-z]?(?:\s*[,/]\s*\d{5}\s+[A-ZÀ-ÿ][a-zà-ÿ]+(?:\s+[a-zà-ÿ]+\.?\s+[A-ZÀ-ÿ][a-zà-ÿ]+)?)?/gi,
464
628
  description: 'German street addresses (Musterstraße 12, 80331 München)',
465
629
  },
466
630
  {
@@ -768,7 +932,7 @@ exports.PII_PATTERNS = [
768
932
  name: 'ldapDN',
769
933
  label: 'LDAP_DN',
770
934
  category: 'enterprise',
771
- regex: /(?:CN|OU|DC|O|C|L|ST)=[^,]+(?:,\s*(?:CN|OU|DC|O|C|L|ST)=[^,]+){1,}/gi,
935
+ regex: /(?<![A-Za-z])(?:CN|OU|DC|O|C|L|ST)=[^,]{1,256}(?:,\s*(?:CN|OU|DC|O|C|L|ST)=[^,]{1,256}){1,32}/gi,
772
936
  description: 'LDAP Distinguished Names (CN=...,OU=...,DC=...)',
773
937
  },
774
938
  {
@@ -796,7 +960,7 @@ exports.PII_PATTERNS = [
796
960
  name: 'connStringKV',
797
961
  label: 'CONN_STRING',
798
962
  category: 'enterprise',
799
- regex: /(?:Server|Data Source|Host)=[^;]+;[^;]*(?:;[^;]*){0,10}(?:Password|Pwd)=[^;]+/gi,
963
+ regex: /(?<![A-Za-z])(?:Server|Data Source|Host)=[^;]{1,512};[^;]{0,512}(?:;[^;]{0,512}){0,10}(?:Password|Pwd)=[^;]{1,512}/gi,
800
964
  description: 'Connection strings with credentials (ADO.NET / key=value style)',
801
965
  },
802
966
  {
@@ -860,6 +1024,8 @@ exports.PII_PATTERNS = [
860
1024
  label: 'PRIVATE_KEY',
861
1025
  category: 'enterprise',
862
1026
  regex: /-----BEGIN (RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----(?:[\s\S]*?-----END \1?PRIVATE KEY-----|[A-Za-z0-9+/=\s]*)/g,
1027
+ anchor: '-----BEGIN',
1028
+ scan: scanPemKeys,
863
1029
  description: 'PEM private keys, the whole block through the END line',
864
1030
  },
865
1031
  {
@@ -978,35 +1144,35 @@ exports.PII_PATTERNS = [
978
1144
  name: 'phoneES',
979
1145
  label: 'PHONE_ES',
980
1146
  category: 'contact',
981
- regex: /\b(?:\+?34[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{3}\b/g,
1147
+ regex: /(?<![\w+])(?:\+?34[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{3}\b/g,
982
1148
  description: 'Spanish phone numbers',
983
1149
  },
984
1150
  {
985
1151
  name: 'phoneAU',
986
1152
  label: 'PHONE_AU',
987
1153
  category: 'contact',
988
- regex: /\b(?:\+?61[\s\-.]?)?0?\d[\s\-.]\d{4}[\s\-.]\d{4}\b/g,
1154
+ regex: /(?<![\w+])(?:\+?61[\s\-.]?)?0?\d[\s\-.]\d{4}[\s\-.]\d{4}\b/g,
989
1155
  description: 'Australian phone numbers',
990
1156
  },
991
1157
  {
992
1158
  name: 'phoneIN',
993
1159
  label: 'PHONE_IN',
994
1160
  category: 'contact',
995
- regex: /\b(?:\+?91[\s\-.]?)?[6-9]\d{4}[\s\-.]\d{5}\b/g,
1161
+ regex: /(?<![\w+])(?:\+?91[\s\-.]?)?[6-9]\d{4}[\s\-.]\d{5}\b/g,
996
1162
  description: 'Indian phone numbers',
997
1163
  },
998
1164
  {
999
1165
  name: 'phoneJP',
1000
1166
  label: 'PHONE_JP',
1001
1167
  category: 'contact',
1002
- regex: /\b(?:\+?81[\s\-.]?)?0?\d{1,4}[\s\-.]\d{1,4}[\s\-.]\d{4}\b/g,
1168
+ regex: /(?<![\w+])(?:\+?81[\s\-.]?)?0?\d{1,4}[\s\-.]\d{1,4}[\s\-.]\d{4}\b/g,
1003
1169
  description: 'Japanese phone numbers',
1004
1170
  },
1005
1171
  {
1006
1172
  name: 'phoneBR',
1007
1173
  label: 'PHONE_BR',
1008
1174
  category: 'contact',
1009
- regex: /\b(?:\+?55[\s\-.]?)?\(?\d{2}\)?[\s\-.]?\d{4,5}[\s\-.]\d{4}\b/g,
1175
+ regex: /(?<![\w+])(?:\+?55[\s\-.]?)?\(?\d{2}\)?[\s\-.]?\d{4,5}[\s\-.]\d{4}\b/g,
1010
1176
  description: 'Brazilian phone numbers',
1011
1177
  },
1012
1178
  {
@@ -1020,35 +1186,35 @@ exports.PII_PATTERNS = [
1020
1186
  name: 'phoneCN',
1021
1187
  label: 'PHONE_CN',
1022
1188
  category: 'contact',
1023
- regex: /\b(?:\+?86[\s\-.]?)?1[3-9]\d{9}\b/g,
1189
+ regex: /(?<![\w+])(?:\+?86[\s\-.]?)?1[3-9]\d{9}\b/g,
1024
1190
  description: 'Chinese mobile phone numbers',
1025
1191
  },
1026
1192
  {
1027
1193
  name: 'phoneKR',
1028
1194
  label: 'PHONE_KR',
1029
1195
  category: 'contact',
1030
- regex: /\b(?:\+?82[\s\-.]?)?0?\d{1,2}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
1196
+ regex: /(?<![\w+])(?:\+?82[\s\-.]?)?0?\d{1,2}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
1031
1197
  description: 'South Korean phone numbers',
1032
1198
  },
1033
1199
  {
1034
1200
  name: 'phoneRU',
1035
1201
  label: 'PHONE_RU',
1036
1202
  category: 'contact',
1037
- regex: /\b(?:\+?7[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{2}[\s\-.]\d{2}\b/g,
1203
+ regex: /(?<![\w+])(?:\+?7[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{2}[\s\-.]\d{2}\b/g,
1038
1204
  description: 'Russian phone numbers',
1039
1205
  },
1040
1206
  {
1041
1207
  name: 'phoneMX',
1042
1208
  label: 'PHONE_MX',
1043
1209
  category: 'contact',
1044
- regex: /\b(?:\+?52[\s\-.]?)?\d{2,3}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
1210
+ regex: /(?<![\w+])(?:\+?52[\s\-.]?)?\d{2,3}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
1045
1211
  description: 'Mexican phone numbers',
1046
1212
  },
1047
1213
  {
1048
1214
  name: 'phoneZA',
1049
1215
  label: 'PHONE_ZA',
1050
1216
  category: 'contact',
1051
- regex: /\b(?:\+?27[\s\-.]?)?0?\d{2}[\s\-.]\d{3}[\s\-.]\d{4}\b/g,
1217
+ regex: /(?<![\w+])(?:\+?27[\s\-.]?)?0?\d{2}[\s\-.]\d{3}[\s\-.]\d{4}\b/g,
1052
1218
  description: 'South African phone numbers',
1053
1219
  },
1054
1220
  {
@@ -1483,6 +1649,8 @@ exports.PII_PATTERNS = [
1483
1649
  label: 'PGP_KEY',
1484
1650
  category: 'enterprise',
1485
1651
  regex: /-----BEGIN PGP PRIVATE KEY BLOCK-----(?:[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----|[A-Za-z0-9+/=:\s]*)/g,
1652
+ anchor: '-----BEGIN',
1653
+ scan: scanPgpKeys,
1486
1654
  description: 'PGP private key blocks, the whole block through the END line',
1487
1655
  },
1488
1656
  {
@@ -1574,6 +1742,8 @@ exports.PII_PATTERNS = [
1574
1742
  label: 'EDU_EMAIL',
1575
1743
  category: 'contact',
1576
1744
  regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.edu/g,
1745
+ anchor: '@',
1746
+ scan: scanEduEmails,
1577
1747
  description: 'Educational email addresses (.edu)',
1578
1748
  },
1579
1749
  {
@@ -6,6 +6,7 @@ exports.analyzeWithPresidio = analyzeWithPresidio;
6
6
  exports.getRedactorLabel = getRedactorLabel;
7
7
  exports.getRedactorCategory = getRedactorCategory;
8
8
  exports.getPresidioEntities = getPresidioEntities;
9
+ const patterns_1 = require("./patterns");
9
10
  const PRESIDIO_TO_REDACTOR_LABEL = {
10
11
  PERSON: 'PERSON_NAME',
11
12
  EMAIL_ADDRESS: 'EMAIL',
@@ -132,7 +133,10 @@ function getRedactorLabel(presidioEntityType) {
132
133
  return PRESIDIO_TO_REDACTOR_LABEL[presidioEntityType] || presidioEntityType;
133
134
  }
134
135
  function getRedactorCategory(presidioEntityType) {
135
- return PRESIDIO_TO_CATEGORY[presidioEntityType] || 'identity';
136
+ var _a, _b;
137
+ const label = getRedactorLabel(presidioEntityType);
138
+ const builtIn = patterns_1.PII_PATTERNS.find((p) => p.label === label);
139
+ return (_b = (_a = builtIn === null || builtIn === void 0 ? void 0 : builtIn.category) !== null && _a !== void 0 ? _a : PRESIDIO_TO_CATEGORY[presidioEntityType]) !== null && _b !== void 0 ? _b : 'identity';
136
140
  }
137
141
  async function getPresidioEntities(baseUrl, request) {
138
142
  if (!request || !isHttpUrl(baseUrl))
@@ -4,7 +4,7 @@ exports.checkRegex = checkRegex;
4
4
  const regexCharset_1 = require("./regexCharset");
5
5
  const MAX_REGEX_LENGTH = 500;
6
6
  const newFrame = (zeroWidth = false, inLook = zeroWidth) => ({
7
- hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
7
+ hasVariableQuantifier: false, hasAlternation: false, run: null, altRun: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
8
8
  branchConsumes: false, allConsume: true, nullable: false, inLook,
9
9
  });
10
10
  function endBranch(frame) {
@@ -58,6 +58,7 @@ function alternation(stack, i) {
58
58
  const top = stack[stack.length - 1];
59
59
  top.hasAlternation = true;
60
60
  top.run = null;
61
+ top.altRun = null;
61
62
  endBranch(top);
62
63
  return { atomEnd: i, closed: null, next: i + 1 };
63
64
  }
@@ -103,6 +104,7 @@ function applyQuantifier(q, closed, top) {
103
104
  return null;
104
105
  }
105
106
  const SEQUENTIAL = 'two repetitions in a row can match the same characters (catastrophic backtracking)';
107
+ const CHAINED = 'two groups of alternatives in a row can match the same characters (catastrophic backtracking)';
106
108
  function applySequence(chars, variable, required, top) {
107
109
  if (chars === null)
108
110
  return null;
@@ -117,6 +119,19 @@ function applySequence(chars, variable, required, top) {
117
119
  top.run = null;
118
120
  return null;
119
121
  }
122
+ function applyBranching(chars, branching, required, top) {
123
+ if (chars === null)
124
+ return null;
125
+ const shared = top.altRun !== null && (0, regexCharset_1.overlaps)(top.altRun, chars);
126
+ if (branching) {
127
+ if (shared)
128
+ return CHAINED;
129
+ top.altRun = top.altRun ? (0, regexCharset_1.union)(top.altRun, chars) : chars;
130
+ }
131
+ else if (required && !shared)
132
+ top.altRun = null;
133
+ return null;
134
+ }
120
135
  function stepChars(src, i, step) {
121
136
  const closed = step.closed;
122
137
  if (!closed)
@@ -140,8 +155,10 @@ function stepProblem(src, i, step, q, top) {
140
155
  const required = consumes(q, chars, step.closed);
141
156
  if (required)
142
157
  top.branchConsumes = true;
143
- return applySequence(chars, isVariable(q) || !!((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier), required, top);
158
+ return (_a = applyBranching(chars, isBranching(step.closed), required, top)) !== null && _a !== void 0 ? _a : applySequence(chars, variesInLength(q, step.closed), required, top);
144
159
  }
160
+ const isBranching = (closed) => !!(closed === null || closed === void 0 ? void 0 : closed.hasAlternation) && !closed.zeroWidth;
161
+ const variesInLength = (q, closed) => isVariable(q) || !!(closed === null || closed === void 0 ? void 0 : closed.hasVariableQuantifier);
145
162
  function structuralProblem(src) {
146
163
  const stack = [newFrame()];
147
164
  let i = 0;
@@ -106,20 +106,26 @@ class MemoryVault {
106
106
  return;
107
107
  assertNoConflict(session, entry);
108
108
  session.entries[entry.token] = entry;
109
- if (!reverseIndex.has(key))
110
- reverseIndex.set(key, new Map());
111
- reverseIndex.get(key).set(entry.original, entry);
109
+ const index = this.index(key, session);
110
+ const first = index.get(entry.original);
111
+ if (!first || first.token === entry.token)
112
+ index.set(entry.original, entry);
112
113
  }
113
114
  findByOriginal(sessionId, original) {
114
- var _a;
115
115
  const key = this.key(sessionId);
116
- const entry = (_a = reverseIndex.get(key)) === null || _a === void 0 ? void 0 : _a.get(original);
117
- if (entry)
118
- return entry;
119
116
  const session = memoryStore.get(key);
120
- if (!session)
121
- return undefined;
122
- return Object.values(session.entries).find((e) => e.original === original);
117
+ return session ? this.index(key, session).get(original) : undefined;
118
+ }
119
+ index(key, session) {
120
+ let index = reverseIndex.get(key);
121
+ if (!index) {
122
+ index = new Map();
123
+ for (const e of Object.values(session.entries))
124
+ if (!index.has(e.original))
125
+ index.set(e.original, e);
126
+ reverseIndex.set(key, index);
127
+ }
128
+ return index;
123
129
  }
124
130
  deleteSession(sessionId) {
125
131
  this.drop(this.key(sessionId));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Redact, mask and anonymize PII before it reaches an LLM, then restore it. 216 patterns for emails, phones, addresses, IBAN, cards, names, SSN, passport and tax IDs. Reversible encrypted vault, GDPR Art.30 ROPA record, prompt-injection check. Runs on n8n Cloud and self-hosted n8n 2.x.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",