n8n-nodes-redactor 7.0.0 → 7.0.1

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.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/mjmirza/n8n-nodes-redactor/master/.github/assets/logo.png" alt="Redactor" width="96" />
2
+ <img src="https://raw.githubusercontent.com/mjmirza/n8n-nodes-redactor/main/.github/assets/logo.png" alt="Redactor" width="96" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">n8n-nodes-redactor</h1>
@@ -12,7 +12,7 @@
12
12
  <p align="center">
13
13
  <a href="https://www.npmjs.com/package/n8n-nodes-redactor"><img src="https://img.shields.io/npm/v/n8n-nodes-redactor?label=npm" alt="npm version" /></a>
14
14
  <a href="https://www.npmjs.com/package/n8n-nodes-redactor"><img src="https://img.shields.io/npm/dm/n8n-nodes-redactor?label=downloads" alt="npm downloads" /></a>
15
- <a href="https://github.com/mjmirza/n8n-nodes-redactor/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mjmirza/n8n-nodes-redactor/ci.yml?branch=master&label=CI" alt="CI status" /></a>
15
+ <a href="https://github.com/mjmirza/n8n-nodes-redactor/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mjmirza/n8n-nodes-redactor/ci.yml?branch=main&label=CI" alt="CI status" /></a>
16
16
  <a href="https://www.npmjs.com/package/n8n-nodes-redactor#provenance"><img src="https://img.shields.io/badge/npm-provenance%20signed-brightgreen" alt="npm provenance" /></a>
17
17
  <a href="#license"><img src="https://img.shields.io/npm/l/n8n-nodes-redactor" alt="MIT License" /></a>
18
18
  <img src="https://img.shields.io/badge/PII%20patterns-218-blue" alt="218 PII patterns" />
@@ -54,7 +54,7 @@ After Restore, your workflow gets the real values back.
54
54
  | Self-hosted n8n 1.x and 2.x | Supported |
55
55
  | n8n verified community nodes | Eligible from 4.0.0. Passes n8n's verification scanner with no findings |
56
56
 
57
- Version 4.0.0 uses no local disk. The vault lives in workflow storage and audit records go to the Report output. Upgrading from 1.x? Read [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md). The 1.x line stays installable with `npm install n8n-nodes-redactor@v1-legacy`.
57
+ Version 4.0.0 uses no local disk. The vault lives in workflow storage and audit records go to the Report output. Upgrading from 1.x? Read [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/main/docs/MIGRATION.md). The 1.x line stays installable with `npm install n8n-nodes-redactor@v1-legacy`.
58
58
 
59
59
  ## Operations
60
60
 
@@ -121,8 +121,8 @@ Set Verify Output on the Verify node to Items, Stop on FAIL. Verify then passes
121
121
  | Audit report | A second output with hits by category and pattern |
122
122
  | Audit log | Turn on Audit Log to get an `audit` record (counts, labels, timing, never values) on the Report output. Send it to any database, sheet or SIEM node |
123
123
  | ROPA record | Turn on Include Audit Report, then Include GDPR Art.30 ROPA Record, and fill in the controller details |
