n8n-nodes-redactor 6.0.0 → 7.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.
package/README.md CHANGED
@@ -15,13 +15,13 @@
15
15
  <a href="https://github.com/mjmirza/n8n-nodes-redactor/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mjmirza/n8n-nodes-redactor/ci.yml?branch=master&label=CI" alt="CI status" /></a>
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
- <img src="https://img.shields.io/badge/PII%20patterns-216-blue" alt="216 PII patterns" />
18
+ <img src="https://img.shields.io/badge/PII%20patterns-218-blue" alt="218 PII patterns" />
19
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
23
23
 
24
- - **Personal data never reaches the model.** Emails, phone numbers, IBANs, card numbers, national IDs and 200 more kinds of data are swapped for safe tokens before the LLM call.
24
+ - **Personal data is kept from the model.** Emails, phone numbers, IBANs, card numbers, national IDs and 200 more kinds of data are swapped for safe tokens before the LLM call. Pattern detection cannot know every name, so add Presidio for free-text names, and keep Verify in the chain to catch what slips through.
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.
@@ -71,9 +71,13 @@ Version 4.0.0 uses no local disk. The vault lives in workflow storage and audit
71
71
  The recommended production chain looks like this.
72
72
 
73
73
  ```
74
- Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
74
+ Data source > Redact > Verify > LLM > Restore > Output > Purge
75
75
  ```
76
76
 
77
+ Detect is for a dry run before you set up redaction. It adds a summary item to its output by default, so keep it out of this chain, or turn on Include Audit Report, which moves the summary to a second output.
78
+
79
+ Set Verify Output on the Verify node to Items, Stop on FAIL. Verify then passes the redacted items on unchanged when nothing leaks, and stops the run before the LLM when something does, so the model never receives data that failed the check. With the default Report output, Verify returns one report instead of the items, so it cannot sit in this chain. Verify also fails when an item carries a binary attachment, because attachments are not scanned. Redact removes attachments by default. Set Binary Attachments to Keep Unscanned only when the files hold no personal data.
80
+
77
81
  ## Redaction modes
78
82
 
79
83
  | Mode | Output | Reversible | Best for |
@@ -88,7 +92,7 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
88
92
 
89
93
  ## What it detects
90
94
 
91
- 216 patterns across 11 categories.
95
+ 218 patterns across 11 categories.
92
96
 
93
97
  | Category | Examples |
94
98
  |---|---|
@@ -117,7 +121,7 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
117
121
  | Audit report | A second output with hits by category and pattern |
118
122
  | 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
123
  | ROPA record | Turn on Include Audit Report, then Include GDPR Art.30 ROPA Record, and fill in the controller details |
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 |
124
+ | 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). When Presidio cannot scan a text the run stops, unless If Presidio Fails is set to continue with patterns only |
121
125
  | 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
126
 
123
127
  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.
@@ -13,7 +13,7 @@ const profiles_1 = require("./profiles");
13
13
  const ropa_1 = require("./ropa");
14
14
  const runtime_1 = require("./runtime");
