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 +15 -1
- package/dist/credentials/RedactorVaultApi.credentials.js +23 -0
- package/dist/credentials/redactorVault.dark.svg +1 -0
- package/dist/credentials/redactorVault.svg +1 -0
- package/dist/nodes/Redactor/Redactor.node.js +60 -47
- package/dist/nodes/Redactor/engine.js +366 -287
- package/dist/nodes/Redactor/injection.js +7 -1
- package/dist/nodes/Redactor/profiles.js +7 -6
- package/dist/nodes/Redactor/regexSafety.js +114 -0
- package/dist/nodes/Redactor/runtime.js +80 -0
- package/dist/nodes/Redactor/vault.js +145 -70
- package/package.json +7 -4
- package/dist/nodes/Redactor/Redactor.node.d.ts +0 -5
- package/dist/nodes/Redactor/audit.d.ts +0 -21
- package/dist/nodes/Redactor/classification.d.ts +0 -13
- package/dist/nodes/Redactor/context.d.ts +0 -7
- package/dist/nodes/Redactor/encryption.d.ts +0 -4
- package/dist/nodes/Redactor/engine.d.ts +0 -6
- package/dist/nodes/Redactor/injection.d.ts +0 -20
- package/dist/nodes/Redactor/names.d.ts +0 -8
- package/dist/nodes/Redactor/patterns.d.ts +0 -8
- package/dist/nodes/Redactor/presidio.d.ts +0 -36
- package/dist/nodes/Redactor/profiles.d.ts +0 -20
- package/dist/nodes/Redactor/pseudonymize.d.ts +0 -1
- package/dist/nodes/Redactor/ropa.d.ts +0 -51
- package/dist/nodes/Redactor/types.d.ts +0 -73
- package/dist/nodes/Redactor/vault.d.ts +0 -57
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: '
|
|
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
|
-
|
|
863
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
message:
|
|
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
|
}
|