124
- | Presidio | Optional Microsoft Presidio container for names and organisations in free text. See [docs/PRESIDIO_SETUP.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/PRESIDIO_SETUP.md). When Presidio cannot scan a text the run stops, unless If Presidio Fails is set to continue with patterns only |
125
- | Per-tenant profiles | Turn on Use Tenant Profile and paste JSON. Validated on every run. Fields are listed in [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md#tenant-profile-fields) |
124
+ | Presidio | Optional Microsoft Presidio container for names and organisations in free text. See [docs/PRESIDIO_SETUP.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/main/docs/PRESIDIO_SETUP.md). When Presidio cannot scan a text the run stops, unless If Presidio Fails is set to continue with patterns only |
125
+ | Per-tenant profiles | Turn on Use Tenant Profile and paste JSON. Validated on every run. Fields are listed in [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/main/docs/MIGRATION.md#tenant-profile-fields) |
126
126
 
127
127
  This node reduces what reaches a model. It is not legal advice and does not on its own make a workflow GDPR or HIPAA compliant.
128
128
 
@@ -864,7 +864,7 @@ class Redactor {
864
864
  };
865
865
  }
866
866
  async execute() {
867
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
867
+ var _a, _b, _c, _d, _e, _f, _g, _h;
868
868
  const items = this.getInputData();
869
869
  const allPaired = items.map((_, i) => ({ item: i }));
870
870
  const operation = (0, runtime_1.runSetting)(this, 'operation');
@@ -1012,10 +1012,7 @@ class Redactor {
1012
1012
  }
1013
1013
  let sessionId = '';
1014
1014
  if (operation === 'redact' || operation === 'restore') {
1015
- sessionId = String((_a = (0, runtime_1.runSetting)(this, 'sessionId')) !== null && _a !== void 0 ? _a : '');
1016
- if (!sessionId) {
1017
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required');
1018
- }
1015
+ sessionId = (0, runtime_1.readSessionId)(this, 'sessionId');
1019
1016
  }
1020
1017
  if (operation === 'redact') {
1021
1018
  let mode = (0, runtime_1.runSetting)(this, 'redactionMode', 'token');
@@ -1030,9 +1027,9 @@ class Redactor {
1030
1027
  let sessionTtl = (0, runtime_1.runSetting)(this, 'sessionTtl', 60);
1031
1028
  const includeReport = (0, runtime_1.runSetting)(this, 'includeReport', false);
1032
1029
  const customPatternsRaw = (0, runtime_1.runSetting)(this, 'customPatterns', {});
1033
- let customPatterns = (_b = customPatternsRaw.patterns) !== null && _b !== void 0 ? _b : [];
1030
+ let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
1034
1031
  const fieldRulesRaw = (0, runtime_1.runSetting)(this, 'fieldRules', {});
1035
- let fieldRules = ((_c = fieldRulesRaw.rules) !== null && _c !== void 0 ? _c : []).map((r) => ({
1032
+ let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
1036
1033
  field: r.field,
1037
1034
  mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
1038
1035
  }));
@@ -1067,12 +1064,12 @@ class Redactor {
1067
1064
  }
1068
1065
  }
1069
1066
  const allowListRaw = (0, runtime_1.runSetting)(this, 'allowList', {});
1070
- let allowList = ((_d = allowListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
1067
+ let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
1071
1068
  value: e.value,
1072
1069
  type: (e.type || 'contains'),
1073
1070
  }));
1074
1071
  const denyListRaw = (0, runtime_1.runSetting)(this, 'denyList', {});
1075
- let denyList = ((_e = denyListRaw.entries) !== null && _e !== void 0 ? _e : []).map((e) => ({
1072
+ let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
1076
1073
  value: e.value,
1077
1074
  type: (e.type || 'exact'),
1078
1075
  }));
@@ -1159,8 +1156,8 @@ class Redactor {
1159
1156
  inputIndex: {},
1160
1157
  lockedSpans: new WeakMap(),
1161
1158
  keyLocks: new WeakMap(),
1162
- priorTokens: (0, engine_1.priorTokenMatcher)((_f = vault.getSession(sessionId)) === null || _f === void 0 ? void 0 : _f.entries),
1163
1159
  };
1160
+ ctx.priorTokens = (0, engine_1.priorTokensInInput)(ctx, (_e = vault.getSession(sessionId)) === null || _e === void 0 ? void 0 : _e.entries);
1164
1161
  const allHits = [];
1165
1162
  const returnData = [];
1166
1163
  for (let i = 0; i < items.length; i++) {
@@ -1229,7 +1226,7 @@ class Redactor {
1229
1226
  if (includeReport && (0, runtime_1.runSetting)(this, 'includeRopa', false)) {
1230
1227
  const d = (0, runtime_1.runSetting)(this, 'ropa', {});
1231
1228
  const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
1232
- report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_g = d.controllerName) !== null && _g !== void 0 ? _g : ''), contactEmail: String((_h = d.controllerContact) !== null && _h !== void 0 ? _h : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_j = d.legalBasis) !== null && _j !== void 0 ? _j : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1229
+ report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_f = d.controllerName) !== null && _f !== void 0 ? _f : ''), contactEmail: String((_g = d.controllerContact) !== null && _g !== void 0 ? _g : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_h = d.legalBasis) !== null && _h !== void 0 ? _h : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1233
1230
  versionRopaSummary(report.ropa, (0, classification_1.classifyData)(allHits).hitCountByLevel, this.getNode().typeVersion);
1234
1231
  }
1235
1232
  return [returnData, [{ json: report, pairedItem: allPaired }]];
