n8n-nodes-redactor 1.11.1 → 4.1.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 CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/n8n-nodes-redactor#provenance"><img src="https://img.shields.io/badge/npm-provenance%20signed-brightgreen" alt="npm provenance" /></a>
17
17
  <a href="#license"><img src="https://img.shields.io/npm/l/n8n-nodes-redactor" alt="MIT License" /></a>
18
18
  <img src="https://img.shields.io/badge/PII%20patterns-216-blue" alt="216 PII patterns" />
19
- <img src="https://img.shields.io/badge/n8n-self--hosted-FF6D5A" alt="Self-hosted n8n" />
19
+ <img src="https://img.shields.io/badge/n8n-Cloud%20%2B%20self--hosted-FF6D5A" alt="n8n Cloud and self-hosted" />
20
20
  </p>
21
21
 
22
22
  ## What you get
@@ -25,7 +25,7 @@
25
25
  - **The answer still makes sense.** After the model replies, the node puts the real values back, so your output reads naturally.
26
26
  - **Fewer false alarms.** Card numbers are Luhn checked, IBANs checksum verified, NHS numbers mod 11 validated, so random digits are left alone.
27
27
  - **Proof for your records.** An optional audit report, and a GDPR Article 30 processing record built from each run.
28
- - **No new vendor.** Everything runs inside your own n8n. No cloud service, no API key, no runtime dependency.
28
+ - **No new vendor.** Everything runs inside your n8n, on n8n Cloud or self-hosted. No extra service, no API key, no runtime dependency.
29
29
 
30
30
  ## From install to first result
31
31
 
@@ -50,11 +50,11 @@ After Restore, your workflow gets the real values back.
50
50
 
51
51
  | Environment | Status |
52
52
  |---|---|
53
+ | n8n Cloud | Supported from 4.0.0 |
53
54
  | Self-hosted n8n 1.x and 2.x | Supported |
54
- | n8n Cloud | Not available |
55
- | n8n verified community nodes | Not eligible |
55
+ | n8n verified community nodes | Eligible from 4.0.0. Passes n8n's verification scanner with no findings |
56
56
 
57
- The persistent vault, encrypted vault files and JSONL audit log write to the local disk. n8n Cloud and n8n's verification program do not allow community nodes to use the file system, so this node is self-hosted only by design.
57
+ Version 4.0.0 uses no local disk. The vault lives in workflow storage and audit records go to the Report output. Upgrading from 1.x? Read [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md). The 1.x line stays installable with `npm install n8n-nodes-redactor@v1-legacy`.
58
58
 
59
59
  ## Operations
60
60
 
@@ -65,7 +65,7 @@ The persistent vault, encrypted vault files and JSONL audit log write to the loc
65
65
  | Detect | Assessing data before deciding | Reports what personal data exists without changing anything |
66
66
  | Verify | After redaction | Rescans the output and returns PASS or FAIL with any leaked values |
67
67
  | Classify | Governance and routing | Labels data PUBLIC, INTERNAL, CONFIDENTIAL or RESTRICTED |
68
- | Purge | Erasure requests and cleanup | Deletes vault sessions from memory or disk, supporting GDPR Article 17 |
68
+ | Purge | Erasure requests and cleanup | Deletes vault sessions from memory or workflow storage, supporting GDPR Article 17 |
69
69
  | Stats | Debugging | Lists active vault sessions with entry counts and TTL |
70
70
 
71
71
  The recommended production chain looks like this.
@@ -109,19 +109,33 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
109
109
  |---|---|
110
110
  | Deduplication | The same value gets the same token everywhere, so the model keeps relationships between fields |
111
111
  | Field targeting | Allowlist or denylist paths such as `user.email`, `*.phone` or `metadata.*` |
