n8n-nodes-redactor 2.0.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.

Potentially problematic release.


This version of n8n-nodes-redactor might be problematic. Click here for more details.

Files changed (37) hide show
  1. package/LICENSE +42 -0
  2. package/README.dev.md +134 -0
  3. package/README.md +376 -0
  4. package/README.npm.md +376 -0
  5. package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
  6. package/dist/nodes/PiiRedactor/PiiRedactor.node.js +872 -0
  7. package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
  8. package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
  9. package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
  10. package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
  11. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
  12. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
  13. package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
  14. package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
  15. package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
  16. package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
  17. package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
  18. package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
  19. package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
  20. package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
  21. package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
  22. package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
  23. package/dist/nodes/PiiRedactor/context.d.ts +57 -0
  24. package/dist/nodes/PiiRedactor/context.js +260 -0
  25. package/dist/nodes/PiiRedactor/engine.d.ts +17 -0
  26. package/dist/nodes/PiiRedactor/engine.js +813 -0
  27. package/dist/nodes/PiiRedactor/names.d.ts +25 -0
  28. package/dist/nodes/PiiRedactor/names.js +188 -0
  29. package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
  30. package/dist/nodes/PiiRedactor/patterns.js +1741 -0
  31. package/dist/nodes/PiiRedactor/redact.png +0 -0
  32. package/dist/nodes/PiiRedactor/redact.svg +3 -0
  33. package/dist/nodes/PiiRedactor/types.d.ts +78 -0
  34. package/dist/nodes/PiiRedactor/types.js +3 -0
  35. package/dist/nodes/PiiRedactor/vault.d.ts +60 -0
  36. package/dist/nodes/PiiRedactor/vault.js +299 -0
  37. package/package.json +87 -0
