n8n-nodes-redactor 1.11.1 → 4.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
@@ -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,16 +109,16 @@ 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 |
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
113
  | Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant |
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
 
@@ -9,6 +9,7 @@ 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");
13
14
  const RECOMMENDED_PATTERNS = [
14
15
  'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
@@ -56,8 +57,21 @@ class Redactor {
56
57
  name: 'Redactor',
57
58
  },
58
59
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
59
- outputs: `={{((parameters) => parameters["includeReport"] ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
60
+ outputs: `={{((parameters) => (parameters["includeReport"] || parameters["auditLog"]) ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
60
61
  properties: [
62
+ {
63
+ displayName: 'Resource',
64
+ name: 'resource',
65
+ type: 'options',
66
+ noDataExpression: true,
67
+ options: [
68
+ {
69
+ name: 'Data',
70
+ value: 'data',
71
+ },
72
+ ],
73
+ default: 'data',
74
+ },
61
75
  {
62
76
  displayName: 'Operation',
63
77
  name: 'operation',
@@ -500,7 +514,7 @@ class Redactor {
500
514
  },
501
515
  },
502
516
  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.',
517
+ 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
518
  },
505
519
  {
506
520
  displayName: 'Enhanced NLP Detection (Optional)',
@@ -561,31 +575,17 @@ class Redactor {
561
575
  },
562
576
  },
563
577
  options: [
564
- {
565
- name: 'File-Based (Recommended, Persistent)',
566
- value: 'file',
567
- },
568
578
  {
569
579
  name: 'In-Memory (Fast, Lost on Restart)',
570
580
  value: 'memory',
571
581
  },
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'],
582
+ {
583
+ name: 'Workflow Storage (Recommended, Persistent)',
584
+ value: 'workflow',
584
585
  },
585
- },
586
- default: '',
587
- placeholder: '/path/to/vault (default: ~/.n8n/pii-vault/)',
588
- description: 'Custom directory for file-based vault storage',
586
+ ],
587
+ default: 'workflow',
588
+ 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
589
  },
590
590
  {
591
591
  displayName: 'Vault Encryption Passphrase',
@@ -597,11 +597,11 @@ class Redactor {
597
597
  displayOptions: {
598
598
  show: {
599
599
  operation: ['redact', 'restore', 'stats', 'purge'],
600
- vaultStorage: ['file'],
600
+ vaultStorage: ['workflow', 'file'],
601
601
  },
602
602
  },
603
603
  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.',
604
+ description: 'Encrypt stored sessions at rest with AES-256-GCM. Leave empty for no encryption. When set, sessions cannot be read without this passphrase. Each tenant should use a different passphrase for complete isolation.',
605
605
  },
606
606
  {
607
607
  displayName: 'Session TTL (Minutes)',
@@ -615,6 +615,32 @@ class Redactor {
615
615
  default: 60,
616
616
  description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
617
617
  },
618
+ {
619
+ displayName: 'Use Tenant Profile',
620
+ name: 'useTenantProfile',
621
+ type: 'boolean',
622
+ displayOptions: {
623
+ show: {
624
+ operation: ['redact'],
625
+ },
626
+ },
627
+ default: false,
628
+ description: 'Whether to apply per-client settings from a JSON profile on top of this node',
629
+ },
630
+ {
631
+ displayName: 'Tenant Profile (JSON)',
632
+ name: 'tenantProfile',
633
+ type: 'json',
634
+ required: true,
635
+ displayOptions: {
636
+ show: {
637
+ operation: ['redact'],
638
+ useTenantProfile: [true],
639
+ },
640
+ },
641
+ default: '{}',
642
+ description: 'Optional per-client settings that override this node: enabledPatterns, customPatterns, confidenceThreshold, redactionMode, dedup, fieldMode, fieldRules, allowList, denyList, retentionMinutes, auditLog. Unknown fields are rejected.',
643
+ },
618
644
  {
619
645
  displayName: 'Purge Scope',
620
646
  name: 'purgeScope',
@@ -719,6 +745,7 @@ class Redactor {
719
745
  const items = this.getInputData();
720
746
  const allPaired = items.map((_, i) => ({ item: i }));
721
747
  const operation = this.getNodeParameter('operation', 0);
748
+ const startTime = Date.now();
722
749
  (0, presidio_1.resetCircuitBreaker)();
723
750
  if (operation === 'detect' || operation === 'verify') {
724
751
  const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
@@ -759,8 +786,21 @@ class Redactor {
759
786
  includeReport = this.getNodeParameter('includeReport', 0, false);
760
787
  }
761
788
  catch { }
762
- if (includeReport) {
763
- return [returnData, [{ json: { ...summary, hits: report.hits }, pairedItem: allPaired }]];
789
+ let auditLog = false;
790
+ try {
791
+ auditLog = this.getNodeParameter('auditLog', 0, false);
792
+ }
793
+ catch { }
794
+ if (includeReport || auditLog) {
795
+ const reportJson = includeReport
796
+ ? { ...summary, hits: report.hits }
797
+ : {};
798
+ if (auditLog) {
799
+ reportJson.audit = (0, audit_1.buildAuditEntry)('DETECT', 'token', allHits, items.length, Date.now() - startTime, enabledPatterns.length, false, (0, classification_1.classifyData)(allHits));
800
+ }
801
+ if (!includeReport)
802
+ returnData.push({ json: summary, pairedItem: allPaired });
803
+ return [returnData, [{ json: reportJson, pairedItem: allPaired }]];
764
804
  }
765
805
  returnData.push({ json: summary, pairedItem: allPaired });
766
806
  return [returnData];
@@ -818,14 +858,13 @@ class Redactor {
818
858
  pairedItem: allPaired,
819
859
  }]];
820
860
  }
821
- const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'file');
822
- const vaultDir = this.getNodeParameter('vaultDir', 0, '');
861
+ const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
823
862
  let vaultPassphrase = '';
824
863
  try {
825
864
  vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
826
865
  }
827
866
  catch { }
828
- const vault = (0, vault_1.createVault)(vaultStorage, vaultDir || undefined, vaultPassphrase || undefined);
867
+ const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined);
829
868
  if (operation === 'stats') {
830
869
  vault.cleanup();
831
870
  const sessions = vault.listSessions();
@@ -857,7 +896,7 @@ class Redactor {
857
896
  }
858
897
  }
859
898
  if (operation === 'redact') {
860
- const mode = this.getNodeParameter('redactionMode', 0, 'token');
899
+ let mode = this.getNodeParameter('redactionMode', 0, 'token');
861
900
  const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
862
901
  let enabledPatterns;
863
902
  if (detectionScope === 'all') {
@@ -869,17 +908,29 @@ class Redactor {
869
908
  else {
870
909
  enabledPatterns = this.getNodeParameter('piiTypes', 0);
871
910
  }
872
- const dedup = this.getNodeParameter('dedup', 0, true);
873
- const fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
874
- const sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
911
+ let dedup = this.getNodeParameter('dedup', 0, true);
912
+ let fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
913
+ let sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
875
914
  const includeReport = this.getNodeParameter('includeReport', 0, false);
876
915
  const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
877
- const customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
916
+ let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
878
917
  const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
879
- const fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
918
+ let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
880
919
  field: r.field,
881
920
  mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
882
921
  }));
922
+ const presidioRequester = async ({ method, url, body, timeout }) => {
923
+ const response = await this.helpers.httpRequest({
924
+ method: method,
925
+ url,
926
+ body,
927
+ json: true,
928
+ timeout,
929
+ returnFullResponse: true,
930
+ ignoreHttpStatusErrors: true,
931
+ });
932
+ return { statusCode: response.statusCode, body: response.body };
933
+ };
883
934
  let enablePresidio = false;
884
935
  let presidioUrl = '';
885
936
  let presidioLanguage = 'en';
@@ -893,16 +944,53 @@ class Redactor {
893
944
  catch {
894
945
  }
895
946
  const allowListRaw = this.getNodeParameter('allowList', 0, {});
896
- const allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
947
+ let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
897
948
  value: e.value,
898
949
  type: (e.type || 'contains'),
899
950
  }));
900
951
  const denyListRaw = this.getNodeParameter('denyList', 0, {});
901
- const denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
952
+ let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
902
953
  value: e.value,
903
954
  type: (e.type || 'exact'),
904
955
  }));
