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.
Files changed (61) hide show
  1. package/LICENSE +42 -0
  2. package/README.dev.md +153 -0
  3. package/README.md +443 -0
  4. package/README.npm.md +443 -0
  5. package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
  6. package/dist/nodes/PiiRedactor/PiiRedactor.node.js +1093 -0
  7. package/dist/nodes/PiiRedactor/__tests__/encryption.test.d.ts +1 -0
  8. package/dist/nodes/PiiRedactor/__tests__/encryption.test.js +200 -0
  9. package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
  10. package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
  11. package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
  12. package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
  13. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
  14. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
  15. package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
  16. package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
  17. package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
  18. package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
  19. package/dist/nodes/PiiRedactor/__tests__/phase3.test.d.ts +1 -0
  20. package/dist/nodes/PiiRedactor/__tests__/phase3.test.js +275 -0
  21. package/dist/nodes/PiiRedactor/__tests__/phase4.test.d.ts +1 -0
  22. package/dist/nodes/PiiRedactor/__tests__/phase4.test.js +184 -0
  23. package/dist/nodes/PiiRedactor/__tests__/presidio.test.d.ts +1 -0
  24. package/dist/nodes/PiiRedactor/__tests__/presidio.test.js +170 -0
  25. package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
  26. package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
  27. package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
  28. package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
  29. package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
  30. package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
  31. package/dist/nodes/PiiRedactor/audit.d.ts +48 -0
  32. package/dist/nodes/PiiRedactor/audit.js +192 -0
  33. package/dist/nodes/PiiRedactor/classification.d.ts +33 -0
  34. package/dist/nodes/PiiRedactor/classification.js +118 -0
  35. package/dist/nodes/PiiRedactor/context.d.ts +57 -0
  36. package/dist/nodes/PiiRedactor/context.js +260 -0
  37. package/dist/nodes/PiiRedactor/encryption.d.ts +45 -0
  38. package/dist/nodes/PiiRedactor/encryption.js +158 -0
  39. package/dist/nodes/PiiRedactor/engine.d.ts +23 -0
  40. package/dist/nodes/PiiRedactor/engine.js +888 -0
  41. package/dist/nodes/PiiRedactor/injection.d.ts +46 -0
  42. package/dist/nodes/PiiRedactor/injection.js +425 -0
  43. package/dist/nodes/PiiRedactor/names.d.ts +25 -0
  44. package/dist/nodes/PiiRedactor/names.js +188 -0
  45. package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
  46. package/dist/nodes/PiiRedactor/patterns.js +1742 -0
  47. package/dist/nodes/PiiRedactor/presidio.d.ts +77 -0
  48. package/dist/nodes/PiiRedactor/presidio.js +264 -0
  49. package/dist/nodes/PiiRedactor/profiles.d.ts +47 -0
  50. package/dist/nodes/PiiRedactor/profiles.js +139 -0
  51. package/dist/nodes/PiiRedactor/pseudonymize.d.ts +20 -0
  52. package/dist/nodes/PiiRedactor/pseudonymize.js +203 -0
  53. package/dist/nodes/PiiRedactor/redact.png +0 -0
  54. package/dist/nodes/PiiRedactor/redact.svg +3 -0
  55. package/dist/nodes/PiiRedactor/ropa.d.ts +63 -0
  56. package/dist/nodes/PiiRedactor/ropa.js +70 -0
  57. package/dist/nodes/PiiRedactor/types.d.ts +82 -0
  58. package/dist/nodes/PiiRedactor/types.js +3 -0
  59. package/dist/nodes/PiiRedactor/vault.d.ts +61 -0
  60. package/dist/nodes/PiiRedactor/vault.js +352 -0
  61. 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;