n8n-nodes-redactor 4.0.0 → 4.2.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.
package/README.md CHANGED
@@ -110,7 +110,7 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
110
110
  | Deduplication | The same value gets the same token everywhere, so the model keeps relationships between fields |
111
111
  | Field targeting | Allowlist or denylist paths such as `user.email`, `*.phone` or `metadata.*` |
112
112
  | Vault | Workflow Storage by default, so Restore works in later production runs of the same workflow, or In-Memory. Sessions expire after 60 minutes by default |
113
- | Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant |
113
+ | Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant, kept in a Redactor Vault credential. See [Vault encryption](#vault-encryption) |
114
114
  | Confidence threshold | Each match carries a 0.0 to 1.0 score. Checksum validated patterns score 0.95 |
115
115
  | Allow and deny lists | Values to never redact, such as your support address, and values to always redact, such as project codenames |
116
116
  | Prompt injection check | 25+ patterns, including instruction overrides, delimiter escapes, encoded payloads and zero width characters. Returns BLOCK, FLAG or CLEAN |
@@ -122,6 +122,20 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
122
122
 
123
123
  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.
124
124
 
125
+ ## Vault encryption
126
+
127
+ Workflow Storage keeps sessions with the workflow. To encrypt them, create a **Redactor Vault** credential with a passphrase of at least 12 characters and attach it to both the Redact and the Restore node. n8n stores credentials encrypted and apart from the workflow, so the passphrase never sits next to the data it protects. Give each tenant its own credential.
128
+
129
+ The older Vault Encryption Passphrase field still works so workflows saved with it keep restoring, but it is saved in plain text inside the workflow. When both are set, the credential wins.
130
+
131
+ Restore needs the same passphrase that Redact used. If Redact meets a session ID that a different passphrase wrote, it stops with an error instead of overwriting it.
132
+
133
+ ### Concurrent runs
134
+
135
+ n8n saves workflow storage as one block at the end of each production run and does not merge it. If two runs of the same workflow overlap, the one that finishes last wins. A session created by the other run can be lost, and a session purged by the other run can come back until its TTL expires. The node cannot prevent this from inside n8n. To stay safe, keep the Session TTL short, avoid overlapping runs of a workflow that uses Purge (for example by limiting production concurrency on your instance), or use In-Memory storage when Redact and Restore run in the same execution.
136
+
137
+ In-Memory sessions are kept per workflow and are never visible to another workflow.
138
+
125
139
  ## For developers
126
140
 
127
141
  | Area | Detail |
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedactorVaultApi = void 0;
4
+ class RedactorVaultApi {
5
+ constructor() {
6
+ this.name = 'redactorVaultApi';
7
+ this.displayName = 'Redactor Vault API';
8
+ this.icon = { light: 'file:redactorVault.svg', dark: 'file:redactorVault.dark.svg' };
9
+ this.documentationUrl = 'https://github.com/mjmirza/n8n-nodes-redactor#vault-encryption';
10
+ this.properties = [
11
+ {
12
+ displayName: 'Vault Passphrase',
13
+ name: 'passphrase',
14
+ type: 'string',
15
+ typeOptions: { password: true },
16
+ default: '',
17
+ required: true,
18
+ description: 'Encrypts stored sessions with AES-256-GCM. Use at least 12 characters, and a different passphrase for each tenant. Sessions written with this passphrase cannot be read without it.',
19
+ },
20
+ ];
21
+ }
22
+ }
23
+ exports.RedactorVaultApi = RedactorVaultApi;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF6B6B" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#FFA42E" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF5757" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#F79410" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
@@ -11,6 +11,7 @@ const audit_1 = require("./audit");
11
11
  const presidio_1 = require("./presidio");
12
12
  const profiles_1 = require("./profiles");
13
13
  const ropa_1 = require("./ropa");
14
+ const runtime_1 = require("./runtime");
14
15
  const RECOMMENDED_PATTERNS = [
15
16
  'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
16
17
  'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
@@ -57,6 +58,19 @@ class Redactor {
57
58
  name: 'Redactor',
58
59
  },
59
60
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
61
+ credentials: [
62
+ {
63
+ name: 'redactorVaultApi',
64
+ required: false,
65
+ testedBy: 'redactorVaultPassphrase',
66
+ displayOptions: {
67
+ show: {
68
+ operation: ['redact', 'restore', 'stats', 'purge'],
69
+ vaultStorage: ['workflow', 'file'],
70
+ },
71
+ },
72
+ },
73
+ ],
60
74
  outputs: `={{((parameters) => (parameters["includeReport"] || parameters["auditLog"]) ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
61
75
  properties: [
62
76
  {
@@ -601,7 +615,7 @@ class Redactor {
601
615
  },
602
616
  },
603
617
  default: '',
604
- description: 'Encrypt stored sessions at rest with AES-256-GCM. Leave empty for no encryption. When set, sessions cannot be read without this passphrase. Each tenant should use a different passphrase for complete isolation.',
618
+ description: 'Deprecated. This field is saved in plain text inside the workflow. Attach a Redactor Vault credential instead, which n8n stores encrypted and which takes priority. Kept so workflows saved with it still restore.',
605
619
  },
606
620
  {
607
621
  displayName: 'Session TTL (Minutes)',
@@ -739,6 +753,18 @@ class Redactor {
739
753
  },
740
754
  ],
741
755
  };
756
+ this.methods = {
757
+ credentialTest: {
758
+ async redactorVaultPassphrase(credential) {
759
+ var _a, _b;
760
+ const passphrase = String((_b = (_a = credential.data) === null || _a === void 0 ? void 0 : _a.passphrase) !== null && _b !== void 0 ? _b : '');
761
+ if (passphrase.length < 12) {
762
+ return { status: 'Error', message: 'Use a passphrase of at least 12 characters.' };
763
+ }
764
+ return { status: 'OK', message: 'Passphrase accepted.' };
765
+ },
766
+ },
767
+ };
742
768
  }
743
769
  async execute() {
744
770
  var _a, _b, _c, _d, _e, _f, _g;
@@ -761,6 +787,7 @@ class Redactor {
761
787
  fieldRules: [],
762
788
  fieldMode: 'all',
763
789
  skipSemantic: operation === 'verify',
790
+ scanNumbers: true,
764
791
  };
765
792
  const allHits = [];
766
793
  for (let i = 0; i < items.length; i++) {
@@ -859,14 +886,9 @@ class Redactor {
859
886
  }]];
860
887
  }
861
888
  const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
862
- let vaultPassphrase = '';
863
- try {
864
- vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
865
- }
866
- catch { }
867
- const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined);
889
+ const vaultPassphrase = vaultStorage === 'memory' ? '' : await (0, runtime_1.resolvePassphrase)(this);
890
+ const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined, (0, runtime_1.workflowNamespace)(this));
868
891
  if (operation === 'stats') {
869
- vault.cleanup();
870
892
  const sessions = vault.listSessions();
871
893
  const totalEntries = sessions.reduce((sum, s) => sum + s.entryCount, 0);
872
894
  return [[
@@ -991,7 +1013,16 @@ class Redactor {
991
1013
  if (profile.auditLog !== undefined)
992
1014
  auditLog = profile.auditLog;
993
1015
  }
994
- vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1016
+ (0, runtime_1.assertUserRegexes)(this, customPatterns, [
1017
+ { name: 'Allow list', entries: allowList },
1018
+ { name: 'Deny list', entries: denyList },
1019
+ ]);
1020
+ try {
1021
+ vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1022
+ }
1023
+ catch (error) {
1024
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
1025
+ }
995
1026
  const ctx = {
996
1027
  enabledPatterns,
997
1028
  customPatterns,
@@ -1005,6 +1036,9 @@ class Redactor {
1005
1036
  presidioUrl: enablePresidio ? presidioUrl : undefined,
1006
1037
  presidioLanguage: enablePresidio ? presidioLanguage : undefined,
1007
1038
  presidioRequest: enablePresidio ? presidioRequester : undefined,
1039
+ scanNumbers: true,
1040
+ reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
1041
+ keyOrigins: new WeakMap(),
1008
1042
  };
1009
1043
  const allHits = [];
1010
1044
  const returnData = [];
@@ -1030,7 +1064,7 @@ class Redactor {
1030
1064
  for (let i = 0; i < items.length; i++) {
1031
1065
  const result = (0, injection_1.scanForInjection)(items[i].json);
1032
1066
  if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
1033
- injectionResult = result;
1067
+ injectionResult = (0, runtime_1.sanitizeInjection)(result);
1034
1068
  }
1035
1069
  }
1036
1070
  }
@@ -1065,7 +1099,7 @@ class Redactor {
1065
1099
  const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
1066
1100
  const session = vault.getSession(sessionId);
1067
1101
  if (!session) {
1068
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}". Ensure a Redact node ran first with the same Session ID and vault storage type.`, {
1102
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}" that this passphrase can open. Ensure a Redact node ran first with the same Session ID, vault storage type and passphrase.`, {
1069
1103
  description: 'Testing step by step? Each "Execute step" gets a new execution ID, and n8n saves workflow storage only after production runs of an active workflow. Run the whole workflow, or set the same fixed Session ID (for example "test-1") on both nodes. Restore only works in the workflow that ran Redact.',
1070
1104
  });