905
- const confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
956
+ let confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
957
+ const hasReportOutput = includeReport || this.getNodeParameter('auditLog', 0, false);
958
+ let auditLog = this.getNodeParameter('auditLog', 0, false);
959
+ const useTenantProfile = this.getNodeParameter('useTenantProfile', 0, false);
960
+ const profileRaw = useTenantProfile ? this.getNodeParameter('tenantProfile', 0, '') : '';
961
+ if (profileRaw !== '' && profileRaw !== undefined && profileRaw !== null) {
962
+ let profile;
963
+ try {
964
+ profile = (0, profiles_1.parseTenantProfile)(profileRaw, (0, patterns_1.getPatternOptions)().map((o) => o.value));
965
+ }
966
+ catch (error) {
967
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
968
+ description: 'Fix the Tenant Profile (JSON) field, or leave it empty to use the node settings.',
969
+ });
970
+ }
971
+ if (profile.enabledPatterns)
972
+ enabledPatterns = profile.enabledPatterns;
973
+ if (profile.customPatterns)
974
+ customPatterns = profile.customPatterns;
975
+ if (profile.confidenceThreshold !== undefined)
976
+ confidenceThreshold = profile.confidenceThreshold;
977
+ if (profile.redactionMode)
978
+ mode = profile.redactionMode;
979
+ if (profile.dedup !== undefined)
980
+ dedup = profile.dedup;
981
+ if (profile.fieldMode)
982
+ fieldMode = profile.fieldMode;
983
+ if (profile.fieldRules)
984
+ fieldRules = profile.fieldRules;
985
+ if (profile.allowList)
986
+ allowList = profile.allowList;
987
+ if (profile.denyList)
988
+ denyList = profile.denyList;
989
+ if (profile.retentionMinutes !== undefined)
990
+ sessionTtl = profile.retentionMinutes;
991
+ if (profile.auditLog !== undefined)
992
+ auditLog = profile.auditLog;
993
+ }
906
994
  vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
