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.
@@ -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: `Decoded: "${decoded.slice(0, 50)}..."`,
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
- try {
58
- new RegExp(c.regex);
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
- getOrCreateSession(sessionId, ttl) {
50
- this.cleanup();
51
- if (memoryStore.size >= MAX_SESSIONS && !memoryStore.has(sessionId)) {
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
- memoryStore.delete(oldest);
55
- reverseIndex.delete(oldest);
56
- }
78
+ if (oldest !== undefined)
79
+ this.drop(oldest);
57
80
  }
58
- if (!memoryStore.has(sessionId)) {
59
- memoryStore.set(sessionId, {
60
- sessionId,
61
- entries: {},
62
- createdAt: new Date().toISOString(),
63
- ttl,
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(sessionId);
89
+ return memoryStore.get(key);
67
90
  }
68
91
  getSession(sessionId) {
69
- const session = memoryStore.get(sessionId);
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
- memoryStore.delete(sessionId);
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 session = memoryStore.get(sessionId);
81
- if (session) {
82
- session.entries[entry.token] = entry;
83
- if (!reverseIndex.has(sessionId))
84
- reverseIndex.set(sessionId, new Map());
85
- reverseIndex.get(sessionId).set(entry.original, entry);
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
- const index = reverseIndex.get(sessionId);
90
- if (index) {
91
- const entry = index.get(original);
92
- if (entry)
93
- return entry;
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
- memoryStore.delete(sessionId);
102
- reverseIndex.delete(sessionId);
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 Array.from(memoryStore.values()).map((s) => ({
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 [id, session] of memoryStore.entries()) {
154
+ for (const [key, session] of memoryStore.entries()) {
118
155
  if (isExpired(session.createdAt, session.ttl, now)) {
119
- memoryStore.delete(id);
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
- if (isExpired(cached.createdAt, cached.ttl)) {
165
- this.deleteSession(sessionId);
166
- return null;
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
- session.entries[entry.token] = entry;
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
- delete this.sessions[this.key(sessionId)];
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 (!this.sessions[k])
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
- this.sessions[k] = stored;
310
+ sessions[k] = stored;
241
311
  }
242
312
  evictIfFull() {
243
- const keys = Object.keys(this.sessions);
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(this.sessions[a].createdAt).getTime() - new Date(this.sessions[b].createdAt).getTime())
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 this.sessions[k]);
320
+ .forEach((k) => delete sessions[k]);
250
321
  }
251
322
  cleanup() {
252
323
  let removed = 0;
253
324
  const now = Date.now();
254
- for (const [k, stored] of Object.entries(this.sessions)) {
255
- if (!stored || typeof stored !== 'object' || isExpired(stored.createdAt, stored.ttl, now)) {
256
- delete this.sessions[k];
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.0.0",
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,5 +0,0 @@
1
- import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
- export declare class Redactor implements INodeType {
3
- description: INodeTypeDescription;
4
- execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
- }
@@ -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;