n8n-nodes-redactor 4.1.0 → 4.3.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.
@@ -1036,6 +1036,11 @@ class Redactor {
1036
1036
  presidioUrl: enablePresidio ? presidioUrl : undefined,
1037
1037
  presidioLanguage: enablePresidio ? presidioLanguage : undefined,
1038
1038
  presidioRequest: enablePresidio ? presidioRequester : undefined,
1039
+ scanNumbers: true,
1040
+ reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
1041
+ keyOrigins: new WeakMap(),
1042
+ inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1043
+ lockedSpans: new Map(),
1039
1044
  };
1040
1045
  const allHits = [];
1041
1046
  const returnData = [];
@@ -1118,48 +1123,27 @@ class Redactor {
1118
1123
  if (!purgeSessionId) {
1119
1124
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
1120
1125
  }
1121
- const session = vault.getSession(purgeSessionId);
1122
- if (session) {
1123
- const entryCount = Object.keys(session.entries).length;
1124
- vault.deleteSession(purgeSessionId);
1125
- return [[{
1126
- json: {
1127
- purged: true,
1128
- sessionId: purgeSessionId,
1129
- entriesDeleted: entryCount,
1130
- message: `Session purged successfully.`,
1131
- },
1132
- pairedItem: allPaired,
1133
- }]];
1134
- }
1135
- else {
1136
- return [[{
1137
- json: {
1138
- purged: false,
1139
- sessionId: purgeSessionId,
1140
- message: `No session found with this ID.`,
1141
- },
1142
- pairedItem: allPaired,
1143
- }]];
1144
- }
1145
- }
1146
- else {
1147
- const sessions = vault.listSessions();
1148
- let totalEntries = 0;
1149
- for (const s of sessions) {
1150
- totalEntries += s.entryCount;
1151
- vault.deleteSession(s.sessionId);
1152
- }
1126
+ const purged = vault.purgeSession(purgeSessionId);
1153
1127
  return [[{
1154
1128
  json: {
1155
- purged: true,
1156
- sessionsPurged: sessions.length,
1157
- totalEntriesDeleted: totalEntries,
1158
- message: `All ${sessions.length} session(s) purged successfully.`,
1129
+ purged: purged.found,
1130
+ sessionId: purgeSessionId,
1131
+ ...(purged.found ? { entriesDeleted: purged.entries } : {}),
1132
+ message: purged.found ? 'Session purged successfully.' : 'No session found with this ID.',
1159
1133
  },
1160
1134
  pairedItem: allPaired,
1161
1135
  }]];
1162
1136
  }
1137
+ const all = vault.purgeAll();
1138
+ return [[{
1139
+ json: {
1140
+ purged: true,
1141
+ sessionsPurged: all.sessions,
1142
+ totalEntriesDeleted: all.entries,
1143
+ message: `All ${all.sessions} session(s) purged successfully.`,
1144
+ },
1145
+ pairedItem: allPaired,
1146
+ }]];
1163
1147
  }
1164
1148
  return [[]];
1165
1149
  }
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.collectTokenLike = collectTokenLike;
37
+ exports.collectInputText = collectInputText;
36
38
  exports.redactValue = redactValue;
37
39
  exports.restoreValue = restoreValue;
38
40
  exports.enhanceWithPresidio = enhanceWithPresidio;