1071
1105
  }
@@ -1087,48 +1121,27 @@ class Redactor {
1087
1121
  if (!purgeSessionId) {
1088
1122
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
1089
1123
  }
1090
- const session = vault.getSession(purgeSessionId);
1091
- if (session) {
1092
- const entryCount = Object.keys(session.entries).length;
1093
- vault.deleteSession(purgeSessionId);
1094
- return [[{
1095
- json: {
1096
- purged: true,
1097
- sessionId: purgeSessionId,
1098
- entriesDeleted: entryCount,
1099
- message: `Session purged successfully.`,
1100
- },
1101
- pairedItem: allPaired,
1102
- }]];
1103
- }
1104
- else {
1105
- return [[{
1106
- json: {
1107
- purged: false,
1108
- sessionId: purgeSessionId,
1109
- message: `No session found with this ID.`,
1110
- },
1111
- pairedItem: allPaired,
1112
- }]];
1113
- }
1114
- }
1115
- else {
1116
- const sessions = vault.listSessions();
1117
- let totalEntries = 0;
1118
- for (const s of sessions) {
1119
- totalEntries += s.entryCount;
1120
- vault.deleteSession(s.sessionId);
1121
- }
1124
+ const purged = vault.purgeSession(purgeSessionId);
1122
1125
  return [[{
1123
1126
  json: {
1124
- purged: true,
1125
- sessionsPurged: sessions.length,
1126
- totalEntriesDeleted: totalEntries,
1127
- message: `All ${sessions.length} session(s) purged successfully.`,
1127
+ purged: purged.found,
1128
+ sessionId: purgeSessionId,
1129
+ ...(purged.found ? { entriesDeleted: purged.entries } : {}),
1130
+ message: purged.found ? 'Session purged successfully.' : 'No session found with this ID.',
1128
1131
  },
1129
1132
  pairedItem: allPaired,
1130
1133
  }]];
1131
1134
  }
1135
+ const all = vault.purgeAll();
1136
+ return [[{
1137
+ json: {
1138
+ purged: true,
1139
+ sessionsPurged: all.sessions,
1140
+ totalEntriesDeleted: all.entries,
1141
+ message: `All ${all.sessions} session(s) purged successfully.`,
1142
+ },
1143
+ pairedItem: allPaired,
1144
+ }]];
1132
1145
  }
1133
1146
  return [[]];
1134
1147
  }