n8n-nodes-redactor 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +42 -0
- package/README.dev.md +153 -0
- package/README.md +443 -0
- package/README.npm.md +443 -0
- package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
- package/dist/nodes/PiiRedactor/PiiRedactor.node.js +1093 -0
- package/dist/nodes/PiiRedactor/__tests__/encryption.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/encryption.test.js +200 -0
- package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
- package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
- package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
- package/dist/nodes/PiiRedactor/__tests__/phase3.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase3.test.js +275 -0
- package/dist/nodes/PiiRedactor/__tests__/phase4.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase4.test.js +184 -0
- package/dist/nodes/PiiRedactor/__tests__/presidio.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/presidio.test.js +170 -0
- package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
- package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
- package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
- package/dist/nodes/PiiRedactor/audit.d.ts +48 -0
- package/dist/nodes/PiiRedactor/audit.js +192 -0
- package/dist/nodes/PiiRedactor/classification.d.ts +33 -0
- package/dist/nodes/PiiRedactor/classification.js +118 -0
- package/dist/nodes/PiiRedactor/context.d.ts +57 -0
- package/dist/nodes/PiiRedactor/context.js +260 -0
- package/dist/nodes/PiiRedactor/encryption.d.ts +45 -0
- package/dist/nodes/PiiRedactor/encryption.js +158 -0
- package/dist/nodes/PiiRedactor/engine.d.ts +23 -0
- package/dist/nodes/PiiRedactor/engine.js +888 -0
- package/dist/nodes/PiiRedactor/injection.d.ts +46 -0
- package/dist/nodes/PiiRedactor/injection.js +425 -0
- package/dist/nodes/PiiRedactor/names.d.ts +25 -0
- package/dist/nodes/PiiRedactor/names.js +188 -0
- package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
- package/dist/nodes/PiiRedactor/patterns.js +1742 -0
- package/dist/nodes/PiiRedactor/presidio.d.ts +77 -0
- package/dist/nodes/PiiRedactor/presidio.js +264 -0
- package/dist/nodes/PiiRedactor/profiles.d.ts +47 -0
- package/dist/nodes/PiiRedactor/profiles.js +139 -0
- package/dist/nodes/PiiRedactor/pseudonymize.d.ts +20 -0
- package/dist/nodes/PiiRedactor/pseudonymize.js +203 -0
- package/dist/nodes/PiiRedactor/redact.png +0 -0
- package/dist/nodes/PiiRedactor/redact.svg +3 -0
- package/dist/nodes/PiiRedactor/ropa.d.ts +63 -0
- package/dist/nodes/PiiRedactor/ropa.js +70 -0
- package/dist/nodes/PiiRedactor/types.d.ts +82 -0
- package/dist/nodes/PiiRedactor/types.js +3 -0
- package/dist/nodes/PiiRedactor/vault.d.ts +61 -0
- package/dist/nodes/PiiRedactor/vault.js +352 -0
- package/package.json +87 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presidio Analyzer HTTP Client
|
|
3
|
+
*
|
|
4
|
+
* Optional integration with Microsoft Presidio for NLP-based entity detection.
|
|
5
|
+
* Presidio uses spaCy NER models to detect person names, locations, and
|
|
6
|
+
* organizations in free text — something regex alone cannot do reliably.
|
|
7
|
+
*
|
|
8
|
+
* This is OPTIONAL. The node works perfectly without Presidio.
|
|
9
|
+
* Users who want NLP-level accuracy spin up the Presidio Docker container
|
|
10
|
+
* and enable "Enhanced NLP Detection" in the node settings.
|
|
11
|
+
*
|
|
12
|
+
* Docker setup (one command):
|
|
13
|
+
* docker run -d -p 5002:3000 mcr.microsoft.com/presidio-analyzer
|
|
14
|
+
*
|
|
15
|
+
* Or with docker-compose alongside n8n:
|
|
16
|
+
* services:
|
|
17
|
+
* n8n:
|
|
18
|
+
* image: n8nio/n8n
|
|
19
|
+
* ports: ["5678:5678"]
|
|
20
|
+
* presidio:
|
|
21
|
+
* image: mcr.microsoft.com/presidio-analyzer
|
|
22
|
+
* ports: ["5002:3000"]
|
|
23
|
+
*/
|
|
24
|
+
/** Result from Presidio /analyze endpoint */
|
|
25
|
+
export interface PresidioEntity {
|
|
26
|
+
entity_type: string;
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
score: number;
|
|
30
|
+
analysis_explanation?: {
|
|
31
|
+
recognizer: string;
|
|
32
|
+
pattern_name?: string;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
original_score: number;
|
|
35
|
+
score: number;
|
|
36
|
+
textual_explanation?: string;
|
|
37
|
+
score_context_improvement?: number;
|
|
38
|
+
supportive_context_word?: string;
|
|
39
|
+
validation_result?: number;
|
|
40
|
+
};
|
|
41
|
+
recognition_metadata?: {
|
|
42
|
+
recognizer_name: string;
|
|
43
|
+
recognizer_identifier?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Presidio /analyze request body */
|
|
47
|
+
export interface PresidioAnalyzeRequest {
|
|
48
|
+
text: string;
|
|
49
|
+
language: string;
|
|
50
|
+
entities?: string[];
|
|
51
|
+
correlation_id?: string;
|
|
52
|
+
score_threshold?: number;
|
|
53
|
+
return_decision_process?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Reset circuit breaker (call at start of each n8n execution) */
|
|
56
|
+
export declare function resetCircuitBreaker(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Check if Presidio is reachable at the given URL.
|
|
59
|
+
*/
|
|
60
|
+
export declare function checkPresidioHealth(baseUrl: string): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Call Presidio /analyze endpoint to detect PII entities in text.
|
|
63
|
+
* Returns empty array if Presidio is unreachable (graceful fallback).
|
|
64
|
+
*/
|
|
65
|
+
export declare function analyzeWithPresidio(baseUrl: string, text: string, language?: string, scoreThreshold?: number, timeoutMs?: number): Promise<PresidioEntity[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Get the redactor label for a Presidio entity type.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getRedactorLabel(presidioEntityType: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Get the PII category for a Presidio entity type.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getRedactorCategory(presidioEntityType: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Get all supported Presidio entity types.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getPresidioEntities(baseUrl: string): Promise<string[]>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Presidio Analyzer HTTP Client
|
|
4
|
+
*
|
|
5
|
+
* Optional integration with Microsoft Presidio for NLP-based entity detection.
|
|
6
|
+
* Presidio uses spaCy NER models to detect person names, locations, and
|
|
7
|
+
* organizations in free text — something regex alone cannot do reliably.
|
|
8
|
+
*
|
|
9
|
+
* This is OPTIONAL. The node works perfectly without Presidio.
|
|
10
|
+
* Users who want NLP-level accuracy spin up the Presidio Docker container
|
|
11
|
+
* and enable "Enhanced NLP Detection" in the node settings.
|
|
12
|
+
*
|
|
13
|
+
* Docker setup (one command):
|
|
14
|
+
* docker run -d -p 5002:3000 mcr.microsoft.com/presidio-analyzer
|
|
15
|
+
*
|
|
16
|
+
* Or with docker-compose alongside n8n:
|
|
17
|
+
* services:
|
|
18
|
+
* n8n:
|
|
19
|
+
* image: n8nio/n8n
|
|
20
|
+
* ports: ["5678:5678"]
|
|
21
|
+
* presidio:
|
|
22
|
+
* image: mcr.microsoft.com/presidio-analyzer
|
|
23
|
+
* ports: ["5002:3000"]
|
|
24
|
+
*/
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.resetCircuitBreaker = resetCircuitBreaker;
|
|
60
|
+
exports.checkPresidioHealth = checkPresidioHealth;
|
|
61
|
+
exports.analyzeWithPresidio = analyzeWithPresidio;
|
|
62
|
+
exports.getRedactorLabel = getRedactorLabel;
|
|
63
|
+
exports.getRedactorCategory = getRedactorCategory;
|
|
64
|
+
exports.getPresidioEntities = getPresidioEntities;
|
|
65
|
+
const http = __importStar(require("http"));
|
|
66
|
+
const https = __importStar(require("https"));
|
|
67
|
+
/** Map Presidio entity types to our label format */
|
|
68
|
+
const PRESIDIO_TO_REDACTOR_LABEL = {
|
|
69
|
+
PERSON: 'PERSON_NAME',
|
|
70
|
+
EMAIL_ADDRESS: 'EMAIL',
|
|
71
|
+
PHONE_NUMBER: 'PHONE',
|
|
72
|
+
CREDIT_CARD: 'CREDIT_CARD',
|
|
73
|
+
IBAN_CODE: 'IBAN',
|
|
74
|
+
US_SSN: 'SSN',
|
|
75
|
+
US_ITIN: 'ITIN_US',
|
|
76
|
+
US_PASSPORT: 'PASSPORT_US',
|
|
77
|
+
US_DRIVER_LICENSE: 'DRIVER_LICENSE',
|
|
78
|
+
US_BANK_NUMBER: 'BANK_ACCOUNT',
|
|
79
|
+
UK_NHS: 'NHS_NUMBER',
|
|
80
|
+
LOCATION: 'ADDRESS',
|
|
81
|
+
DATE_TIME: 'DATE',
|
|
82
|
+
NRP: 'ETHNICITY',
|
|
83
|
+
MEDICAL_LICENSE: 'MEDICAL',
|
|
84
|
+
IP_ADDRESS: 'IP_ADDRESS',
|
|
85
|
+
URL: 'URL',
|
|
86
|
+
CRYPTO: 'BTC_ADDRESS',
|
|
87
|
+
ORGANIZATION: 'COMPANY',
|
|
88
|
+
AU_ABN: 'ABN_AU',
|
|
89
|
+
AU_ACN: 'ACN_AU',
|
|
90
|
+
AU_TFN: 'TFN_AU',
|
|
91
|
+
AU_MEDICARE: 'MEDICARE_AU',
|
|
92
|
+
SG_NRIC_FIN: 'NRIC_SG',
|
|
93
|
+
IN_PAN: 'PAN_IN',
|
|
94
|
+
IN_AADHAAR: 'AADHAAR_IN',
|
|
95
|
+
ES_NIF: 'NIF_ES',
|
|
96
|
+
IT_FISCAL_CODE: 'CODICE_FISCALE_IT',
|
|
97
|
+
IT_DRIVER_LICENSE: 'DRIVER_LICENSE',
|
|
98
|
+
IT_IDENTITY_CARD: 'CARTA_ID_IT',
|
|
99
|
+
IT_PASSPORT: 'PASSPORT_EU',
|
|
100
|
+
IT_VAT_CODE: 'VAT_EU',
|
|
101
|
+
PL_PESEL: 'PESEL_PL',
|
|
102
|
+
};
|
|
103
|
+
/** Map Presidio entity types to our PII categories */
|
|
104
|
+
const PRESIDIO_TO_CATEGORY = {
|
|
105
|
+
PERSON: 'identity',
|
|
106
|
+
EMAIL_ADDRESS: 'contact',
|
|
107
|
+
PHONE_NUMBER: 'contact',
|
|
108
|
+
CREDIT_CARD: 'financial',
|
|
109
|
+
IBAN_CODE: 'financial',
|
|
110
|
+
US_SSN: 'identity',
|
|
111
|
+
US_ITIN: 'identity',
|
|
112
|
+
US_PASSPORT: 'identity',
|
|
113
|
+
US_DRIVER_LICENSE: 'identity',
|
|
114
|
+
US_BANK_NUMBER: 'financial',
|
|
115
|
+
UK_NHS: 'identity',
|
|
116
|
+
LOCATION: 'location',
|
|
117
|
+
DATE_TIME: 'temporal',
|
|
118
|
+
NRP: 'identity',
|
|
119
|
+
MEDICAL_LICENSE: 'medical',
|
|
120
|
+
IP_ADDRESS: 'network',
|
|
121
|
+
URL: 'network',
|
|
122
|
+
CRYPTO: 'crypto',
|
|
123
|
+
ORGANIZATION: 'identity',
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Circuit breaker: after 3 consecutive failures, stop calling Presidio
|
|
127
|
+
* for the rest of this execution. Per-URL to prevent cross-workflow bleed.
|
|
128
|
+
* Resets on success or at start of each n8n execution.
|
|
129
|
+
*/
|
|
130
|
+
const failureCountByUrl = new Map();
|
|
131
|
+
const MAX_FAILURES = 3;
|
|
132
|
+
function circuitOpen(url) {
|
|
133
|
+
if (!url)
|
|
134
|
+
return false;
|
|
135
|
+
return (failureCountByUrl.get(url) || 0) >= MAX_FAILURES;
|
|
136
|
+
}
|
|
137
|
+
function recordSuccess(url) {
|
|
138
|
+
if (url)
|
|
139
|
+
failureCountByUrl.set(url, 0);
|
|
140
|
+
}
|
|
141
|
+
function recordFailure(url) {
|
|
142
|
+
if (url)
|
|
143
|
+
failureCountByUrl.set(url, (failureCountByUrl.get(url) || 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
/** Reset circuit breaker (call at start of each n8n execution) */
|
|
146
|
+
function resetCircuitBreaker() {
|
|
147
|
+
failureCountByUrl.clear();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if Presidio is reachable at the given URL.
|
|
151
|
+
*/
|
|
152
|
+
async function checkPresidioHealth(baseUrl) {
|
|
153
|
+
try {
|
|
154
|
+
const result = await httpGet(`${baseUrl}/health`, 3000);
|
|
155
|
+
return result.statusCode === 200;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Call Presidio /analyze endpoint to detect PII entities in text.
|
|
163
|
+
* Returns empty array if Presidio is unreachable (graceful fallback).
|
|
164
|
+
*/
|
|
165
|
+
async function analyzeWithPresidio(baseUrl, text, language = 'en', scoreThreshold = 0.4, timeoutMs = 10000) {
|
|
166
|
+
if (!text || text.trim().length === 0)
|
|
167
|
+
return [];
|
|
168
|
+
// Circuit breaker: skip if too many consecutive failures for this URL
|
|
169
|
+
if (circuitOpen(baseUrl))
|
|
170
|
+
return [];
|
|
171
|
+
const requestBody = {
|
|
172
|
+
text,
|
|
173
|
+
language,
|
|
174
|
+
score_threshold: scoreThreshold,
|
|
175
|
+
return_decision_process: false,
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
const result = await httpPost(`${baseUrl}/analyze`, JSON.stringify(requestBody), timeoutMs);
|
|
179
|
+
if (result.statusCode !== 200) {
|
|
180
|
+
recordFailure(baseUrl);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const entities = JSON.parse(result.body);
|
|
184
|
+
if (!Array.isArray(entities)) {
|
|
185
|
+
recordFailure(baseUrl);
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
recordSuccess(baseUrl);
|
|
189
|
+
return entities;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
recordFailure();
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the redactor label for a Presidio entity type.
|
|
198
|
+
*/
|
|
199
|
+
function getRedactorLabel(presidioEntityType) {
|
|
200
|
+
return PRESIDIO_TO_REDACTOR_LABEL[presidioEntityType] || presidioEntityType;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get the PII category for a Presidio entity type.
|
|
204
|
+
*/
|
|
205
|
+
function getRedactorCategory(presidioEntityType) {
|
|
206
|
+
return PRESIDIO_TO_CATEGORY[presidioEntityType] || 'identity';
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get all supported Presidio entity types.
|
|
210
|
+
*/
|
|
211
|
+
async function getPresidioEntities(baseUrl) {
|
|
212
|
+
try {
|
|
213
|
+
const result = await httpGet(`${baseUrl}/supportedentities`, 5000);
|
|
214
|
+
if (result.statusCode !== 200)
|
|
215
|
+
return [];
|
|
216
|
+
const entities = JSON.parse(result.body);
|
|
217
|
+
return Array.isArray(entities) ? entities : [];
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function httpGet(url, timeoutMs) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const client = url.startsWith('https') ? https : http;
|
|
226
|
+
const req = client.get(url, { timeout: timeoutMs }, (res) => {
|
|
227
|
+
let body = '';
|
|
228
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
229
|
+
res.on('end', () => {
|
|
230
|
+
resolve({ statusCode: res.statusCode || 0, body });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
req.on('error', reject);
|
|
234
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function httpPost(url, data, timeoutMs) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const parsedUrl = new URL(url);
|
|
240
|
+
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
241
|
+
const options = {
|
|
242
|
+
hostname: parsedUrl.hostname,
|
|
243
|
+
port: parsedUrl.port,
|
|
244
|
+
path: parsedUrl.pathname,
|
|
245
|
+
method: 'POST',
|
|
246
|
+
timeout: timeoutMs,
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
'Content-Length': Buffer.byteLength(data),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const req = client.request(options, (res) => {
|
|
253
|
+
let body = '';
|
|
254
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
255
|
+
res.on('end', () => {
|
|
256
|
+
resolve({ statusCode: res.statusCode || 0, body });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
req.on('error', reject);
|
|
260
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
261
|
+
req.write(data);
|
|
262
|
+
req.end();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Tenant Profile Management
|
|
3
|
+
*
|
|
4
|
+
* Profiles allow agencies to define different redaction rules per client/tenant.
|
|
5
|
+
* Each profile is a JSON config containing: enabled patterns, allow/deny lists,
|
|
6
|
+
* redaction mode, field rules, confidence threshold, and compliance settings.
|
|
7
|
+
*
|
|
8
|
+
* Storage: ~/.n8n/pii-profiles/profile_{profileId}.json
|
|
9
|
+
* Loading: By profileId parameter in node settings
|
|
10
|
+
* Isolation: Each profile has its own vault passphrase recommendation
|
|
11
|
+
*/
|
|
12
|
+
import { RedactionMode, CustomPattern, AllowDenyEntry, FieldRule } from './types';
|
|
13
|
+
export interface TenantProfile {
|
|
14
|
+
profileId: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
enabledPatterns?: string[];
|
|
17
|
+
customPatterns?: CustomPattern[];
|
|
18
|
+
confidenceThreshold?: number;
|
|
19
|
+
redactionMode?: RedactionMode;
|
|
20
|
+
dedup?: boolean;
|
|
21
|
+
fieldMode?: 'all' | 'allowlist' | 'denylist';
|
|
22
|
+
fieldRules?: FieldRule[];
|
|
23
|
+
allowList?: AllowDenyEntry[];
|
|
24
|
+
denyList?: AllowDenyEntry[];
|
|
25
|
+
region?: string;
|
|
26
|
+
retentionMinutes?: number;
|
|
27
|
+
auditLog?: boolean;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load a tenant profile by ID.
|
|
33
|
+
* Returns null if not found.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadProfile(profileId: string, profileDir?: string): TenantProfile | null;
|
|
36
|
+
/**
|
|
37
|
+
* Save a tenant profile.
|
|
38
|
+
*/
|
|
39
|
+
export declare function saveProfile(profile: TenantProfile, profileDir?: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* List all available profiles.
|
|
42
|
+
*/
|
|
43
|
+
export declare function listProfiles(profileDir?: string): TenantProfile[];
|
|
44
|
+
/**
|
|
45
|
+
* Delete a tenant profile.
|
|
46
|
+
*/
|
|
47
|
+
export declare function deleteProfile(profileId: string, profileDir?: string): boolean;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-Tenant Profile Management
|
|
4
|
+
*
|
|
5
|
+
* Profiles allow agencies to define different redaction rules per client/tenant.
|
|
6
|
+
* Each profile is a JSON config containing: enabled patterns, allow/deny lists,
|
|
7
|
+
* redaction mode, field rules, confidence threshold, and compliance settings.
|
|
8
|
+
*
|
|
9
|
+
* Storage: ~/.n8n/pii-profiles/profile_{profileId}.json
|
|
10
|
+
* Loading: By profileId parameter in node settings
|
|
11
|
+
* Isolation: Each profile has its own vault passphrase recommendation
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.loadProfile = loadProfile;
|
|
48
|
+
exports.saveProfile = saveProfile;
|
|
49
|
+
exports.listProfiles = listProfiles;
|
|
50
|
+
exports.deleteProfile = deleteProfile;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const DEFAULT_PROFILE_DIR = path.join(process.env.HOME || '/tmp', '.n8n', 'pii-profiles');
|
|
54
|
+
/**
|
|
55
|
+
* Load a tenant profile by ID.
|
|
56
|
+
* Returns null if not found.
|
|
57
|
+
*/
|
|
58
|
+
function loadProfile(profileId, profileDir) {
|
|
59
|
+
if (!profileId || profileId.trim().length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const dir = profileDir || DEFAULT_PROFILE_DIR;
|
|
63
|
+
// Sanitize profileId to prevent path traversal
|
|
64
|
+
const safeId = profileId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
65
|
+
const fp = path.join(dir, `profile_${safeId}.json`);
|
|
66
|
+
if (!fs.existsSync(fp))
|
|
67
|
+
return null;
|
|
68
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Save a tenant profile.
|
|
77
|
+
*/
|
|
78
|
+
function saveProfile(profile, profileDir) {
|
|
79
|
+
try {
|
|
80
|
+
const dir = profileDir || DEFAULT_PROFILE_DIR;
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const safeId = profile.profileId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
85
|
+
const fp = path.join(dir, `profile_${safeId}.json`);
|
|
86
|
+
const tmpFp = fp + '.tmp';
|
|
87
|
+
profile.updatedAt = new Date().toISOString();
|
|
88
|
+
fs.writeFileSync(tmpFp, JSON.stringify(profile, null, 2), 'utf-8');
|
|
89
|
+
fs.renameSync(tmpFp, fp);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* List all available profiles.
|
|
98
|
+
*/
|
|
99
|
+
function listProfiles(profileDir) {
|
|
100
|
+
try {
|
|
101
|
+
const dir = profileDir || DEFAULT_PROFILE_DIR;
|
|
102
|
+
if (!fs.existsSync(dir))
|
|
103
|
+
return [];
|
|
104
|
+
const files = fs.readdirSync(dir)
|
|
105
|
+
.filter((f) => f.startsWith('profile_') && f.endsWith('.json'));
|
|
106
|
+
const profiles = [];
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
|
|
110
|
+
profiles.push(data);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Skip corrupt profiles
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return profiles;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Delete a tenant profile.
|
|
124
|
+
*/
|
|
125
|
+
function deleteProfile(profileId, profileDir) {
|
|
126
|
+
try {
|
|
127
|
+
const dir = profileDir || DEFAULT_PROFILE_DIR;
|
|
128
|
+
const safeId = profileId.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
129
|
+
const fp = path.join(dir, `profile_${safeId}.json`);
|
|
130
|
+
if (fs.existsSync(fp)) {
|
|
131
|
+
fs.unlinkSync(fp);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pseudonymization Engine
|
|
3
|
+
*
|
|
4
|
+
* Replaces PII with realistic-looking fake data that preserves format.
|
|
5
|
+
* GDPR Article 4(5): "processing personal data so it can no longer be
|
|
6
|
+
* attributed to a specific data subject without additional information."
|
|
7
|
+
*
|
|
8
|
+
* Key properties:
|
|
9
|
+
* - Format-preserving: emails stay email-shaped, phones stay phone-shaped
|
|
10
|
+
* - Deterministic: same input + same session = same output (seeded)
|
|
11
|
+
* - Reversible: mapping stored in vault for restoration
|
|
12
|
+
* - GDPR-compliant: mapping stored separately from pseudonymized data
|
|
13
|
+
*
|
|
14
|
+
* No external dependencies (no Faker.js). Uses built-in deterministic generation.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Generate a pseudonym for a given PII value based on its type.
|
|
18
|
+
* Deterministic: same sessionId + original = same pseudonym.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generatePseudonym(sessionId: string, original: string, label: string): string;
|