@@ -96,10 +98,10 @@ function maskValue(value, label) {
96
98
  function hashValue(value) {
97
99
  return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
98
100
  }
99
- function storeReversible(match, pattern, vault, sessionId, dedup, makeToken) {
100
- if (dedup) {
101
+ function storeReversible(match, pattern, ctx, vault, sessionId, makeToken) {
102
+ if (ctx.dedup) {
101
103
  const existing = vault.findByOriginal(sessionId, match);
102
- if (existing)
104
+ if (existing && !existing.numeric)
103
105
  return existing.token;
104
106
  }
105
107
  const token = makeToken();
@@ -119,30 +121,66 @@ const IRREVERSIBLE = {
119
121
  blackout: (match) => '\u2588'.repeat(match.length),
120
122
  remove: () => '',
121
123
  };
122
- function applyRedaction(match, pattern, mode, vault, sessionId, dedup) {
123
- if (mode === 'token') {
124
- return storeReversible(match, pattern, vault, sessionId, dedup, () => {
125
- const session = vault.getSession(sessionId);
126
- return generateToken(pattern.label, session ? Object.keys(session.entries).length : 0);
127
- });
124
+ function applyRedaction(match, pattern, ctx, vault, sessionId) {
125
+ if (ctx.mode === 'token') {
126
+ return storeReversible(match, pattern, ctx, vault, sessionId, () => freshToken(pattern.label, vault, sessionId, ctx.reservedTokens));
128
127
  }
129
- if (mode === 'pseudonymize') {
130
- return storeReversible(match, pattern, vault, sessionId, dedup, () => uniquePseudonym(vault, sessionId, match, pattern.label));
128
+ if (ctx.mode === 'pseudonymize') {
129
+ return storeReversible(match, pattern, ctx, vault, sessionId, () => uniquePseudonym(vault, sessionId, match, pattern.label, ctx));
131
130
  }
132
- const irreversible = IRREVERSIBLE[mode];
131
+ const irreversible = IRREVERSIBLE[ctx.mode];
133
132
  return irreversible ? irreversible(match, pattern.label) : match;
134
133
  }
135
- function uniquePseudonym(vault, sessionId, original, label) {
134
+ function freshToken(label, vault, sessionId, reserved) {
136
135
  var _a, _b;
137
136
  const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
137
+ let index = Object.keys(entries).length;
138
+ let token = generateToken(label, index);
139
+ while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
140
+ token = generateToken(label, ++index);
141
+ return token;
142
+ }
143
+ const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
144
+ function walkText(value, visit) {
145
+ if (typeof value === 'string' || typeof value === 'number')
146
+ visit(String(value));
147
+ else if (Array.isArray(value))
148
+ value.forEach((v) => walkText(v, visit));
149
+ else if (value !== null && typeof value === 'object') {
150
+ for (const [k, v] of Object.entries(value)) {
151
+ visit(k);
152
+ walkText(v, visit);
153
+ }
154
+ }
155
+ }
156
+ function collectTokenLike(value, into = new Set()) {
157
+ walkText(value, (text) => {
158
+ for (const m of text.matchAll(TOKEN_LIKE_RE))
159
+ into.add(m[0]);
160
+ });
161
+ return into;
162
+ }
163
+ function collectInputText(value) {
164
+ const parts = [];
165
+ walkText(value, (text) => parts.push(text));
166
+ return parts.join('\u0000');
167
+ }
168
+ function reusable(taken, original, ctx) {
169
+ return !!taken && taken.original === original && !!taken.numeric === !!ctx.numeric;
170
+ }
171
+ function uniquePseudonym(vault, sessionId, original, label, ctx) {
172
+ var _a, _b, _c;
173
+ const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
138
174
  for (let attempt = 0; attempt < 32; attempt++) {
139
175
  const seed = attempt === 0 ? original : `${original}\u0000${attempt}`;
140
176
  const candidate = (0, pseudonymize_1.generatePseudonym)(sessionId, seed, label);
177
+ if (candidate === original || ((_c = ctx.inputText) === null || _c === void 0 ? void 0 : _c.includes(candidate)))
178
+ continue;
141
179
  const taken = entries[candidate];
142
- if (!taken || taken.original === original)
180
+ if (!taken || reusable(taken, original, ctx))
143
181
  return candidate;
144
182
  }
145
- return generateToken(label, Object.keys(entries).length);
183
+ return freshToken(label, vault, sessionId, ctx.reservedTokens);
146
184
  }
147
185
  function shouldProcessField(fieldPath, fieldMode, fieldRules) {
148
186
  if (fieldMode === 'all')
@@ -376,7 +414,7 @@ function getSemanticLabel(fieldKey, siblingKeys) {
376
414
  return null;
377
415
  }
378
416
  function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
379
- var _a;
417
+ var _a, _b;
380
418
  const semantic = getSemanticLabel(path, siblingKeys);
381
419
  if (!semantic || !semantic.label || raw.trim().length === 0)
382
420
  return null;
@@ -385,7 +423,9 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
385
423
  const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
386
424
  if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
387
425
  return null;
388
- const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup);
426
+ const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx, vault, sessionId);
427
+ if (replacement)
428
+ (_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.set(lockKey(itemIndex, display), [[0, replacement.length]]);
389
429
  hits.push({
390
430
  token: replacement, original: '***',
391
431
  patternName: `semantic_${semantic.label}`,
@@ -396,42 +436,75 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
396
436
  });
397
437
  return replacement;
398
438
  }
439
+ function numericMapping(out, text, ctx, vault, sessionId) {
440
+ var _a, _b;
441
+ const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
442
+ const existing = entries[out];
443
+ if ((existing === null || existing === void 0 ? void 0 : existing.original) === text) {
444
+ existing.numeric = true;
445
+ return out;
446
+ }
447
+ const token = existing ? freshToken('NUMBER', vault, sessionId, ctx.reservedTokens) : out;
448
+ vault.addEntry(sessionId, {
449
+ token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
450
+ });
451
+ return token;
452
+ }
399
453
  function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
454
+ var _a;
400
455
  const text = String(value);
401
- if (!ctx.skipSemantic) {
402
- const semantic = redactSemantic(text, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
403
- if (semantic !== null)
404
- return semantic;
405
- }
406
- if (!ctx.scanNumbers)
456
+ const numCtx = { ...ctx, dedup: false, numeric: true };
457
+ let out = null;
458
+ if (!ctx.skipSemantic)
459
+ out = redactSemantic(text, numCtx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
460
+ if (out === null && ctx.scanNumbers)
461
+ out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
462
+ if (out === null || out === text)
407
463
  return value;
408
- const scanned = redactText(text, ctx, vault, sessionId, hits, itemIndex, display);
409
- return scanned === text ? value : scanned;
464
+ if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize')
465
+ return out;
466
+ const mapped = numericMapping(out, text, ctx, vault, sessionId);
467
+ (_a = ctx.lockedSpans) === null || _a === void 0 ? void 0 : _a.set(lockKey(itemIndex, display), [[0, mapped.length]]);
468
+ return mapped;
410
469
  }
411
- function redactKey(key, taken, ctx, vault, sessionId, itemIndex, keyHits) {
412
- let out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
470
+ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
471
+ var _a;
472
+ const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
413
473
  if (out === key)
414
474
  return key;
415
- const base = out;
416
- for (let n = 1; out === '' || out in taken; n++)
417
- out = `${base}_${n}`;
418
- return out;
475
+ const clashes = (k) => k === '' || k in taken || sourceKeys.has(k);
476
+ if (!clashes(out))
477
+ return out;
478
+ if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
479
+ const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...sourceKeys, ...Object.keys(taken)]));
480
+ vault.addEntry(sessionId, { token, original: key, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
481
+ return token;
482
+ }
483
+ let suffixed = out;
484
+ for (let n = 1; clashes(suffixed); n++)
485
+ suffixed = `${out}_${n}`;
486
+ return suffixed;
419
487
  }
420
488
  function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
489
+ var _a;
421
490
  const entries = Object.entries(value);
422
491
  const siblingKeys = entries.map(([k]) => k);
492
+ const sourceKeys = new Set(siblingKeys);
493
+ const origins = new Map();
423
494
  const obj = {};
424
495
  for (const [k, v] of entries) {
425
496
  const fieldPath = path ? `${path}.${k}` : k;
426
497
  const keyHits = [];
427
498
  const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
428
- ? redactKey(k, obj, ctx, vault, sessionId, itemIndex, keyHits)
499
+ ? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
429
500
  : k;
501
+ origins.set(outKey, k);
430
502
  const childDisplay = display ? `${display}.${outKey}` : outKey;
431
503
  for (const h of keyHits)
432
504
  hits.push({ ...h, field: childDisplay });
433
505
  obj[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
434
506
  }
507
+ (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
435
508
  return obj;
436
509
  }
437
510
  function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath = '', siblingKeys, displayPath = currentPath) {
@@ -459,123 +532,170 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
459
532
  }
460
533
  return value;
461
534
  }
462
- function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
535
+ const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
536
+ const USER_REGEX_WINDOW = 2048;
537
+ function windows(seg, size) {
538
+ const out = [];
539
+ let at = 0;
540
+ while (seg.text.length - at > size) {
541
+ const cut = seg.text.lastIndexOf(' ', at + size);
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;
548
+ }
549
+ function replaceInFree(seg, re, fn, into) {
463
550
  var _a;
464
- let result = text;
465
- const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
466
- if (ctx.denyList && ctx.denyList.length > 0) {
467
- for (const entry of ctx.denyList) {
468
- try {
469
- let re;
470
- switch (entry.type) {
471
- case 'regex':
472
- if (!isSafeRegex(entry.value))
473
- continue;
474
- re = new RegExp(entry.value, 'gi');
475
- break;
476
- case 'contains':
477
- re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
478
- break;
479
- default:
480
- re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
481
- }
482
- result = result.replace(re, (match) => {
483
- if (TOKEN_RE.test(match))
484
- return match;
485
- const replacement = applyRedaction(match, { label: 'DENY_LIST', category: 'other' }, ctx.mode, vault, sessionId, ctx.dedup);
486
- hits.push({
487
- token: replacement, original: '***',
488
- patternName: 'denyList', patternLabel: 'DENY_LIST',
489
- category: 'other', field: fieldPath, itemIndex,
490
- confidence: context_1.DENY_LIST_CONFIDENCE,
491
- });
492
- return replacement;
493
- });
494
- }
495
- catch { }
496
- }
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;
497
563
  }
498
- const enabled = (0, patterns_1.getPatternsByNames)(ctx.enabledPatterns);
499
- const activePatterns = [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)];
500
- for (const pattern of activePatterns) {
501
- const re = new RegExp(pattern.regex.source, pattern.regex.flags);
502
- result = result.replace(re, (match, ...args) => {
503
- if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
504
- return match;
505
- const validatorPassed = pattern.validate ? pattern.validate(match) : true;
506
- if (pattern.validate && !validatorPassed)
507
- return match;
508
- if (isAllowListed(match, ctx.allowList))
509
- return match;
510
- const matchOffset = typeof args[args.length - 2] === 'number' ? args[args.length - 2] : 0;
511
- const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, text, matchOffset, matchOffset + match.length);
512
- if (confidence < threshold)
513
- return match;
514
- const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup);
515
- hits.push({
516
- token: replacement, original: '***',
517
- patternName: pattern.name, patternLabel: pattern.label,
518
- category: pattern.category, field: fieldPath, itemIndex,
519
- confidence,
520
- });
521
- return replacement;
522
- });
564
+ if (last < seg.text.length)
565
+ into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
566
+ }
567
+ function replaceFree(pass, re, fn, window) {
568
+ const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
569
+ const global = new RegExp(re.source, flags);
570
+ const next = [];
571
+ for (const seg of pass.segs) {
572
+ if (seg.locked)
573
+ next.push(seg);
574
+ else
575
+ for (const piece of window ? windows(seg, window) : [seg])
576
+ replaceInFree(piece, global, fn, next);
523
577
  }
524
- if (!ctx.skipSemantic) {
525
- const nameMatches = (0, names_1.detectNamesInText)(result);
526
- for (const nm of nameMatches) {
527
- if (nm.name.includes('[') && nm.name.includes(']'))
528
- continue;
529
- if (isAllowListed(nm.name, ctx.allowList))
530
- continue;
531
- let nameConfidence = 0.65;
532
- if ((0, context_1.hasContextWords)(text, nm.start, nm.end, names_1.NAME_CONTEXT_WORDS)) {
533
- nameConfidence = 0.80;
534
- }
535
- if (nameConfidence < threshold)
578
+ pass.segs = next;
579
+ }
580
+ function redactMatch(pass, match, pattern, detail) {
581
+ const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
582
+ pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
583
+ return replacement;
584
+ }
585
+ function denyListRegex(entry) {
586
+ if (entry.type === 'regex')
587
+ return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
588
+ return new RegExp(escapeRe(entry.value), 'gi');
589
+ }
590
+ function denyListStage(pass) {
591
+ var _a;
592
+ for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
593
+ try {
594
+ const re = denyListRegex(entry);
595
+ if (!re)
536
596
  continue;
537
- const replacement = applyRedaction(nm.name, { label: 'PERSON_NAME', category: 'identity' }, ctx.mode, vault, sessionId, ctx.dedup);
538
- result = result.split(nm.name).join(replacement);
539
- hits.push({
540
- token: replacement, original: '***',
541
- patternName: 'personNameDict', patternLabel: 'PERSON_NAME',
542
- category: 'identity', field: fieldPath, itemIndex,
543
- confidence: nameConfidence,
544
- });
597
+ const window = entry.type === 'regex' ? USER_REGEX_WINDOW : undefined;
598
+ replaceFree(pass, re, (match) => TOKEN_RE.test(match)
599
+ ? null
600
+ : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
601
+ patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
602
+ }), window);
545
603
  }
604
+ catch { }
546
605
  }
547
- for (const cp of ctx.customPatterns) {
548
- if (!cp.label || !cp.regex)
606
+ }
607
+ function builtInMatch(pass, pattern, match, offset) {
608
+ if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
609
+ return null;
610
+ const validatorPassed = pattern.validate ? pattern.validate(match) : true;
611
+ if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
612
+ return null;
613
+ const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
614
+ if (confidence < pass.threshold)
615
+ return null;
616
+ return redactMatch(pass, match, pattern, {
617
+ patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
618
+ });
619
+ }
620
+ function builtInStage(pass) {
621
+ const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
622
+ for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
623
+ replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
624
+ }
625
+ }
626
+ function freeNames(pass) {
627
+ const names = new Map();
628
+ for (const seg of pass.segs) {
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;
636
+ }
637
+ function nameStage(pass) {
638
+ if (pass.ctx.skipSemantic)
639
+ return;
640
+ for (const [name, start] of freeNames(pass)) {
641
+ if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
549
642
  continue;
550
- if (!isSafeRegex(cp.regex))
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)
551
645
  continue;
646
+ let replacement;
647
+ replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match) => {
648
+ replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
649
+ patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
650
+ }));
651
+ return replacement;
652
+ });
653
+ }
654
+ }
655
+ function customStage(pass) {
656
+ if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
657
+ return;
658
+ for (const cp of pass.ctx.customPatterns) {
659
+ if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
660
+ continue;
661
+ const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
552
662
  try {
553
- const re = new RegExp(cp.regex, 'g');
554
- result = result.replace(re, (match) => {
555
- if (TOKEN_RE.test(match))
556
- return match;
557
- if (isAllowListed(match, ctx.allowList))
558
- return match;
559
- if (context_1.CUSTOM_PATTERN_CONFIDENCE < threshold)
560
- return match;
561
- const pseudoPattern = {
562
- label: cp.label.toUpperCase(),
563
- category: cp.category || 'identity',
564
- };
565
- const replacement = applyRedaction(match, pseudoPattern, ctx.mode, vault, sessionId, ctx.dedup);
566
- hits.push({
567
- token: replacement, original: '***',
568
- patternName: `custom_${cp.label}`, patternLabel: pseudoPattern.label,
569
- category: pseudoPattern.category, field: fieldPath, itemIndex,
663
+ replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
664
+ ? null
665
+ : redactMatch(pass, match, pattern, {
666
+ patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
570
667
  confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
571
- });
572
- return replacement;
573
- });
574
- }
575
- catch {
668
+ }), USER_REGEX_WINDOW);
576
669
  }
670
+ catch { }
671
+ }
672
+ }
673
+ function recordLocked(ctx, itemIndex, fieldPath, segs) {
674
+ if (!ctx.lockedSpans || fieldPath === '')
675
+ return;
676
+ const spans = [];
677
+ let at = 0;
678
+ for (const seg of segs) {
679
+ if (seg.locked)
680
+ spans.push([at, at + seg.text.length]);
681
+ at += seg.text.length;
577
682
  }
578
- return result;
683
+ if (spans.length > 0)
684
+ ctx.lockedSpans.set(lockKey(itemIndex, fieldPath), spans);
685
+ }
686
+ const lockKey = (itemIndex, fieldPath) => `${itemIndex}\u0000${fieldPath}`;
687
+ function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
688
+ var _a;
689
+ const pass = {
690
+ source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
691
+ ctx, vault, sessionId, hits, itemIndex, fieldPath,
692
+ };
693
+ denyListStage(pass);
694
+ builtInStage(pass);
695
+ nameStage(pass);
696
+ customStage(pass);
697
+ recordLocked(ctx, itemIndex, fieldPath, pass.segs);
698
+ return pass.segs.map((s) => s.text).join('');
579
699
  }