112
- | Vault | In memory, or file based in `~/.n8n/pii-vault/` so it survives restarts. Sessions expire after 60 minutes by default |
113
- | Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant |
112
+ | Vault | Workflow Storage by default, so Restore works in later production runs of the same workflow, or In-Memory. Sessions expire after 60 minutes by default |
113
+ | Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant, kept in a Redactor Vault credential. See [Vault encryption](#vault-encryption) |
114
114
  | Confidence threshold | Each match carries a 0.0 to 1.0 score. Checksum validated patterns score 0.95 |
115
115
  | Allow and deny lists | Values to never redact, such as your support address, and values to always redact, such as project codenames |
116
116
  | Prompt injection check | 25+ patterns, including instruction overrides, delimiter escapes, encoded payloads and zero width characters. Returns BLOCK, FLAG or CLEAN |
117
117
  | Audit report | A second output with hits by category and pattern |
118
- | Audit log | JSONL with daily rotation and a 100 MB cap, readable by Splunk, ELK and Datadog |
118
+ | Audit log | Turn on Audit Log to get an `audit` record (counts, labels, timing, never values) on the Report output. Send it to any database, sheet or SIEM node |
119
119
  | ROPA record | Turn on Include Audit Report, then Include GDPR Art.30 ROPA Record, and fill in the controller details |
120
120
  | Presidio | Optional Microsoft Presidio container for names and organisations in free text. See [docs/PRESIDIO_SETUP.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/PRESIDIO_SETUP.md). Falls back to built-in patterns when unreachable |
121
- | Per-tenant profiles | Planned for 2.0.0 as JSON settings in the node |
121
+ | Per-tenant profiles | Turn on Use Tenant Profile and paste JSON. Validated on every run. Fields are listed in [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md#tenant-profile-fields) |
122
122
 
123
123
  This node reduces what reaches a model. It is not legal advice and does not on its own make a workflow GDPR or HIPAA compliant.
124
124
 
125
+ ## Vault encryption
126
+
127
+ Workflow Storage keeps sessions with the workflow. To encrypt them, create a **Redactor Vault** credential with a passphrase of at least 12 characters and attach it to both the Redact and the Restore node. n8n stores credentials encrypted and apart from the workflow, so the passphrase never sits next to the data it protects. Give each tenant its own credential.
128
+
129
+ The older Vault Encryption Passphrase field still works so workflows saved with it keep restoring, but it is saved in plain text inside the workflow. When both are set, the credential wins.
130
+
131
+ Restore needs the same passphrase that Redact used. If Redact meets a session ID that a different passphrase wrote, it stops with an error instead of overwriting it.
132
+
133
+ ### Concurrent runs
134
+
135
+ n8n saves workflow storage as one block at the end of each production run and does not merge it. If two runs of the same workflow overlap, the one that finishes last wins. A session created by the other run can be lost, and a session purged by the other run can come back until its TTL expires. The node cannot prevent this from inside n8n. To stay safe, keep the Session TTL short, avoid overlapping runs of a workflow that uses Purge (for example by limiting production concurrency on your instance), or use In-Memory storage when Redact and Restore run in the same execution.
136
+
137
+ In-Memory sessions are kept per workflow and are never visible to another workflow.
138
+
125
139
  ## For developers
126
140
 
127
141
  | Area | Detail |
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedactorVaultApi = void 0;
4
+ class RedactorVaultApi {
5
+ constructor() {
6
+ this.name = 'redactorVaultApi';
7
+ this.displayName = 'Redactor Vault API';
8
+ this.icon = { light: 'file:redactorVault.svg', dark: 'file:redactorVault.dark.svg' };
9
+ this.documentationUrl = 'https://github.com/mjmirza/n8n-nodes-redactor#vault-encryption';
10
+ this.properties = [
11
+ {
12
+ displayName: 'Vault Passphrase',
13
+ name: 'passphrase',
14
+ type: 'string',
15
+ typeOptions: { password: true },
16
+ default: '',
17
+ required: true,
18
+ description: 'Encrypts stored sessions with AES-256-GCM. Use at least 12 characters, and a different passphrase for each tenant. Sessions written with this passphrase cannot be read without it.',
19
+ },
20
+ ];
21
+ }
22
+ }
23
+ exports.RedactorVaultApi = RedactorVaultApi;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF6B6B" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#FFA42E" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF5757" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#F79410" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
@@ -9,7 +9,9 @@ const classification_1 = require("./classification");
9
9
  const injection_1 = require("./injection");
10
10
  const audit_1 = require("./audit");
11
11
  const presidio_1 = require("./presidio");
12
+ const profiles_1 = require("./profiles");
12
13
  const ropa_1 = require("./ropa");
14
+ const runtime_1 = require("./runtime");
13
15
  const RECOMMENDED_PATTERNS = [
14
16
  'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
15
17
  'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
@@ -56,8 +58,34 @@ class Redactor {
56
58
  name: 'Redactor',
57
59
  },
58
60
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
59
- outputs: `={{((parameters) => parameters["includeReport"] ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
61
+ credentials: [
62
+ {
63
+ name: 'redactorVaultApi',
64
+ required: false,
65
+ testedBy: 'redactorVaultPassphrase',
66
+ displayOptions: {
67
+ show: {
68
+ operation: ['redact', 'restore', 'stats', 'purge'],
69
+ vaultStorage: ['workflow', 'file'],
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ outputs: `={{((parameters) => (parameters["includeReport"] || parameters["auditLog"]) ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
60
75
  properties: [
76
+ {
77
+ displayName: 'Resource',
78
+ name: 'resource',
79
+ type: 'options',
80
+ noDataExpression: true,
81
+ options: [
82
+ {
83
+ name: 'Data',
84
+ value: 'data',
85
+ },
86
+ ],
87
+ default: 'data',
88
+ },
61
89
  {
62
90
  displayName: 'Operation',
63
91
  name: 'operation',
@@ -500,7 +528,7 @@ class Redactor {
500
528
  },
501
529
  },
502
530
  default: false,
503
- description: 'Whether to write a persistent JSONL audit log of all redaction activity to ~/.n8n/pii-audit/. Self-hosted n8n only. Supports GDPR Art.30, HIPAA, SOX and PCI DSS record-keeping.',
531
+ description: 'Whether to add an audit record (counts, labels, severity, timing, no PII values) to the Report output. Store it with any node, for example a database or a file. Supports GDPR Art.30, HIPAA, SOX and PCI DSS record-keeping.',
504
532
  },
505
533
  {
506
534
  displayName: 'Enhanced NLP Detection (Optional)',
@@ -561,31 +589,17 @@ class Redactor {
561
589
  },
562
590
  },
563
591
  options: [
564
- {
565
- name: 'File-Based (Recommended, Persistent)',
566
- value: 'file',
567
- },
568
592
  {
569
593
  name: 'In-Memory (Fast, Lost on Restart)',
570
594
  value: 'memory',
571
595
  },
572
- ],
573
- default: 'file',
574
- 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.',
575
- },
576
- {
577
- displayName: 'Vault Directory',
578
- name: 'vaultDir',
579
- type: 'string',
580
- displayOptions: {
581
- show: {
582
- operation: ['redact', 'restore', 'stats', 'purge'],
583
- vaultStorage: ['file'],
596
+ {
597
+ name: 'Workflow Storage (Recommended, Persistent)',
598
+ value: 'workflow',
584
599
  },
585
- },
586
- default: '',
587
- placeholder: '/path/to/vault (default: ~/.n8n/pii-vault/)',
588
- description: 'Custom directory for file-based vault storage',
600
+ ],
601
+ default: 'workflow',
602
+ description: 'Workflow storage keeps sessions with the workflow, so Redact and Restore nodes in the same workflow share them and they survive restarts. n8n saves it after production runs of an active workflow. In-memory is faster but lost on restart and capped at 1000 sessions.',
589
603
  },
590
604
  {
591
605
  displayName: 'Vault Encryption Passphrase',
@@ -597,11 +611,11 @@ class Redactor {
597
611
  displayOptions: {
598
612
  show: {
599
613
  operation: ['redact', 'restore', 'stats', 'purge'],
600
- vaultStorage: ['file'],
614
+ vaultStorage: ['workflow', 'file'],
601
615
  },
602
616
  },
603
617
  default: '',
604
- description: 'Encrypt vault files at rest with AES-256-GCM. Leave empty for no encryption. When set, vault files cannot be read without this passphrase. Each tenant should use a different passphrase for complete isolation.',
618
+ description: 'Deprecated. This field is saved in plain text inside the workflow. Attach a Redactor Vault credential instead, which n8n stores encrypted and which takes priority. Kept so workflows saved with it still restore.',
605
619
  },
606
620
  {
607
621
  displayName: 'Session TTL (Minutes)',
@@ -615,6 +629,32 @@ class Redactor {
615
629
  default: 60,
616
630
  description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
617
631
  },
632
+ {
633
+ displayName: 'Use Tenant Profile',
634
+ name: 'useTenantProfile',
635
+ type: 'boolean',
636
+ displayOptions: {
637
+ show: {
638
+ operation: ['redact'],
639
+ },
640
+ },
641
+ default: false,
642
+ description: 'Whether to apply per-client settings from a JSON profile on top of this node',
643
+ },
644
+ {
645
+ displayName: 'Tenant Profile (JSON)',
646
+ name: 'tenantProfile',
647
+ type: 'json',
648
+ required: true,
649
+ displayOptions: {
650
+ show: {
651
+ operation: ['redact'],
652
+ useTenantProfile: [true],
653
+ },
654
+ },
655
+ default: '{}',
656
+ description: 'Optional per-client settings that override this node: enabledPatterns, customPatterns, confidenceThreshold, redactionMode, dedup, fieldMode, fieldRules, allowList, denyList, retentionMinutes, auditLog. Unknown fields are rejected.',
657
+ },
618
658
  {
619
659
  displayName: 'Purge Scope',
620
660
  name: 'purgeScope',
@@ -713,12 +753,25 @@ class Redactor {
713
753
  },
714
754
  ],
715
755
  };
756
+ this.methods = {
757
+ credentialTest: {
758
+ async redactorVaultPassphrase(credential) {
759
+ var _a, _b;
760
+ const passphrase = String((_b = (_a = credential.data) === null || _a === void 0 ? void 0 : _a.passphrase) !== null && _b !== void 0 ? _b : '');
761
+ if (passphrase.length < 12) {
762
+ return { status: 'Error', message: 'Use a passphrase of at least 12 characters.' };
763
+ }
764
+ return { status: 'OK', message: 'Passphrase accepted.' };
765
+ },
766
+ },
767
+ };
716
768
  }
717
769
  async execute() {
718
770
  var _a, _b, _c, _d, _e, _f, _g;
719
771
  const items = this.getInputData();
720
772
  const allPaired = items.map((_, i) => ({ item: i }));
721
773
  const operation = this.getNodeParameter('operation', 0);
774
+ const startTime = Date.now();
722
775
  (0, presidio_1.resetCircuitBreaker)();
723
776
  if (operation === 'detect' || operation === 'verify') {
724
777
  const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
@@ -734,6 +787,7 @@ class Redactor {
734
787
  fieldRules: [],
735
788
  fieldMode: 'all',
736
789
  skipSemantic: operation === 'verify',
790
+ scanNumbers: true,
737
791
  };
738
792
  const allHits = [];
739
793
  for (let i = 0; i < items.length; i++) {
@@ -759,8 +813,21 @@ class Redactor {
759
813
  includeReport = this.getNodeParameter('includeReport', 0, false);
760
814
  }
761
815
  catch { }
762
- if (includeReport) {
763
- return [returnData, [{ json: { ...summary, hits: report.hits }, pairedItem: allPaired }]];
816
+ let auditLog = false;
817
+ try {
818
+ auditLog = this.getNodeParameter('auditLog', 0, false);
819
+ }
820
+ catch { }
821
+ if (includeReport || auditLog) {
822
+ const reportJson = includeReport
823
+ ? { ...summary, hits: report.hits }
824
+ : {};
825
+ if (auditLog) {
826
+ reportJson.audit = (0, audit_1.buildAuditEntry)('DETECT', 'token', allHits, items.length, Date.now() - startTime, enabledPatterns.length, false, (0, classification_1.classifyData)(allHits));
827
+ }
828
+ if (!includeReport)
829
+ returnData.push({ json: summary, pairedItem: allPaired });
830
+ return [returnData, [{ json: reportJson, pairedItem: allPaired }]];
764
831
  }
765
832
  returnData.push({ json: summary, pairedItem: allPaired });
766
833
  return [returnData];
@@ -818,16 +885,10 @@ class Redactor {
818
885
  pairedItem: allPaired,
819
886
  }]];
820
887
  }
821
- const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'file');
822
- const vaultDir = this.getNodeParameter('vaultDir', 0, '');
823
- let vaultPassphrase = '';
824
- try {
825
- vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
826
- }
827
- catch { }
828
- const vault = (0, vault_1.createVault)(vaultStorage, vaultDir || undefined, vaultPassphrase || undefined);
888
+ const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
889
+ const vaultPassphrase = vaultStorage === 'memory' ? '' : await (0, runtime_1.resolvePassphrase)(this);
890
+ const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined, (0, runtime_1.workflowNamespace)(this));
829
891
  if (operation === 'stats') {
830
- vault.cleanup();
831
892
  const sessions = vault.listSessions();
832
893
  const totalEntries = sessions.reduce((sum, s) => sum + s.entryCount, 0);
833
894
  return [[
@@ -857,7 +918,7 @@ class Redactor {
857
918
  }
858
919
  }
859
920
  if (operation === 'redact') {
860
- const mode = this.getNodeParameter('redactionMode', 0, 'token');
921
+ let mode = this.getNodeParameter('redactionMode', 0, 'token');
861
922
  const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
862
923
  let enabledPatterns;
863
924
  if (detectionScope === 'all') {
@@ -869,17 +930,29 @@ class Redactor {
869
930
  else {
870
931
  enabledPatterns = this.getNodeParameter('piiTypes', 0);
871
932
  }
872
- const dedup = this.getNodeParameter('dedup', 0, true);
873
- const fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
874
- const sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
933
+ let dedup = this.getNodeParameter('dedup', 0, true);
934
+ let fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
935
+ let sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
875
936
  const includeReport = this.getNodeParameter('includeReport', 0, false);
876
937
  const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
877
- const customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
938
+ let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
878
939
  const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
879
- const fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
940
+ let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
880
941
  field: r.field,
881
942
  mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
882
943
  }));
944
+ const presidioRequester = async ({ method, url, body, timeout }) => {
945
+ const response = await this.helpers.httpRequest({
946
+ method: method,
947
+ url,
948
+ body,
949
+ json: true,
950
+ timeout,
951
+ returnFullResponse: true,
952
+ ignoreHttpStatusErrors: true,
953
+ });
954
+ return { statusCode: response.statusCode, body: response.body };
955
+ };
883
956
  let enablePresidio = false;
884
957
  let presidioUrl = '';
885
958
  let presidioLanguage = 'en';
@@ -893,17 +966,63 @@ class Redactor {
893
966
  catch {
894
967
  }
895
968
  const allowListRaw = this.getNodeParameter('allowList', 0, {});
896
- const allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
969
+ let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
897
970
  value: e.value,
898
971
  type: (e.type || 'contains'),
899
972
  }));
900
973
  const denyListRaw = this.getNodeParameter('denyList', 0, {});
901
- const denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
974
+ let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
902
975
  value: e.value,
903
976
  type: (e.type || 'exact'),
904
977
  }));
905
- const confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
906
- vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
978
+ let confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
979
+ const hasReportOutput = includeReport || this.getNodeParameter('auditLog', 0, false);
980
+ let auditLog = this.getNodeParameter('auditLog', 0, false);
981
+ const useTenantProfile = this.getNodeParameter('useTenantProfile', 0, false);
982
+ const profileRaw = useTenantProfile ? this.getNodeParameter('tenantProfile', 0, '') : '';
983
+ if (profileRaw !== '' && profileRaw !== undefined && profileRaw !== null) {
984
+ let profile;
985
+ try {
986
+ profile = (0, profiles_1.parseTenantProfile)(profileRaw, (0, patterns_1.getPatternOptions)().map((o) => o.value));
987
+ }
988
+ catch (error) {
989
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
990
+ description: 'Fix the Tenant Profile (JSON) field, or leave it empty to use the node settings.',
991
+ });
992
+ }
993
+ if (profile.enabledPatterns)
994
+ enabledPatterns = profile.enabledPatterns;
995
+ if (profile.customPatterns)
996
+ customPatterns = profile.customPatterns;
997
+ if (profile.confidenceThreshold !== undefined)
998
+ confidenceThreshold = profile.confidenceThreshold;
999
+ if (profile.redactionMode)
1000
+ mode = profile.redactionMode;
1001
+ if (profile.dedup !== undefined)
1002
+ dedup = profile.dedup;
1003
+ if (profile.fieldMode)
1004
+ fieldMode = profile.fieldMode;
1005
+ if (profile.fieldRules)
1006
+ fieldRules = profile.fieldRules;
1007
+ if (profile.allowList)
1008
+ allowList = profile.allowList;
1009
+ if (profile.denyList)
1010
+ denyList = profile.denyList;
1011
+ if (profile.retentionMinutes !== undefined)
1012
+ sessionTtl = profile.retentionMinutes;
1013
+ if (profile.auditLog !== undefined)
1014
+ auditLog = profile.auditLog;
1015
+ }
1016
+ (0, runtime_1.assertUserRegexes)(this, customPatterns, [
1017
+ { name: 'Allow list', entries: allowList },
1018
+ { name: 'Deny list', entries: denyList },
1019
+ ]);
1020
+ try {
1021
+ vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1022
+ }
1023
+ catch (error) {
1024
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
1025
+ }
907
1026
  const ctx = {
908
1027
  enabledPatterns,
909
1028
  customPatterns,
@@ -916,6 +1035,7 @@ class Redactor {
916
1035
  confidenceThreshold,
917
1036
  presidioUrl: enablePresidio ? presidioUrl : undefined,
918
1037
  presidioLanguage: enablePresidio ? presidioLanguage : undefined,
1038
+ presidioRequest: enablePresidio ? presidioRequester : undefined,
919
1039
  };
920
1040
  const allHits = [];
921
1041
  const returnData = [];
@@ -941,23 +1061,16 @@ class Redactor {
941
1061
  for (let i = 0; i < items.length; i++) {
942
1062
  const result = (0, injection_1.scanForInjection)(items[i].json);
943
1063
  if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
944
- injectionResult = result;
1064
+ injectionResult = (0, runtime_1.sanitizeInjection)(result);
945
1065
  }
946
1066
  }
947
1067
  }
