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,524 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vault_1 = require("../vault");
|
|
4
|
+
const engine_1 = require("../engine");
|
|
5
|
+
function createContext(overrides = {}) {
|
|
6
|
+
return {
|
|
7
|
+
enabledPatterns: ['email', 'phone', 'personName', 'ssn', 'creditCard', 'iban', 'ipv4'],
|
|
8
|
+
customPatterns: [],
|
|
9
|
+
mode: 'token',
|
|
10
|
+
dedup: true,
|
|
11
|
+
fieldRules: [],
|
|
12
|
+
fieldMode: 'all',
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// ═══════════════════════════════════════════════════════
|
|
17
|
+
// BASIC REDACTION
|
|
18
|
+
// ═══════════════════════════════════════════════════════
|
|
19
|
+
describe('Redaction engine — basic', () => {
|
|
20
|
+
let vault;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vault = new vault_1.MemoryVault();
|
|
23
|
+
});
|
|
24
|
+
test('redacts email in simple string field', () => {
|
|
25
|
+
vault.getOrCreateSession('s1', 0);
|
|
26
|
+
const hits = [];
|
|
27
|
+
const result = (0, engine_1.redactValue)({ message: 'Contact me at john@example.com' }, createContext(), vault, 's1', hits, 0);
|
|
28
|
+
expect(result.message).toBe('Contact me at [EMAIL_0]');
|
|
29
|
+
expect(hits).toHaveLength(1);
|
|
30
|
+
expect(hits[0].patternLabel).toBe('EMAIL');
|
|
31
|
+
expect(hits[0].original).toBe('***'); // SECURITY: originals never exposed in reports
|
|
32
|
+
});
|
|
33
|
+
test('redacts multiple PII types in one field', () => {
|
|
34
|
+
vault.getOrCreateSession('s2', 0);
|
|
35
|
+
const hits = [];
|
|
36
|
+
const result = (0, engine_1.redactValue)({ info: 'Mr. John Smith, email: john@test.com, SSN: 123-45-6789' }, createContext(), vault, 's2', hits, 0);
|
|
37
|
+
expect(result.info).not.toContain('john@test.com');
|
|
38
|
+
expect(result.info).not.toContain('123-45-6789');
|
|
39
|
+
expect(result.info).not.toContain('Mr. John Smith');
|
|
40
|
+
expect(hits.length).toBeGreaterThanOrEqual(3);
|
|
41
|
+
});
|
|
42
|
+
test('redacts nested JSON objects', () => {
|
|
43
|
+
vault.getOrCreateSession('s3', 0);
|
|
44
|
+
const hits = [];
|
|
45
|
+
const data = {
|
|
46
|
+
user: {
|
|
47
|
+
profile: {
|
|
48
|
+
email: 'deep@nested.com',
|
|
49
|
+
phone: '+49 30 1234-5678',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 's3', hits, 0);
|
|
54
|
+
expect(result.user.profile.email).toContain('[EMAIL_');
|
|
55
|
+
expect(result.user.profile.phone).toContain('[PHONE_');
|
|
56
|
+
expect(hits).toHaveLength(2);
|
|
57
|
+
});
|
|
58
|
+
test('redacts arrays', () => {
|
|
59
|
+
vault.getOrCreateSession('s4', 0);
|
|
60
|
+
const hits = [];
|
|
61
|
+
const data = {
|
|
62
|
+
emails: ['alice@foo.com', 'bob@bar.com'],
|
|
63
|
+
};
|
|
64
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 's4', hits, 0);
|
|
65
|
+
expect(result.emails[0]).toContain('[EMAIL_');
|
|
66
|
+
expect(result.emails[1]).toContain('[EMAIL_');
|
|
67
|
+
expect(hits).toHaveLength(2);
|
|
68
|
+
});
|
|
69
|
+
test('preserves non-string values', () => {
|
|
70
|
+
vault.getOrCreateSession('s5', 0);
|
|
71
|
+
const hits = [];
|
|
72
|
+
const data = {
|
|
73
|
+
count: 42,
|
|
74
|
+
active: true,
|
|
75
|
+
tags: null,
|
|
76
|
+
score: 3.14,
|
|
77
|
+
};
|
|
78
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 's5', hits, 0);
|
|
79
|
+
expect(result.count).toBe(42);
|
|
80
|
+
expect(result.active).toBe(true);
|
|
81
|
+
expect(result.tags).toBeNull();
|
|
82
|
+
expect(result.score).toBe(3.14);
|
|
83
|
+
expect(hits).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
test('handles empty strings', () => {
|
|
86
|
+
vault.getOrCreateSession('s6', 0);
|
|
87
|
+
const hits = [];
|
|
88
|
+
const result = (0, engine_1.redactValue)({ field: '' }, createContext(), vault, 's6', hits, 0);
|
|
89
|
+
expect(result.field).toBe('');
|
|
90
|
+
expect(hits).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
test('handles deeply nested arrays of objects', () => {
|
|
93
|
+
vault.getOrCreateSession('s7', 0);
|
|
94
|
+
const hits = [];
|
|
95
|
+
const data = {
|
|
96
|
+
contacts: [
|
|
97
|
+
{ name: 'Mr. Alice Smith', email: 'alice@test.com' },
|
|
98
|
+
{ name: 'Dr. Bob Jones', email: 'bob@test.com' },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 's7', hits, 0);
|
|
102
|
+
expect(result.contacts[0].email).toContain('[EMAIL_');
|
|
103
|
+
expect(result.contacts[1].email).toContain('[EMAIL_');
|
|
104
|
+
expect(result.contacts[0].name).toContain('[PERSON_');
|
|
105
|
+
expect(hits.length).toBeGreaterThanOrEqual(4);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// ═══════════════════════════════════════════════════════
|
|
109
|
+
// DEDUPLICATION
|
|
110
|
+
// ═══════════════════════════════════════════════════════
|
|
111
|
+
describe('Deduplication', () => {
|
|
112
|
+
let vault;
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
vault = new vault_1.MemoryVault();
|
|
115
|
+
});
|
|
116
|
+
test('same email gets same token when dedup=true', () => {
|
|
117
|
+
vault.getOrCreateSession('dedup-1', 0);
|
|
118
|
+
const hits = [];
|
|
119
|
+
const data = {
|
|
120
|
+
primary: 'Contact: same@email.com',
|
|
121
|
+
secondary: 'Also: same@email.com',
|
|
122
|
+
};
|
|
123
|
+
const result = (0, engine_1.redactValue)(data, createContext({ dedup: true }), vault, 'dedup-1', hits, 0);
|
|
124
|
+
// Both fields should have the SAME token
|
|
125
|
+
const token1 = result.primary.match(/\[EMAIL_\d+\]/)?.[0];
|
|
126
|
+
const token2 = result.secondary.match(/\[EMAIL_\d+\]/)?.[0];
|
|
127
|
+
expect(token1).toBe(token2);
|
|
128
|
+
});
|
|
129
|
+
test('same email gets different tokens when dedup=false', () => {
|
|
130
|
+
vault.getOrCreateSession('dedup-2', 0);
|
|
131
|
+
const hits = [];
|
|
132
|
+
const data = {
|
|
133
|
+
primary: 'Contact: same@email.com',
|
|
134
|
+
secondary: 'Also: same@email.com',
|
|
135
|
+
};
|
|
136
|
+
const result = (0, engine_1.redactValue)(data, createContext({ dedup: false }), vault, 'dedup-2', hits, 0);
|
|
137
|
+
const token1 = result.primary.match(/\[EMAIL_\d+\]/)?.[0];
|
|
138
|
+
const token2 = result.secondary.match(/\[EMAIL_\d+\]/)?.[0];
|
|
139
|
+
expect(token1).not.toBe(token2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// ═══════════════════════════════════════════════════════
|
|
143
|
+
// REDACTION MODES
|
|
144
|
+
// ═══════════════════════════════════════════════════════
|
|
145
|
+
describe('Redaction modes', () => {
|
|
146
|
+
let vault;
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
vault = new vault_1.MemoryVault();
|
|
149
|
+
});
|
|
150
|
+
test('token mode produces [LABEL_N] format', () => {
|
|
151
|
+
vault.getOrCreateSession('mode-token', 0);
|
|
152
|
+
const hits = [];
|
|
153
|
+
const result = (0, engine_1.redactValue)({ email: 'test@example.com' }, createContext({ mode: 'token' }), vault, 'mode-token', hits, 0);
|
|
154
|
+
expect(result.email).toMatch(/^\[EMAIL_\d+\]$/);
|
|
155
|
+
});
|
|
156
|
+
test('mask mode partially hides email', () => {
|
|
157
|
+
vault.getOrCreateSession('mode-mask', 0);
|
|
158
|
+
const hits = [];
|
|
159
|
+
const result = (0, engine_1.redactValue)({ email: 'john@example.com' }, createContext({ mode: 'mask' }), vault, 'mode-mask', hits, 0);
|
|
160
|
+
expect(result.email).toContain('***');
|
|
161
|
+
expect(result.email).toContain('@');
|
|
162
|
+
expect(result.email).not.toBe('john@example.com');
|
|
163
|
+
});
|
|
164
|
+
test('hash mode produces [LABEL:hash] format', () => {
|
|
165
|
+
vault.getOrCreateSession('mode-hash', 0);
|
|
166
|
+
const hits = [];
|
|
167
|
+
const result = (0, engine_1.redactValue)({ email: 'test@example.com' }, createContext({ mode: 'hash' }), vault, 'mode-hash', hits, 0);
|
|
168
|
+
expect(result.email).toMatch(/^\[EMAIL:[a-f0-9]{12}\]$/);
|
|
169
|
+
});
|
|
170
|
+
test('hash mode is deterministic', () => {
|
|
171
|
+
vault.getOrCreateSession('mode-hash-det', 0);
|
|
172
|
+
const hits1 = [];
|
|
173
|
+
const hits2 = [];
|
|
174
|
+
const r1 = (0, engine_1.redactValue)({ email: 'test@example.com' }, createContext({ mode: 'hash' }), vault, 'mode-hash-det', hits1, 0);
|
|
175
|
+
const r2 = (0, engine_1.redactValue)({ email: 'test@example.com' }, createContext({ mode: 'hash' }), vault, 'mode-hash-det', hits2, 0);
|
|
176
|
+
expect(r1.email).toBe(r2.email);
|
|
177
|
+
});
|
|
178
|
+
test('redact mode produces [REDACTED]', () => {
|
|
179
|
+
vault.getOrCreateSession('mode-redact', 0);
|
|
180
|
+
const hits = [];
|
|
181
|
+
const result = (0, engine_1.redactValue)({ email: 'test@example.com' }, createContext({ mode: 'redact' }), vault, 'mode-redact', hits, 0);
|
|
182
|
+
expect(result.email).toContain('[REDACTED]');
|
|
183
|
+
});
|
|
184
|
+
test('mask mode for credit card shows last 4', () => {
|
|
185
|
+
vault.getOrCreateSession('mode-mask-cc', 0);
|
|
186
|
+
const hits = [];
|
|
187
|
+
const result = (0, engine_1.redactValue)({ card: '4532 0151 1283 0366' }, createContext({ mode: 'mask', enabledPatterns: ['creditCard'] }), vault, 'mode-mask-cc', hits, 0);
|
|
188
|
+
expect(result.card).toContain('0366');
|
|
189
|
+
expect(result.card).toContain('****');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
// ═══════════════════════════════════════════════════════
|
|
193
|
+
// FIELD TARGETING
|
|
194
|
+
// ═══════════════════════════════════════════════════════
|
|
195
|
+
describe('Field targeting', () => {
|
|
196
|
+
let vault;
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
vault = new vault_1.MemoryVault();
|
|
199
|
+
});
|
|
200
|
+
test('allowlist — only scans specified fields', () => {
|
|
201
|
+
vault.getOrCreateSession('field-allow', 0);
|
|
202
|
+
const hits = [];
|
|
203
|
+
const data = {
|
|
204
|
+
email: 'scan@me.com',
|
|
205
|
+
notes: 'skip@me.com',
|
|
206
|
+
};
|
|
207
|
+
const result = (0, engine_1.redactValue)(data, createContext({
|
|
208
|
+
fieldMode: 'allowlist',
|
|
209
|
+
fieldRules: [{ field: 'email', mode: 'include' }],
|
|
210
|
+
}), vault, 'field-allow', hits, 0);
|
|
211
|
+
expect(result.email).toContain('[EMAIL_');
|
|
212
|
+
expect(result.notes).toBe('skip@me.com'); // Not redacted
|
|
213
|
+
});
|
|
214
|
+
test('denylist — skips specified fields', () => {
|
|
215
|
+
vault.getOrCreateSession('field-deny', 0);
|
|
216
|
+
const hits = [];
|
|
217
|
+
const data = {
|
|
218
|
+
email: 'scan@me.com',
|
|
219
|
+
internalId: 'skip@me.com',
|
|
220
|
+
};
|
|
221
|
+
const result = (0, engine_1.redactValue)(data, createContext({
|
|
222
|
+
fieldMode: 'denylist',
|
|
223
|
+
fieldRules: [{ field: 'internalId', mode: 'exclude' }],
|
|
224
|
+
}), vault, 'field-deny', hits, 0);
|
|
225
|
+
expect(result.email).toContain('[EMAIL_');
|
|
226
|
+
expect(result.internalId).toBe('skip@me.com');
|
|
227
|
+
});
|
|
228
|
+
test('wildcard field matching with *.field', () => {
|
|
229
|
+
vault.getOrCreateSession('field-wild', 0);
|
|
230
|
+
const hits = [];
|
|
231
|
+
const data = {
|
|
232
|
+
user: { email: 'user@test.com' },
|
|
233
|
+
admin: { email: 'admin@test.com' },
|
|
234
|
+
};
|
|
235
|
+
const result = (0, engine_1.redactValue)(data, createContext({
|
|
236
|
+
fieldMode: 'allowlist',
|
|
237
|
+
fieldRules: [{ field: '*.email', mode: 'include' }],
|
|
238
|
+
}), vault, 'field-wild', hits, 0);
|
|
239
|
+
expect(result.user.email).toContain('[EMAIL_');
|
|
240
|
+
expect(result.admin.email).toContain('[EMAIL_');
|
|
241
|
+
});
|
|
242
|
+
test('all mode scans everything', () => {
|
|
243
|
+
vault.getOrCreateSession('field-all', 0);
|
|
244
|
+
const hits = [];
|
|
245
|
+
const data = {
|
|
246
|
+
a: 'a@test.com',
|
|
247
|
+
b: { c: 'b@test.com' },
|
|
248
|
+
};
|
|
249
|
+
const result = (0, engine_1.redactValue)(data, createContext({ fieldMode: 'all' }), vault, 'field-all', hits, 0);
|
|
250
|
+
expect(result.a).toContain('[EMAIL_');
|
|
251
|
+
expect(result.b.c).toContain('[EMAIL_');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
// ═══════════════════════════════════════════════════════
|
|
255
|
+
// CUSTOM PATTERNS
|
|
256
|
+
// ═══════════════════════════════════════════════════════
|
|
257
|
+
describe('Custom patterns', () => {
|
|
258
|
+
let vault;
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
vault = new vault_1.MemoryVault();
|
|
261
|
+
});
|
|
262
|
+
test('matches custom regex pattern', () => {
|
|
263
|
+
vault.getOrCreateSession('custom-1', 0);
|
|
264
|
+
const hits = [];
|
|
265
|
+
const result = (0, engine_1.redactValue)({ order: 'Your order ORD-123456 is confirmed' }, createContext({
|
|
266
|
+
enabledPatterns: [],
|
|
267
|
+
customPatterns: [{ label: 'ORDER_ID', regex: 'ORD-\\d{6}' }],
|
|
268
|
+
}), vault, 'custom-1', hits, 0);
|
|
269
|
+
expect(result.order).toContain('[ORDER_ID_');
|
|
270
|
+
expect(result.order).not.toContain('ORD-123456');
|
|
271
|
+
expect(hits).toHaveLength(1);
|
|
272
|
+
});
|
|
273
|
+
test('skips invalid regex gracefully', () => {
|
|
274
|
+
vault.getOrCreateSession('custom-bad', 0);
|
|
275
|
+
const hits = [];
|
|
276
|
+
const result = (0, engine_1.redactValue)({ text: 'hello world' }, createContext({
|
|
277
|
+
enabledPatterns: [],
|
|
278
|
+
customPatterns: [{ label: 'BAD', regex: '[invalid(' }],
|
|
279
|
+
}), vault, 'custom-bad', hits, 0);
|
|
280
|
+
// Should not crash
|
|
281
|
+
expect(result.text).toBe('hello world');
|
|
282
|
+
expect(hits).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
test('custom pattern with category', () => {
|
|
285
|
+
vault.getOrCreateSession('custom-cat', 0);
|
|
286
|
+
const hits = [];
|
|
287
|
+
(0, engine_1.redactValue)({ ticket: 'TICKET-999' }, createContext({
|
|
288
|
+
enabledPatterns: [],
|
|
289
|
+
customPatterns: [{ label: 'TICKET', regex: 'TICKET-\\d+', category: 'identity' }],
|
|
290
|
+
}), vault, 'custom-cat', hits, 0);
|
|
291
|
+
expect(hits[0].category).toBe('identity');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// ═══════════════════════════════════════════════════════
|
|
295
|
+
// RESTORE
|
|
296
|
+
// ═══════════════════════════════════════════════════════
|
|
297
|
+
describe('Restore engine', () => {
|
|
298
|
+
let vault;
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
vault = new vault_1.MemoryVault();
|
|
301
|
+
});
|
|
302
|
+
test('full round-trip: redact then restore', () => {
|
|
303
|
+
vault.getOrCreateSession('rt-1', 0);
|
|
304
|
+
const original = {
|
|
305
|
+
message: 'Contact Mr. John Smith at john@example.com or 555-123-4567',
|
|
306
|
+
meta: { ssn: '123-45-6789' },
|
|
307
|
+
};
|
|
308
|
+
const hits = [];
|
|
309
|
+
const redacted = (0, engine_1.redactValue)(original, createContext(), vault, 'rt-1', hits, 0);
|
|
310
|
+
// Verify redaction happened
|
|
311
|
+
expect(JSON.stringify(redacted)).not.toContain('john@example.com');
|
|
312
|
+
expect(JSON.stringify(redacted)).not.toContain('123-45-6789');
|
|
313
|
+
// Restore
|
|
314
|
+
const restored = (0, engine_1.restoreValue)(redacted, vault, 'rt-1');
|
|
315
|
+
expect(restored.message).toContain('john@example.com');
|
|
316
|
+
expect(restored.meta.ssn).toBe('123-45-6789');
|
|
317
|
+
});
|
|
318
|
+
test('restore handles arrays', () => {
|
|
319
|
+
vault.getOrCreateSession('rt-arr', 0);
|
|
320
|
+
const original = { emails: ['alice@test.com', 'bob@test.com'] };
|
|
321
|
+
const hits = [];
|
|
322
|
+
const redacted = (0, engine_1.redactValue)(original, createContext(), vault, 'rt-arr', hits, 0);
|
|
323
|
+
const restored = (0, engine_1.restoreValue)(redacted, vault, 'rt-arr');
|
|
324
|
+
expect(restored.emails).toContain('alice@test.com');
|
|
325
|
+
expect(restored.emails).toContain('bob@test.com');
|
|
326
|
+
});
|
|
327
|
+
test('restore preserves non-string values', () => {
|
|
328
|
+
vault.getOrCreateSession('rt-nonstr', 0);
|
|
329
|
+
const data = { count: 42, active: true };
|
|
330
|
+
const restored = (0, engine_1.restoreValue)(data, vault, 'rt-nonstr');
|
|
331
|
+
expect(restored.count).toBe(42);
|
|
332
|
+
expect(restored.active).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
test('restore returns data as-is if session missing', () => {
|
|
335
|
+
const data = { message: 'no session here [EMAIL_0]' };
|
|
336
|
+
const result = (0, engine_1.restoreValue)(data, vault, 'nonexistent');
|
|
337
|
+
expect(result.message).toBe('no session here [EMAIL_0]');
|
|
338
|
+
});
|
|
339
|
+
test('restore works when LLM rephrases around tokens', () => {
|
|
340
|
+
vault.getOrCreateSession('rt-llm', 0);
|
|
341
|
+
const original = { text: 'Email me at alice@company.com' };
|
|
342
|
+
const hits = [];
|
|
343
|
+
(0, engine_1.redactValue)(original, createContext(), vault, 'rt-llm', hits, 0);
|
|
344
|
+
// Simulate LLM response that uses the token in a different sentence
|
|
345
|
+
const llmResponse = { text: 'I will send the report to [EMAIL_0] by tomorrow.' };
|
|
346
|
+
const restored = (0, engine_1.restoreValue)(llmResponse, vault, 'rt-llm');
|
|
347
|
+
expect(restored.text).toBe('I will send the report to alice@company.com by tomorrow.');
|
|
348
|
+
});
|
|
349
|
+
test('restore handles multiple tokens in one field', () => {
|
|
350
|
+
vault.getOrCreateSession('rt-multi', 0);
|
|
351
|
+
const original = { text: 'From alice@a.com to bob@b.com' };
|
|
352
|
+
const hits = [];
|
|
353
|
+
(0, engine_1.redactValue)(original, createContext(), vault, 'rt-multi', hits, 0);
|
|
354
|
+
// Simulate LLM using both tokens
|
|
355
|
+
const session = vault.getSession('rt-multi');
|
|
356
|
+
const tokens = Object.keys(session.entries);
|
|
357
|
+
const llmResponse = { text: `Forwarded from ${tokens[0]} to ${tokens[1]}` };
|
|
358
|
+
const restored = (0, engine_1.restoreValue)(llmResponse, vault, 'rt-multi');
|
|
359
|
+
expect(restored.text).toContain('alice@a.com');
|
|
360
|
+
expect(restored.text).toContain('bob@b.com');
|
|
361
|
+
});
|
|
362
|
+
test('restore with deduplication — same token appears twice', () => {
|
|
363
|
+
vault.getOrCreateSession('rt-dedup', 0);
|
|
364
|
+
const original = {
|
|
365
|
+
field1: 'Contact: same@email.com',
|
|
366
|
+
field2: 'CC: same@email.com',
|
|
367
|
+
};
|
|
368
|
+
const hits = [];
|
|
369
|
+
const redacted = (0, engine_1.redactValue)(original, createContext({ dedup: true }), vault, 'rt-dedup', hits, 0);
|
|
370
|
+
// Both should have same token
|
|
371
|
+
const token = redacted.field1.match(/\[EMAIL_\d+\]/)[0];
|
|
372
|
+
// LLM uses the token once
|
|
373
|
+
const llmResponse = { reply: `I sent it to ${token}` };
|
|
374
|
+
const restored = (0, engine_1.restoreValue)(llmResponse, vault, 'rt-dedup');
|
|
375
|
+
expect(restored.reply).toBe('I sent it to same@email.com');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
// ═══════════════════════════════════════════════════════
|
|
379
|
+
// AUDIT REPORT
|
|
380
|
+
// ═══════════════════════════════════════════════════════
|
|
381
|
+
describe('Audit report', () => {
|
|
382
|
+
test('buildReport creates accurate summary', () => {
|
|
383
|
+
const hits = [
|
|
384
|
+
{ token: '[EMAIL_0]', original: 'a@b.com', patternName: 'email', patternLabel: 'EMAIL', category: 'contact', field: 'email', itemIndex: 0, confidence: 0.90 },
|
|
385
|
+
{ token: '[PHONE_1]', original: '555-1234', patternName: 'phone', patternLabel: 'PHONE', category: 'contact', field: 'phone', itemIndex: 0, confidence: 0.70 },
|
|
386
|
+
{ token: '[SSN_2]', original: '123-45-6789', patternName: 'ssn', patternLabel: 'SSN', category: 'identity', field: 'ssn', itemIndex: 0, confidence: 0.85 },
|
|
387
|
+
{ token: '[EMAIL_3]', original: 'c@d.com', patternName: 'email', patternLabel: 'EMAIL', category: 'contact', field: 'email2', itemIndex: 1, confidence: 0.90 },
|
|
388
|
+
];
|
|
389
|
+
const report = (0, engine_1.buildReport)('test-session', hits);
|
|
390
|
+
expect(report.sessionId).toBe('test-session');
|
|
391
|
+
expect(report.totalHits).toBe(4);
|
|
392
|
+
expect(report.hitsByCategory.contact).toBe(3);
|
|
393
|
+
expect(report.hitsByCategory.identity).toBe(1);
|
|
394
|
+
expect(report.hitsByPattern.EMAIL).toBe(2);
|
|
395
|
+
expect(report.hitsByPattern.PHONE).toBe(1);
|
|
396
|
+
expect(report.hitsByPattern.SSN).toBe(1);
|
|
397
|
+
expect(report.hits).toHaveLength(4);
|
|
398
|
+
expect(report.timestamp).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
test('buildReport handles empty hits', () => {
|
|
401
|
+
const report = (0, engine_1.buildReport)('empty', []);
|
|
402
|
+
expect(report.totalHits).toBe(0);
|
|
403
|
+
expect(report.hits).toHaveLength(0);
|
|
404
|
+
expect(report.hitsByCategory).toEqual({});
|
|
405
|
+
expect(report.hitsByPattern).toEqual({});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// ═══════════════════════════════════════════════════════
|
|
409
|
+
// EDGE CASES & STRESS TESTS
|
|
410
|
+
// ═══════════════════════════════════════════════════════
|
|
411
|
+
describe('Edge cases', () => {
|
|
412
|
+
let vault;
|
|
413
|
+
beforeEach(() => {
|
|
414
|
+
vault = new vault_1.MemoryVault();
|
|
415
|
+
});
|
|
416
|
+
test('does not re-redact already-tokenized text', () => {
|
|
417
|
+
vault.getOrCreateSession('no-re-redact', 0);
|
|
418
|
+
const hits = [];
|
|
419
|
+
const data = { text: 'This is [EMAIL_0] already tokenized' };
|
|
420
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 'no-re-redact', hits, 0);
|
|
421
|
+
expect(result.text).toBe('This is [EMAIL_0] already tokenized');
|
|
422
|
+
});
|
|
423
|
+
test('handles long strings', () => {
|
|
424
|
+
vault.getOrCreateSession('long-str', 0);
|
|
425
|
+
const hits = [];
|
|
426
|
+
const longText = 'x'.repeat(5000) + ' john@test.com ' + 'y'.repeat(5000);
|
|
427
|
+
const result = (0, engine_1.redactValue)({ text: longText }, createContext(), vault, 'long-str', hits, 0);
|
|
428
|
+
expect(result.text).toContain('[EMAIL_');
|
|
429
|
+
expect(result.text).not.toContain('john@test.com');
|
|
430
|
+
expect(hits).toHaveLength(1);
|
|
431
|
+
});
|
|
432
|
+
test('handles data with many PII items', () => {
|
|
433
|
+
vault.getOrCreateSession('many-pii', 0);
|
|
434
|
+
const hits = [];
|
|
435
|
+
const emails = {};
|
|
436
|
+
for (let i = 0; i < 100; i++) {
|
|
437
|
+
emails[`email_${i}`] = `user${i}@domain${i}.com`;
|
|
438
|
+
}
|
|
439
|
+
const result = (0, engine_1.redactValue)(emails, createContext(), vault, 'many-pii', hits, 0);
|
|
440
|
+
expect(hits).toHaveLength(100);
|
|
441
|
+
for (let i = 0; i < 100; i++) {
|
|
442
|
+
expect(result[`email_${i}`]).toContain('[EMAIL_');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
test('handles Unicode/emoji in surrounding text', () => {
|
|
446
|
+
vault.getOrCreateSession('unicode', 0);
|
|
447
|
+
const hits = [];
|
|
448
|
+
const data = { text: 'Send to test@example.com please' };
|
|
449
|
+
const result = (0, engine_1.redactValue)(data, createContext(), vault, 'unicode', hits, 0);
|
|
450
|
+
expect(result.text).toContain('[EMAIL_');
|
|
451
|
+
expect(result.text).not.toContain('test@example.com');
|
|
452
|
+
});
|
|
453
|
+
test('mixed PII in one sentence — all detected', () => {
|
|
454
|
+
vault.getOrCreateSession('mixed', 0);
|
|
455
|
+
const hits = [];
|
|
456
|
+
const data = {
|
|
457
|
+
text: 'Mr. John Doe, SSN 123-45-6789, email john@test.com, IP 192.168.1.100',
|
|
458
|
+
};
|
|
459
|
+
const result = (0, engine_1.redactValue)(data, createContext({ enabledPatterns: ['personName', 'ssn', 'email', 'ipv4'] }), vault, 'mixed', hits, 0);
|
|
460
|
+
expect(result.text).not.toContain('Mr. John Doe');
|
|
461
|
+
expect(result.text).not.toContain('123-45-6789');
|
|
462
|
+
expect(result.text).not.toContain('john@test.com');
|
|
463
|
+
expect(result.text).not.toContain('192.168.1.100');
|
|
464
|
+
expect(hits.length).toBeGreaterThanOrEqual(4);
|
|
465
|
+
});
|
|
466
|
+
test('empty object returns empty object', () => {
|
|
467
|
+
vault.getOrCreateSession('empty-obj', 0);
|
|
468
|
+
const hits = [];
|
|
469
|
+
const result = (0, engine_1.redactValue)({}, createContext(), vault, 'empty-obj', hits, 0);
|
|
470
|
+
expect(result).toEqual({});
|
|
471
|
+
expect(hits).toHaveLength(0);
|
|
472
|
+
});
|
|
473
|
+
test('null values are preserved', () => {
|
|
474
|
+
vault.getOrCreateSession('null-val', 0);
|
|
475
|
+
const hits = [];
|
|
476
|
+
const result = (0, engine_1.redactValue)(null, createContext(), vault, 'null-val', hits, 0);
|
|
477
|
+
expect(result).toBeNull();
|
|
478
|
+
});
|
|
479
|
+
test('credit card Luhn validation prevents false positives', () => {
|
|
480
|
+
vault.getOrCreateSession('luhn-fp', 0);
|
|
481
|
+
const hits = [];
|
|
482
|
+
// 1234567890123456 fails Luhn — should NOT be redacted
|
|
483
|
+
const result = (0, engine_1.redactValue)({ text: 'Code: 1234 5678 9012 3456' }, createContext({ enabledPatterns: ['creditCard'] }), vault, 'luhn-fp', hits, 0);
|
|
484
|
+
expect(hits).toHaveLength(0);
|
|
485
|
+
expect(result.text).toBe('Code: 1234 5678 9012 3456');
|
|
486
|
+
});
|
|
487
|
+
test('IBAN checksum validation prevents false positives', () => {
|
|
488
|
+
vault.getOrCreateSession('iban-fp', 0);
|
|
489
|
+
const hits = [];
|
|
490
|
+
// DE00... is an invalid checksum
|
|
491
|
+
const result = (0, engine_1.redactValue)({ text: 'IBAN: DE00370400440532013000' }, createContext({ enabledPatterns: ['iban'] }), vault, 'iban-fp', hits, 0);
|
|
492
|
+
expect(hits).toHaveLength(0);
|
|
493
|
+
});
|
|
494
|
+
test('full lifecycle with file-like data structure', () => {
|
|
495
|
+
vault.getOrCreateSession('lifecycle', 0);
|
|
496
|
+
const hits = [];
|
|
497
|
+
const customerTicket = {
|
|
498
|
+
id: 'TICKET-001',
|
|
499
|
+
customer: {
|
|
500
|
+
name: 'Mrs. Sarah Johnson',
|
|
501
|
+
email: 'sarah.johnson@bigcorp.com',
|
|
502
|
+
phone: '+1-555-987-6543',
|
|
503
|
+
address: {
|
|
504
|
+
street: '123 Main St',
|
|
505
|
+
zip: '90210',
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
issue: 'Customer Mrs. Sarah Johnson reported billing error. SSN on file: 987-65-4321. Card ending 0366.',
|
|
509
|
+
internal: {
|
|
510
|
+
agentNotes: 'Called back at +1-555-987-6543, confirmed identity via SSN 987-65-4321.',
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
const redacted = (0, engine_1.redactValue)(customerTicket, createContext({ enabledPatterns: ['email', 'phone', 'personName', 'ssn'] }), vault, 'lifecycle', hits, 0);
|
|
514
|
+
// Verify redaction
|
|
515
|
+
const json = JSON.stringify(redacted);
|
|
516
|
+
expect(json).not.toContain('sarah.johnson@bigcorp.com');
|
|
517
|
+
expect(json).not.toContain('987-65-4321');
|
|
518
|
+
expect(json).not.toContain('Mrs. Sarah Johnson');
|
|
519
|
+
// Verify restore
|
|
520
|
+
const restored = (0, engine_1.restoreValue)(redacted, vault, 'lifecycle');
|
|
521
|
+
expect(restored.customer.email).toBe('sarah.johnson@bigcorp.com');
|
|
522
|
+
expect(restored.internal.agentNotes).toContain('987-65-4321');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|