@@ -1264,10 +1261,7 @@ class Redactor {
1264
1261
  if (operation === 'purge') {
1265
1262
  const purgeScope = (0, runtime_1.runSetting)(this, 'purgeScope', 'all');
1266
1263
  if (purgeScope === 'specific') {
1267
- const purgeSessionId = (0, runtime_1.runSetting)(this, 'purgeSessionId', '');
1268
- if (!purgeSessionId) {
1269
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
1270
- }
1264
+ const purgeSessionId = (0, runtime_1.readSessionId)(this, 'purgeSessionId');
1271
1265
  const purged = vault.purgeSession(purgeSessionId);
1272
1266
  return [[{
1273
1267
  json: {
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.collectTokenLike = collectTokenLike;
37
37
  exports.collectInputText = collectInputText;
38
38
  exports.redactValue = redactValue;
39
- exports.priorTokenMatcher = priorTokenMatcher;
39
+ exports.priorTokensInInput = priorTokensInInput;
40
40
  exports.ensureRestorable = ensureRestorable;
41
41
  exports.restoreValue = restoreValue;
42
42
  exports.restoreFromSession = restoreFromSession;
@@ -1095,8 +1095,29 @@ function lockChild(ctx, container, key, compute) {
1095
1095
  ctx.lastLocked = undefined;
1096
1096
  return out;
1097
1097
  }
1098
- function priorTokenMatcher(entries) {
1099
- const tokens = Object.keys(entries !== null && entries !== void 0 ? entries : {}).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
1098
+ function priorTokensPresent(input, entries) {
1099
+ const shape = tokenShape(entries);
1100
+ if (shape.byFirst.size === 0)
1101
+ return [];
1102
+ const found = new Set();
1103
+ for (let at = 0; at < input.length; at++) {
1104
+ const lengths = shape.byFirst.get(input.charCodeAt(at));
1105
+ if (!lengths)
1106
+ continue;
1107
+ for (const length of lengths) {
1108
+ if (at + length > input.length)
1109
+ break;
1110
+ const token = input.slice(at, at + length);
1111
+ if (Object.prototype.hasOwnProperty.call(entries, token))
1112
+ found.add(token);
1113
+ }
1114
+ }
1115
+ return [...found].sort((a, b) => b.length - a.length);
1116
+ }
1117
+ function priorTokensInInput(ctx, entries) {
1118
+ if (!entries || !ctx.inputText)
1119
+ return undefined;
1120
+ const tokens = priorTokensPresent(ctx.inputText, entries);
1100
1121
  return tokens.length > 0 ? new RegExp(tokens.map(escapeRe).join('|'), 'g') : undefined;
1101
1122
  }
1102
1123
  function literalStage(pass) {
@@ -484,17 +484,22 @@ function scanLdapDns(text) {
484
484
  return spans;
485
485
  }
486
486
  const isLineBreak = (ch) => ch === '\n' || ch === '\r';
487
- function connValueEnd(text, from, limit) {
488
- var _a;
487
+ const CONN_SPACE = String.raw `(?:[^\S\r\n]|\u0085)`;
488
+ const connSpace = new RegExp(CONN_SPACE);
489
+ const CONN_HOST_KEY = 'Server|Data Source|Host';
490
+ const CONN_PASSWORD_KEY = 'Password|Pwd';
491
+ const CONN_SENSITIVE_KEY = `${CONN_PASSWORD_KEY}|User ID|Uid|User|Access Token|Token|Client Secret|Secret|API Key`;
492
+ const CONN_KEY = `(?:${CONN_HOST_KEY}|${CONN_SENSITIVE_KEY})`;
493
+ const connHostKey = new RegExp(`^(?:${CONN_HOST_KEY})$`, 'i');
494
+ const connPasswordKey = new RegExp(`^(?:${CONN_PASSWORD_KEY})$`, 'i');
495
+ function connValueStart(text, from) {
489
496
  let at = from;
490
- while (at < limit && /\s/.test(text[at]))
491
- at++;
492
- if (/["'{]/.test((_a = text[at]) !== null && _a !== void 0 ? _a : ''))
493
- at = quotedValueEnd(text, at, limit);
494
- while (at < limit && text[at] !== ';')
497
+ while (at < text.length && connSpace.test(text[at]))
495
498
  at++;
496
499
  return at;
497
500
  }
501
+ const connQuoteAt = (text, at, braceEnd) => { var _a; return /["']/.test((_a = text[at]) !== null && _a !== void 0 ? _a : '') || (text[at] === '{' && at < braceEnd); };
502
+ const connHasValue = (text, at) => at < text.length && text[at] !== ';';
498
503
  function quotedValueEnd(text, at, limit) {
499
504
  const opener = text[at++];
500
505
  const quote = opener === '{' ? '}' : opener;
@@ -507,60 +512,88 @@ function quotedValueEnd(text, at, limit) {
507
512
  }
508
513
  return at;
509
514
  }
510
- function connSegmentEnd(text, from, limit) {
511
- let at = from;
512
- while (at < limit && text[at] !== '=' && text[at] !== ';')
513
- at++;
514
- return text[at] === '=' ? connValueEnd(text, at + 1, limit) : at;
515
+ function lastBraceClose(text, from, limit) {
516
+ let at = limit;
517
+ while (at > from) {
518
+ if (text[--at] !== '}')
519
+ continue;
520
+ const end = at + 1;
521
+ while (at > from && text[at - 1] === '}')
522
+ at--;
523
+ if ((end - at) % 2 === 1)
524
+ return end;
525
+ }
526
+ return -1;
515
527
  }
516
- function connLine(text, at) {
517
- let lineStart = at;
518
- while (lineStart > 0 && !isLineBreak(text[lineStart - 1]))
519
- lineStart--;
520
- let end = at;
521
- while (end < text.length && !isLineBreak(text[end]))
522
- end++;
523
- return { end, password: lastPasswordSegment(text, lineStart, end), semi: -2 };
528
+ function recordConnKey(chain, key) {
529
+ if (connPasswordKey.test(key))
530
+ chain.password = true;
531
+ if (!connHostKey.test(key))
532
+ return true;
533
+ chain.host = true;
534
+ return false;
524
535
  }
525
- function nextSemi(text, line, from) {
526
- if (line.semi !== -1 && line.semi < from) {
527
- line.semi = connValueEnd(text, from, line.end);
528
- if (line.semi === line.end)
529
- line.semi = -1;
536
+ function connChain(text, from, limit, braceEnd) {
537
+ const chain = { start: from, end: from, host: false, password: false };
538
+ const line = text.slice(from, limit);
539
+ const elements = new RegExp(`(?<![A-Za-z])(${CONN_KEY})${CONN_SPACE}*=|[=;]`, 'gi');
540
+ let sensitive = false;
541
+ let match;
542
+ while ((match = elements.exec(line)) !== null) {
543
+ if (match[0] === ';') {
544
+ if (sensitive)
545
+ chain.end = from + match.index;
546
+ sensitive = false;
547
+ continue;
548
+ }
549
+ const at = connValueStart(line, elements.lastIndex);
550
+ if (match[1] && connHasValue(line, at))
551
+ sensitive = recordConnKey(chain, match[1]) || sensitive;
552
+ if (connQuoteAt(line, at, braceEnd - from))
553
+ elements.lastIndex = quotedValueEnd(line, at, line.length);
530
554
  }
531
- return line.semi;
555
+ if (sensitive)
556
+ chain.end = limit;
557
+ return chain;
532
558
  }
533
- function lastPasswordSegment(text, lineStart, lineEnd) {
534
- let found = null;
535
- let segStart = lineStart;
536
- while (segStart <= lineEnd) {
537
- const segEnd = connSegmentEnd(text, segStart, lineEnd);
538
- const segment = text.slice(segStart, segEnd);
539
- const key = /^[ \t]*(?:Password|Pwd)[ \t]*=/i.exec(segment);
540
- if (key && key[0].length < segment.length)
541
- found = { semi: segStart - 1, end: segEnd };
542
- segStart = segEnd + 1;
559
+ function scanConnGrammar(text, odbc) {
560
+ const spans = [];
561
+ const starts = new RegExp(`(?<![A-Za-z])${CONN_KEY}${CONN_SPACE}*=`, 'gi');
562
+ let match;
563
+ while ((match = starts.exec(text)) !== null) {
564
+ const start = match.index;
565
+ let end = starts.lastIndex;
566
+ while (end < text.length && !isLineBreak(text[end]))
567
+ end++;
568
+ const braceEnd = odbc ? lastBraceClose(text, start, end) : -1;
569
+ const chain = connChain(text, start, end, braceEnd);
570
+ if (chain.host && chain.password)
571
+ spans.push({ start: chain.start, end: chain.end });
572
+ starts.lastIndex = end;
543
573
  }
544
- return found;
574
+ return spans;
545
575
  }
546
- function scanConnStrings(text) {
576
+ function mergeConnSpans(ado, odbc) {
547
577
  const spans = [];
548
- const starts = /(?<![A-Za-z])(?:Server|Data Source|Host)[ \t]*=/gi;
549
- let line = { end: -1, password: null, semi: -2 };
550
- let m;
551
- while ((m = starts.exec(text)) !== null) {
552
- const start = m.index;
553
- const valueStart = start + m[0].length;
554
- if (start > line.end)
555
- line = connLine(text, start);
556
- const semi = nextSemi(text, line, valueStart);
557
- if (semi <= valueStart || !line.password || line.password.semi < semi)
558
- continue;
559
- spans.push({ start, end: line.password.end });
560
- starts.lastIndex = line.password.end;
578
+ let a = 0;
579
+ let b = 0;
580
+ while (a < ado.length || b < odbc.length) {
581
+ let next;
582
+ if (b >= odbc.length || (a < ado.length && ado[a].start <= odbc[b].start))
583
+ next = ado[a++];
584
+ else
585
+ next = odbc[b++];
586
+ const previous = spans[spans.length - 1];
587
+ if (previous && next.start < previous.end)
588
+ previous.end = Math.max(previous.end, next.end);
589
+ else
590
+ spans.push({ ...next });
561
591
  }
562
592
  return spans;
563
593
  }
594
+ function scanConnStrings(text) {
595
+ return mergeConnSpans(scanConnGrammar(text, false), scanConnGrammar(text, true));
596
+ }
564
597
  const CTX_NAME_WORD = String.raw `\p{L}[\p{L}\p{M}]*(?:['’-]\p{L}[\p{L}\p{M}]*)*`;
565
598
  const FULL_NAME_LABELS = String.raw `full\s*name|first\s*name|last\s*name|surname|family\s*name|given\s*name|Vorname|Nachname|Familienname|nom\s*de\s*famille|prenom|nombre|apellido`;
566
599
  const TITLED_WORD = String.raw `(?:\p{Lu}{2,}(?:['’-]\p{Lu}+)*|\p{Lu}[\p{Ll}\p{M}]*(?:\p{Lu}[\p{Ll}\p{M}]+)*(?:['’-]\p{Lu}?[\p{Ll}\p{M}]+(?:\p{Lu}[\p{Ll}\p{M}]+)*)*)`;
@@ -1320,8 +1353,8 @@ exports.PII_PATTERNS = [
1320
1353
  name: 'dbConnectionString',
1321
1354
  label: 'DB_CONN_STRING',
1322
1355
  category: 'enterprise',
1323
- regex: /(?:jdbc|mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis(?:s)?|mssql|mariadb):\/\/[^\s"']+/gi,
1324
- description: 'Database connection strings (JDBC, MongoDB, PostgreSQL, MySQL, Redis)',
1356
+ regex: /(?:jdbc|mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis(?:s)?|amqps?|mssql|mariadb):\/\/[^\s"]+/gi,
1357
+ description: 'Database and broker connection strings (JDBC, MongoDB, PostgreSQL, MySQL, Redis, AMQP)',
1325
1358
  },
1326
1359
  {
1327
1360
  name: 'connStringKV',
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.selectedPatterns = selectedPatterns;
4
4
  exports.runSetting = runSetting;
5
+ exports.readSessionId = readSessionId;
5
6
  exports.resolvePassphrase = resolvePassphrase;
6
7
  exports.workflowNamespace = workflowNamespace;
7
8
  exports.assertUserRegexes = assertUserRegexes;
@@ -53,6 +54,13 @@ function runSetting(fx, name, fallback) {
53
54
  }
54
55
  return first;
55
56
  }
57
+ function readSessionId(fns, name) {
58
+ var _a;
59
+ const sessionId = String((_a = runSetting(fns, name, '')) !== null && _a !== void 0 ? _a : '');
60
+ if (!sessionId)
61
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'Session ID is required');
62
+ return sessionId;
63
+ }
56
64
  async function resolvePassphrase(fns) {
57
65
  var _a;
58
66
  if ((_a = fns.getNode().credentials) === null || _a === void 0 ? void 0 : _a[VAULT_CREDENTIAL]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "7.0.0",
3
+ "version": "7.0.1",
4
4
  "description": "Redact, mask and anonymize PII before it reaches an LLM, then restore it. 218 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",
@@ -94,6 +94,7 @@
94
94
  "@typescript-eslint/parser": "^8.72.1",
95
95
  "eslint": "^9.29.0",
96
96
  "eslint-plugin-n8n-nodes-base": "^2.0.0",
97
+ "fast-check": "4.10.2",
97
98
  "jest": "^29.7.0",
98
99
  "n8n-workflow": "^2.45.4",
99
100
  "prettier": "^3.6.2",