@@ -0,0 +1,872 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PiiRedactor = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const patterns_1 = require("./patterns");
6
+ const vault_1 = require("./vault");
7
+ const engine_1 = require("./engine");
8
+ /**
9
+ * Curated high-precision pattern set. These patterns have low false-positive
10
+ * rates and are safe to enable by default. Excludes broad patterns like
11
+ * bare digit-only matches (postalCodeDE, postalCodeAT, bsnNL, etc.)
12
+ */
13
+ const RECOMMENDED_PATTERNS = [
14
+ // Contact
15
+ 'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
16
+ 'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
17
+ // Identity - high precision
18
+ 'personName', 'ssn', 'itinUS', 'sinCA', 'ninoUK', 'passportUS', 'passportEU',
19
+ 'passportDE', 'nationalIdDE', 'nhsNumber', 'taxIdUS', 'taxIdDE',
20
+ 'sozialversicherungDE', 'ahvCH', 'nationalIdFR', 'codiceFiscaleIT',
21
+ 'dniES', 'nieES', 'nifES', 'peselPL', 'bsnNL', 'hetuFI', 'ppsIE',
22
+ 'nricSG', 'panIN', 'cpfBR', 'driverLicenseCtx', 'employeeIdCtx',
23
+ 'fullNameCtx', 'mothersMaidenName', 'passportCtx',
24
+ // Financial - validated
25
+ 'creditCard', 'amex', 'iban', 'bic', 'vatEU', 'abaRouting', 'sortCodeUK',
26
+ 'cardExpiry', 'cvvCtx', 'bankAccountCtx', 'insurancePolicyCtx',
27
+ 'salaryCtx', 'loyaltyNumberCtx',
28
+ // Network
29
+ 'ipv4', 'ipv6', 'macAddress', 'ipv4Port', 'url', 'privateIp',
30
+ // Location - validated formats
31
+ 'postalCodeUK', 'postalCodeNL', 'postalCodePL', 'postalCodeCA',
32
+ 'postalCodeJP', 'gpsCoordinates',
33
+ 'addressDE', 'addressLabeledDE', 'addressLabeledEN', 'addressLabeledFR',
34
+ 'addressEUStreetNumber',
35
+ // Dates
36
+ 'dateSlash', 'dateDash', 'dateDot', 'dateOfBirth',
37
+ // Medical
38
+ 'medicalRecordNumber', 'deaNumber', 'npiNumber', 'rxNumber',
39
+ 'healthPlanCtx', 'bloodTypeCtx',
40
+ // Enterprise secrets
41
+ 'internalHostname', 'uncPath', 'ldapDN', 'adUsername',
42
+ 'dbConnectionString', 'connStringKV',
43
+ 'awsAccessKey', 'gcpApiKey', 'stripeKey', 'openaiKey', 'githubToken',
44
+ 'slackToken', 'bearerToken', 'jwtToken', 'pemPrivateKey', 'sshPublicKey',
45
+ 'genericSecret', 'azureKey', 'slackWebhook',
46
+ 'sendgridKey', 'twilioKey', 'anthropicKey', 'gitlabToken',
47
+ 'x509Certificate', 'pgpPrivateKey', 'envSecrets',
48
+ // Crypto
49
+ 'bitcoinAddress', 'ethereumAddress',
50
+ // Vehicle
51
+ 'vin',
52
+ // Biometric
53
+ 'uuid', 'imei', 'iccid',
54
+ ];
55
+ class PiiRedactor {
56
+ constructor() {
57
+ this.description = {
58
+ displayName: 'Redactor',
59
+ name: 'redactor',
60
+ icon: 'file:redact.png',
61
+ group: ['transform'],
62
+ version: 1,
63
+ subtitle: '={{$parameter["operation"]}}',
64
+ usableAsTool: true,
65
+ description: 'Redact and remove sensitive data, PII, personal information, emails, phone numbers, addresses, IBAN, credit cards, names before sending to LLM. Anonymize, mask, sanitize, scrub customer data. Restore original values after. GDPR HIPAA CCPA compliant. Data privacy, data protection, data masking, data anonymization.',
66
+ defaults: {
67
+ name: 'Redactor',
68
+ },
69
+ inputs: ['main'],
70
+ // @ts-ignore dynamic outputs using n8n's function pattern (same as Switch node)
71
+ outputs: `={{((parameters) => parameters["includeReport"] ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
72
+ properties: [
73
+ // ═══════════════════════════════════════
74
+ // OPERATION
75
+ // ═══════════════════════════════════════
76
+ {
77
+ displayName: 'Operation',
78
+ name: 'operation',
79
+ type: 'options',
80
+ noDataExpression: true,
81
+ options: [
82
+ {
83
+ name: 'Redact',
84
+ value: 'redact',
85
+ description: 'Detect and replace PII with tokens/masks/hashes',
86
+ },
87
+ {
88
+ name: 'Restore',
89
+ value: 'restore',
90
+ description: 'Replace tokens back with original PII values from vault',
91
+ },
92
+ {
93
+ name: 'Detect (Scan Only)',
94
+ value: 'detect',
95
+ description: 'Scan for PII without modifying the data. Returns a report of what was found.',
96
+ },
97
+ {
98
+ name: 'Verify',
99
+ value: 'verify',
100
+ description: 'Re-scan redacted data to confirm no PII leaked through. Returns pass/fail.',
101
+ },
102
+ {
103
+ name: 'Purge',
104
+ value: 'purge',
105
+ description: 'Delete vault sessions. Supports GDPR right to erasure.',
106
+ },
107
+ {
108
+ name: 'Stats',
109
+ value: 'stats',
110
+ description: 'List active vault sessions and entry counts',
111
+ },
112
+ ],
113
+ default: 'redact',
114
+ },
115
+ // ═══════════════════════════════════════
116
+ // SESSION ID (shared between Redact & Restore)
117
+ // ═══════════════════════════════════════
118
+ {
119
+ displayName: 'Session ID',
120
+ name: 'sessionId',
121
+ type: 'string',
122
+ default: '={{$execution.id}}',
123
+ description: 'Unique ID linking a Redact and Restore step. Defaults to the current execution ID. Use the same value in both the Redact and Restore nodes.',
124
+ required: true,
125
+ displayOptions: {
126
+ show: {
127
+ operation: ['redact', 'restore'],
128
+ },
129
+ },
130
+ },
131
+ {
132
+ displayName: 'Session ID to Purge',
133
+ name: 'purgeSessionId',
134
+ type: 'string',
135
+ default: '',
136
+ description: 'The session ID to purge. Leave empty is not needed when purging all.',
137
+ displayOptions: {
138
+ show: {
139
+ operation: ['purge'],
140
+ purgeScope: ['specific'],
141
+ },
142
+ },
143
+ },
144
+ // ═══════════════════════════════════════
145
+ // REDACTION MODE
146
+ // ═══════════════════════════════════════
147
+ {
148
+ displayName: 'Redaction Mode',
149
+ name: 'redactionMode',
150
+ type: 'options',
151
+ displayOptions: {
152
+ show: {
153
+ operation: ['redact'],
154
+ },
155
+ },
156
+ options: [
157
+ {
158
+ name: 'Token (Reversible)',
159
+ value: 'token',
160
+ description: 'Replace PII with [EMAIL_0], [PHONE_1] etc. Restore via vault later. Best for LLM workflows.',
161
+ },
162
+ {
163
+ name: 'Mask (Partial)',
164
+ value: 'mask',
165
+ description: 'Show partial values: j***@e***.com, ****-****-****-1234. NOT reversible.',
166
+ },
167
+ {
168
+ name: 'Hash (Deterministic)',
169
+ value: 'hash',
170
+ description: 'Replace with truncated SHA-256 hash. Deterministic but NOT reversible.',
171
+ },
172
+ {
173
+ name: 'Redact (Full Removal)',
174
+ value: 'redact',
175
+ description: 'Replace with [REDACTED]. NOT reversible.',
176
+ },
177
+ ],
178
+ default: 'token',
179
+ },
180
+ // ═══════════════════════════════════════
181
+ // DETECTION SCOPE
182
+ // ═══════════════════════════════════════
183
+ {
184
+ displayName: 'Detection Scope',
185
+ name: 'detectionScope',
186
+ type: 'options',
187
+ displayOptions: {
188
+ show: {
189
+ operation: ['redact'],
190
+ },
191
+ },
192
+ options: [
193
+ {
194
+ name: 'Recommended (High Precision)',
195
+ value: 'recommended',
196
+ description: 'Curated set of high-precision patterns. Best for most workflows. Minimal false positives.',
197
+ },
198
+ {
199
+ name: 'All Patterns (Maximum Coverage)',
200
+ value: 'all',
201
+ description: 'All 200+ patterns including broad ones. May produce false positives on short numbers.',
202
+ },
203
+ {
204
+ name: 'Select Specific Types',
205
+ value: 'select',
206
+ description: 'Choose which specific data types to detect.',
207
+ },
208
+ ],
209
+ default: 'recommended',
210
+ description: 'Detect all sensitive data types automatically, or select specific types to scan for.',
211
+ },
212
+ // ═══════════════════════════════════════
213
+ // PII TYPE SELECTION (only shown when scope is 'select')
214
+ // ═══════════════════════════════════════
215
+ {
216
+ displayName: 'PII Types to Detect',
217
+ name: 'piiTypes',
218
+ type: 'multiOptions',
219
+ displayOptions: {
220
+ show: {
221
+ operation: ['redact'],
222
+ detectionScope: ['select'],
223
+ },
224
+ },
225
+ options: (0, patterns_1.getPatternOptions)(),
226
+ default: [
227
+ 'email',
228
+ 'phone',
229
+ 'phoneDE',
230
+ 'phoneUK',
231
+ 'personName',
232
+ 'ssn',
233
+ 'ninoUK',
234
+ 'creditCard',
235
+ 'iban',
236
+ 'bic',
237
+ 'ipv4',
238
+ 'privateIp',
239
+ 'addressDE',
240
+ 'addressLabeledEN',
241
+ 'addressLabeledDE',
242
+ 'awsAccessKey',
243
+ 'genericSecret',
244
+ 'dbConnectionString',
245
+ 'jwtToken',
246
+ 'pemPrivateKey',
247
+ ],
248
+ description: 'Select which PII types to detect. Each type uses optimized regex with validation where possible.',
249
+ },
250
+ // ═══════════════════════════════════════
251
+ // DEDUPLICATION
252
+ // ═══════════════════════════════════════
253
+ {
254
+ displayName: 'Deduplicate',
255
+ name: 'dedup',
256
+ type: 'boolean',
257
+ displayOptions: {
258
+ show: {
259
+ operation: ['redact'],
260
+ redactionMode: ['token'],
261
+ },
262
+ },
263
+ default: true,
264
+ description: 'When enabled, the same PII value always gets the same token. This preserves relationships in the data (e.g. if an email appears in 3 fields, all 3 get [EMAIL_0]).',
265
+ },
266
+ // ═══════════════════════════════════════
267
+ // FIELD TARGETING
268
+ // ═══════════════════════════════════════
269
+ {
270
+ displayName: 'Field Targeting',
271
+ name: 'fieldMode',
272
+ type: 'options',
273
+ displayOptions: {
274
+ show: {
275
+ operation: ['redact'],
276
+ },
277
+ },
278
+ options: [
279
+ {
280
+ name: 'All Fields',
281
+ value: 'all',
282
+ description: 'Scan every field in the JSON data',
283
+ },
284
+ {
285
+ name: 'Allowlist -Only These Fields',
286
+ value: 'allowlist',
287
+ description: 'Only scan the fields listed below',
288
+ },
289
+ {
290
+ name: 'Denylist -Skip These Fields',
291
+ value: 'denylist',
292
+ description: 'Scan everything except the fields listed below',
293
+ },
294
+ ],
295
+ default: 'all',
296
+ description: 'Control which JSON fields get scanned. Use allowlist to target specific fields, or denylist to skip fields like internal IDs.',
297
+ },
298
+ {
299
+ displayName: 'Field Rules',
300
+ name: 'fieldRules',
301
+ type: 'fixedCollection',
302
+ typeOptions: {
303
+ multipleValues: true,
304
+ },
305
+ displayOptions: {
306
+ show: {
307
+ operation: ['redact'],
308
+ fieldMode: ['allowlist', 'denylist'],
309
+ },
310
+ },
311
+ default: {},
312
+ options: [
313
+ {
314
+ name: 'rules',
315
+ displayName: 'Rule',
316
+ values: [
317
+ {
318
+ displayName: 'Field Path',
319
+ name: 'field',
320
+ type: 'string',
321
+ default: '',
322
+ placeholder: 'e.g. user.email, contacts[*].phone, *.address',
323
+ description: 'JSON path to target. Supports dot notation and wildcards: "user.email", "*.phone", "items[*].name"',
324
+ },
325
+ ],
326
+ },
327
+ ],
328
+ },
329
+ // ═══════════════════════════════════════
330
+ // CONFIDENCE THRESHOLD
331
+ // ═══════════════════════════════════════
332
+ {
333
+ displayName: 'Confidence Threshold',
334
+ name: 'confidenceThreshold',
335
+ type: 'number',
336
+ displayOptions: {
337
+ show: {
338
+ operation: ['redact'],
339
+ },
340
+ },
341
+ typeOptions: {
342
+ minValue: 0,
343
+ maxValue: 1,
344
+ numberPrecision: 2,
345
+ },
346
+ default: 0,
347
+ description: 'Only redact detections with confidence score above this threshold (0.0 to 1.0). Set to 0 to redact everything. Set to 0.7 to only redact high-confidence matches. Checksum-validated patterns score 0.95, regex-only patterns score 0.60-0.85.',
348
+ },
349
+ // ═══════════════════════════════════════
350
+ // ALLOW LIST (bypass specific values)
351
+ // ═══════════════════════════════════════
352
+ {
353
+ displayName: 'Allow List (Never Redact)',
354
+ name: 'allowList',
355
+ type: 'fixedCollection',
356
+ typeOptions: {
357
+ multipleValues: true,
358
+ },
359
+ displayOptions: {
360
+ show: {
361
+ operation: ['redact'],
362
+ },
363
+ },
364
+ default: {},
365
+ options: [
366
+ {
367
+ name: 'entries',
368
+ displayName: 'Entry',
369
+ values: [
370
+ {
371
+ displayName: 'Value',
372
+ name: 'value',
373
+ type: 'string',
374
+ default: '',
375
+ placeholder: 'e.g. @mycompany.com, +1-800-555-0100, support@acme.com',
376
+ description: 'Value to never redact, even if a pattern matches it',
377
+ },
378
+ {
379
+ displayName: 'Match Type',
380
+ name: 'type',
381
+ type: 'options',
382
+ options: [
383
+ { name: 'Exact Match', value: 'exact' },
384
+ { name: 'Contains', value: 'contains' },
385
+ { name: 'Regex', value: 'regex' },
386
+ ],
387
+ default: 'contains',
388
+ description: 'How to match the value',
389
+ },
390
+ ],
391
+ },
392
+ ],
393
+ description: 'Values that should NEVER be redacted. Use this for company email domains, public phone numbers, or known-safe identifiers.',
394
+ },
395
+ // ═══════════════════════════════════════
396
+ // DENY LIST (always redact specific values)
397
+ // ═══════════════════════════════════════
398
+ {
399
+ displayName: 'Deny List (Always Redact)',
400
+ name: 'denyList',
401
+ type: 'fixedCollection',
402
+ typeOptions: {
403
+ multipleValues: true,
404
+ },
405
+ displayOptions: {
406
+ show: {
407
+ operation: ['redact'],
408
+ },
409
+ },
410
+ default: {},
411
+ options: [
412
+ {
413
+ name: 'entries',
414
+ displayName: 'Entry',
415
+ values: [
416
+ {
417
+ displayName: 'Value',
418
+ name: 'value',
419
+ type: 'string',
420
+ default: '',
421
+ placeholder: 'e.g. Project Falcon, INTERNAL-2024-SECRET',
422
+ description: 'Value to ALWAYS redact, even if no pattern matches it',
423
+ },
424
+ {
425
+ displayName: 'Match Type',
426
+ name: 'type',
427
+ type: 'options',
428
+ options: [
429
+ { name: 'Exact Match', value: 'exact' },
430
+ { name: 'Contains', value: 'contains' },
431
+ { name: 'Regex', value: 'regex' },
432
+ ],
433
+ default: 'exact',
434
+ description: 'How to match the value',
435
+ },
436
+ ],
437
+ },
438
+ ],
439
+ description: 'Values that should ALWAYS be redacted, regardless of whether any pattern matches. Use for project codenames, internal secrets, or business-specific sensitive terms.',
440
+ },
441
+ // ═══════════════════════════════════════
442
+ // CUSTOM PATTERNS
443
+ // ═══════════════════════════════════════
444
+ {
445
+ displayName: 'Custom Patterns',
446
+ name: 'customPatterns',
447
+ type: 'fixedCollection',
448
+ typeOptions: {
449
+ multipleValues: true,
450
+ },
451
+ displayOptions: {
452
+ show: {
453
+ operation: ['redact'],
454
+ },
455
+ },
456
+ default: {},
457
+ options: [
458
+ {
459
+ name: 'patterns',
460
+ displayName: 'Pattern',
461
+ values: [
462
+ {
463
+ displayName: 'Label',
464
+ name: 'label',
465
+ type: 'string',
466
+ default: '',
467
+ placeholder: 'e.g. ORDER_ID, SKU, TICKET',
468
+ description: 'Label for the redacted placeholder',
469
+ },
470
+ {
471
+ displayName: 'Regex',
472
+ name: 'regex',
473
+ type: 'string',
474
+ default: '',
475
+ placeholder: 'e.g. ORD-\\d{6}, SKU-[A-Z0-9]{8}',
476
+ description: 'Regular expression to match this pattern',
477
+ },
478
+ {
479
+ displayName: 'Category',
480
+ name: 'category',
481
+ type: 'options',
482
+ options: [
483
+ { name: 'Identity', value: 'identity' },
484
+ { name: 'Financial', value: 'financial' },
485
+ { name: 'Contact', value: 'contact' },
486
+ { name: 'Network', value: 'network' },
487
+ { name: 'Location', value: 'location' },
488
+ { name: 'Medical', value: 'medical' },
489
+ { name: 'Other', value: 'other' },
490
+ ],
491
+ default: 'identity',
492
+ description: 'Category for audit reporting',
493
+ },
494
+ ],
495
+ },
496
+ ],
497
+ description: 'Add your own regex patterns for business-specific identifiers (order IDs, SKUs, ticket numbers, etc.)',
498
+ },
499
+ // ═══════════════════════════════════════
500
+ // VAULT SETTINGS
501
+ // ═══════════════════════════════════════
502
+ {
503
+ displayName: 'Vault Storage',
504
+ name: 'vaultStorage',
505
+ type: 'options',
506
+ displayOptions: {
507
+ show: {
508
+ operation: ['redact', 'restore', 'stats', 'purge'],
509
+ },
510
+ },
511
+ options: [
512
+ {
513
+ name: 'File-Based (Recommended, Persistent)',
514
+ value: 'file',
515
+ },
516
+ {
517
+ name: 'In-Memory (Fast, Lost on Restart)',
518
+ value: 'memory',
519
+ },
520
+ ],
521
+ default: 'file',
522
+ description: 'File-based (recommended): Persists to disk, survives restarts, no memory bloat. Stored in ~/.n8n/pii-vault/. In-memory: Faster but lost on restart, capped at 1000 sessions to prevent memory bloat.',
523
+ },
524
+ {
525
+ displayName: 'Vault Directory',
526
+ name: 'vaultDir',
527
+ type: 'string',
528
+ displayOptions: {
529
+ show: {
530
+ operation: ['redact', 'restore', 'stats', 'purge'],
531
+ vaultStorage: ['file'],
532
+ },
533
+ },
534
+ default: '',
535
+ placeholder: '/path/to/vault (default: ~/.n8n/pii-vault/)',
536
+ description: 'Custom directory for file-based vault storage',
537
+ },
538
+ {
539
+ displayName: 'Session TTL (minutes)',
540
+ name: 'sessionTtl',
541
+ type: 'number',
542
+ displayOptions: {
543
+ show: {
544
+ operation: ['redact'],
545
+ },
546
+ },
547
+ default: 60,
548
+ description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
549
+ },
550
+ // ═══════════════════════════════════════
551
+ // PURGE OPTIONS
552
+ // ═══════════════════════════════════════
553
+ {
554
+ displayName: 'Purge Scope',
555
+ name: 'purgeScope',
556
+ type: 'options',
557
+ displayOptions: {
558
+ show: {
559
+ operation: ['purge'],
560
+ },
561
+ },
562
+ options: [
563
+ {
564
+ name: 'Purge All Sessions',
565
+ value: 'all',
566
+ description: 'Delete all vault sessions. GDPR right to erasure.',
567
+ },
568
+ {
569
+ name: 'Purge Specific Session',
570
+ value: 'specific',
571
+ description: 'Delete a specific session by ID.',
572
+ },
573
+ ],
574
+ default: 'all',
575
+ },
576
+ // ═══════════════════════════════════════
577
+ // RESTORE OPTIONS
578
+ // ═══════════════════════════════════════
579
+ {
580
+ displayName: 'Delete Vault After Restore',
581
+ name: 'deleteAfterRestore',
582
+ type: 'boolean',
583
+ displayOptions: {
584
+ show: {
585
+ operation: ['restore'],
586
+ },
587
+ },
588
+ default: true,
589
+ description: 'Delete the vault session after restoring. Disable for multi-step workflows that need multiple restores.',
590
+ },
591
+ // ═══════════════════════════════════════
592
+ // AUDIT REPORT
593
+ // ═══════════════════════════════════════
594
+ {
595
+ displayName: 'Include Audit Report',
596
+ name: 'includeReport',
597
+ type: 'boolean',
598
+ displayOptions: {
599
+ show: {
600
+ operation: ['redact', 'detect'],
601
+ },
602
+ },
603
+ default: false,
604
+ description: 'When enabled, adds a second output with an audit report listing all PII detected, categories, and counts. Useful for compliance logging.',
605
+ },
606
+ ],
607
+ };
608
+ }
609
+ async execute() {
610
+ const items = this.getInputData();
611
+ const operation = this.getNodeParameter('operation', 0);
612
+ // ─── DETECT / VERIFY (zero-config, scans everything) ──
613
+ if (operation === 'detect' || operation === 'verify') {
614
+ // Detect and Verify always scan ALL 216 patterns.
615
+ // No parameters needed. Simple, reliable, no "Could not get parameter".
616
+ const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
617
+ const customPatterns = [];
618
+ const tempVault = (0, vault_1.createVault)('memory');
619
+ const tempSessionId = `${operation}_${Date.now()}`;
620
+ tempVault.getOrCreateSession(tempSessionId, 60000);
621
+ const ctx = {
622
+ enabledPatterns,
623
+ customPatterns,
624
+ mode: 'token',
625
+ dedup: true,
626
+ fieldRules: [],
627
+ fieldMode: 'all',
628
+ // Verify mode: skip semantic field-name detection.
629
+ // Only check if actual PII VALUES leaked through (regex only).
630
+ skipSemantic: operation === 'verify',
631
+ };
632
+ const allHits = [];
633
+ for (let i = 0; i < items.length; i++) {
634
+ const hits = [];
635
+ (0, engine_1.redactValue)(items[i].json, ctx, tempVault, tempSessionId, hits, i);
636
+ allHits.push(...hits);
637
+ }
638
+ tempVault.deleteSession(tempSessionId);
639
+ if (operation === 'detect') {
640
+ // Always return original data + detection summary as last item
641
+ const returnData = items.map((item) => ({ json: item.json }));
642
+ const report = (0, engine_1.buildReport)(tempSessionId, allHits);
643
+ // Summary always appended so user sees results
644
+ const summary = {
645
+ _redactorScan: true,
646
+ status: allHits.length === 0 ? 'CLEAN' : 'PII_FOUND',
647
+ totalPiiFound: allHits.length,
648
+ itemsScanned: items.length,
649
+ hitsByCategory: report.hitsByCategory,
650
+ hitsByPattern: report.hitsByPattern,
651
+ timestamp: report.timestamp,
652
+ };
653
+ let includeReport = false;
654
+ try {
655
+ includeReport = this.getNodeParameter('includeReport', 0, false);
656
+ }
657
+ catch { }
658
+ if (includeReport) {
659
+ // Full report on second output
660
+ return [returnData, [{ json: { ...summary, hits: report.hits } }]];
661
+ }
662
+ // Append summary as last item in single output
663
+ returnData.push({ json: summary });
664
+ return [returnData];
665
+ }
666
+ // Verify
667
+ const passed = allHits.length === 0;
668
+ const verifyReport = (0, engine_1.buildReport)(tempSessionId, allHits);
669
+ return [[{
670
+ json: {
671
+ verified: passed,
672
+ status: passed ? 'PASS' : 'FAIL',
673
+ leaksFound: allHits.length,
674
+ itemsScanned: items.length,
675
+ message: passed
676
+ ? 'No PII detected in values. Data appears properly redacted.'
677
+ : `Found ${allHits.length} potential PII leak(s). Values contain detectable sensitive data.`,
678
+ hitsByCategory: verifyReport.hitsByCategory,
679
+ hitsByPattern: verifyReport.hitsByPattern,
680
+ leaks: allHits.map((h) => ({
681
+ field: h.field,
682
+ patternLabel: h.patternLabel,
683
+ category: h.category,
684
+ itemIndex: h.itemIndex,
685
+ })),
686
+ },
687
+ }]];
688
+ }
689
+ // All other operations (redact, restore, stats, purge) use vault
690
+ const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'file');
691
+ const vaultDir = this.getNodeParameter('vaultDir', 0, '');
692
+ const vault = (0, vault_1.createVault)(vaultStorage, vaultDir || undefined);
693
+ // ─── STATS ───────────────────────────────
694
+ if (operation === 'stats') {
695
+ vault.cleanup();
696
+ const sessions = vault.listSessions();
697
+ const totalEntries = sessions.reduce((sum, s) => sum + s.entryCount, 0);
698
+ return [[
699
+ {
700
+ json: {
701
+ totalSessions: sessions.length,
702
+ totalEntries,
703
+ vaultStorage: vaultStorage,
704
+ timestamp: new Date().toISOString(),
705
+ sessions: sessions.map((s) => ({
706
+ sessionId: s.sessionId,
707
+ entryCount: s.entryCount,
708
+ createdAt: s.createdAt,
709
+ ttl: s.ttl,
710
+ ttlMinutes: s.ttl > 0 ? Math.round(s.ttl / 60000) : 'no expiry',
711
+ })),
712
+ },
713
+ },
714
+ ]];
715
+ }
716
+ const sessionId = this.getNodeParameter('sessionId', 0);
717
+ if (!sessionId) {
718
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required');
719
+ }
720
+ // ─── REDACT ──────────────────────────────
721
+ if (operation === 'redact') {
722
+ const mode = this.getNodeParameter('redactionMode', 0, 'token');
723
+ const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
724
+ let enabledPatterns;
725
+ if (detectionScope === 'all') {
726
+ enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
727
+ }
728
+ else if (detectionScope === 'recommended') {
729
+ enabledPatterns = RECOMMENDED_PATTERNS;
730
+ }
731
+ else {
732
+ enabledPatterns = this.getNodeParameter('piiTypes', 0);
733
+ }
734
+ const dedup = this.getNodeParameter('dedup', 0, true);
735
+ const fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
736
+ const sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
737
+ const includeReport = this.getNodeParameter('includeReport', 0, false);
738
+ const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
739
+ const customPatterns = customPatternsRaw.patterns ?? [];
740
+ const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
741
+ const fieldRules = (fieldRulesRaw.rules ?? []).map((r) => ({
742
+ field: r.field,
743
+ mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
744
+ }));
745
+ // Parse allow list
746
+ const allowListRaw = this.getNodeParameter('allowList', 0, {});
747
+ const allowList = (allowListRaw.entries ?? []).map((e) => ({
748
+ value: e.value,
749
+ type: (e.type || 'contains'),
750
+ }));
751
+ // Parse deny list
752
+ const denyListRaw = this.getNodeParameter('denyList', 0, {});
753
+ const denyList = (denyListRaw.entries ?? []).map((e) => ({
754
+ value: e.value,
755
+ type: (e.type || 'exact'),
756
+ }));
757
+ // Confidence threshold
758
+ const confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
759
+ // Initialize vault session
760
+ vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
761
+ const ctx = {
762
+ enabledPatterns,
763
+ customPatterns,
764
+ mode,
765
+ dedup,
766
+ fieldRules,
767
+ fieldMode,
768
+ allowList,
769
+ denyList,
770
+ confidenceThreshold,
771
+ };
772
+ const allHits = [];
773
+ const returnData = [];
774
+ for (let i = 0; i < items.length; i++) {
775
+ const item = items[i];
776
+ const hits = [];
777
+ const redacted = (0, engine_1.redactValue)(item.json, ctx, vault, sessionId, hits, i);
778
+ returnData.push({ json: redacted });
779
+ allHits.push(...hits);
780
+ }
781
+ // Persist vault
782
+ vault.save(sessionId);
783
+ if (includeReport) {
784
+ const report = (0, engine_1.buildReport)(sessionId, allHits);
785
+ return [returnData, [{ json: report }]];
786
+ }
787
+ return [returnData];
788
+ }
789
+ // ─── RESTORE ─────────────────────────────
790
+ if (operation === 'restore') {
791
+ const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
792
+ let session = vault.getSession(sessionId);
793
+ let actualSessionId = sessionId;
794
+ // Safe fallback: if exact session not found and there is EXACTLY ONE
795
+ // session in the vault, use it. This handles step-by-step testing in
796
+ // n8n where each "Execute step" creates a new execution ID.
797
+ // We ONLY fall back when there is no ambiguity (1 session).
798
+ // With multiple sessions, we refuse to guess to prevent cross-user leakage.
799
+ if (!session) {
800
+ const allSessions = vault.listSessions();
801
+ if (allSessions.length === 1) {
802
+ actualSessionId = allSessions[0].sessionId;
803
+ session = vault.getSession(actualSessionId);
804
+ }
805
+ }
806
+ if (!session) {
807
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}". Ensure a Redact node ran first with the same Session ID and vault storage type.`);
808
+ }
809
+ const returnData = [];
810
+ for (let i = 0; i < items.length; i++) {
811
+ const item = items[i];
812
+ const restored = (0, engine_1.restoreValue)(item.json, vault, actualSessionId);
813
+ returnData.push({ json: restored });
814
+ }
815
+ if (deleteAfterRestore) {
816
+ vault.deleteSession(actualSessionId);
817
+ }
818
+ return [returnData];
819
+ }
820
+ // ─── PURGE (delete vault sessions) ──
821
+ if (operation === 'purge') {
822
+ const purgeScope = this.getNodeParameter('purgeScope', 0, 'all');
823
+ if (purgeScope === 'specific') {
824
+ const purgeSessionId = this.getNodeParameter('purgeSessionId', 0, '');
825
+ if (!purgeSessionId) {
826
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
827
+ }
828
+ const session = vault.getSession(purgeSessionId);
829
+ if (session) {
830
+ const entryCount = Object.keys(session.entries).length;
831
+ vault.deleteSession(purgeSessionId);
832
+ return [[{
833
+ json: {
834
+ purged: true,
835
+ sessionId: purgeSessionId,
836
+ entriesDeleted: entryCount,
837
+ message: `Session purged successfully.`,
838
+ },
839
+ }]];
840
+ }
841
+ else {
842
+ return [[{
843
+ json: {
844
+ purged: false,
845
+ sessionId: purgeSessionId,
846
+ message: `No session found with this ID.`,
847
+ },
848
+ }]];
849
+ }
850
+ }
851
+ else {
852
+ // Purge ALL sessions
853
+ const sessions = vault.listSessions();
854
+ let totalEntries = 0;
855
+ for (const s of sessions) {
856
+ totalEntries += s.entryCount;
857
+ vault.deleteSession(s.sessionId);
858
+ }
859
+ return [[{
860
+ json: {
861
+ purged: true,
862
+ sessionsPurged: sessions.length,
863
+ totalEntriesDeleted: totalEntries,
864
+ message: `All ${sessions.length} session(s) purged successfully.`,
865
+ },
866
+ }]];
867
+ }
868
+ }
869
+ return [[]];
870
+ }
871
+ }
872
+ exports.PiiRedactor = PiiRedactor;