580
700
  function makeRestorer(entries) {
581
701
  const tokens = Object.keys(entries).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
@@ -584,15 +704,19 @@ function makeRestorer(entries) {
584
704
  const re = new RegExp(tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
585
705
  return (text) => text.replace(re, (token) => entries[token].original);
586
706
  }
587
- function restoreWith(value, restore) {
588
- if (typeof value === 'string')
707
+ function restoreWith(value, restore, entries) {
708
+ if (typeof value === 'string') {
709
+ const exact = entries[value];
710
+ if ((exact === null || exact === void 0 ? void 0 : exact.numeric) && Number.isFinite(Number(exact.original)))
711
+ return Number(exact.original);
589
712
  return restore(value);
713
+ }
590
714
  if (Array.isArray(value))
591
- return value.map((v) => restoreWith(v, restore));
715
+ return value.map((v) => restoreWith(v, restore, entries));
592
716
  if (value !== null && typeof value === 'object') {
593
717
  const obj = {};
594
718
  for (const [k, v] of Object.entries(value)) {
595
- obj[restore(k)] = restoreWith(v, restore);
719
+ obj[restore(k)] = restoreWith(v, restore, entries);
596
720
  }
597
721
  return obj;
598
722
  }
@@ -602,7 +726,7 @@ function restoreValue(value, vault, sessionId) {
602
726
  const session = vault.getSession(sessionId);
603
727
  if (!session)
604
728
  return value;
605
- return restoreWith(value, makeRestorer(session.entries));
729
+ return restoreWith(value, makeRestorer(session.entries), session.entries);
606
730
  }
607
731
  function toUtf16Spans(text, entities) {
608
732
  const offsets = [0];
@@ -621,13 +745,18 @@ function resolveOverlaps(spans) {
621
745
  }
622
746
  return kept.sort((a, b) => b.start - a.start);
623
747
  }
748
+ function presidioSpans(value, entities, ctx, key) {
749
+ var _a, _b, _c;
750
+ 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
+ const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
753
+ return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
754
+ }
624
755
  async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
625
- var _a;
626
756
  if (value.trim().length === 0 || TOKEN_RE.test(value))
627
757
  return value;
628
758
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
629
- const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
630
- const spans = resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold));
759
+ const spans = presidioSpans(value, entities, ctx, lockKey(itemIndex, path));
631
760
  let text = value;
632
761
  for (const span of spans) {
633
762
  const matchedText = value.slice(span.start, span.end);
@@ -637,7 +766,7 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
637
766
  continue;
638
767
  const label = (0, presidio_1.getRedactorLabel)(span.entityType);
639
768
  const category = (0, presidio_1.getRedactorCategory)(span.entityType);
640
- const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup);
769
+ const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
641
770
  text = text.slice(0, span.start) + replacement + text.slice(span.end);
642
771
  hits.push({
643
772
  token: replacement, original: '***',
@@ -647,9 +776,10 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
647
776
  }
648
777
  return text;
649
778
  }
650
- async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path) {
779
+ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
780
+ var _a, _b;
651
781
  if (typeof value === 'string') {
652
- if (!shouldProcessField(path, ctx.fieldMode, ctx.fieldRules))
782
+ if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
653
783
  return value;
654
784
  try {
655
785
  return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
@@ -660,14 +790,19 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path)
660
790
  }
661
791
  if (Array.isArray(value)) {
662
792
  const out = [];
663
- for (let i = 0; i < value.length; i++)
664
- out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`));
793
+ for (let i = 0; i < value.length; i++) {
794
+ out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
795
+ }
665
796
  return out;
666
797
  }
667
798
  if (value !== null && typeof value === 'object') {
799
+ const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
668
800
  const out = {};
669
801
  for (const [k, v] of Object.entries(value)) {
670
- out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, path ? `${path}.${k}` : k);
802
+ const original = (_b = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _b !== void 0 ? _b : k;
803
+ const child = path ? `${path}.${k}` : k;
804
+ const childPolicy = policyPath ? `${policyPath}.${original}` : original;
805
+ out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
671
806
  }
672
807
  return out;
673
808
  }
@@ -676,7 +811,7 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path)
676
811
  async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex) {
677
812
  if (!ctx.presidioUrl)
678
813
  return data;
679
- return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, ''));
814
+ return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, '', ''));
680
815
  }
681
816
  function buildReport(sessionId, hits) {
682
817
  const hitsByCategory = {};
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INJECTION_DESCRIPTIONS = void 0;
3
4
  exports.detectPromptInjection = detectPromptInjection;
4
5
  exports.scanForInjection = scanForInjection;
5
6
  const INJECTION_PATTERNS = [
@@ -200,6 +201,11 @@ const INJECTION_PATTERNS = [
200
201
  description: 'Fake end-of-prompt marker',
201
202
  },
202
203
  ];
204
+ exports.INJECTION_DESCRIPTIONS = [
205
+ ...INJECTION_PATTERNS.map((p) => p.description),
206
+ 'Invisible Unicode characters detected',
207
+ 'Base64-encoded injection payload detected',
208
+ ];
203
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;
204
210
  const BASE64_SUSPICIOUS_RE = /[A-Za-z0-9+/]{40,}={0,2}/g;
205
211
  const INJECTION_SEEDS = [
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emptySet = void 0;
4
+ exports.union = union;
5
+ exports.overlaps = overlaps;
6
+ exports.atomCharset = atomCharset;
7
+ const emptySet = () => ({ ascii: new Array(128).fill(false), other: false });
8
+ exports.emptySet = emptySet;
9
+ const fullSet = () => ({ ascii: new Array(128).fill(true), other: true });
10
+ function addCode(set, code) {
11
+ if (code >= 128) {
12
+ set.other = true;
13
+ return;
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;
19
+ }
20
+ function addRange(set, from, to) {
21
+ for (let c = from; c <= Math.min(to, 127); c++)
22
+ addCode(set, c);
23
+ if (to >= 128)
24
+ set.other = true;
25
+ }
26
+ function union(a, b) {
27
+ return { ascii: a.ascii.map((v, i) => v || b.ascii[i]), other: a.other || b.other };
28
+ }
29
+ function overlaps(a, b) {
30
+ return (a.other && b.other) || a.ascii.some((v, i) => v && b.ascii[i]);
31
+ }
32
+ function complement(set) {
33
+ return { ascii: set.ascii.map((v) => !v), other: true };
34
+ }
35
+ const CLASS_ESCAPES = {
36
+ d: (s) => addRange(s, 48, 57),
37
+ w: (s) => {
38
+ addRange(s, 48, 57);
39
+ addRange(s, 65, 90);
40
+ addCode(s, 95);
41
+ },
42
+ s: (s) => {
43
+ for (const c of [9, 10, 11, 12, 13, 32])
44
+ addCode(s, c);
45
+ s.other = true;
46
+ },
47
+ };
48
+ function escapeSet(letter) {
49
+ const lower = letter.toLowerCase();
50
+ const fill = CLASS_ESCAPES[lower];
51
+ if (fill) {
52
+ const set = (0, exports.emptySet)();
53
+ fill(set);
54
+ return letter === lower ? set : complement(set);
55
+ }
56
+ if (/[a-zA-Z0-9]/.test(letter))
57
+ return fullSet();
58
+ const set = (0, exports.emptySet)();
59
+ addCode(set, letter.charCodeAt(0));
60
+ return set;
61
+ }
62
+ function classMember(body, j) {
63
+ var _a, _b;
64
+ if (body[j] === '\\') {
65
+ const set = escapeSet((_a = body[j + 1]) !== null && _a !== void 0 ? _a : '\\');
66
+ const single = /[a-zA-Z0-9]/.test((_b = body[j + 1]) !== null && _b !== void 0 ? _b : '') ? null : body.charCodeAt(j + 1);
67
+ return { set, next: j + 2, code: single };
68
+ }
69
+ const set = (0, exports.emptySet)();
70
+ addCode(set, body.charCodeAt(j));
71
+ return { set, next: j + 1, code: body.charCodeAt(j) };
72
+ }
73
+ function classSet(body) {
74
+ const negated = body.startsWith('^');
75
+ let set = (0, exports.emptySet)();
76
+ let j = negated ? 1 : 0;
77
+ while (j < body.length) {
78
+ const first = classMember(body, j);
79
+ j = first.next;
80
+ if (body[j] === '-' && j + 1 < body.length && first.code !== null) {
81
+ const last = classMember(body, j + 1);
82
+ if (last.code !== null)
83
+ addRange(set, first.code, last.code);
84
+ else
85
+ set = union(union(set, first.set), last.set);
86
+ j = last.next;
87
+ }
88
+ else
89
+ set = union(set, first.set);
90
+ }
91
+ return negated ? complement(set) : set;
92
+ }
93
+ function atomCharset(src, start, end) {
94
+ var _a, _b;
95
+ const c = src[start];
96
+ if (c === '^' || c === '$')
97
+ return null;
98
+ if (c === '\\')
99
+ return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet((_b = src[start + 1]) !== null && _b !== void 0 ? _b : '\\');
100
+ if (c === '[')
101
+ return classSet(src.slice(start + 1, end - 1));
102
+ if (c === '.')
103
+ return fullSet();
104
+ const set = (0, exports.emptySet)();
105
+ addCode(set, src.charCodeAt(start));
106
+ return set;
107
+ }
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkRegex = checkRegex;
4
+ const regexCharset_1 = require("./regexCharset");
4
5
  const MAX_REGEX_LENGTH = 500;
6
+ const newFrame = (zeroWidth = false) => ({
7
+ hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
8
+ });
5
9
  function readBraces(src, i) {
6
10
  const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
7
11
  if (!m)
@@ -55,11 +59,12 @@ function readToken(src, i, stack) {
55
59
  if (c === '[')
56
60
  return { atomEnd: skipClass(src, i), closed: null };
57
61
  if (c === '(') {
58
- stack.push({ hasVariableQuantifier: false, hasAlternation: false });
62
+ stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
59
63
  return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
60
64
  }
61
65
  if (c === '|') {
62
66
  stack[stack.length - 1].hasAlternation = true;
67
+ stack[stack.length - 1].run = null;
63
68
  return { atomEnd: i, closed: null, next: i + 1 };
64
69
  }
65
70
  const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
@@ -69,16 +74,48 @@ const NESTED = 'a repeated group contains a quantifier or alternation (catastrop
69
74
  function isRisky(frame) {
70
75
  return !!frame && (frame.hasVariableQuantifier || frame.hasAlternation);
71
76
  }
77
+ const NO_GROUP = newFrame();
78
+ const repeats = (q) => q !== null && q.max > 1;
79
+ const isVariable = (q) => q !== null && q.min !== q.max;
72
80
  function applyQuantifier(q, closed, top) {
73
- const repeats = q !== null && q.max > 1;
74
- if (repeats && isRisky(closed))
81
+ if (repeats(q) && isRisky(closed))
75
82
  return NESTED;
76
- const variable = repeats && q.min !== q.max;
77
- top.hasVariableQuantifier = top.hasVariableQuantifier || variable || !!(closed === null || closed === void 0 ? void 0 : closed.hasVariableQuantifier);
83
+ const inner = closed !== null && closed !== void 0 ? closed : NO_GROUP;
84
+ top.hasVariableQuantifier || (top.hasVariableQuantifier = isVariable(q) || inner.hasVariableQuantifier);
85
+ top.hasAlternation || (top.hasAlternation = inner.hasAlternation);
78
86
  return null;
79
87
  }
88
+ const SEQUENTIAL = 'two repetitions in a row can match the same characters (catastrophic backtracking)';
89
+ function applySequence(chars, variable, required, top) {
90
+ if (chars === null)
91
+ return null;
92
+ top.chars = (0, regexCharset_1.union)(top.chars, chars);
93
+ const shared = top.run !== null && (0, regexCharset_1.overlaps)(top.run, chars);
94
+ if (variable) {
95
+ if (shared)
96
+ return SEQUENTIAL;
97
+ top.run = top.run ? (0, regexCharset_1.union)(top.run, chars) : chars;
98
+ }
99
+ else if (required && !shared)
100
+ top.run = null;
101
+ return null;
102
+ }
103
+ function stepChars(src, i, step) {
104
+ const closed = step.closed;
105
+ if (!closed)
106
+ return (0, regexCharset_1.atomCharset)(src, i, step.atomEnd);
107
+ return closed.zeroWidth ? null : closed.chars;
108
+ }
109
+ function stepProblem(src, i, step, q, top) {
110
+ var _a, _b;
111
+ const problem = applyQuantifier(q, step.closed, top);
112
+ if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
113
+ return problem;
114
+ const variable = isVariable(q) || ((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier) === true;
115
+ return applySequence(stepChars(src, i, step), variable, ((_b = q === null || q === void 0 ? void 0 : q.min) !== null && _b !== void 0 ? _b : 1) >= 1, top);
116
+ }
80
117
  function structuralProblem(src) {
81
- const stack = [{ hasVariableQuantifier: false, hasAlternation: false }];
118
+ const stack = [newFrame()];
82
119
  let i = 0;
83
120
  while (i < src.length) {
84
121
  const step = readToken(src, i, stack);
@@ -89,7 +126,7 @@ function structuralProblem(src) {
89
126
  continue;
90
127
  }
91
128
  const q = readQuantifier(src, step.atomEnd);
92
- const problem = applyQuantifier(q, step.closed, stack[stack.length - 1]);
129
+ const problem = stepProblem(src, i, step, q, stack[stack.length - 1]);
93
130
  if (problem)
94
131
  return problem;
95
132
  i = step.atomEnd + (q ? q.length : 0);
@@ -5,25 +5,48 @@ exports.workflowNamespace = workflowNamespace;
5
5
  exports.assertUserRegexes = assertUserRegexes;
6
6
  exports.sanitizeInjection = sanitizeInjection;
7
7
  const n8n_workflow_1 = require("n8n-workflow");
8
- const patterns_1 = require("./patterns");
9
- const vault_1 = require("./vault");
10
- const engine_1 = require("./engine");
11
8
  const regexSafety_1 = require("./regexSafety");
9
+ const injection_1 = require("./injection");
12
10
  const VAULT_CREDENTIAL = 'redactorVaultApi';
13
11
  async function resolvePassphrase(fns) {
14
- try {
15
- const cred = await fns.getCredentials(VAULT_CREDENTIAL);
16
- if (cred && typeof cred.passphrase === 'string' && cred.passphrase)
17
- return cred.passphrase;
18
- }
19
- catch {
12
+ var _a;
13
+ if ((_a = fns.getNode().credentials) === null || _a === void 0 ? void 0 : _a[VAULT_CREDENTIAL]) {
14
+ let cred;
15
+ try {
16
+ cred = (await fns.getCredentials(VAULT_CREDENTIAL));
17
+ }
18
+ catch (error) {
19
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The Redactor Vault credential could not be read, so the vault stays locked.', {
20
+ description: `Nothing was stored. Re-enter the passphrase in the credential. Cause: ${error.message}`,
21
+ });
22
+ }
23
+ if (typeof cred.passphrase !== 'string' || cred.passphrase === '') {
24
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The Redactor Vault credential has an empty passphrase.', {
25
+ description: 'Nothing was stored. Set a passphrase in the credential, or remove the credential from the node.',
26
+ });
27
+ }
28
+ return cred.passphrase;
20
29
  }
30
+ return legacyPassphrase(fns);
31
+ }
32
+ function legacyPassphrase(fns) {
33
+ let raw;
21
34
  try {
22
- return fns.getNodeParameter('vaultPassphrase', 0, '') || '';
35
+ raw = fns.getNodeParameter('vaultPassphrase', 0, '');
23
36
  }
24
- catch {
37
+ catch (error) {
38
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase could not be evaluated, so the vault stays locked.', {
39
+ description: `Nothing was stored. Fix the expression, or move the passphrase into a Redactor Vault credential. Cause: ${error.message}`,
40
+ });
41
+ }
42
+ if (raw === undefined || raw === null || raw === '')
25
43
  return '';
44
+ if (typeof raw !== 'string') {
45
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase is not text, so the vault stays locked.', {
46
+ description: 'Nothing was stored. Set the passphrase to a text value, or use a Redactor Vault credential.',
47
+ });
26
48
  }
49
+ return raw;
27
50
  }
28
51
  function workflowNamespace(fns) {
29
52
  try {
@@ -38,7 +61,7 @@ function workflowNamespace(fns) {
38
61
  function assertUserRegexes(fns, customPatterns, lists) {
39
62
  const refuse = (what, problem) => {
40
63
  throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `${what} regex ${problem}`, {
41
- description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+.',
64
+ description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+, and without two repetitions in a row that can match the same characters, for example \\d+ instead of \\d+\\d+. Put a fixed separator such as - or @ between repeated parts.',
42
65
  });
43
66
  };
44
67
  for (const cp of customPatterns) {
@@ -57,19 +80,15 @@ function assertUserRegexes(fns, customPatterns, lists) {
57
80
  }
58
81
  }
59
82
  function sanitizeInjection(result) {
60
- const vault = (0, vault_1.createVault)('memory', undefined, undefined, 'injection-scrub');
61
- const sessionId = 'scrub';
62
- vault.getOrCreateSession(sessionId, 60000);
63
- const ctx = {
64
- enabledPatterns: (0, patterns_1.getPatternOptions)().map((p) => p.value),
65
- customPatterns: [],
66
- mode: 'redact',
67
- dedup: false,
68
- fieldRules: [],
69
- fieldMode: 'all',
70
- };
71
- const scrub = (text) => (0, engine_1.redactValue)(text, ctx, vault, sessionId, [], 0);
72
- const matchedPatterns = result.matchedPatterns.map((m) => ({ ...m, pattern: scrub(m.pattern), matchedText: scrub(m.matchedText) }));
73
- vault.deleteSession(sessionId);
83
+ const matchedPatterns = result.matchedPatterns.map((m) => {
84
+ var _a;
85
+ return ({
86
+ category: m.category,
87
+ pattern: (_a = injection_1.INJECTION_DESCRIPTIONS.find((d) => m.pattern === d || m.pattern.endsWith(` ${d}`))) !== null && _a !== void 0 ? _a : 'Prompt injection detector',
88
+ matchedText: '(withheld)',
89
+ severity: m.severity,
90
+ score: m.score,
91
+ });
92
+ });
74
93
  return { ...result, matchedPatterns };
75
94
  }
@@ -124,6 +124,17 @@ class MemoryVault {
124
124
  deleteSession(sessionId) {
125
125
  this.drop(this.key(sessionId));
126
126
  }
127
+ purgeAll() {
128
+ const keys = this.ownKeys();
129
+ const entries = keys.reduce((n, k) => { var _a, _b; return n + Object.keys((_b = (_a = memoryStore.get(k)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {}).length; }, 0);
130
+ keys.forEach((k) => this.drop(k));
131
+ return { sessions: keys.length, entries };
132
+ }
133
+ purgeSession(sessionId) {
134
+ const session = memoryStore.get(this.key(sessionId));
135
+ this.deleteSession(sessionId);
136
+ return { found: !!session, entries: session ? Object.keys(session.entries).length : null };
137
+ }
127
138
  listSessions() {
128
139
  return this.ownKeys()
129
140
  .map((k) => memoryStore.get(k))
@@ -242,6 +253,25 @@ class WorkflowVault {
242
253
  if (k in this.sessions)
243
254
  delete this.writableSessions()[k];
244
255
  }
256
+ purgeAll() {
257
+ const stored = Object.values(this.sessions);
258
+ const entries = stored.reduce((n, rec) => {
259
+ const session = rec && typeof rec === 'object' ? this.decode(rec) : null;
260
+ return n + (session ? Object.keys(session.entries).length : 0);
261
+ }, 0);
262
+ this.cache.clear();
263
+ if (stored.length)
264
+ this.store[STORE_KEY] = {};
265
+ return { sessions: stored.length, entries };
266
+ }
267
+ purgeSession(sessionId) {
268
+ var _a;
269
+ const stored = this.sessions[this.key(sessionId)];
270
+ const live = (_a = this.cache.get(sessionId)) !== null && _a !== void 0 ? _a : (stored && typeof stored === 'object' ? this.decode(stored) : null);
271
+ const found = !!stored || this.cache.has(sessionId);
272
+ this.deleteSession(sessionId);
273
+ return { found, entries: live && live.sessionId === sessionId ? Object.keys(live.entries).length : null };
274
+ }
245
275
  listSessions() {
246
276
  const out = [];
247
277
  for (const stored of Object.values(this.sessions)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "4.1.0",
3
+ "version": "4.3.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",