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
|
@@ -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 = [
|
|
@@ -258,7 +264,7 @@ function detectPromptInjection(text) {
|
|
|
258
264
|
matches.push({
|
|
259
265
|
category: 'encoded_payload',
|
|
260
266
|
pattern: 'Base64-encoded injection payload detected',
|
|
261
|
-
matchedText: `
|
|
267
|
+
matchedText: `(decoded payload of ${decoded.length} characters withheld)`,
|
|
262
268
|
severity: 'HIGH',
|
|
263
269
|
score: 0.35,
|
|
264
270
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseTenantProfile = parseTenantProfile;
|
|
4
|
+
const regexSafety_1 = require("./regexSafety");
|
|
4
5
|
const MODES = ['token', 'mask', 'hash', 'redact', 'pseudonymize', 'blackout', 'remove'];
|
|
5
6
|
const FIELD_MODES = ['all', 'allowlist', 'denylist'];
|
|
6
7
|
const MATCH_TYPES = ['exact', 'contains', 'regex'];
|
|
@@ -25,6 +26,9 @@ function entries(value, name, defaultType) {
|
|
|
25
26
|
const type = (_a = e.type) !== null && _a !== void 0 ? _a : defaultType;
|
|
26
27
|
if (typeof type !== 'string' || !MATCH_TYPES.includes(type))
|
|
27
28
|
fail(`${name}[${i}].type must be one of ${MATCH_TYPES.join(', ')}`);
|
|
29
|
+
const problem = type === 'regex' ? (0, regexSafety_1.checkRegex)(e.value) : null;
|
|
30
|
+
if (problem)
|
|
31
|
+
fail(`${name}[${i}].value regex ${problem}`);
|
|
28
32
|
return { value: e.value, type: type };
|
|
29
33
|
});
|
|
30
34
|
}
|
|
@@ -54,12 +58,9 @@ function parseCustomPatterns(value) {
|
|
|
54
58
|
if (!isObject(c) || typeof c.label !== 'string' || typeof c.regex !== 'string') {
|
|
55
59
|
fail(`customPatterns[${i}] needs string label and regex`);
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
catch {
|
|
61
|
-
fail(`customPatterns[${i}].regex is not a valid regular expression`);
|
|
62
|
-
}
|
|
61
|
+
const problem = (0, regexSafety_1.checkRegex)(c.regex);
|
|
62
|
+
if (problem)
|
|
63
|
+
fail(`customPatterns[${i}].regex ${problem}`);
|
|
63
64
|
return { label: c.label, regex: c.regex, category: typeof c.category === 'string' ? c.category : undefined };
|
|
64
65
|
});
|
|
65
66
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkRegex = checkRegex;
|
|
4
|
+
const MAX_REGEX_LENGTH = 500;
|
|
5
|
+
function readBraces(src, i) {
|
|
6
|
+
const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
|
|
7
|
+
if (!m)
|
|
8
|
+
return null;
|
|
9
|
+
const min = Number(m[1]);
|
|
10
|
+
const max = m[2] === undefined ? min : m[3] === '' ? Infinity : Number(m[3]);
|
|
11
|
+
return { min, max, length: m[0].length };
|
|
12
|
+
}
|
|
13
|
+
function readQuantifier(src, i) {
|
|
14
|
+
const c = src[i];
|
|
15
|
+
let q = null;
|
|
16
|
+
if (c === '*')
|
|
17
|
+
q = { min: 0, max: Infinity, length: 1 };
|
|
18
|
+
else if (c === '+')
|
|
19
|
+
q = { min: 1, max: Infinity, length: 1 };
|
|
20
|
+
else if (c === '?')
|
|
21
|
+
q = { min: 0, max: 1, length: 1 };
|
|
22
|
+
else if (c === '{')
|
|
23
|
+
q = readBraces(src, i);
|
|
24
|
+
if (q && src[i + q.length] === '?')
|
|
25
|
+
q.length += 1;
|
|
26
|
+
return q;
|
|
27
|
+
}
|
|
28
|
+
function skipClass(src, i) {
|
|
29
|
+
let j = i + 1;
|
|
30
|
+
if (src[j] === '^')
|
|
31
|
+
j++;
|
|
32
|
+
if (src[j] === ']')
|
|
33
|
+
j++;
|
|
34
|
+
while (j < src.length && src[j] !== ']')
|
|
35
|
+
j += src[j] === '\\' ? 2 : 1;
|
|
36
|
+
return j + 1;
|
|
37
|
+
}
|
|
38
|
+
function groupPrefixLength(src, i) {
|
|
39
|
+
if (src[i + 1] !== '?')
|
|
40
|
+
return 1;
|
|
41
|
+
const named = /^\(\?<[A-Za-z_$][\w$]*>/.exec(src.slice(i));
|
|
42
|
+
if (named)
|
|
43
|
+
return named[0].length;
|
|
44
|
+
const look = /^\(\?(?::|=|!|<=|<!)/.exec(src.slice(i));
|
|
45
|
+
return look ? look[0].length : 1;
|
|
46
|
+
}
|
|
47
|
+
function readToken(src, i, stack) {
|
|
48
|
+
var _a;
|
|
49
|
+
const c = src[i];
|
|
50
|
+
if (c === '\\') {
|
|
51
|
+
if (/[1-9k]/.test((_a = src[i + 1]) !== null && _a !== void 0 ? _a : ''))
|
|
52
|
+
return { atomEnd: i, closed: null, error: 'back-references are not supported' };
|
|
53
|
+
return { atomEnd: i + 2, closed: null };
|
|
54
|
+
}
|
|
55
|
+
if (c === '[')
|
|
56
|
+
return { atomEnd: skipClass(src, i), closed: null };
|
|
57
|
+
if (c === '(') {
|
|
58
|
+
stack.push({ hasVariableQuantifier: false, hasAlternation: false });
|
|
59
|
+
return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
|
|
60
|
+
}
|
|
61
|
+
if (c === '|') {
|
|
62
|
+
stack[stack.length - 1].hasAlternation = true;
|
|
63
|
+
return { atomEnd: i, closed: null, next: i + 1 };
|
|
64
|
+
}
|
|
65
|
+
const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
|
|
66
|
+
return { atomEnd: i + 1, closed };
|
|
67
|
+
}
|
|
68
|
+
const NESTED = 'a repeated group contains a quantifier or alternation (catastrophic backtracking)';
|
|
69
|
+
function isRisky(frame) {
|
|
70
|
+
return !!frame && (frame.hasVariableQuantifier || frame.hasAlternation);
|
|
71
|
+
}
|
|
72
|
+
const NO_GROUP = { hasVariableQuantifier: false, hasAlternation: false };
|
|
73
|
+
const repeats = (q) => q !== null && q.max > 1;
|
|
74
|
+
const isVariable = (q) => q !== null && q.min !== q.max;
|
|
75
|
+
function applyQuantifier(q, closed, top) {
|
|
76
|
+
if (repeats(q) && isRisky(closed))
|
|
77
|
+
return NESTED;
|
|
78
|
+
const inner = closed !== null && closed !== void 0 ? closed : NO_GROUP;
|
|
79
|
+
top.hasVariableQuantifier || (top.hasVariableQuantifier = isVariable(q) || inner.hasVariableQuantifier);
|
|
80
|
+
top.hasAlternation || (top.hasAlternation = inner.hasAlternation);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function structuralProblem(src) {
|
|
84
|
+
const stack = [{ hasVariableQuantifier: false, hasAlternation: false }];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < src.length) {
|
|
87
|
+
const step = readToken(src, i, stack);
|
|
88
|
+
if (step.error)
|
|
89
|
+
return step.error;
|
|
90
|
+
if (step.next !== undefined) {
|
|
91
|
+
i = step.next;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const q = readQuantifier(src, step.atomEnd);
|
|
95
|
+
const problem = applyQuantifier(q, step.closed, stack[stack.length - 1]);
|
|
96
|
+
if (problem)
|
|
97
|
+
return problem;
|
|
98
|
+
i = step.atomEnd + (q ? q.length : 0);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function checkRegex(regexStr) {
|
|
103
|
+
if (typeof regexStr !== 'string' || regexStr.length === 0)
|
|
104
|
+
return 'is empty';
|
|
105
|
+
if (regexStr.length > MAX_REGEX_LENGTH)
|
|
106
|
+
return `is longer than ${MAX_REGEX_LENGTH} characters`;
|
|
107
|
+
try {
|
|
108
|
+
new RegExp(regexStr, 'g');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return 'is not a valid regular expression';
|
|
112
|
+
}
|
|
113
|
+
return structuralProblem(regexStr);
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePassphrase = resolvePassphrase;
|
|
4
|
+
exports.workflowNamespace = workflowNamespace;
|
|
5
|
+
exports.assertUserRegexes = assertUserRegexes;
|
|
6
|
+
exports.sanitizeInjection = sanitizeInjection;
|
|
7
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
+
const regexSafety_1 = require("./regexSafety");
|
|
9
|
+
const injection_1 = require("./injection");
|
|
10
|
+
const VAULT_CREDENTIAL = 'redactorVaultApi';
|
|
11
|
+
async function resolvePassphrase(fns) {
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return fns.getNodeParameter('vaultPassphrase', 0, '') || '';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function workflowNamespace(fns) {
|
|
38
|
+
try {
|
|
39
|
+
const id = fns.getWorkflow().id;
|
|
40
|
+
if (id !== undefined && id !== null && String(id) !== '')
|
|
41
|
+
return `wf:${String(id)}`;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
}
|
|
45
|
+
return 'wf:unsaved';
|
|
46
|
+
}
|
|
47
|
+
function assertUserRegexes(fns, customPatterns, lists) {
|
|
48
|
+
const refuse = (what, problem) => {
|
|
49
|
+
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `${what} regex ${problem}`, {
|
|
50
|
+
description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+.',
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
for (const cp of customPatterns) {
|
|
54
|
+
if (!cp.label || !cp.regex)
|
|
55
|
+
continue;
|
|
56
|
+
const problem = (0, regexSafety_1.checkRegex)(cp.regex);
|
|
57
|
+
if (problem)
|
|
58
|
+
refuse(`Custom pattern "${cp.label}"`, problem);
|
|
59
|
+
}
|
|
60
|
+
for (const list of lists) {
|
|
61
|
+
for (const entry of list.entries) {
|
|
62
|
+
const problem = entry.type === 'regex' ? (0, regexSafety_1.checkRegex)(entry.value) : null;
|
|
63
|
+
if (problem)
|
|
64
|
+
refuse(`${list.name} entry "${entry.value}"`, problem);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sanitizeInjection(result) {
|
|
69
|
+
const matchedPatterns = result.matchedPatterns.map((m) => {
|
|
70
|
+
var _a;
|
|
71
|
+
return ({
|
|
72
|
+
category: m.category,
|
|
73
|
+
pattern: (_a = injection_1.INJECTION_DESCRIPTIONS.find((d) => m.pattern === d || m.pattern.endsWith(` ${d}`))) !== null && _a !== void 0 ? _a : 'Prompt injection detector',
|
|
74
|
+
matchedText: '(withheld)',
|
|
75
|
+
severity: m.severity,
|
|
76
|
+
score: m.score,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return { ...result, matchedPatterns };
|
|
80
|
+
}
|
|
@@ -45,64 +45,101 @@ function isExpired(createdAt, ttl, now = Date.now()) {
|
|
|
45
45
|
const memoryStore = new Map();
|
|
46
46
|
const reverseIndex = new Map();
|
|
47
47
|
const MAX_SESSIONS = 1000;
|
|
48
|
+
const MAX_TOTAL_SESSIONS = 20000;
|
|
49
|
+
function assertNoConflict(session, entry) {
|
|
50
|
+
const existing = session.entries[entry.token];
|
|
51
|
+
if (existing && existing.original !== entry.original) {
|
|
52
|
+
throw new Error(`Vault token "${entry.token}" already maps to a different value in session "${session.sessionId}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
48
55
|
class MemoryVault {
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
|
|
56
|
+
constructor(namespace = '') {
|
|
57
|
+
this.namespace = namespace;
|
|
58
|
+
}
|
|
59
|
+
key(sessionId) {
|
|
60
|
+
return `${this.namespace}\u0000${sessionId}`;
|
|
61
|
+
}
|
|
62
|
+
ownKeys() {
|
|
63
|
+
const prefix = `${this.namespace}\u0000`;
|
|
64
|
+
return Array.from(memoryStore.keys()).filter((k) => k.startsWith(prefix));
|
|
65
|
+
}
|
|
66
|
+
drop(key) {
|
|
67
|
+
memoryStore.delete(key);
|
|
68
|
+
reverseIndex.delete(key);
|
|
69
|
+
}
|
|
70
|
+
evictIfFull(key) {
|
|
71
|
+
if (memoryStore.has(key))
|
|
72
|
+
return;
|
|
73
|
+
const own = this.ownKeys();
|
|
74
|
+
if (own.length >= MAX_SESSIONS)
|
|
75
|
+
this.drop(own[0]);
|
|
76
|
+
if (memoryStore.size >= MAX_TOTAL_SESSIONS) {
|
|
52
77
|
const oldest = memoryStore.keys().next().value;
|
|
53
|
-
if (oldest !== undefined)
|
|
54
|
-
|
|
55
|
-
reverseIndex.delete(oldest);
|
|
56
|
-
}
|
|
78
|
+
if (oldest !== undefined)
|
|
79
|
+
this.drop(oldest);
|
|
57
80
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
81
|
+
}
|
|
82
|
+
getOrCreateSession(sessionId, ttl) {
|
|
83
|
+
this.cleanup();
|
|
84
|
+
const key = this.key(sessionId);
|
|
85
|
+
this.evictIfFull(key);
|
|
86
|
+
if (!memoryStore.has(key)) {
|
|
87
|
+
memoryStore.set(key, { sessionId, entries: {}, createdAt: new Date().toISOString(), ttl });
|
|
65
88
|
}
|
|
66
|
-
return memoryStore.get(
|
|
89
|
+
return memoryStore.get(key);
|
|
67
90
|
}
|
|
68
91
|
getSession(sessionId) {
|
|
69
|
-
const
|
|
92
|
+
const key = this.key(sessionId);
|
|
93
|
+
const session = memoryStore.get(key);
|
|
70
94
|
if (!session)
|
|
71
95
|
return null;
|
|
72
96
|
if (isExpired(session.createdAt, session.ttl)) {
|
|
73
|
-
|
|
74
|
-
reverseIndex.delete(sessionId);
|
|
97
|
+
this.drop(key);
|
|
75
98
|
return null;
|
|
76
99
|
}
|
|
77
100
|
return session;
|
|
78
101
|
}
|
|
79
102
|
addEntry(sessionId, entry) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
const key = this.key(sessionId);
|
|
104
|
+
const session = memoryStore.get(key);
|
|
105
|
+
if (!session)
|
|
106
|
+
return;
|
|
107
|
+
assertNoConflict(session, entry);
|
|
108
|
+
session.entries[entry.token] = entry;
|
|
109
|
+
if (!reverseIndex.has(key))
|
|
110
|
+
reverseIndex.set(key, new Map());
|
|
111
|
+
reverseIndex.get(key).set(entry.original, entry);
|
|
87
112
|
}
|
|
88
113
|
findByOriginal(sessionId, original) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const session = memoryStore.get(sessionId);
|
|
114
|
+
var _a;
|
|
115
|
+
const key = this.key(sessionId);
|
|
116
|
+
const entry = (_a = reverseIndex.get(key)) === null || _a === void 0 ? void 0 : _a.get(original);
|
|
117
|
+
if (entry)
|
|
118
|
+
return entry;
|
|
119
|
+
const session = memoryStore.get(key);
|
|
96
120
|
if (!session)
|
|
97
121
|
return undefined;
|
|
98
122
|
return Object.values(session.entries).find((e) => e.original === original);
|
|
99
123
|
}
|
|
100
124
|
deleteSession(sessionId) {
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
this.drop(this.key(sessionId));
|
|
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 };
|
|
103
137
|
}
|
|
104
138
|
listSessions() {
|
|
105
|
-
return
|
|
139
|
+
return this.ownKeys()
|
|
140
|
+
.map((k) => memoryStore.get(k))
|
|
141
|
+
.filter((s) => !isExpired(s.createdAt, s.ttl))
|
|
142
|
+
.map((s) => ({
|
|
106
143
|
sessionId: s.sessionId,
|
|
107
144
|
entryCount: Object.keys(s.entries).length,
|
|
108
145
|
createdAt: s.createdAt,
|
|
@@ -114,10 +151,9 @@ class MemoryVault {
|
|
|
114
151
|
cleanup() {
|
|
115
152
|
let removed = 0;
|
|
116
153
|
const now = Date.now();
|
|
117
|
-
for (const [
|
|
154
|
+
for (const [key, session] of memoryStore.entries()) {
|
|
118
155
|
if (isExpired(session.createdAt, session.ttl, now)) {
|
|
119
|
-
|
|
120
|
-
reverseIndex.delete(id);
|
|
156
|
+
this.drop(key);
|
|
121
157
|
removed++;
|
|
122
158
|
}
|
|
123
159
|
}
|
|
@@ -132,13 +168,18 @@ class WorkflowVault {
|
|
|
132
168
|
this.store = store;
|
|
133
169
|
this.passphrase = passphrase;
|
|
134
170
|
this.cache = new Map();
|
|
135
|
-
const existing = store[STORE_KEY];
|
|
136
|
-
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
137
|
-
store[STORE_KEY] = {};
|
|
138
|
-
}
|
|
139
|
-
this.cleanup();
|
|
140
171
|
}
|
|
141
172
|
get sessions() {
|
|
173
|
+
const existing = this.store[STORE_KEY];
|
|
174
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
175
|
+
return existing;
|
|
176
|
+
}
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
writableSessions() {
|
|
180
|
+
const existing = this.store[STORE_KEY];
|
|
181
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing))
|
|
182
|
+
this.store[STORE_KEY] = {};
|
|
142
183
|
return this.store[STORE_KEY];
|
|
143
184
|
}
|
|
144
185
|
key(sessionId) {
|
|
@@ -158,23 +199,20 @@ class WorkflowVault {
|
|
|
158
199
|
}
|
|
159
200
|
return null;
|
|
160
201
|
}
|
|
202
|
+
isUnreadable(sessionId) {
|
|
203
|
+
const stored = this.sessions[this.key(sessionId)];
|
|
204
|
+
if (!stored || typeof stored !== 'object' || isExpired(stored.createdAt, stored.ttl))
|
|
205
|
+
return false;
|
|
206
|
+
const session = this.decode(stored);
|
|
207
|
+
return !session || session.sessionId !== sessionId;
|
|
208
|
+
}
|
|
161
209
|
load(sessionId) {
|
|
162
210
|
const cached = this.cache.get(sessionId);
|
|
163
|
-
if (cached)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
return cached;
|
|
169
|
-
}
|
|
170
|
-
const k = this.key(sessionId);
|
|
171
|
-
const stored = this.sessions[k];
|
|
172
|
-
if (!stored)
|
|
211
|
+
if (cached)
|
|
212
|
+
return isExpired(cached.createdAt, cached.ttl) ? null : cached;
|
|
213
|
+
const stored = this.sessions[this.key(sessionId)];
|
|
214
|
+
if (!stored || isExpired(stored.createdAt, stored.ttl))
|
|
173
215
|
return null;
|
|
174
|
-
if (isExpired(stored.createdAt, stored.ttl)) {
|
|
175
|
-
delete this.sessions[k];
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
216
|
const session = this.decode(stored);
|
|
179
217
|
if (!session || session.sessionId !== sessionId)
|
|
180
218
|
return null;
|
|
@@ -186,6 +224,9 @@ class WorkflowVault {
|
|
|
186
224
|
const existing = this.load(sessionId);
|
|
187
225
|
if (existing)
|
|
188
226
|
return existing;
|
|
227
|
+
if (this.isUnreadable(sessionId)) {
|
|
228
|
+
throw new Error(`Session "${sessionId}" exists but cannot be decrypted with this passphrase. Use the passphrase it was written with, or a different Session ID.`);
|
|
229
|
+
}
|
|
189
230
|
const session = { sessionId, entries: {}, createdAt: new Date().toISOString(), ttl };
|
|
190
231
|
this.cache.set(sessionId, session);
|
|
191
232
|
return session;
|
|
@@ -195,8 +236,10 @@ class WorkflowVault {
|
|
|
195
236
|
}
|
|
196
237
|
addEntry(sessionId, entry) {
|
|
197
238
|
const session = this.cache.get(sessionId);
|
|
198
|
-
if (session)
|
|
199
|
-
|
|
239
|
+
if (!session)
|
|
240
|
+
return;
|
|
241
|
+
assertNoConflict(session, entry);
|
|
242
|
+
session.entries[entry.token] = entry;
|
|
200
243
|
}
|
|
201
244
|
findByOriginal(sessionId, original) {
|
|
202
245
|
const session = this.cache.get(sessionId);
|
|
@@ -206,11 +249,34 @@ class WorkflowVault {
|
|
|
206
249
|
}
|
|
207
250
|
deleteSession(sessionId) {
|
|
208
251
|
this.cache.delete(sessionId);
|
|
209
|
-
|
|
252
|
+
const k = this.key(sessionId);
|
|
253
|
+
if (k in this.sessions)
|
|
254
|
+
delete this.writableSessions()[k];
|
|
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 };
|
|
210
274
|
}
|
|
211
275
|
listSessions() {
|
|
212
276
|
const out = [];
|
|
213
277
|
for (const stored of Object.values(this.sessions)) {
|
|
278
|
+
if (!stored || typeof stored !== 'object' || isExpired(stored.createdAt, stored.ttl))
|
|
279
|
+
continue;
|
|
214
280
|
const session = this.decode(stored);
|
|
215
281
|
if (!session)
|
|
216
282
|
continue;
|
|
@@ -227,8 +293,12 @@ class WorkflowVault {
|
|
|
227
293
|
const session = this.cache.get(sessionId);
|
|
228
294
|
if (!session)
|
|
229
295
|
return;
|
|
296
|
+
if (this.isUnreadable(sessionId)) {
|
|
297
|
+
throw new Error(`Refusing to overwrite session "${sessionId}": it cannot be decrypted with this passphrase.`);
|
|
298
|
+
}
|
|
299
|
+
const sessions = this.writableSessions();
|
|
230
300
|
const k = this.key(sessionId);
|
|
231
|
-
if (!
|
|
301
|
+
if (!sessions[k])
|
|
232
302
|
this.evictIfFull();
|
|
233
303
|
const stored = { createdAt: session.createdAt, ttl: session.ttl };
|
|
234
304
|
if (this.passphrase) {
|
|
@@ -237,23 +307,28 @@ class WorkflowVault {
|
|
|
237
307
|
else {
|
|
238
308
|
stored.session = session;
|
|
239
309
|
}
|
|
240
|
-
|
|
310
|
+
sessions[k] = stored;
|
|
241
311
|
}
|
|
242
312
|
evictIfFull() {
|
|
243
|
-
const
|
|
313
|
+
const sessions = this.writableSessions();
|
|
314
|
+
const keys = Object.keys(sessions);
|
|
244
315
|
if (keys.length < MAX_STORED_SESSIONS)
|
|
245
316
|
return;
|
|
246
317
|
keys
|
|
247
|
-
.sort((a, b) => new Date(
|
|
318
|
+
.sort((a, b) => new Date(sessions[a].createdAt).getTime() - new Date(sessions[b].createdAt).getTime())
|
|
248
319
|
.slice(0, keys.length - MAX_STORED_SESSIONS + 1)
|
|
249
|
-
.forEach((k) => delete
|
|
320
|
+
.forEach((k) => delete sessions[k]);
|
|
250
321
|
}
|
|
251
322
|
cleanup() {
|
|
252
323
|
let removed = 0;
|
|
253
324
|
const now = Date.now();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
325
|
+
const stale = Object.entries(this.sessions)
|
|
326
|
+
.filter(([, stored]) => !stored || typeof stored !== 'object' || isExpired(stored.createdAt, stored.ttl, now))
|
|
327
|
+
.map(([k]) => k);
|
|
328
|
+
if (stale.length) {
|
|
329
|
+
const sessions = this.writableSessions();
|
|
330
|
+
for (const k of stale) {
|
|
331
|
+
delete sessions[k];
|
|
257
332
|
removed++;
|
|
258
333
|
}
|
|
259
334
|
}
|
|
@@ -265,9 +340,9 @@ class WorkflowVault {
|
|
|
265
340
|
}
|
|
266
341
|
}
|
|
267
342
|
exports.WorkflowVault = WorkflowVault;
|
|
268
|
-
function createVault(storage, store, passphrase) {
|
|
343
|
+
function createVault(storage, store, passphrase, namespace = '') {
|
|
269
344
|
if ((storage === 'workflow' || storage === 'file') && store) {
|
|
270
345
|
return new WorkflowVault(store, passphrase || '');
|
|
271
346
|
}
|
|
272
|
-
return new MemoryVault();
|
|
347
|
+
return new MemoryVault(namespace);
|
|
273
348
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-redactor",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.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",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"build": "n8n-node build",
|
|
64
64
|
"build:watch": "tsc --watch",
|
|
65
65
|
"dev": "n8n-node dev",
|
|
66
|
-
"lint": "eslint package.json nodes",
|
|
67
|
-
"lint:fix": "eslint package.json nodes --fix",
|
|
66
|
+
"lint": "eslint package.json nodes credentials",
|
|
67
|
+
"lint:fix": "eslint package.json nodes credentials --fix",
|
|
68
68
|
"test": "jest",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
70
|
"hooks": "git config core.hooksPath .husky"
|
|
@@ -72,13 +72,16 @@
|
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
|
74
74
|
"!dist/**/*.map",
|
|
75
|
+
"!dist/**/*.d.ts",
|
|
75
76
|
"README.md",
|
|
76
77
|
"LICENSE"
|
|
77
78
|
],
|
|
78
79
|
"n8n": {
|
|
79
80
|
"n8nNodesApiVersion": 1,
|
|
80
81
|
"strict": false,
|
|
81
|
-
"credentials": [
|
|
82
|
+
"credentials": [
|
|
83
|
+
"dist/credentials/RedactorVaultApi.credentials.js"
|
|
84
|
+
],
|
|
82
85
|
"nodes": [
|
|
83
86
|
"dist/nodes/Redactor/Redactor.node.js"
|
|
84
87
|
]
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { RedactionHit, RedactionMode } from './types';
|
|
2
|
-
import { ClassificationLabel } from './classification';
|
|
3
|
-
export interface AuditLogEntry {
|
|
4
|
-
version: string;
|
|
5
|
-
timestamp: string;
|
|
6
|
-
eventId: string;
|
|
7
|
-
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
8
|
-
action: string;
|
|
9
|
-
mode: RedactionMode | string;
|
|
10
|
-
status: 'SUCCESS' | 'PARTIAL' | 'FAILED';
|
|
11
|
-
totalHits: number;
|
|
12
|
-
hitsByCategory: Record<string, number>;
|
|
13
|
-
hitsByPattern: Record<string, number>;
|
|
14
|
-
classificationLevel?: string;
|
|
15
|
-
inputItemCount: number;
|
|
16
|
-
processingTimeMs: number;
|
|
17
|
-
patternsUsed: number;
|
|
18
|
-
presidioEnabled: boolean;
|
|
19
|
-
sessionId?: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function buildAuditEntry(action: string, mode: RedactionMode | string, hits: RedactionHit[], inputItemCount: number, processingTimeMs: number, patternsUsed: number, presidioEnabled: boolean, classification?: ClassificationLabel, sessionId?: string): AuditLogEntry;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { PiiCategory, RedactionHit } from './types';
|
|
2
|
-
export type SensitivityLevel = 'PUBLIC' | 'INTERNAL' | 'CONFIDENTIAL' | 'RESTRICTED';
|
|
3
|
-
export interface ClassificationLabel {
|
|
4
|
-
level: SensitivityLevel;
|
|
5
|
-
numericLevel: 0 | 1 | 2 | 3;
|
|
6
|
-
reason: string;
|
|
7
|
-
topCategory: PiiCategory | 'none';
|
|
8
|
-
hitCountByLevel: Record<SensitivityLevel, number>;
|
|
9
|
-
escalated: boolean;
|
|
10
|
-
totalHits: number;
|
|
11
|
-
averageConfidence: number;
|
|
12
|
-
}
|
|
13
|
-
export declare function classifyData(hits: RedactionHit[]): ClassificationLabel;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare const CONTEXT_WORDS: Record<string, string[]>;
|
|
2
|
-
export declare const SEMANTIC_CONFIDENCE = 0.9;
|
|
3
|
-
export declare const AMBIGUOUS_FIELD_CONFIDENCE = 0.85;
|
|
4
|
-
export declare const CUSTOM_PATTERN_CONFIDENCE = 0.7;
|
|
5
|
-
export declare const DENY_LIST_CONFIDENCE = 1;
|
|
6
|
-
export declare function hasContextWords(text: string, matchStart: number, matchEnd: number, words: string[]): boolean;
|
|
7
|
-
export declare function calculateConfidence(patternName: string, hasValidator: boolean, validatorPassed: boolean, text: string, matchStart: number, matchEnd: number): number;
|