948
1068
  }
949
1069
  catch {
950
1070
  }
951
- try {
952
- const auditLog = this.getNodeParameter('auditLog', 0, false);
953
- if (auditLog) {
954
- const startTime = Date.now();
955
- const classification = (0, classification_1.classifyData)(allHits);
956
- (0, audit_1.writeAuditLog)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, classification, sessionId);
957
- }
958
- }
959
- catch {
960
- }
1071
+ const auditEntry = auditLog && hasReportOutput
1072
+ ? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId)
1073
+ : undefined;
961
1074
  if (injectionResult && injectionResult.detected) {
962
1075
  returnData.push({
963
1076
  json: {
@@ -966,12 +1079,14 @@ class Redactor {
966
1079
  pairedItem: allPaired,
967
1080
  });
968
1081
  }
969
- if (includeReport) {
970
- const report = (0, engine_1.buildReport)(sessionId, allHits);
971
- if (this.getNodeParameter('includeRopa', 0, false)) {
1082
+ if (hasReportOutput) {
1083
+ const report = includeReport ? (0, engine_1.buildReport)(sessionId, allHits) : {};
1084
+ if (auditEntry)
1085
+ report.audit = auditEntry;
1086
+ if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
972
1087
  const d = this.getNodeParameter('ropa', 0, {});
973
1088
  const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
974
- report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, this.getNodeParameter('auditLog', 0, false), { name: String((_e = d.controllerName) !== null && _e !== void 0 ? _e : ''), contactEmail: String((_f = d.controllerContact) !== null && _f !== void 0 ? _f : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_g = d.legalBasis) !== null && _g !== void 0 ? _g : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1089
+ report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_e = d.controllerName) !== null && _e !== void 0 ? _e : ''), contactEmail: String((_f = d.controllerContact) !== null && _f !== void 0 ? _f : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_g = d.legalBasis) !== null && _g !== void 0 ? _g : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
975
1090
  }
976
1091
  return [returnData, [{ json: report, pairedItem: allPaired }]];
977
1092
  }
@@ -981,8 +1096,8 @@ class Redactor {
981
1096
  const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
982
1097
  const session = vault.getSession(sessionId);
983
1098
  if (!session) {
984
- 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.`, {
985
- description: 'Testing step by step? Each "Execute step" gets a new execution ID, so Redact and Restore no longer share a session. Run the whole workflow, or set the same fixed Session ID (for example "test-1") on both nodes.',
1099
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}" that this passphrase can open. Ensure a Redact node ran first with the same Session ID, vault storage type and passphrase.`, {
1100
+ description: 'Testing step by step? Each "Execute step" gets a new execution ID, and n8n saves workflow storage only after production runs of an active workflow. Run the whole workflow, or set the same fixed Session ID (for example "test-1") on both nodes. Restore only works in the workflow that ran Redact.',
986
1101
  });
987
1102
  }
988
1103
  const returnData = [];
@@ -33,23 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.writeAuditLog = writeAuditLog;
37
- exports.readAuditLog = readAuditLog;
38
- const fs = __importStar(require("fs"));
39
- const path = __importStar(require("path"));
40
- const MAX_FILE_SIZE = 104857600;
41
- const DEFAULT_AUDIT_DIR = path.join(process.env.HOME || '/tmp', '.n8n', 'pii-audit');
42
- function generateEventId() {
43
- const bytes = new Array(16).fill(0).map(() => Math.floor(Math.random() * 256));
44
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
45
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
46
- const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
47
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
48
- }
49
- function getLogFilePath(auditDir) {
50
- const today = new Date().toISOString().split('T')[0];
51
- return path.join(auditDir, `pii-audit-${today}.jsonl`);
52
- }
36
+ exports.buildAuditEntry = buildAuditEntry;
37
+ const crypto = __importStar(require("crypto"));
53
38
  function determineSeverity(classificationLevel, totalHits = 0) {
54
39
  if (classificationLevel === 'RESTRICTED' || totalHits >= 50)
55
40
  return 'CRITICAL';
@@ -59,87 +44,29 @@ function determineSeverity(classificationLevel, totalHits = 0) {
59
44
  return 'MEDIUM';
60
45
  return 'LOW';
61
46
  }
62
- function writeAuditLog(action, mode, hits, inputItemCount, processingTimeMs, patternsUsed, presidioEnabled, classification, sessionId, auditDir) {
63
- try {
64
- const dir = auditDir || DEFAULT_AUDIT_DIR;
65
- if (!fs.existsSync(dir)) {
66
- fs.mkdirSync(dir, { recursive: true });
67
- }
68
- const hitsByCategory = {};
69
- const hitsByPattern = {};
70
- for (const hit of hits) {
71
- hitsByCategory[hit.category] = (hitsByCategory[hit.category] || 0) + 1;
72
- hitsByPattern[hit.patternLabel] = (hitsByPattern[hit.patternLabel] || 0) + 1;
73
- }
74
- const entry = {
75
- version: '1.0',
76
- timestamp: new Date().toISOString(),
77
- eventId: generateEventId(),
78
- severity: determineSeverity(classification === null || classification === void 0 ? void 0 : classification.level, hits.length),
79
- action,
80
- mode,
81
- status: 'SUCCESS',
82
- totalHits: hits.length,
83
- hitsByCategory,
84
- hitsByPattern,
85
- classificationLevel: classification === null || classification === void 0 ? void 0 : classification.level,
86
- inputItemCount,
87
- processingTimeMs,
88
- patternsUsed,
89
- presidioEnabled,
90
- sessionId,
91
- };
92
- const logFile = getLogFilePath(dir);
93
- try {
94
- if (fs.existsSync(logFile)) {
95
- const stats = fs.statSync(logFile);
96
- if (stats.size >= MAX_FILE_SIZE) {
97
- let seq = 1;
98
- while (fs.existsSync(`${logFile}.${seq}`))
99
- seq++;
100
- fs.renameSync(logFile, `${logFile}.${seq}`);
101
- }
102
- }
103
- }
104
- catch {
105
- }
106
- fs.appendFileSync(logFile, JSON.stringify(entry) + '\n', 'utf-8');
107
- }
108
- catch {
109
- }
110
- }
111
- function readAuditLog(startDate, endDate, auditDir) {
112
- try {
113
- const dir = auditDir || DEFAULT_AUDIT_DIR;
114
- if (!fs.existsSync(dir))
115
- return [];
116
- const files = fs.readdirSync(dir)
117
- .filter((f) => f.startsWith('pii-audit-') && f.endsWith('.jsonl'))
118
- .sort();
119
- const entries = [];
120
- for (const file of files) {
121
- const fileDate = file.replace('pii-audit-', '').replace('.jsonl', '');
122
- if (startDate && fileDate < startDate)
123
- continue;
124
- if (endDate && fileDate > endDate)
125
- continue;
126
- try {
127
- const content = fs.readFileSync(path.join(dir, file), 'utf-8');
128
- const lines = content.split('\n').filter((l) => l.trim().length > 0);
129
- for (const line of lines) {
130
- try {
131
- entries.push(JSON.parse(line));
132
- }
133
- catch {
134
- }
135
- }
136
- }
137
- catch {
138
- }
139
- }
140
- return entries;
141
- }
142
- catch {
143
- return [];
47
+ function buildAuditEntry(action, mode, hits, inputItemCount, processingTimeMs, patternsUsed, presidioEnabled, classification, sessionId) {
48
+ const hitsByCategory = {};
49
+ const hitsByPattern = {};
50
+ for (const hit of hits) {
51
+ hitsByCategory[hit.category] = (hitsByCategory[hit.category] || 0) + 1;
52
+ hitsByPattern[hit.patternLabel] = (hitsByPattern[hit.patternLabel] || 0) + 1;
144
53
  }
54
+ return {
55
+ version: '2.0',
56
+ timestamp: new Date().toISOString(),
57
+ eventId: crypto.randomUUID(),
58
+ severity: determineSeverity(classification === null || classification === void 0 ? void 0 : classification.level, hits.length),
59
+ action,
60
+ mode,
61
+ status: 'SUCCESS',
62
+ totalHits: hits.length,
63
+ hitsByCategory,
64
+ hitsByPattern,
65
+ classificationLevel: classification === null || classification === void 0 ? void 0 : classification.level,
66
+ inputItemCount,
67
+ processingTimeMs,
68
+ patternsUsed,
69
+ presidioEnabled,
70
+ sessionId,
71
+ };
145
72
  }