n8n-nodes-redactor 5.0.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.
@@ -70,7 +70,11 @@ function isAllowListed(value, allowList) {
70
70
  });
71
71
  }
72
72
  function generateToken(label, index) {
73
- return `[${label}_${index}]`;
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) {
@@ -460,7 +464,7 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
460
464
  if (out === null || out === text)
461
465
  return value;
462
466
  if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
463
- ctx.lastLocked = [[0, out.length]];
467
+ ctx.lastLocked = numCtx.lastLocked;
464
468
  return out;
465
469
  }
466
470
  const mapped = numericMapping(out, text, ctx, vault, sessionId);
@@ -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 in taken || sourceKeys.has(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[outKey] = lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay));
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;
@@ -544,35 +548,33 @@ function assertUserRegexInput(text, what) {
544
548
  throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
545
549
  'Exclude the field with field rules, or remove the regex.');
546
550
  }
547
- function replaceInFree(seg, re, fn, into) {
548
- var _a;
549
- let last = 0;
550
- for (const m of seg.text.matchAll(re)) {
551
- const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
552
- if (m[0] === '')
553
- continue;
554
- const replacement = fn(m[0], seg.src + at);
555
- if (replacement === null)
556
- continue;
557
- if (at > last)
558
- into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
559
- into.push({ text: replacement, locked: true, src: -1 });
560
- last = at + m[0].length;
561
- }
562
- if (last < seg.text.length)
563
- 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);
553
+ }
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
564
  }
565
565
  function replaceFree(pass, re, fn) {
566
+ var _a;
566
567
  const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
567
- const global = new RegExp(re.source, flags);
568
- const next = [];
569
- for (const seg of pass.segs) {
570
- if (seg.locked)
571
- next.push(seg);
572
- else
573
- replaceInFree(seg, global, fn, next);
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);
574
577
  }
575
- pass.segs = next;
576
578
  }
577
579
  function redactMatch(pass, match, pattern, detail) {
578
580
  const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
@@ -621,19 +623,14 @@ function builtInStage(pass) {
621
623
  replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
622
624
  }
623
625
  }
624
- function freeNames(pass) {
625
- const names = new Set();
626
- for (const seg of pass.segs) {
627
- if (!seg.locked)
628
- for (const nm of (0, names_1.detectNamesInText)(seg.text))
629
- names.add(nm.name);
630
- }
631
- 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);
632
629
  }
633
630
  function nameStage(pass) {
634
631
  if (pass.ctx.skipSemantic)
635
632
  return;
636
- for (const name of freeNames(pass)) {
633
+ for (const name of sourceNames(pass)) {
637
634
  if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
638
635
  continue;
639
636
  let replacement;
@@ -721,7 +718,7 @@ function restoreWith(value, restore, entries) {
721
718
  if (value !== null && typeof value === 'object') {
722
719
  const obj = {};
723
720
  for (const [k, v] of Object.entries(value)) {
724
- obj[restore(k)] = restoreWith(v, restore, entries);
721
+ setOwn(obj, restore(k), restoreWith(v, restore, entries));
725
722
  }
726
723
  return obj;
727
724
  }
@@ -808,7 +805,7 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
808
805
  const child = path ? `${path}.${k}` : k;
809
806
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
810
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);
811
- out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock);
808
+ setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
812
809
  }
813
810
  return out;
814
811
  }
@@ -50,6 +50,7 @@ const CLASS_ESCAPES = {
50
50
  },
51
51
  };
52
52
  const HEX = /^[0-9a-fA-F]+$/;
53
+ const CONTROL = { n: 10, r: 13, t: 9, v: 11, f: 12 };
53
54
  function readEscape(src, i) {
54
55
  var _a, _b;
55
56
  const e = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
@@ -62,10 +63,18 @@ function readEscape(src, i) {
62
63
  return { length: 3, code: src.charCodeAt(i + 2) % 32 };
63
64
  if (e === '0')
64
65
  return { length: 2, code: 0 };
66
+ const control = CONTROL[e];
67
+ if (control !== undefined)
68
+ return { length: 2, code: control };
65
69
  if (/[a-zA-Z0-9]/.test(e))
66
70
  return { length: 2, code: null };
67
71
  return { length: 2, code: e.charCodeAt(0) };
68
72
  }
73
+ function codeSet(code) {
74
+ const set = (0, exports.emptySet)();
75
+ addCode(set, code);
76
+ return set;
77
+ }
69
78
  function escapeSet(src, i) {
70
79
  var _a;
71
80
  const { code } = readEscape(src, i);
@@ -82,6 +91,8 @@ function escapeSet(src, i) {
82
91
  return letter === letter.toLowerCase() ? set : complement(set);
83
92
  }
84
93
  function classMember(body, j) {
94
+ if (body[j] === '\\' && body[j + 1] === 'b')
95
+ return { set: codeSet(8), next: j + 2, code: 8 };
85
96
  if (body[j] === '\\') {
86
97
  const { length, code } = readEscape(body, j);
87
98
  return { set: escapeSet(body, j), next: j + length, code };
@@ -157,7 +157,45 @@ function structuralProblem(src) {
157
157
  }
158
158
  return null;
159
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
+ }
160
197
  function checkRegex(regexStr) {
198
+ var _a;
161
199
  if (typeof regexStr !== 'string' || regexStr.length === 0)
162
200
  return 'is empty';
163
201
  if (regexStr.length > MAX_REGEX_LENGTH)
@@ -168,5 +206,5 @@ function checkRegex(regexStr) {
168
206
  catch {
169
207
  return 'is not a valid regular expression';
170
208
  }
171
- return structuralProblem(regexStr);
209
+ return (_a = unsupportedSyntax(regexStr)) !== null && _a !== void 0 ? _a : structuralProblem(regexStr);
172
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "5.0.0",
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",