15
15
  const RECOMMENDED_PATTERNS = [
16
- 'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
16
+ 'email', 'phone', 'phoneE164', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
17
17
  'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
18
18
  'personName', 'ssn', 'itinUS', 'sinCA', 'ninoUK', 'passportUS', 'passportEU',
19
19
  'passportDE', 'nationalIdDE', 'nhsNumber', 'taxIdUS', 'taxIdDE',
@@ -35,7 +35,7 @@ const RECOMMENDED_PATTERNS = [
35
35
  'internalHostname', 'uncPath', 'ldapDN', 'adUsername',
36
36
  'dbConnectionString', 'connStringKV',
37
37
  'awsAccessKey', 'gcpApiKey', 'stripeKey', 'openaiKey', 'githubToken',
38
- 'slackToken', 'bearerToken', 'jwtToken', 'pemPrivateKey', 'sshPublicKey',
38
+ 'slackToken', 'bearerToken', 'httpBasicAuth', 'httpBasicCredential', 'jwtToken', 'pemPrivateKey', 'sshPublicKey',
39
39
  'genericSecret', 'azureKey', 'slackWebhook',
40
40
  'sendgridKey', 'twilioKey', 'anthropicKey', 'gitlabToken',
41
41
  'x509Certificate', 'pgpPrivateKey', 'envSecrets',
@@ -43,6 +43,19 @@ const RECOMMENDED_PATTERNS = [
43
43
  'vin',
44
44
  'uuid', 'imei', 'iccid',
45
45
  ];
46
+ function verifyMessage(leaks, attachments) {
47
+ if (leaks === 0 && attachments === 0)
48
+ return 'No PII detected in values. Data appears properly redacted.';
49
+ const parts = [];
50
+ if (leaks > 0)
51
+ parts.push(`Found ${leaks} potential PII leak(s). Values contain detectable sensitive data.`);
52
+ if (attachments > 0)
53
+ parts.push(`${attachments} binary attachment(s) were not scanned and may contain PII.`);
54
+ return parts.join(' ');
55
+ }
56
+ function withBinary(out, source) {
57
+ return source.binary ? { ...out, binary: source.binary } : out;
58
+ }
46
59
  function versionRopaSummary(ropa, byLevel, typeVersion) {
47
60
  const summary = ropa.processingSummary;
48
61
  if (typeVersion >= 2) {
@@ -529,6 +542,54 @@ class Redactor {
529
542
  default: false,
530
543
  description: 'Whether to scan input for prompt injection attempts (jailbreaks, instruction overrides, delimiter escapes, encoded payloads) before it reaches an LLM. Detected injections are flagged in the output.',
531
544
  },
545
+ {
546
+ displayName: 'Verify Output',
547
+ name: 'verifyOutput',
548
+ type: 'options',
549
+ displayOptions: {
550
+ show: {
551
+ operation: ['verify'],
552
+ },
553
+ },
554
+ options: [
555
+ {
556
+ name: 'Report',
557
+ value: 'report',
558
+ description: 'Return one report with PASS or FAIL',
559
+ },
560
+ {
561
+ name: 'Items, Stop on FAIL',
562
+ value: 'gate',
563
+ description: 'Pass the checked items on unchanged when they pass, and stop the run when they fail. Place it before the LLM.',
564
+ },
565
+ ],
566
+ default: 'report',
567
+ description: 'What Verify returns',
568
+ },
569
+ {
570
+ displayName: 'Binary Attachments',
571
+ name: 'binaryAttachments',
572
+ type: 'options',
573
+ displayOptions: {
574
+ show: {
575
+ operation: ['redact'],
576
+ },
577
+ },
578
+ options: [
579
+ {
580
+ name: 'Remove',
581
+ value: 'remove',
582
+ description: 'Drop attachments from the output, so no unscanned data reaches the next node',
583
+ },
584
+ {
585
+ name: 'Keep Unscanned',
586
+ value: 'keep',
587
+ description: 'Pass attachments through unchanged. Their content is not scanned or redacted.',
588
+ },
589
+ ],
590
+ default: 'remove',
591
+ description: 'What to do with binary attachments. The node redacts JSON only, so a file can still hold personal data.',
592
+ },
532
593
  {
533
594
  displayName: 'Audit Log',
534
595
  name: 'auditLog',
@@ -551,7 +612,7 @@ class Redactor {
551
612
  },
552
613
  },
553
614
  default: false,
554
- description: 'Whether to add NLP-based detection using Microsoft Presidio. Catches person names, locations, and organizations in free text that regex cannot detect. Requires a Presidio Docker container running locally. Setup: docker run -d -p 5002:3000 mcr.microsoft.com/presidio-analyzer',
615
+ description: 'Whether to add NLP-based detection using Microsoft Presidio. Catches person names, locations, and organizations in free text that regex cannot detect. Requires a Presidio Docker container running locally. Setup: docker run -d -p 127.0.0.1:5002:3000 mcr.microsoft.com/presidio-analyzer',
555
616
  },
556
617
  {
557
618
  displayName: 'Presidio URL',
@@ -567,6 +628,31 @@ class Redactor {
567
628
  placeholder: 'http://localhost:5002',
568
629
  description: 'URL of the Presidio Analyzer service. Default: http://localhost:5002. If using docker-compose, use the service name: http://presidio:3000',
569
630
  },
631
+ {
632
+ displayName: 'If Presidio Fails',
633
+ name: 'presidioOnFailure',
634
+ type: 'options',
635
+ displayOptions: {
636
+ show: {
637
+ operation: ['redact'],
638
+ enablePresidio: [true],
639
+ },
640
+ },
641
+ options: [
642
+ {
643
+ name: 'Stop the Run',
644
+ value: 'stop',
645
+ description: 'Output nothing when any text could not be scanned by Presidio',
646
+ },
647
+ {
648
+ name: 'Continue With Patterns Only',
649
+ value: 'continue',
650
+ description: 'Output the pattern-only result and count the unscanned texts in the report',
651
+ },
652
+ ],
653
+ default: 'stop',
654
+ description: 'What to do when Presidio cannot be reached or returns an error for a text',
655
+ },
570
656
  {
571
657
  displayName: 'Presidio Language',
572
658
  name: 'presidioLanguage',
@@ -778,12 +864,11 @@ class Redactor {
778
864
  };
779
865
  }
780
866
  async execute() {
781
- var _a, _b, _c, _d, _e, _f, _g, _h;
867
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
782
868
  const items = this.getInputData();
783
869
  const allPaired = items.map((_, i) => ({ item: i }));
784
- const operation = this.getNodeParameter('operation', 0);
870
+ const operation = (0, runtime_1.runSetting)(this, 'operation');
785
871
  const startTime = Date.now();
786
- (0, presidio_1.resetCircuitBreaker)();
787
872
  if (operation === 'detect' || operation === 'verify') {
788
873
  const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
789
874
  const customPatterns = [];
@@ -798,6 +883,7 @@ class Redactor {
798
883
  fieldRules: [],
799
884
  fieldMode: 'all',
800
885
  skipSemantic: operation === 'verify',
886
+ skipNames: operation === 'verify' && this.getNode().typeVersion < 2,
801
887
  scanNumbers: true,
802
888
  };
803
889
  const allHits = [];
@@ -809,7 +895,7 @@ class Redactor {
809
895
  }
810
896
  tempVault.deleteSession(tempSessionId);
811
897
  if (operation === 'detect') {
812
- const returnData = items.map((item, i) => ({ json: item.json, pairedItem: { item: i } }));
898
+ const returnData = items.map((item, i) => withBinary({ json: item.json, pairedItem: { item: i } }, item));
813
899
  const report = (0, engine_1.buildReport)(tempSessionId, allHits);
814
900
  const summary = {
815
901
  _redactorScan: true,
@@ -820,16 +906,8 @@ class Redactor {
820
906
  hitsByPattern: report.hitsByPattern,
821
907
  timestamp: report.timestamp,
822
908
  };
823
- let includeReport = false;
824
- try {
825
- includeReport = this.getNodeParameter('includeReport', 0, false);
826
- }
827
- catch { }
828
- let auditLog = false;
829
- try {
830
- auditLog = this.getNodeParameter('auditLog', 0, false);
831
- }
832
- catch { }
909
+ const includeReport = (0, runtime_1.runSetting)(this, 'includeReport', false);
910
+ const auditLog = (0, runtime_1.runSetting)(this, 'auditLog', false);
833
911
  if (includeReport || auditLog) {
834
912
  const reportJson = includeReport
835
913
  ? { ...summary, hits: report.hits }
@@ -844,17 +922,25 @@ class Redactor {
844
922
  returnData.push({ json: summary, pairedItem: allPaired });
845
923
  return [returnData];
846
924
  }
847
- const passed = allHits.length === 0;
925
+ const unscannedAttachments = items.reduce((n, item) => { var _a; return n + Object.keys((_a = item.binary) !== null && _a !== void 0 ? _a : {}).length; }, 0);
926
+ const passed = allHits.length === 0 && unscannedAttachments === 0;
848
927
  const verifyReport = (0, engine_1.buildReport)(tempSessionId, allHits);
928
+ if ((0, runtime_1.runSetting)(this, 'verifyOutput', 'report') === 'gate') {
929
+ if (!passed) {
930
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), verifyMessage(allHits.length, unscannedAttachments), {
931
+ description: 'Verify stopped the run before the data reached the next node. Fix the redaction settings and run again.',
932
+ });
933
+ }
934
+ return [items.map((item, i) => ({ ...item, pairedItem: { item: i } }))];
935
+ }
849
936
  return [[{
850
937
  json: {
851
938
  verified: passed,
852
939
  status: passed ? 'PASS' : 'FAIL',
853
940
  leaksFound: allHits.length,
941
+ unscannedAttachments,
854
942
  itemsScanned: items.length,
855
- message: passed
856
- ? 'No PII detected in values. Data appears properly redacted.'
857
- : `Found ${allHits.length} potential PII leak(s). Values contain detectable sensitive data.`,
943
+ message: verifyMessage(allHits.length, unscannedAttachments),
858
944
  hitsByCategory: verifyReport.hitsByCategory,
859
945
  hitsByPattern: verifyReport.hitsByPattern,
860
946
  leaks: allHits.map((h) => ({
@@ -899,7 +985,7 @@ class Redactor {
899
985
  pairedItem: allPaired,
900
986
  }]];
901
987
  }
902
- const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
988
+ const vaultStorage = (0, runtime_1.runSetting)(this, 'vaultStorage', 'workflow');
903
989
  const vaultPassphrase = vaultStorage === 'memory' ? '' : await (0, runtime_1.resolvePassphrase)(this);
904
990
  const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined, (0, runtime_1.workflowNamespace)(this));
905
991
  if (operation === 'stats') {
@@ -926,32 +1012,27 @@ class Redactor {
926
1012
  }
927
1013
  let sessionId = '';
928
1014
  if (operation === 'redact' || operation === 'restore') {
929
- sessionId = this.getNodeParameter('sessionId', 0);
1015
+ sessionId = String((_a = (0, runtime_1.runSetting)(this, 'sessionId')) !== null && _a !== void 0 ? _a : '');
930
1016
  if (!sessionId) {
931
1017
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required');
932
1018
  }
933
1019
  }
934
1020
  if (operation === 'redact') {
935
- let mode = this.getNodeParameter('redactionMode', 0, 'token');
936
- const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
937
- let enabledPatterns;
938
- if (detectionScope === 'all') {
939
- enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
940
- }
941
- else if (detectionScope === 'recommended') {
942
- enabledPatterns = RECOMMENDED_PATTERNS;
943
- }
944
- else {
945
- enabledPatterns = this.getNodeParameter('piiTypes', 0);
1021
+ let mode = (0, runtime_1.runSetting)(this, 'redactionMode', 'token');
1022
+ if (!profiles_1.MODES.includes(mode)) {
1023
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown redaction mode "${String(mode)}"`, {
1024
+ description: `Use one of: ${profiles_1.MODES.join(', ')}.`,
1025
+ });
946
1026
  }
947
- let dedup = this.getNodeParameter('dedup', 0, true);
948
- let fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
949
- let sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
950
- const includeReport = this.getNodeParameter('includeReport', 0, false);
951
- const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
952
- let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
953
- const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
954
- let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
1027
+ let enabledPatterns = (0, runtime_1.selectedPatterns)(this, (0, patterns_1.getPatternOptions)().map((p) => p.value), RECOMMENDED_PATTERNS);
1028
+ let dedup = (0, runtime_1.runSetting)(this, 'dedup', true);
1029
+ let fieldMode = (0, runtime_1.runSetting)(this, 'fieldMode', 'all');
1030
+ let sessionTtl = (0, runtime_1.runSetting)(this, 'sessionTtl', 60);
1031
+ const includeReport = (0, runtime_1.runSetting)(this, 'includeReport', false);
1032
+ const customPatternsRaw = (0, runtime_1.runSetting)(this, 'customPatterns', {});
1033
+ let customPatterns = (_b = customPatternsRaw.patterns) !== null && _b !== void 0 ? _b : [];
1034
+ const fieldRulesRaw = (0, runtime_1.runSetting)(this, 'fieldRules', {});
1035
+ let fieldRules = ((_c = fieldRulesRaw.rules) !== null && _c !== void 0 ? _c : []).map((r) => ({
955
1036
  field: r.field,
956
1037
  mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
957
1038
  }));
@@ -967,33 +1048,39 @@ class Redactor {
967
1048
  });
968
1049
  return { statusCode: response.statusCode, body: response.body };
969
1050
  };
970
- let enablePresidio = false;
1051
+ const binaryAttachments = (0, runtime_1.runSetting)(this, 'binaryAttachments', 'remove');
1052
+ const enablePresidio = (0, runtime_1.runSetting)(this, 'enablePresidio', false);
971
1053
  let presidioUrl = '';
972
1054
  let presidioLanguage = 'en';
973
- try {
974
- enablePresidio = this.getNodeParameter('enablePresidio', 0, false);
975
- if (enablePresidio) {
976
- presidioUrl = this.getNodeParameter('presidioUrl', 0, 'http://localhost:5002');
977
- presidioLanguage = this.getNodeParameter('presidioLanguage', 0, 'en');
1055
+ let presidioOnFailure = 'stop';
1056
+ if (enablePresidio) {
1057
+ presidioUrl = (0, runtime_1.runSetting)(this, 'presidioUrl', 'http://localhost:5002');
1058
+ presidioLanguage = (0, runtime_1.runSetting)(this, 'presidioLanguage', 'en');
1059
+ presidioOnFailure = (0, runtime_1.runSetting)(this, 'presidioOnFailure', 'stop');
1060
+ if (presidioOnFailure !== 'stop' && presidioOnFailure !== 'continue') {
1061
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'If Presidio Fails must be stop or continue');
1062
+ }
1063
+ if (!/^https?:[/][/][^/\s]/i.test(String(presidioUrl).trim())) {
1064
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Presidio URL must be an http or https address', {
1065
+ description: 'Set Presidio URL, for example http://localhost:5002, or turn off Enhanced NLP Detection.',
1066
+ });
978
1067
  }
979
1068
  }
980
- catch {
981
- }
982
- const allowListRaw = this.getNodeParameter('allowList', 0, {});
983
- let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
1069
+ const allowListRaw = (0, runtime_1.runSetting)(this, 'allowList', {});
1070
+ let allowList = ((_d = allowListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
984
1071
  value: e.value,
985
1072
  type: (e.type || 'contains'),
986
1073
  }));
987
- const denyListRaw = this.getNodeParameter('denyList', 0, {});
988
- let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
1074
+ const denyListRaw = (0, runtime_1.runSetting)(this, 'denyList', {});
1075
+ let denyList = ((_e = denyListRaw.entries) !== null && _e !== void 0 ? _e : []).map((e) => ({
989
1076
  value: e.value,
990
1077
  type: (e.type || 'exact'),
991
1078
  }));
992
- let confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
993
- const hasReportOutput = includeReport || this.getNodeParameter('auditLog', 0, false);
994
- let auditLog = this.getNodeParameter('auditLog', 0, false);
995
- const useTenantProfile = this.getNodeParameter('useTenantProfile', 0, false);
996
- const profileRaw = useTenantProfile ? this.getNodeParameter('tenantProfile', 0, '') : '';
1079
+ let confidenceThreshold = (0, runtime_1.runSetting)(this, 'confidenceThreshold', 0);
1080
+ const hasReportOutput = includeReport || (0, runtime_1.runSetting)(this, 'auditLog', false);
1081
+ let auditLog = (0, runtime_1.runSetting)(this, 'auditLog', false);
1082
+ const useTenantProfile = (0, runtime_1.runSetting)(this, 'useTenantProfile', false);
1083
+ const profileRaw = useTenantProfile ? (0, runtime_1.runSetting)(this, 'tenantProfile', '') : '';
997
1084
  if (profileRaw !== '' && profileRaw !== undefined && profileRaw !== null) {
998
1085
  let profile;
999
1086
  try {
@@ -1026,13 +1113,28 @@ class Redactor {
1026
1113
  sessionTtl = profile.retentionMinutes;
1027
1114
  if (profile.auditLog !== undefined)
1028
1115
  auditLog = profile.auditLog;
1116
+ if (profile.fieldMode && !profile.fieldRules) {
1117
+ fieldRules = fieldRules.map((rule) => ({
1118
+ field: rule.field,
1119
+ mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
1120
+ }));
1121
+ }
1122
+ }
1123
+ if (typeof confidenceThreshold !== 'number' || !Number.isFinite(confidenceThreshold) || confidenceThreshold < 0 || confidenceThreshold > 1) {
1124
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Confidence Threshold must be a number from 0 to 1, got ${String(confidenceThreshold)}`);
1125
+ }
1126
+ if (typeof sessionTtl !== 'number' || !Number.isFinite(sessionTtl) || sessionTtl < 0) {
1127
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Session TTL must be 0 or more minutes, got ${String(sessionTtl)}`, {
1128
+ description: 'Use 0 to keep the session until it is purged.',
1129
+ });
1029
1130
  }
1030
1131
  (0, runtime_1.assertUserRegexes)(this, customPatterns, [
1031
1132
  { name: 'Allow list', entries: allowList },
1032
1133
  { name: 'Deny list', entries: denyList },
1033
1134
  ]);
1034
1135
  try {
1035
- vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1136
+ const session = vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
1137
+ sessionTtl = session.ttl / 60000;
1036
1138
  }
1037
1139
  catch (error) {
1038
1140
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
@@ -1054,9 +1156,10 @@ class Redactor {
1054
1156
  reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
1055
1157
  keyOrigins: new WeakMap(),
1056
1158
  inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1159
+ inputIndex: {},
1057
1160
  lockedSpans: new WeakMap(),
1058
1161
  keyLocks: new WeakMap(),
1059
- priorTokens: (0, engine_1.priorTokenMatcher)((_e = vault.getSession(sessionId)) === null || _e === void 0 ? void 0 : _e.entries),
1162
+ priorTokens: (0, engine_1.priorTokenMatcher)((_f = vault.getSession(sessionId)) === null || _f === void 0 ? void 0 : _f.entries),
1060
1163
  };
1061
1164
  const allHits = [];
1062
1165
  const returnData = [];
@@ -1064,34 +1167,50 @@ class Redactor {
1064
1167
  const item = items[i];
1065
1168
  const hits = [];
1066
1169
  let redacted = (0, engine_1.redactValue)(item.json, ctx, vault, sessionId, hits, i);
1067
- if (ctx.presidioUrl) {
1170
+ if (enablePresidio) {
1068
1171
  try {
1069
1172
  redacted = await (0, engine_1.enhanceWithPresidio)(redacted, ctx, vault, sessionId, hits, i);
1070
1173
  }
1071
- catch {
1174
+ catch (error) {
1175
+ if (presidioOnFailure === 'stop') {
1176
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Presidio scan failed: ${error.message}`, { itemIndex: i });
1177
+ }
1178
+ (0, presidio_1.markUnscanned)(presidioRequester);
1072
1179
  }
1073
1180
  }
1074
- returnData.push({ json: redacted, pairedItem: { item: i } });
1181
+ const redactedItem = { json: redacted, pairedItem: { item: i } };
1182
+ returnData.push(binaryAttachments === 'keep' ? withBinary(redactedItem, items[i]) : redactedItem);
1075
1183
  for (const hit of hits)
1076
1184
  allHits.push(hit);
1077
1185
  }
1186
+ const restorable = (0, engine_1.ensureRestorable)(items.map((it) => it.json), returnData.map((r) => r.json), ctx, vault, sessionId, allHits);
1187
+ restorable.forEach((json, i) => {
1188
+ returnData[i].json = json;
1189
+ });
1190
+ const unscannedTexts = enablePresidio ? (0, presidio_1.presidioUnscanned)(presidioRequester) : 0;
1191
+ if (unscannedTexts > 0 && presidioOnFailure === 'stop') {
1192
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Presidio could not scan ${unscannedTexts} text(s), so nothing was output`, {
1193
+ description: 'Check that Presidio is running at the Presidio URL, or set If Presidio Fails to Continue With Patterns Only.',
1194
+ });
1195
+ }
1196
+ const promptInjection = (0, runtime_1.runSetting)(this, 'promptInjection', false);
1197
+ if (!vault.getSession(sessionId)) {
1198
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session expired during the run', {
1199
+ description: 'Its TTL ran out before Redact finished, so nothing was output. Run again, or raise Session TTL.',
1200
+ });
1201
+ }
1078
1202
  vault.save(sessionId);
1079
1203
  let injectionResult = null;
1080
- try {
1081
- const promptInjection = this.getNodeParameter('promptInjection', 0, false);
1082
- if (promptInjection) {
1083
- for (let i = 0; i < items.length; i++) {
1084
- const result = (0, injection_1.scanForInjection)(items[i].json);
1085
- if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
1086
- injectionResult = (0, runtime_1.sanitizeInjection)(result);
1087
- }
1204
+ if (promptInjection) {
1205
+ for (let i = 0; i < items.length; i++) {
1206
+ const result = (0, injection_1.scanForInjection)(items[i].json);
1207
+ if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
1208
+ injectionResult = (0, runtime_1.sanitizeInjection)(result);
1088
1209
  }
1089
1210
  }
1090
1211
  }
1091
- catch {
1092
- }
1093
1212
  const auditEntry = auditLog && hasReportOutput
1094
- ? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId)
1213
+ ? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId, unscannedTexts)
1095
1214
  : undefined;
1096
1215
  if (injectionResult && injectionResult.detected) {
1097
1216
  returnData.push({
@@ -1105,10 +1224,12 @@ class Redactor {
1105
1224
  const report = includeReport ? (0, engine_1.buildReport)(sessionId, allHits) : {};
1106
1225
  if (auditEntry)
1107
1226
  report.audit = auditEntry;
1108
- if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
1109
- const d = this.getNodeParameter('ropa', 0, {});
1227
+ if (unscannedTexts > 0)
1228
+ report.presidioUnscannedTexts = unscannedTexts;
1229
+ if (includeReport && (0, runtime_1.runSetting)(this, 'includeRopa', false)) {
1230
+ const d = (0, runtime_1.runSetting)(this, 'ropa', {});
1110
1231
  const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
1111
- report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_f = d.controllerName) !== null && _f !== void 0 ? _f : ''), contactEmail: String((_g = d.controllerContact) !== null && _g !== void 0 ? _g : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_h = d.legalBasis) !== null && _h !== void 0 ? _h : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1232
+ report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_g = d.controllerName) !== null && _g !== void 0 ? _g : ''), contactEmail: String((_h = d.controllerContact) !== null && _h !== void 0 ? _h : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_j = d.legalBasis) !== null && _j !== void 0 ? _j : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1112
1233
  versionRopaSummary(report.ropa, (0, classification_1.classifyData)(allHits).hitCountByLevel, this.getNode().typeVersion);
1113
1234
  }
1114
1235
  return [returnData, [{ json: report, pairedItem: allPaired }]];
@@ -1116,18 +1237,24 @@ class Redactor {
1116
1237
  return [returnData];
1117
1238
  }
1118
1239
  if (operation === 'restore') {
1119
- const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
1240
+ const deleteAfterRestore = (0, runtime_1.runSetting)(this, 'deleteAfterRestore', true);
1120
1241
  const session = vault.getSession(sessionId);
1121
1242
  if (!session) {
1122
- 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.`, {
1243
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No vault found for this session that this passphrase can open. Ensure a Redact node ran first with the same Session ID, vault storage type and passphrase.', {
1123
1244
  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.',
1124
1245
  });
1125
1246
  }
1126
1247
  const returnData = [];
1127
1248
  for (let i = 0; i < items.length; i++) {
1128
1249
  const item = items[i];
1129
- const restored = (0, engine_1.restoreValue)(item.json, vault, sessionId);
1130
- returnData.push({ json: restored, pairedItem: { item: i } });
1250
+ let restored;
1251
+ try {
1252
+ restored = (0, engine_1.restoreFromSession)(item.json, session);
1253
+ }
1254
+ catch (error) {
1255
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, { itemIndex: i });
1256
+ }
1257
+ returnData.push(withBinary({ json: restored, pairedItem: { item: i } }, item));
1131
1258
  }
1132
1259
  if (deleteAfterRestore) {
1133
1260
  vault.deleteSession(sessionId);
@@ -1135,9 +1262,9 @@ class Redactor {
1135
1262
  return [returnData];
1136
1263
  }
1137
1264
  if (operation === 'purge') {
1138
- const purgeScope = this.getNodeParameter('purgeScope', 0, 'all');
1265
+ const purgeScope = (0, runtime_1.runSetting)(this, 'purgeScope', 'all');
1139
1266
  if (purgeScope === 'specific') {
1140
- const purgeSessionId = this.getNodeParameter('purgeSessionId', 0, '');
1267
+ const purgeSessionId = (0, runtime_1.runSetting)(this, 'purgeSessionId', '');
1141
1268
  if (!purgeSessionId) {
1142
1269
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
1143
1270
  }
@@ -33,8 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sessionReference = sessionReference;
37
+ exports.countHits = countHits;
36
38
  exports.buildAuditEntry = buildAuditEntry;
37
39
  const crypto = __importStar(require("crypto"));
40
+ function sessionReference(sessionId) {
41
+ return `session:${crypto.createHash('sha256').update('redactor-session:').update(sessionId).digest('hex')}`;
42
+ }
43
+ function countHits(hits, field) {
44
+ var _a;
45
+ const counts = new Map();
46
+ for (const hit of hits)
47
+ counts.set(hit[field], ((_a = counts.get(hit[field])) !== null && _a !== void 0 ? _a : 0) + 1);
48
+ return Object.fromEntries(counts);
49
+ }
38
50
  function determineSeverity(classificationLevel, totalHits = 0) {
39
51
  if (classificationLevel === 'RESTRICTED' || totalHits >= 50)
40
52
  return 'CRITICAL';
@@ -44,13 +56,9 @@ function determineSeverity(classificationLevel, totalHits = 0) {
44
56
  return 'MEDIUM';
45
57
  return 'LOW';
46
58
  }
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;
53
- }
59
+ function buildAuditEntry(action, mode, hits, inputItemCount, processingTimeMs, patternsUsed, presidioEnabled, classification, sessionId, presidioUnscannedTexts = 0) {
60
+ const hitsByCategory = countHits(hits, 'category');
61
+ const hitsByPattern = countHits(hits, 'patternLabel');
54
62
  return {
55
63
  version: '2.0',
56
64
  timestamp: new Date().toISOString(),
@@ -58,7 +66,7 @@ function buildAuditEntry(action, mode, hits, inputItemCount, processingTimeMs, p
58
66
  severity: determineSeverity(classification === null || classification === void 0 ? void 0 : classification.level, hits.length),
59
67
  action,
60
68
  mode,
61
- status: 'SUCCESS',
69
+ status: presidioUnscannedTexts > 0 ? 'PARTIAL' : 'SUCCESS',
62
70
  totalHits: hits.length,
63
71
  hitsByCategory,
64
72
  hitsByPattern,
@@ -67,6 +75,7 @@ function buildAuditEntry(action, mode, hits, inputItemCount, processingTimeMs, p
67
75
  processingTimeMs,
68
76
  patternsUsed,
69
77
  presidioEnabled,
70
- sessionId,
78
+ ...(presidioUnscannedTexts > 0 ? { presidioUnscannedTexts } : {}),
79
+ sessionId: sessionId === undefined ? undefined : sessionReference(sessionId),
71
80
  };
72
81
  }