n8n-nodes-redactor 4.3.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1040,7 +1040,7 @@ class Redactor {
|
|
|
1040
1040
|
reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
|
|
1041
1041
|
keyOrigins: new WeakMap(),
|
|
1042
1042
|
inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
|
|
1043
|
-
lockedSpans: new
|
|
1043
|
+
lockedSpans: new WeakMap(),
|
|
1044
1044
|
};
|
|
1045
1045
|
const allHits = [];
|
|
1046
1046
|
const returnData = [];
|
|
@@ -57,7 +57,7 @@ function isAllowListed(value, allowList) {
|
|
|
57
57
|
case 'contains':
|
|
58
58
|
return value.includes(entry.value);
|
|
59
59
|
case 'regex':
|
|
60
|
-
if (!isSafeRegex(entry.value))
|
|
60
|
+
if (!isSafeRegex(entry.value) || value.length > USER_REGEX_MAX_INPUT)
|
|
61
61
|
return false;
|
|
62
62
|
return new RegExp(entry.value, 'i').test(value);
|
|
63
63
|
default:
|
|
@@ -70,7 +70,11 @@ function isAllowListed(value, allowList) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
function generateToken(label, index) {
|
|
73
|
-
|
|
73
|
+
const safe = label.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
|
|
74
|
+
return `[${/^[A-Z]/.test(safe) ? safe : `X${safe}`}_${index}]`;
|
|
75
|
+
}
|
|
76
|
+
function setOwn(obj, key, value) {
|
|
77
|
+
Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
|
|
74
78
|
}
|
|
75
79
|
const TOKEN_RE = /^\[[A-Z][A-Z0-9_]*_\d+\]$/;
|
|
76
80
|
function maskValue(value, label) {
|
|
@@ -414,7 +418,7 @@ function getSemanticLabel(fieldKey, siblingKeys) {
|
|
|
414
418
|
return null;
|
|
415
419
|
}
|
|
416
420
|
function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
417
|
-
var _a
|
|
421
|
+
var _a;
|
|
418
422
|
const semantic = getSemanticLabel(path, siblingKeys);
|
|
419
423
|
if (!semantic || !semantic.label || raw.trim().length === 0)
|
|
420
424
|
return null;
|
|
@@ -424,8 +428,7 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
424
428
|
if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
|
|
425
429
|
return null;
|
|
426
430
|
const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx, vault, sessionId);
|
|
427
|
-
|
|
428
|
-
(_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.set(lockKey(itemIndex, display), [[0, replacement.length]]);
|
|
431
|
+
ctx.lastLocked = replacement ? [[0, replacement.length]] : undefined;
|
|
429
432
|
hits.push({
|
|
430
433
|
token: replacement, original: '***',
|
|
431
434
|
patternName: `semantic_${semantic.label}`,
|
|
@@ -444,14 +447,13 @@ function numericMapping(out, text, ctx, vault, sessionId) {
|
|
|
444
447
|
existing.numeric = true;
|
|
445
448
|
return out;
|
|
446
449
|
}
|
|
447
|
-
const token =
|
|
450
|
+
const token = freshToken('NUMBER', vault, sessionId, ctx.reservedTokens);
|
|
448
451
|
vault.addEntry(sessionId, {
|
|
449
452
|
token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
|
|
450
453
|
});
|
|
451
454
|
return token;
|
|
452
455
|
}
|
|
453
456
|
function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
454
|
-
var _a;
|
|
455
457
|
const text = String(value);
|
|
456
458
|
const numCtx = { ...ctx, dedup: false, numeric: true };
|
|
457
459
|
let out = null;
|
|
@@ -461,10 +463,12 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
461
463
|
out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
|
|
462
464
|
if (out === null || out === text)
|
|
463
465
|
return value;
|
|
464
|
-
if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize')
|
|
466
|
+
if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
|
|
467
|
+
ctx.lastLocked = numCtx.lastLocked;
|
|
465
468
|
return out;
|
|
469
|
+
}
|
|
466
470
|
const mapped = numericMapping(out, text, ctx, vault, sessionId);
|
|
467
|
-
|
|
471
|
+
ctx.lastLocked = [[0, mapped.length]];
|
|
468
472
|
return mapped;
|
|
469
473
|
}
|
|
470
474
|
function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
|
|
@@ -472,7 +476,7 @@ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, key
|
|
|
472
476
|
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
473
477
|
if (out === key)
|
|
474
478
|
return key;
|
|
475
|
-
const clashes = (k) => k === '' || k
|
|
479
|
+
const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
|
|
476
480
|
if (!clashes(out))
|
|
477
481
|
return out;
|
|
478
482
|
if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
|
|
@@ -502,7 +506,7 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
502
506
|
const childDisplay = display ? `${display}.${outKey}` : outKey;
|
|
503
507
|
for (const h of keyHits)
|
|
504
508
|
hits.push({ ...h, field: childDisplay });
|
|
505
|
-
obj
|
|
509
|
+
setOwn(obj, outKey, lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay)));
|
|
506
510
|
}
|
|
507
511
|
(_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
|
|
508
512
|
return obj;
|
|
@@ -525,7 +529,11 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
|
|
|
525
529
|
return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
|
|
526
530
|
}
|
|
527
531
|
if (Array.isArray(value)) {
|
|
528
|
-
|
|
532
|
+
const out = [];
|
|
533
|
+
value.forEach((v, i) => {
|
|
534
|
+
out.push(lockChild(ctx, out, String(i), () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`)));
|
|
535
|
+
});
|
|
536
|
+
return out;
|
|
529
537
|
}
|
|
530
538
|
if (value !== null && typeof value === 'object') {
|
|
531
539
|
return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
|
|
@@ -533,49 +541,40 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
|
|
|
533
541
|
return value;
|
|
534
542
|
}
|
|
535
543
|
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
536
|
-
const
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const end = cut > at + size / 2 ? cut + 1 : at + size;
|
|
543
|
-
out.push({ text: seg.text.slice(at, end), locked: false, src: seg.src + at });
|
|
544
|
-
at = end;
|
|
545
|
-
}
|
|
546
|
-
out.push({ text: seg.text.slice(at), locked: false, src: seg.src + at });
|
|
547
|
-
return out;
|
|
544
|
+
const USER_REGEX_MAX_INPUT = 20000;
|
|
545
|
+
function assertUserRegexInput(text, what) {
|
|
546
|
+
if (text.length <= USER_REGEX_MAX_INPUT)
|
|
547
|
+
return;
|
|
548
|
+
throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
|
|
549
|
+
'Exclude the field with field rules, or remove the regex.');
|
|
548
550
|
}
|
|
549
|
-
function
|
|
550
|
-
|
|
551
|
-
let last = 0;
|
|
552
|
-
for (const m of seg.text.matchAll(re)) {
|
|
553
|
-
const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
554
|
-
if (m[0] === '')
|
|
555
|
-
continue;
|
|
556
|
-
const replacement = fn(m[0], seg.src + at);
|
|
557
|
-
if (replacement === null)
|
|
558
|
-
continue;
|
|
559
|
-
if (at > last)
|
|
560
|
-
into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
|
|
561
|
-
into.push({ text: replacement, locked: true, src: -1 });
|
|
562
|
-
last = at + m[0].length;
|
|
563
|
-
}
|
|
564
|
-
if (last < seg.text.length)
|
|
565
|
-
into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
|
|
551
|
+
function freeSegmentAt(segs, start, end) {
|
|
552
|
+
return segs.findIndex((seg) => !seg.locked && seg.src <= start && end <= seg.src + seg.text.length);
|
|
566
553
|
}
|
|
567
|
-
function
|
|
554
|
+
function lockRange(segs, index, start, end, replacement) {
|
|
555
|
+
const seg = segs[index];
|
|
556
|
+
const parts = [];
|
|
557
|
+
if (start > seg.src)
|
|
558
|
+
parts.push({ text: seg.text.slice(0, start - seg.src), locked: false, src: seg.src });
|
|
559
|
+
parts.push({ text: replacement, locked: true, src: -1 });
|
|
560
|
+
const tail = end - seg.src;
|
|
561
|
+
if (tail < seg.text.length)
|
|
562
|
+
parts.push({ text: seg.text.slice(tail), locked: false, src: end });
|
|
563
|
+
segs.splice(index, 1, ...parts);
|
|
564
|
+
}
|
|
565
|
+
function replaceFree(pass, re, fn) {
|
|
566
|
+
var _a;
|
|
568
567
|
const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
568
|
+
for (const m of pass.source.matchAll(new RegExp(re.source, flags))) {
|
|
569
|
+
const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
570
|
+
const end = start + m[0].length;
|
|
571
|
+
const index = m[0] === '' ? -1 : freeSegmentAt(pass.segs, start, end);
|
|
572
|
+
if (index < 0)
|
|
573
|
+
continue;
|
|
574
|
+
const replacement = fn(m[0], start);
|
|
575
|
+
if (replacement !== null)
|
|
576
|
+
lockRange(pass.segs, index, start, end, replacement);
|
|
577
577
|
}
|
|
578
|
-
pass.segs = next;
|
|
579
578
|
}
|
|
580
579
|
function redactMatch(pass, match, pattern, detail) {
|
|
581
580
|
const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
|
|
@@ -590,16 +589,17 @@ function denyListRegex(entry) {
|
|
|
590
589
|
function denyListStage(pass) {
|
|
591
590
|
var _a;
|
|
592
591
|
for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
|
|
592
|
+
if (entry.type === 'regex')
|
|
593
|
+
assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
|
|
593
594
|
try {
|
|
594
595
|
const re = denyListRegex(entry);
|
|
595
596
|
if (!re)
|
|
596
597
|
continue;
|
|
597
|
-
const window = entry.type === 'regex' ? USER_REGEX_WINDOW : undefined;
|
|
598
598
|
replaceFree(pass, re, (match) => TOKEN_RE.test(match)
|
|
599
599
|
? null
|
|
600
600
|
: redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
|
|
601
601
|
patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
602
|
-
})
|
|
602
|
+
}));
|
|
603
603
|
}
|
|
604
604
|
catch { }
|
|
605
605
|
}
|
|
@@ -623,28 +623,21 @@ function builtInStage(pass) {
|
|
|
623
623
|
replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
|
-
function
|
|
627
|
-
const names = new
|
|
628
|
-
|
|
629
|
-
if (seg.locked)
|
|
630
|
-
continue;
|
|
631
|
-
for (const nm of (0, names_1.detectNamesInText)(seg.text))
|
|
632
|
-
if (!names.has(nm.name))
|
|
633
|
-
names.set(nm.name, seg.src + nm.start);
|
|
634
|
-
}
|
|
635
|
-
return names;
|
|
626
|
+
function sourceNames(pass) {
|
|
627
|
+
const names = new Set((0, names_1.detectNamesInText)(pass.source).map((nm) => nm.name));
|
|
628
|
+
return [...names].sort((x, y) => y.length - x.length);
|
|
636
629
|
}
|
|
637
630
|
function nameStage(pass) {
|
|
638
631
|
if (pass.ctx.skipSemantic)
|
|
639
632
|
return;
|
|
640
|
-
for (const
|
|
633
|
+
for (const name of sourceNames(pass)) {
|
|
641
634
|
if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
|
|
642
635
|
continue;
|
|
643
|
-
const confidence = (0, context_1.hasContextWords)(pass.source, start, start + name.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
|
|
644
|
-
if (confidence < pass.threshold)
|
|
645
|
-
continue;
|
|
646
636
|
let replacement;
|
|
647
|
-
replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match) => {
|
|
637
|
+
replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match, start) => {
|
|
638
|
+
const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
|
|
639
|
+
if (confidence < pass.threshold)
|
|
640
|
+
return null;
|
|
648
641
|
replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
|
|
649
642
|
patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
|
|
650
643
|
}));
|
|
@@ -659,20 +652,19 @@ function customStage(pass) {
|
|
|
659
652
|
if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
|
|
660
653
|
continue;
|
|
661
654
|
const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
|
|
655
|
+
assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
|
|
662
656
|
try {
|
|
663
657
|
replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
|
|
664
658
|
? null
|
|
665
659
|
: redactMatch(pass, match, pattern, {
|
|
666
660
|
patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
|
|
667
661
|
confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
|
|
668
|
-
})
|
|
662
|
+
}));
|
|
669
663
|
}
|
|
670
664
|
catch { }
|
|
671
665
|
}
|
|
672
666
|
}
|
|
673
|
-
function recordLocked(ctx,
|
|
674
|
-
if (!ctx.lockedSpans || fieldPath === '')
|
|
675
|
-
return;
|
|
667
|
+
function recordLocked(ctx, segs) {
|
|
676
668
|
const spans = [];
|
|
677
669
|
let at = 0;
|
|
678
670
|
for (const seg of segs) {
|
|
@@ -680,10 +672,20 @@ function recordLocked(ctx, itemIndex, fieldPath, segs) {
|
|
|
680
672
|
spans.push([at, at + seg.text.length]);
|
|
681
673
|
at += seg.text.length;
|
|
682
674
|
}
|
|
683
|
-
|
|
684
|
-
|
|
675
|
+
ctx.lastLocked = spans.length > 0 ? spans : undefined;
|
|
676
|
+
}
|
|
677
|
+
function lockChild(ctx, container, key, compute) {
|
|
678
|
+
var _a;
|
|
679
|
+
ctx.lastLocked = undefined;
|
|
680
|
+
const out = compute();
|
|
681
|
+
if (ctx.lastLocked && ctx.lockedSpans && typeof out === 'string') {
|
|
682
|
+
const byKey = (_a = ctx.lockedSpans.get(container)) !== null && _a !== void 0 ? _a : new Map();
|
|
683
|
+
byKey.set(key, ctx.lastLocked);
|
|
684
|
+
ctx.lockedSpans.set(container, byKey);
|
|
685
|
+
}
|
|
686
|
+
ctx.lastLocked = undefined;
|
|
687
|
+
return out;
|
|
685
688
|
}
|
|
686
|
-
const lockKey = (itemIndex, fieldPath) => `${itemIndex}\u0000${fieldPath}`;
|
|
687
689
|
function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
|
|
688
690
|
var _a;
|
|
689
691
|
const pass = {
|
|
@@ -694,7 +696,7 @@ function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
|
|
|
694
696
|
builtInStage(pass);
|
|
695
697
|
nameStage(pass);
|
|
696
698
|
customStage(pass);
|
|
697
|
-
recordLocked(ctx,
|
|
699
|
+
recordLocked(ctx, pass.segs);
|
|
698
700
|
return pass.segs.map((s) => s.text).join('');
|
|
699
701
|
}
|
|
700
702
|
function makeRestorer(entries) {
|
|
@@ -716,7 +718,7 @@ function restoreWith(value, restore, entries) {
|
|
|
716
718
|
if (value !== null && typeof value === 'object') {
|
|
717
719
|
const obj = {};
|
|
718
720
|
for (const [k, v] of Object.entries(value)) {
|
|
719
|
-
obj
|
|
721
|
+
setOwn(obj, restore(k), restoreWith(v, restore, entries));
|
|
720
722
|
}
|
|
721
723
|
return obj;
|
|
722
724
|
}
|
|
@@ -745,18 +747,17 @@ function resolveOverlaps(spans) {
|
|
|
745
747
|
}
|
|
746
748
|
return kept.sort((a, b) => b.start - a.start);
|
|
747
749
|
}
|
|
748
|
-
function presidioSpans(value, entities, ctx,
|
|
749
|
-
var _a
|
|
750
|
+
function presidioSpans(value, entities, ctx, locked) {
|
|
751
|
+
var _a;
|
|
750
752
|
const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
|
|
751
|
-
const locked = (_c = (_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
|
|
752
753
|
const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
|
|
753
754
|
return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
|
|
754
755
|
}
|
|
755
|
-
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
|
|
756
|
+
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
|
|
756
757
|
if (value.trim().length === 0 || TOKEN_RE.test(value))
|
|
757
758
|
return value;
|
|
758
759
|
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
759
|
-
const spans = presidioSpans(value, entities, ctx,
|
|
760
|
+
const spans = presidioSpans(value, entities, ctx, locked);
|
|
760
761
|
let text = value;
|
|
761
762
|
for (const span of spans) {
|
|
762
763
|
const matchedText = value.slice(span.start, span.end);
|
|
@@ -776,13 +777,13 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
|
|
|
776
777
|
}
|
|
777
778
|
return text;
|
|
778
779
|
}
|
|
779
|
-
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
780
|
-
var _a, _b;
|
|
780
|
+
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
|
|
781
|
+
var _a, _b, _c, _d, _e, _f;
|
|
781
782
|
if (typeof value === 'string') {
|
|
782
783
|
if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
|
|
783
784
|
return value;
|
|
784
785
|
try {
|
|
785
|
-
return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
|
|
786
|
+
return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked);
|
|
786
787
|
}
|
|
787
788
|
catch {
|
|
788
789
|
return value;
|
|
@@ -791,18 +792,20 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
|
|
|
791
792
|
if (Array.isArray(value)) {
|
|
792
793
|
const out = [];
|
|
793
794
|
for (let i = 0; i < value.length; i++) {
|
|
794
|
-
|
|
795
|
+
const lock = (_b = (_a = ctx.lockedSpans) === null || _a === void 0 ? void 0 : _a.get(value)) === null || _b === void 0 ? void 0 : _b.get(String(i));
|
|
796
|
+
out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`, lock));
|
|
795
797
|
}
|
|
796
798
|
return out;
|
|
797
799
|
}
|
|
798
800
|
if (value !== null && typeof value === 'object') {
|
|
799
|
-
const origins = (
|
|
801
|
+
const origins = (_c = ctx.keyOrigins) === null || _c === void 0 ? void 0 : _c.get(value);
|
|
800
802
|
const out = {};
|
|
801
803
|
for (const [k, v] of Object.entries(value)) {
|
|
802
|
-
const original = (
|
|
804
|
+
const original = (_d = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _d !== void 0 ? _d : k;
|
|
803
805
|
const child = path ? `${path}.${k}` : k;
|
|
804
806
|
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
805
|
-
|
|
807
|
+
const lock = (_f = (_e = ctx.lockedSpans) === null || _e === void 0 ? void 0 : _e.get(value)) === null || _f === void 0 ? void 0 : _f.get(k);
|
|
808
|
+
setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
|
|
806
809
|
}
|
|
807
810
|
return out;
|
|
808
811
|
}
|
|
@@ -3,19 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.emptySet = void 0;
|
|
4
4
|
exports.union = union;
|
|
5
5
|
exports.overlaps = overlaps;
|
|
6
|
+
exports.readEscape = readEscape;
|
|
6
7
|
exports.atomCharset = atomCharset;
|
|
7
8
|
const emptySet = () => ({ ascii: new Array(128).fill(false), other: false });
|
|
8
9
|
exports.emptySet = emptySet;
|
|
9
10
|
const fullSet = () => ({ ascii: new Array(128).fill(true), other: true });
|
|
10
11
|
function addCode(set, code) {
|
|
11
|
-
if (code >= 128)
|
|
12
|
+
if (code >= 128)
|
|
12
13
|
set.other = true;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
set.ascii[code] = true;
|
|
16
|
-
const ch = String.fromCharCode(code);
|
|
17
|
-
for (const folded of [ch.toLowerCase(), ch.toUpperCase()])
|
|
18
|
-
set.ascii[folded.charCodeAt(0)] = true;
|
|
14
|
+
else
|
|
15
|
+
set.ascii[code] = true;
|
|
19
16
|
}
|
|
20
17
|
function addRange(set, from, to) {
|
|
21
18
|
for (let c = from; c <= Math.min(to, 127); c++)
|
|
@@ -32,11 +29,18 @@ function overlaps(a, b) {
|
|
|
32
29
|
function complement(set) {
|
|
33
30
|
return { ascii: set.ascii.map((v) => !v), other: true };
|
|
34
31
|
}
|
|
32
|
+
function fold(set) {
|
|
33
|
+
const ascii = [...set.ascii];
|
|
34
|
+
for (let c = 65; c <= 90; c++)
|
|
35
|
+
ascii[c] = ascii[c + 32] = set.ascii[c] || set.ascii[c + 32];
|
|
36
|
+
return { ascii, other: set.other };
|
|
37
|
+
}
|
|
35
38
|
const CLASS_ESCAPES = {
|
|
36
39
|
d: (s) => addRange(s, 48, 57),
|
|
37
40
|
w: (s) => {
|
|
38
41
|
addRange(s, 48, 57);
|
|
39
42
|
addRange(s, 65, 90);
|
|
43
|
+
addRange(s, 97, 122);
|
|
40
44
|
addCode(s, 95);
|
|
41
45
|
},
|
|
42
46
|
s: (s) => {
|
|
@@ -45,26 +49,53 @@ const CLASS_ESCAPES = {
|
|
|
45
49
|
s.other = true;
|
|
46
50
|
},
|
|
47
51
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
52
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
53
|
+
const CONTROL = { n: 10, r: 13, t: 9, v: 11, f: 12 };
|
|
54
|
+
function readEscape(src, i) {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
const e = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
|
|
57
|
+
const hex = (n) => (HEX.test(src.slice(i + 2, i + 2 + n)) && src.length >= i + 2 + n ? n : 0);
|
|
58
|
+
if (e === 'x' && hex(2))
|
|
59
|
+
return { length: 4, code: parseInt(src.slice(i + 2, i + 4), 16) };
|
|
60
|
+
if (e === 'u' && hex(4))
|
|
61
|
+
return { length: 6, code: parseInt(src.slice(i + 2, i + 6), 16) };
|
|
62
|
+
if (e === 'c' && /[A-Za-z]/.test((_b = src[i + 2]) !== null && _b !== void 0 ? _b : ''))
|
|
63
|
+
return { length: 3, code: src.charCodeAt(i + 2) % 32 };
|
|
64
|
+
if (e === '0')
|
|
65
|
+
return { length: 2, code: 0 };
|
|
66
|
+
const control = CONTROL[e];
|
|
67
|
+
if (control !== undefined)
|
|
68
|
+
return { length: 2, code: control };
|
|
69
|
+
if (/[a-zA-Z0-9]/.test(e))
|
|
70
|
+
return { length: 2, code: null };
|
|
71
|
+
return { length: 2, code: e.charCodeAt(0) };
|
|
72
|
+
}
|
|
73
|
+
function codeSet(code) {
|
|
58
74
|
const set = (0, exports.emptySet)();
|
|
59
|
-
addCode(set,
|
|
75
|
+
addCode(set, code);
|
|
60
76
|
return set;
|
|
61
77
|
}
|
|
78
|
+
function escapeSet(src, i) {
|
|
79
|
+
var _a;
|
|
80
|
+
const { code } = readEscape(src, i);
|
|
81
|
+
const set = (0, exports.emptySet)();
|
|
82
|
+
if (code !== null) {
|
|
83
|
+
addCode(set, code);
|
|
84
|
+
return set;
|
|
85
|
+
}
|
|
86
|
+
const letter = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
|
|
87
|
+
const fill = CLASS_ESCAPES[letter.toLowerCase()];
|
|
88
|
+
if (!fill)
|
|
89
|
+
return fullSet();
|
|
90
|
+
fill(set);
|
|
91
|
+
return letter === letter.toLowerCase() ? set : complement(set);
|
|
92
|
+
}
|
|
62
93
|
function classMember(body, j) {
|
|
63
|
-
|
|
94
|
+
if (body[j] === '\\' && body[j + 1] === 'b')
|
|
95
|
+
return { set: codeSet(8), next: j + 2, code: 8 };
|
|
64
96
|
if (body[j] === '\\') {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
return { set, next: j + 2, code: single };
|
|
97
|
+
const { length, code } = readEscape(body, j);
|
|
98
|
+
return { set: escapeSet(body, j), next: j + length, code };
|
|
68
99
|
}
|
|
69
100
|
const set = (0, exports.emptySet)();
|
|
70
101
|
addCode(set, body.charCodeAt(j));
|
|
@@ -90,13 +121,13 @@ function classSet(body) {
|
|
|
90
121
|
}
|
|
91
122
|
return negated ? complement(set) : set;
|
|
92
123
|
}
|
|
93
|
-
function
|
|
94
|
-
var _a
|
|
124
|
+
function rawCharset(src, start, end) {
|
|
125
|
+
var _a;
|
|
95
126
|
const c = src[start];
|
|
96
127
|
if (c === '^' || c === '$')
|
|
97
128
|
return null;
|
|
98
129
|
if (c === '\\')
|
|
99
|
-
return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet(
|
|
130
|
+
return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet(src, start);
|
|
100
131
|
if (c === '[')
|
|
101
132
|
return classSet(src.slice(start + 1, end - 1));
|
|
102
133
|
if (c === '.')
|
|
@@ -105,3 +136,7 @@ function atomCharset(src, start, end) {
|
|
|
105
136
|
addCode(set, src.charCodeAt(start));
|
|
106
137
|
return set;
|
|
107
138
|
}
|
|
139
|
+
function atomCharset(src, start, end) {
|
|
140
|
+
const set = rawCharset(src, start, end);
|
|
141
|
+
return set && fold(set);
|
|
142
|
+
}
|
|
@@ -5,7 +5,13 @@ const regexCharset_1 = require("./regexCharset");
|
|
|
5
5
|
const MAX_REGEX_LENGTH = 500;
|
|
6
6
|
const newFrame = (zeroWidth = false) => ({
|
|
7
7
|
hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
|
|
8
|
+
branchConsumes: false, allConsume: true, nullable: false,
|
|
8
9
|
});
|
|
10
|
+
function endBranch(frame) {
|
|
11
|
+
frame.allConsume && (frame.allConsume = frame.branchConsumes);
|
|
12
|
+
frame.branchConsumes = false;
|
|
13
|
+
frame.nullable = !frame.allConsume;
|
|
14
|
+
}
|
|
9
15
|
function readBraces(src, i) {
|
|
10
16
|
const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
|
|
11
17
|
if (!m)
|
|
@@ -48,13 +54,27 @@ function groupPrefixLength(src, i) {
|
|
|
48
54
|
const look = /^\(\?(?::|=|!|<=|<!)/.exec(src.slice(i));
|
|
49
55
|
return look ? look[0].length : 1;
|
|
50
56
|
}
|
|
57
|
+
function alternation(stack, i) {
|
|
58
|
+
const top = stack[stack.length - 1];
|
|
59
|
+
top.hasAlternation = true;
|
|
60
|
+
top.run = null;
|
|
61
|
+
endBranch(top);
|
|
62
|
+
return { atomEnd: i, closed: null, next: i + 1 };
|
|
63
|
+
}
|
|
64
|
+
function closeGroup(c, stack) {
|
|
65
|
+
if (c !== ')' || stack.length <= 1)
|
|
66
|
+
return null;
|
|
67
|
+
const closed = stack.pop();
|
|
68
|
+
endBranch(closed);
|
|
69
|
+
return closed;
|
|
70
|
+
}
|
|
51
71
|
function readToken(src, i, stack) {
|
|
52
72
|
var _a;
|
|
53
73
|
const c = src[i];
|
|
54
74
|
if (c === '\\') {
|
|
55
75
|
if (/[1-9k]/.test((_a = src[i + 1]) !== null && _a !== void 0 ? _a : ''))
|
|
56
76
|
return { atomEnd: i, closed: null, error: 'back-references are not supported' };
|
|
57
|
-
return { atomEnd: i +
|
|
77
|
+
return { atomEnd: i + (0, regexCharset_1.readEscape)(src, i).length, closed: null };
|
|
58
78
|
}
|
|
59
79
|
if (c === '[')
|
|
60
80
|
return { atomEnd: skipClass(src, i), closed: null };
|
|
@@ -62,13 +82,9 @@ function readToken(src, i, stack) {
|
|
|
62
82
|
stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
|
|
63
83
|
return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
|
|
64
84
|
}
|
|
65
|
-
if (c === '|')
|
|
66
|
-
stack
|
|
67
|
-
|
|
68
|
-
return { atomEnd: i, closed: null, next: i + 1 };
|
|
69
|
-
}
|
|
70
|
-
const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
|
|
71
|
-
return { atomEnd: i + 1, closed };
|
|
85
|
+
if (c === '|')
|
|
86
|
+
return alternation(stack, i);
|
|
87
|
+
return { atomEnd: i + 1, closed: closeGroup(c, stack) };
|
|
72
88
|
}
|
|
73
89
|
const NESTED = 'a repeated group contains a quantifier or alternation (catastrophic backtracking)';
|
|
74
90
|
function isRisky(frame) {
|
|
@@ -106,13 +122,21 @@ function stepChars(src, i, step) {
|
|
|
106
122
|
return (0, regexCharset_1.atomCharset)(src, i, step.atomEnd);
|
|
107
123
|
return closed.zeroWidth ? null : closed.chars;
|
|
108
124
|
}
|
|
125
|
+
function consumes(q, chars, closed) {
|
|
126
|
+
if (chars === null || (q !== null && q.min < 1))
|
|
127
|
+
return false;
|
|
128
|
+
return !(closed === null || closed === void 0 ? void 0 : closed.nullable);
|
|
129
|
+
}
|
|
109
130
|
function stepProblem(src, i, step, q, top) {
|
|
110
|
-
var _a
|
|
131
|
+
var _a;
|
|
111
132
|
const problem = applyQuantifier(q, step.closed, top);
|
|
112
133
|
if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
|
|
113
134
|
return problem;
|
|
114
|
-
const
|
|
115
|
-
|
|
135
|
+
const chars = stepChars(src, i, step);
|
|
136
|
+
const required = consumes(q, chars, step.closed);
|
|
137
|
+
if (required)
|
|
138
|
+
top.branchConsumes = true;
|
|
139
|
+
return applySequence(chars, isVariable(q) || !!((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier), required, top);
|
|
116
140
|
}
|
|
117
141
|
function structuralProblem(src) {
|
|
118
142
|
const stack = [newFrame()];
|
|
@@ -133,7 +157,45 @@ function structuralProblem(src) {
|
|
|
133
157
|
}
|
|
134
158
|
return null;
|
|
135
159
|
}
|
|
160
|
+
const KNOWN_ESCAPES = /^(?:[dDwWsSbBnrtvf]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|c[A-Za-z]|0(?![0-9])|[^A-Za-z0-9])/;
|
|
161
|
+
const CLASS_ESCAPE_RANGE = /\\[dDwWsS]-[^\]]|[^[^\\]-\\[dDwWsS]/;
|
|
162
|
+
const BAD_ESCAPE = 'uses an escape that is not supported (octal, back-reference or unknown letter)';
|
|
163
|
+
const EMPTY_CLASS = 'uses an empty class [] or [^], which is not supported';
|
|
164
|
+
const ESCAPE_RANGE = 'uses a range with \\d, \\w or \\s as an end, which is not supported';
|
|
165
|
+
function classProblem(src, i) {
|
|
166
|
+
if (/^\[\^?\]/.test(src.slice(i)))
|
|
167
|
+
return { end: i, problem: EMPTY_CLASS };
|
|
168
|
+
const end = skipClass(src, i);
|
|
169
|
+
const body = src.slice(i, end);
|
|
170
|
+
for (let j = 1; j < body.length; j++) {
|
|
171
|
+
if (body[j] === '\\' && !KNOWN_ESCAPES.test(body.slice(j + 1)))
|
|
172
|
+
return { end, problem: BAD_ESCAPE };
|
|
173
|
+
if (body[j] === '\\')
|
|
174
|
+
j++;
|
|
175
|
+
}
|
|
176
|
+
return { end, problem: CLASS_ESCAPE_RANGE.test(body) ? ESCAPE_RANGE : null };
|
|
177
|
+
}
|
|
178
|
+
function unsupportedSyntax(src) {
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < src.length) {
|
|
181
|
+
if (src[i] === '[') {
|
|
182
|
+
const { end, problem } = classProblem(src, i);
|
|
183
|
+
if (problem)
|
|
184
|
+
return problem;
|
|
185
|
+
i = end;
|
|
186
|
+
}
|
|
187
|
+
else if (src[i] === '\\') {
|
|
188
|
+
if (!KNOWN_ESCAPES.test(src.slice(i + 1)))
|
|
189
|
+
return BAD_ESCAPE;
|
|
190
|
+
i += 2;
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
136
197
|
function checkRegex(regexStr) {
|
|
198
|
+
var _a;
|
|
137
199
|
if (typeof regexStr !== 'string' || regexStr.length === 0)
|
|
138
200
|
return 'is empty';
|
|
139
201
|
if (regexStr.length > MAX_REGEX_LENGTH)
|
|
@@ -144,5 +206,5 @@ function checkRegex(regexStr) {
|
|
|
144
206
|
catch {
|
|
145
207
|
return 'is not a valid regular expression';
|
|
146
208
|
}
|
|
147
|
-
return structuralProblem(regexStr);
|
|
209
|
+
return (_a = unsupportedSyntax(regexStr)) !== null && _a !== void 0 ? _a : structuralProblem(regexStr);
|
|
148
210
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-redactor",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Redact, mask and anonymize PII before it reaches an LLM, then restore it. 216 patterns for emails, phones, addresses, IBAN, cards, names, SSN, passport and tax IDs. Reversible encrypted vault, GDPR Art.30 ROPA record, prompt-injection check. Runs on n8n Cloud and self-hosted n8n 2.x.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|