n8n-nodes-redactor 3.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.
Files changed (61) hide show
  1. package/LICENSE +42 -0
  2. package/README.dev.md +153 -0
  3. package/README.md +443 -0
  4. package/README.npm.md +443 -0
  5. package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
  6. package/dist/nodes/PiiRedactor/PiiRedactor.node.js +1093 -0
  7. package/dist/nodes/PiiRedactor/__tests__/encryption.test.d.ts +1 -0
  8. package/dist/nodes/PiiRedactor/__tests__/encryption.test.js +200 -0
  9. package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
  10. package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
  11. package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
  12. package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
  13. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
  14. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
  15. package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
  16. package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
  17. package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
  18. package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
  19. package/dist/nodes/PiiRedactor/__tests__/phase3.test.d.ts +1 -0
  20. package/dist/nodes/PiiRedactor/__tests__/phase3.test.js +275 -0
  21. package/dist/nodes/PiiRedactor/__tests__/phase4.test.d.ts +1 -0
  22. package/dist/nodes/PiiRedactor/__tests__/phase4.test.js +184 -0
  23. package/dist/nodes/PiiRedactor/__tests__/presidio.test.d.ts +1 -0
  24. package/dist/nodes/PiiRedactor/__tests__/presidio.test.js +170 -0
  25. package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
  26. package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
  27. package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
  28. package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
  29. package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
  30. package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
  31. package/dist/nodes/PiiRedactor/audit.d.ts +48 -0
  32. package/dist/nodes/PiiRedactor/audit.js +192 -0
  33. package/dist/nodes/PiiRedactor/classification.d.ts +33 -0
  34. package/dist/nodes/PiiRedactor/classification.js +118 -0
  35. package/dist/nodes/PiiRedactor/context.d.ts +57 -0
  36. package/dist/nodes/PiiRedactor/context.js +260 -0
  37. package/dist/nodes/PiiRedactor/encryption.d.ts +45 -0
  38. package/dist/nodes/PiiRedactor/encryption.js +158 -0
  39. package/dist/nodes/PiiRedactor/engine.d.ts +23 -0
  40. package/dist/nodes/PiiRedactor/engine.js +888 -0
  41. package/dist/nodes/PiiRedactor/injection.d.ts +46 -0
  42. package/dist/nodes/PiiRedactor/injection.js +425 -0
  43. package/dist/nodes/PiiRedactor/names.d.ts +25 -0
  44. package/dist/nodes/PiiRedactor/names.js +188 -0
  45. package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
  46. package/dist/nodes/PiiRedactor/patterns.js +1742 -0
  47. package/dist/nodes/PiiRedactor/presidio.d.ts +77 -0
  48. package/dist/nodes/PiiRedactor/presidio.js +264 -0
  49. package/dist/nodes/PiiRedactor/profiles.d.ts +47 -0
  50. package/dist/nodes/PiiRedactor/profiles.js +139 -0
  51. package/dist/nodes/PiiRedactor/pseudonymize.d.ts +20 -0
  52. package/dist/nodes/PiiRedactor/pseudonymize.js +203 -0
  53. package/dist/nodes/PiiRedactor/redact.png +0 -0
  54. package/dist/nodes/PiiRedactor/redact.svg +3 -0
  55. package/dist/nodes/PiiRedactor/ropa.d.ts +63 -0
  56. package/dist/nodes/PiiRedactor/ropa.js +70 -0
  57. package/dist/nodes/PiiRedactor/types.d.ts +82 -0
  58. package/dist/nodes/PiiRedactor/types.js +3 -0
  59. package/dist/nodes/PiiRedactor/vault.d.ts +61 -0
  60. package/dist/nodes/PiiRedactor/vault.js +352 -0
  61. package/package.json +87 -0
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vault_1 = require("../vault");
4
+ const engine_1 = require("../engine");
5
+ function createContext(overrides = {}) {
6
+ return {
7
+ enabledPatterns: ['email', 'phone', 'personName', 'ssn'],
8
+ customPatterns: [],
9
+ mode: 'token',
10
+ dedup: true,
11
+ fieldRules: [],
12
+ fieldMode: 'all',
13
+ ...overrides,
14
+ };
15
+ }
16
+ // ═══════════════════════════════════════════════════════
17
+ // SEMANTIC FIELD-NAME DETECTION
18
+ // ═══════════════════════════════════════════════════════
19
+ describe('Semantic detection: Person names', () => {
20
+ let vault;
21
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
22
+ test('redacts "name" field without title prefix', () => {
23
+ vault.getOrCreateSession('s1', 0);
24
+ const hits = [];
25
+ const result = (0, engine_1.redactValue)({ name: 'Amelia Ramirez' }, createContext(), vault, 's1', hits, 0);
26
+ expect(result.name).toContain('[PERSON_NAME_');
27
+ expect(result.name).not.toBe('Amelia Ramirez');
28
+ });
29
+ test('redacts "firstName" and "lastName" fields', () => {
30
+ vault.getOrCreateSession('s2', 0);
31
+ const hits = [];
32
+ const result = (0, engine_1.redactValue)({ firstName: 'Sarah', lastName: 'Johnson' }, createContext(), vault, 's2', hits, 0);
33
+ expect(result.firstName).toContain('[PERSON_NAME_');
34
+ expect(result.lastName).toContain('[PERSON_NAME_');
35
+ });
36
+ test('redacts German field names: Vorname, Nachname', () => {
37
+ vault.getOrCreateSession('s3', 0);
38
+ const hits = [];
39
+ const result = (0, engine_1.redactValue)({ Vorname: 'Hans', Nachname: 'Mueller' }, createContext(), vault, 's3', hits, 0);
40
+ expect(result.Vorname).toContain('[PERSON_NAME_');
41
+ expect(result.Nachname).toContain('[PERSON_NAME_');
42
+ });
43
+ test('redacts nested name fields', () => {
44
+ vault.getOrCreateSession('s4', 0);
45
+ const hits = [];
46
+ const result = (0, engine_1.redactValue)({ customer: { name: 'Liam Clark' } }, createContext(), vault, 's4', hits, 0);
47
+ expect(result.customer.name).toContain('[PERSON_NAME_');
48
+ });
49
+ test('redacts displayName, contactName, customerName', () => {
50
+ vault.getOrCreateSession('s5', 0);
51
+ const hits = [];
52
+ const result = (0, engine_1.redactValue)({ displayName: 'Grace Taylor', contactName: 'Mia Lewis', customerName: 'Ryan Torres' }, createContext(), vault, 's5', hits, 0);
53
+ expect(result.displayName).toContain('[PERSON_NAME_');
54
+ expect(result.contactName).toContain('[PERSON_NAME_');
55
+ expect(result.customerName).toContain('[PERSON_NAME_');
56
+ });
57
+ test('redacts French/Spanish field names', () => {
58
+ vault.getOrCreateSession('s6', 0);
59
+ const hits = [];
60
+ const result = (0, engine_1.redactValue)({ nom: 'Dupont', prenom: 'Jean', nombre: 'Carlos', apellido: 'Garcia' }, createContext(), vault, 's6', hits, 0);
61
+ expect(result.nom).toContain('[PERSON_NAME_');
62
+ expect(result.prenom).toContain('[PERSON_NAME_');
63
+ expect(result.nombre).toContain('[PERSON_NAME_');
64
+ expect(result.apellido).toContain('[PERSON_NAME_');
65
+ });
66
+ });
67
+ describe('Semantic detection: Employee / HR data', () => {
68
+ let vault;
69
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
70
+ test('redacts employeeId field', () => {
71
+ vault.getOrCreateSession('hr1', 0);
72
+ const hits = [];
73
+ const result = (0, engine_1.redactValue)({ employeeId: 'D41BRX4M' }, createContext(), vault, 'hr1', hits, 0);
74
+ expect(result.employeeId).toContain('[EMPLOYEE_ID_');
75
+ });
76
+ test('redacts department field', () => {
77
+ vault.getOrCreateSession('hr2', 0);
78
+ const hits = [];
79
+ const result = (0, engine_1.redactValue)({ department: 'Finance' }, createContext(), vault, 'hr2', hits, 0);
80
+ expect(result.department).toContain('[DEPARTMENT_');
81
+ });
82
+ test('redacts position/jobTitle field', () => {
83
+ vault.getOrCreateSession('hr3', 0);
84
+ const hits = [];
85
+ const result = (0, engine_1.redactValue)({ position: 'Senior Developer', jobTitle: 'CTO' }, createContext(), vault, 'hr3', hits, 0);
86
+ expect(result.position).toContain('[JOB_TITLE_');
87
+ expect(result.jobTitle).toContain('[JOB_TITLE_');
88
+ });
89
+ test('redacts numeric salary field', () => {
90
+ vault.getOrCreateSession('hr4', 0);
91
+ const hits = [];
92
+ const result = (0, engine_1.redactValue)({ salary: 110905 }, createContext(), vault, 'hr4', hits, 0);
93
+ expect(result.salary).toContain('[SALARY_');
94
+ });
95
+ test('redacts hireDate field', () => {
96
+ vault.getOrCreateSession('hr5', 0);
97
+ const hits = [];
98
+ const result = (0, engine_1.redactValue)({ hireDate: '2024-01-15' }, createContext(), vault, 'hr5', hits, 0);
99
+ expect(result.hireDate).toContain('[EMPLOYMENT_DATE_');
100
+ // Should NOT be broken into postal code + card expiry
101
+ expect(result.hireDate).not.toContain('POSTCODE');
102
+ expect(result.hireDate).not.toContain('CARD_EXPIRY');
103
+ });
104
+ test('redacts German HR field names', () => {
105
+ vault.getOrCreateSession('hr6', 0);
106
+ const hits = [];
107
+ const result = (0, engine_1.redactValue)({ Personalnummer: 'EMP-4821', Abteilung: 'Vertrieb', Stelle: 'Teamleiter' }, createContext(), vault, 'hr6', hits, 0);
108
+ expect(result.Personalnummer).toContain('[EMPLOYEE_ID_');
109
+ expect(result.Abteilung).toContain('[DEPARTMENT_');
110
+ expect(result.Stelle).toContain('[JOB_TITLE_');
111
+ });
112
+ });
113
+ describe('Semantic detection: Address fields', () => {
114
+ let vault;
115
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
116
+ test('redacts address field', () => {
117
+ vault.getOrCreateSession('addr1', 0);
118
+ const hits = [];
119
+ const result = (0, engine_1.redactValue)({ address: '123 Main Street, Apt 4B' }, createContext(), vault, 'addr1', hits, 0);
120
+ expect(result.address).toContain('[ADDRESS_');
121
+ });
122
+ test('redacts city, state, zipCode fields', () => {
123
+ vault.getOrCreateSession('addr2', 0);
124
+ const hits = [];
125
+ const result = (0, engine_1.redactValue)({ city: 'Munich', state: 'Bavaria', zipCode: '80331' }, createContext(), vault, 'addr2', hits, 0);
126
+ expect(result.city).toContain('[ADDRESS_');
127
+ expect(result.state).toContain('[ADDRESS_');
128
+ expect(result.zipCode).toContain('[ADDRESS_');
129
+ });
130
+ test('redacts German address fields', () => {
131
+ vault.getOrCreateSession('addr3', 0);
132
+ const hits = [];
133
+ const result = (0, engine_1.redactValue)({ Anschrift: 'Musterstr. 12', PLZ: '80331', Adresse: 'Berlin' }, createContext(), vault, 'addr3', hits, 0);
134
+ expect(result.Anschrift).toContain('[ADDRESS_');
135
+ expect(result.PLZ).toContain('[ADDRESS_');
136
+ expect(result.Adresse).toContain('[ADDRESS_');
137
+ });
138
+ });
139
+ describe('Semantic detection: Financial fields', () => {
140
+ let vault;
141
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
142
+ test('redacts account_number field', () => {
143
+ vault.getOrCreateSession('fin1', 0);
144
+ const hits = [];
145
+ const result = (0, engine_1.redactValue)({ accountNumber: 'DE89370400440532013000' }, createContext(), vault, 'fin1', hits, 0);
146
+ expect(result.accountNumber).toContain('[ACCOUNT_NUMBER_');
147
+ });
148
+ test('redacts insurance policy field', () => {
149
+ vault.getOrCreateSession('fin2', 0);
150
+ const hits = [];
151
+ const result = (0, engine_1.redactValue)({ insurancePolicy: 'POL-2024-XYZ' }, createContext(), vault, 'fin2', hits, 0);
152
+ expect(result.insurancePolicy).toContain('[INSURANCE_');
153
+ });
154
+ });
155
+ describe('Semantic detection: Company/Organization', () => {
156
+ let vault;
157
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
158
+ test('redacts company field', () => {
159
+ vault.getOrCreateSession('comp1', 0);
160
+ const hits = [];
161
+ const result = (0, engine_1.redactValue)({ company: 'Acme Corporation' }, createContext(), vault, 'comp1', hits, 0);
162
+ expect(result.company).toContain('[COMPANY_');
163
+ });
164
+ test('redacts employer, organization fields', () => {
165
+ vault.getOrCreateSession('comp2', 0);
166
+ const hits = [];
167
+ const result = (0, engine_1.redactValue)({ employer: 'BigCorp GmbH', organization: 'Red Cross' }, createContext(), vault, 'comp2', hits, 0);
168
+ expect(result.employer).toContain('[COMPANY_');
169
+ expect(result.organization).toContain('[COMPANY_');
170
+ });
171
+ test('redacts German: Firma, Unternehmen, Arbeitgeber', () => {
172
+ vault.getOrCreateSession('comp3', 0);
173
+ const hits = [];
174
+ const result = (0, engine_1.redactValue)({ Firma: 'Siemens AG', Arbeitgeber: 'BMW Group' }, createContext(), vault, 'comp3', hits, 0);
175
+ expect(result.Firma).toContain('[COMPANY_');
176
+ expect(result.Arbeitgeber).toContain('[COMPANY_');
177
+ });
178
+ });
179
+ describe('Semantic detection: Full round-trip with real-world data', () => {
180
+ let vault;
181
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
182
+ test('complete employee record redaction and restoration', () => {
183
+ vault.getOrCreateSession('rt1', 0);
184
+ const hits = [];
185
+ const original = {
186
+ employeeId: 'D41BRX4M',
187
+ name: 'Amelia Ramirez',
188
+ email: 'amelia.ramirez@company.com',
189
+ department: 'Finance',
190
+ position: 'Senior Analyst',
191
+ hireDate: '2024-01-15',
192
+ salary: 85000,
193
+ company: 'Exacta GmbH',
194
+ address: 'Berliner Str. 45, 10115 Berlin',
195
+ };
196
+ const redacted = (0, engine_1.redactValue)(original, createContext(), vault, 'rt1', hits, 0);
197
+ // Every field should be redacted
198
+ expect(redacted.employeeId).toContain('[');
199
+ expect(redacted.name).toContain('[PERSON_NAME_');
200
+ expect(redacted.email).toContain('['); // caught by regex or semantic
201
+ expect(redacted.department).toContain('[DEPARTMENT_');
202
+ expect(redacted.position).toContain('[JOB_TITLE_');
203
+ expect(redacted.hireDate).toContain('[EMPLOYMENT_DATE_');
204
+ expect(redacted.salary).toContain('[SALARY_');
205
+ expect(redacted.company).toContain('[COMPANY_');
206
+ expect(redacted.address).toContain('[ADDRESS_');
207
+ // None of the original values should be visible
208
+ const json = JSON.stringify(redacted);
209
+ expect(json).not.toContain('Amelia Ramirez');
210
+ expect(json).not.toContain('Finance');
211
+ expect(json).not.toContain('Senior Analyst');
212
+ expect(json).not.toContain('85000');
213
+ expect(json).not.toContain('Exacta GmbH');
214
+ // Restore should bring everything back
215
+ const restored = (0, engine_1.restoreValue)(redacted, vault, 'rt1');
216
+ expect(restored.name).toBe('Amelia Ramirez');
217
+ expect(restored.department).toBe('Finance');
218
+ expect(restored.position).toBe('Senior Analyst');
219
+ expect(restored.salary).toBe('85000'); // restored as string (currency-safe)
220
+ expect(restored.company).toBe('Exacta GmbH');
221
+ });
222
+ });
223
+ describe('Semantic detection: Language/cultural fields', () => {
224
+ let vault;
225
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
226
+ test('redacts "language" field as GDPR Art.9 sensitive', () => {
227
+ vault.getOrCreateSession('lang1', 0);
228
+ const hits = [];
229
+ const result = (0, engine_1.redactValue)({ name: 'Adeel', language: 'Sindhi' }, createContext(), vault, 'lang1', hits, 0);
230
+ expect(result.language).toContain('[LANGUAGE_');
231
+ expect(result.language).not.toBe('Sindhi');
232
+ });
233
+ test('redacts mother tongue fields', () => {
234
+ vault.getOrCreateSession('lang2', 0);
235
+ const hits = [];
236
+ const result = (0, engine_1.redactValue)({ motherTongue: 'Urdu', Muttersprache: 'Deutsch' }, createContext(), vault, 'lang2', hits, 0);
237
+ expect(result.motherTongue).toContain('[LANGUAGE_');
238
+ expect(result.Muttersprache).toContain('[LANGUAGE_');
239
+ });
240
+ });
241
+ describe('Semantic detection: Context-aware ambiguous fields', () => {
242
+ let vault;
243
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
244
+ test('redacts bare "id" when sibling has "name" (PII context)', () => {
245
+ vault.getOrCreateSession('ctx1', 0);
246
+ const hits = [];
247
+ const result = (0, engine_1.redactValue)({ id: 'D41BRX4M', name: 'Amelia Ramirez', department: 'Finance' }, createContext(), vault, 'ctx1', hits, 0);
248
+ expect(result.id).toContain('[IDENTIFIER_');
249
+ expect(result.id).not.toBe('D41BRX4M');
250
+ });
251
+ test('does NOT redact bare "id" when no PII sibling fields', () => {
252
+ vault.getOrCreateSession('ctx2', 0);
253
+ const hits = [];
254
+ const result = (0, engine_1.redactValue)({ id: '12345', status: 'active', type: 'widget' }, createContext(), vault, 'ctx2', hits, 0);
255
+ // No PII indicator siblings, so id should NOT be redacted
256
+ expect(result.id).toBe('12345');
257
+ });
258
+ test('redacts "code" and "ref" when email sibling exists', () => {
259
+ vault.getOrCreateSession('ctx3', 0);
260
+ const hits = [];
261
+ const result = (0, engine_1.redactValue)({ code: 'EMP-4821', ref: 'REF-999', email: 'john@test.com' }, createContext(), vault, 'ctx3', hits, 0);
262
+ expect(result.code).toContain('[IDENTIFIER_');
263
+ expect(result.ref).toContain('[IDENTIFIER_');
264
+ });
265
+ test('redacts "number" when salary sibling exists', () => {
266
+ vault.getOrCreateSession('ctx4', 0);
267
+ const hits = [];
268
+ const result = (0, engine_1.redactValue)({ number: 'ACC-12345', salary: 85000, name: 'Jane Doe' }, createContext(), vault, 'ctx4', hits, 0);
269
+ expect(result.number).toContain('[IDENTIFIER_');
270
+ });
271
+ test('does NOT redact "status" in non-PII record', () => {
272
+ vault.getOrCreateSession('ctx5', 0);
273
+ const hits = [];
274
+ const result = (0, engine_1.redactValue)({ status: 'completed', category: 'urgent', label: 'bug' }, createContext(), vault, 'ctx5', hits, 0);
275
+ expect(result.status).toBe('completed');
276
+ expect(result.category).toBe('urgent');
277
+ expect(result.label).toBe('bug');
278
+ });
279
+ test('context-aware detection in nested objects', () => {
280
+ vault.getOrCreateSession('ctx6', 0);
281
+ const hits = [];
282
+ const result = (0, engine_1.redactValue)({ user: { id: 'USR-999', name: 'John Smith', code: 'ABC123' } }, createContext(), vault, 'ctx6', hits, 0);
283
+ expect(result.user.id).toContain('[IDENTIFIER_');
284
+ expect(result.user.code).toContain('[IDENTIFIER_');
285
+ });
286
+ test('full employee record with ambiguous fields all redacted', () => {
287
+ vault.getOrCreateSession('ctx7', 0);
288
+ const hits = [];
289
+ const result = (0, engine_1.redactValue)({
290
+ id: 'V59OF92YF62',
291
+ name: 'Adeel Solangi',
292
+ language: 'Sindhi',
293
+ code: 'EMP001',
294
+ ref: 'HR-2024-001',
295
+ email: 'adeel@company.com',
296
+ }, createContext(), vault, 'ctx7', hits, 0);
297
+ const json = JSON.stringify(result);
298
+ expect(json).not.toContain('V59OF92YF62');
299
+ expect(json).not.toContain('Adeel Solangi');
300
+ expect(json).not.toContain('Sindhi');
301
+ expect(json).not.toContain('EMP001');
302
+ expect(json).not.toContain('HR-2024-001');
303
+ expect(json).not.toContain('adeel@company.com');
304
+ });
305
+ });
306
+ describe('Semantic detection: Notes/text fields fall through to regex', () => {
307
+ let vault;
308
+ beforeEach(() => { vault = new vault_1.MemoryVault(); });
309
+ test('notes field is NOT fully redacted but regex runs on it', () => {
310
+ vault.getOrCreateSession('notes1', 0);
311
+ const hits = [];
312
+ const result = (0, engine_1.redactValue)({ notes: 'Customer john@test.com called about order' }, createContext(), vault, 'notes1', hits, 0);
313
+ // The whole field should NOT be a single token
314
+ expect(result.notes).toContain('called about order');
315
+ // But the email inside should be redacted
316
+ expect(result.notes).toContain('[EMAIL_');
317
+ expect(result.notes).not.toContain('john@test.com');
318
+ });
319
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
38
+ const os = __importStar(require("os"));
39
+ const vault_1 = require("../vault");
40
+ function makeEntry(token, original) {
41
+ return {
42
+ token,
43
+ original,
44
+ patternLabel: 'EMAIL',
45
+ category: 'contact',
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ }
49
+ // ═══════════════════════════════════════════════════════
50
+ // MEMORY VAULT
51
+ // ═══════════════════════════════════════════════════════
52
+ describe('MemoryVault', () => {
53
+ let vault;
54
+ beforeEach(() => {
55
+ vault = new vault_1.MemoryVault();
56
+ });
57
+ test('creates a new session', () => {
58
+ const session = vault.getOrCreateSession('test-1', 0);
59
+ expect(session.sessionId).toBe('test-1');
60
+ expect(Object.keys(session.entries)).toHaveLength(0);
61
+ });
62
+ test('returns existing session on second call', () => {
63
+ vault.getOrCreateSession('test-2', 0);
64
+ vault.addEntry('test-2', makeEntry('[EMAIL_0]', 'a@b.com'));
65
+ const session = vault.getOrCreateSession('test-2', 0);
66
+ expect(Object.keys(session.entries)).toHaveLength(1);
67
+ });
68
+ test('adds and retrieves entries', () => {
69
+ vault.getOrCreateSession('test-3', 0);
70
+ vault.addEntry('test-3', makeEntry('[EMAIL_0]', 'alice@example.com'));
71
+ vault.addEntry('test-3', makeEntry('[PHONE_1]', '+49123456'));
72
+ const session = vault.getSession('test-3');
73
+ expect(session).not.toBeNull();
74
+ expect(Object.keys(session.entries)).toHaveLength(2);
75
+ expect(session.entries['[EMAIL_0]'].original).toBe('alice@example.com');
76
+ });
77
+ test('findByOriginal returns matching entry', () => {
78
+ vault.getOrCreateSession('test-4', 0);
79
+ vault.addEntry('test-4', makeEntry('[EMAIL_0]', 'test@test.com'));
80
+ const found = vault.findByOriginal('test-4', 'test@test.com');
81
+ expect(found).toBeDefined();
82
+ expect(found.token).toBe('[EMAIL_0]');
83
+ });
84
+ test('findByOriginal returns undefined for non-existent', () => {
85
+ vault.getOrCreateSession('test-5', 0);
86
+ expect(vault.findByOriginal('test-5', 'nope')).toBeUndefined();
87
+ });
88
+ test('deleteSession removes session', () => {
89
+ vault.getOrCreateSession('test-6', 0);
90
+ vault.deleteSession('test-6');
91
+ expect(vault.getSession('test-6')).toBeNull();
92
+ });
93
+ test('listSessions returns all active sessions', () => {
94
+ vault.getOrCreateSession('s1', 0);
95
+ vault.getOrCreateSession('s2', 0);
96
+ vault.addEntry('s1', makeEntry('[EMAIL_0]', 'a@b.com'));
97
+ const sessions = vault.listSessions();
98
+ expect(sessions.length).toBeGreaterThanOrEqual(2);
99
+ const s1 = sessions.find((s) => s.sessionId === 's1');
100
+ expect(s1?.entryCount).toBe(1);
101
+ });
102
+ test('TTL expiry — expired session returns null', () => {
103
+ // Create session with 1ms TTL
104
+ vault.getOrCreateSession('ttl-test', 1);
105
+ vault.addEntry('ttl-test', makeEntry('[EMAIL_0]', 'a@b.com'));
106
+ // Wait for expiry
107
+ const start = Date.now();
108
+ while (Date.now() - start < 10) {
109
+ // busy wait
110
+ }
111
+ expect(vault.getSession('ttl-test')).toBeNull();
112
+ });
113
+ test('TTL = 0 means no expiry', () => {
114
+ vault.getOrCreateSession('no-ttl', 0);
115
+ vault.addEntry('no-ttl', makeEntry('[EMAIL_0]', 'a@b.com'));
116
+ expect(vault.getSession('no-ttl')).not.toBeNull();
117
+ });
118
+ test('cleanup removes expired sessions', () => {
119
+ vault.getOrCreateSession('expired', 1);
120
+ vault.getOrCreateSession('alive', 0);
121
+ const start = Date.now();
122
+ while (Date.now() - start < 10) {
123
+ // busy wait
124
+ }
125
+ const removed = vault.cleanup();
126
+ expect(removed).toBeGreaterThanOrEqual(1);
127
+ expect(vault.getSession('alive')).not.toBeNull();
128
+ });
129
+ });
130
+ // ═══════════════════════════════════════════════════════
131
+ // FILE VAULT
132
+ // ═══════════════════════════════════════════════════════
133
+ describe('FileVault', () => {
134
+ let vault;
135
+ let testDir;
136
+ beforeEach(() => {
137
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pii-vault-test-'));
138
+ vault = new vault_1.FileVault(testDir);
139
+ });
140
+ afterEach(() => {
141
+ // Cleanup
142
+ try {
143
+ fs.rmSync(testDir, { recursive: true, force: true });
144
+ }
145
+ catch {
146
+ // ignore
147
+ }
148
+ });
149
+ test('creates vault directory', () => {
150
+ expect(fs.existsSync(testDir)).toBe(true);
151
+ });
152
+ test('creates and persists session to file', () => {
153
+ vault.getOrCreateSession('file-test-1', 0);
154
+ vault.addEntry('file-test-1', makeEntry('[EMAIL_0]', 'test@file.com'));
155
+ vault.save('file-test-1');
156
+ // Verify file was created
157
+ const files = fs.readdirSync(testDir);
158
+ expect(files.some((f) => f.startsWith('vault_') && f.endsWith('.json'))).toBe(true);
159
+ });
160
+ test('loads session from file after re-instantiation', () => {
161
+ vault.getOrCreateSession('persist-test', 0);
162
+ vault.addEntry('persist-test', makeEntry('[EMAIL_0]', 'persisted@test.com'));
163
+ vault.save('persist-test');
164
+ // Create a new vault instance pointing to same dir
165
+ const vault2 = new vault_1.FileVault(testDir);
166
+ const session = vault2.getSession('persist-test');
167
+ expect(session).not.toBeNull();
168
+ expect(session.entries['[EMAIL_0]'].original).toBe('persisted@test.com');
169
+ });
170
+ test('deleteSession removes file', () => {
171
+ vault.getOrCreateSession('delete-test', 0);
172
+ vault.addEntry('delete-test', makeEntry('[EMAIL_0]', 'x@y.com'));
173
+ vault.save('delete-test');
174
+ vault.deleteSession('delete-test');
175
+ const vault2 = new vault_1.FileVault(testDir);
176
+ expect(vault2.getSession('delete-test')).toBeNull();
177
+ });
178
+ test('listSessions reads all vault files', () => {
179
+ vault.getOrCreateSession('list-1', 0);
180
+ vault.save('list-1');
181
+ vault.getOrCreateSession('list-2', 0);
182
+ vault.addEntry('list-2', makeEntry('[EMAIL_0]', 'a@b.com'));
183
+ vault.save('list-2');
184
+ const sessions = vault.listSessions();
185
+ expect(sessions).toHaveLength(2);
186
+ });
187
+ test('TTL expiry on file vault', () => {
188
+ vault.getOrCreateSession('file-ttl', 1);
189
+ vault.save('file-ttl');
190
+ const start = Date.now();
191
+ while (Date.now() - start < 10) {
192
+ // busy wait
193
+ }
194
+ const vault2 = new vault_1.FileVault(testDir);
195
+ expect(vault2.getSession('file-ttl')).toBeNull();
196
+ });
197
+ test('cleanup removes expired files', () => {
198
+ vault.getOrCreateSession('expired-file', 1);
199
+ vault.save('expired-file');
200
+ vault.getOrCreateSession('alive-file', 0);
201
+ vault.save('alive-file');
202
+ const start = Date.now();
203
+ while (Date.now() - start < 10) {
204
+ // busy wait
205
+ }
206
+ // Auto-cleanup fires on getOrCreateSession and constructor,
207
+ // so the expired file may already be gone. Verify it's not accessible.
208
+ const vault2 = new vault_1.FileVault(testDir);
209
+ expect(vault2.getSession('expired-file')).toBeNull();
210
+ // The alive session should still exist
211
+ expect(vault2.getSession('alive-file')).not.toBeNull();
212
+ });
213
+ test('findByOriginal works with file vault', () => {
214
+ vault.getOrCreateSession('find-test', 0);
215
+ vault.addEntry('find-test', makeEntry('[EMAIL_0]', 'find@me.com'));
216
+ const found = vault.findByOriginal('find-test', 'find@me.com');
217
+ expect(found).toBeDefined();
218
+ expect(found.token).toBe('[EMAIL_0]');
219
+ });
220
+ test('handles corrupt JSON files gracefully', () => {
221
+ // Write a corrupt file
222
+ const corruptFile = path.join(testDir, 'vault_corrupt1234567.json');
223
+ fs.writeFileSync(corruptFile, 'not valid json{{{', 'utf-8');
224
+ const sessions = vault.listSessions();
225
+ // Should not crash, just skip the corrupt file
226
+ expect(Array.isArray(sessions)).toBe(true);
227
+ });
228
+ });
229
+ // ═══════════════════════════════════════════════════════
230
+ // createVault FACTORY
231
+ // ═══════════════════════════════════════════════════════
232
+ describe('createVault factory', () => {
233
+ test('returns MemoryVault for "memory"', () => {
234
+ const v = (0, vault_1.createVault)('memory');
235
+ expect(v).toBeInstanceOf(vault_1.MemoryVault);
236
+ });
237
+ test('returns FileVault for "file"', () => {
238
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'factory-test-'));
239
+ const v = (0, vault_1.createVault)('file', testDir);
240
+ expect(v).toBeInstanceOf(vault_1.FileVault);
241
+ fs.rmSync(testDir, { recursive: true, force: true });
242
+ });
243
+ test('defaults to MemoryVault for unknown type', () => {
244
+ const v = (0, vault_1.createVault)('unknown');
245
+ expect(v).toBeInstanceOf(vault_1.MemoryVault);
246
+ });
247
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Persistent Audit Log
3
+ *
4
+ * JSONL (JSON Lines) audit logger for compliance recording.
5
+ * One JSON object per line, append-only, auto-rotating.
6
+ *
7
+ * Compliant with:
8
+ * - GDPR Article 30 (Records of Processing Activities)
9
+ * - HIPAA (6-year retention)
10
+ * - SOX (7-year retention)
11
+ * - PCI DSS (1-year minimum)
12
+ *
13
+ * Format: JSONL (compatible with Splunk, ELK, Datadog, CloudWatch)
14
+ * Location: ~/.n8n/pii-audit/pii-audit-YYYY-MM-DD.jsonl
15
+ * Rotation: Daily + 100MB size limit
16
+ */
17
+ import { RedactionHit, RedactionMode } from './types';
18
+ import { ClassificationLabel } from './classification';
19
+ export interface AuditLogEntry {
20
+ version: string;
21
+ timestamp: string;
22
+ eventId: string;
23
+ severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
24
+ action: string;
25
+ mode: RedactionMode | string;
26
+ status: 'SUCCESS' | 'PARTIAL' | 'FAILED';
27
+ totalHits: number;
28
+ hitsByCategory: Record<string, number>;
29
+ hitsByPattern: Record<string, number>;
30
+ classificationLevel?: string;
31
+ inputItemCount: number;
32
+ processingTimeMs: number;
33
+ patternsUsed: number;
34
+ presidioEnabled: boolean;
35
+ sessionId?: string;
36
+ }
37
+ /**
38
+ * Write an audit log entry.
39
+ * Creates the audit directory if it doesn't exist.
40
+ * Handles file rotation (daily + size-based).
41
+ * Graceful: never throws, never blocks the workflow.
42
+ */
43
+ export declare function writeAuditLog(action: string, mode: RedactionMode | string, hits: RedactionHit[], inputItemCount: number, processingTimeMs: number, patternsUsed: number, presidioEnabled: boolean, classification?: ClassificationLabel, sessionId?: string, auditDir?: string): void;
44
+ /**
45
+ * Read audit log entries for a given date range.
46
+ * Returns parsed entries. Used by Stats operation for audit review.
47
+ */
48
+ export declare function readAuditLog(startDate?: string, endDate?: string, auditDir?: string): AuditLogEntry[];