907
995
  const ctx = {
908
996
  enabledPatterns,
@@ -916,6 +1004,7 @@ class Redactor {
916
1004
  confidenceThreshold,
917
1005
  presidioUrl: enablePresidio ? presidioUrl : undefined,
918
1006
  presidioLanguage: enablePresidio ? presidioLanguage : undefined,
1007
+ presidioRequest: enablePresidio ? presidioRequester : undefined,
919
1008
  };
920
1009
  const allHits = [];
921
1010
  const returnData = [];
@@ -948,16 +1037,9 @@ class Redactor {
948
1037
  }
949
1038
  catch {
950
1039
  }
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
- }
1040
+ const auditEntry = auditLog && hasReportOutput
1041
+ ? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId)
1042
+ : undefined;
961
1043
  if (injectionResult && injectionResult.detected) {
962
1044
  returnData.push({
963
1045
  json: {
@@ -966,12 +1048,14 @@ class Redactor {
966
1048
  pairedItem: allPaired,
967
1049
  });
968
1050
  }
969
- if (includeReport) {
970
- const report = (0, engine_1.buildReport)(sessionId, allHits);
971
- if (this.getNodeParameter('includeRopa', 0, false)) {
1051
+ if (hasReportOutput) {
1052
+ const report = includeReport ? (0, engine_1.buildReport)(sessionId, allHits) : {};
1053
+ if (auditEntry)
1054
+ report.audit = auditEntry;
1055
+ if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
972
1056
  const d = this.getNodeParameter('ropa', 0, {});
973
1057
  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), []);
1058
+ 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
1059
  }
976
1060
  return [returnData, [{ json: report, pairedItem: allPaired }]];
977
1061
  }
@@ -982,7 +1066,7 @@ class Redactor {
982
1066
  const session = vault.getSession(sessionId);
983
1067
  if (!session) {
984
1068
  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.',
1069
+ 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
1070
  });
987
1071
  }
988
1072
  const returnData = [];
@@ -18,5 +18,4 @@ export interface AuditLogEntry {
18
18
  presidioEnabled: boolean;
19
19
  sessionId?: string;
20
20
  }
21
- export declare function writeAuditLog(action: string, mode: RedactionMode | string, hits: RedactionHit[], inputItemCount: number, processingTimeMs: number, patternsUsed: number, presidioEnabled: boolean, classification?: ClassificationLabel, sessionId?: string, auditDir?: string): void;
22
- export declare function readAuditLog(startDate?: string, endDate?: string, auditDir?: string): AuditLogEntry[];
21
+ export declare function buildAuditEntry(action: string, mode: RedactionMode | string, hits: RedactionHit[], inputItemCount: number, processingTimeMs: number, patternsUsed: number, presidioEnabled: boolean, classification?: ClassificationLabel, sessionId?: string): AuditLogEntry;
@@ -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
  }
@@ -624,7 +624,7 @@ async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex)
624
624
  if (TOKEN_RE.test(value))
625
625
  continue;
626
626
  try {
627
- const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000);
627
+ const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
628
628
  if (entities.length === 0)
629
629
  continue;
630
630
  const sorted = entities.sort((a, b) => b.start - a.start);
@@ -1,3 +1,12 @@
1
+ export type PresidioRequester = (options: {
2
+ method: 'GET' | 'POST';
3
+ url: string;
4
+ body?: object;
5
+ timeout: number;
6
+ }) => Promise<{
7
+ statusCode: number;
8
+ body: unknown;
9
+ }>;
1
10
  export interface PresidioEntity {
2
11
  entity_type: string;
3
12
  start: number;
@@ -20,8 +29,8 @@ export interface PresidioEntity {
20
29
  };
21
30
  }
22
31
  export declare function resetCircuitBreaker(): void;
23
- export declare function checkPresidioHealth(baseUrl: string): Promise<boolean>;
24
- export declare function analyzeWithPresidio(baseUrl: string, text: string, language?: string, scoreThreshold?: number, timeoutMs?: number): Promise<PresidioEntity[]>;
32
+ export declare function checkPresidioHealth(baseUrl: string, request?: PresidioRequester): Promise<boolean>;
33
+ export declare function analyzeWithPresidio(baseUrl: string, text: string, language?: string, scoreThreshold?: number, timeoutMs?: number, request?: PresidioRequester): Promise<PresidioEntity[]>;
25
34
  export declare function getRedactorLabel(presidioEntityType: string): string;
26
35
  export declare function getRedactorCategory(presidioEntityType: string): string;
27
- export declare function getPresidioEntities(baseUrl: string): Promise<string[]>;
36
+ export declare function getPresidioEntities(baseUrl: string, request?: PresidioRequester): Promise<string[]>;