n8n-nodes-redactor 4.0.0 → 4.2.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.
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.collectTokenLike = collectTokenLike;
36
37
  exports.redactValue = redactValue;
37
38
  exports.restoreValue = restoreValue;
38
39
  exports.enhanceWithPresidio = enhanceWithPresidio;
39
40
  exports.buildReport = buildReport;
40
41
  const crypto = __importStar(require("crypto"));
41
42
  const patterns_1 = require("./patterns");
43
+ const regexSafety_1 = require("./regexSafety");
42
44
  const context_1 = require("./context");
43
45
  const names_1 = require("./names");
44
46
  const pseudonymize_1 = require("./pseudonymize");
@@ -95,55 +97,79 @@ function maskValue(value, label) {
95
97
  function hashValue(value) {
96
98
  return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
97
99
  }
98
- function applyRedaction(match, pattern, mode, vault, sessionId, dedup) {
99
- switch (mode) {
100
- case 'token': {
101
- if (dedup) {
102
- const existing = vault.findByOriginal(sessionId, match);
103
- if (existing)
104
- return existing.token;
105
- }
106
- const session = vault.getSession(sessionId);
107
- const index = session ? Object.keys(session.entries).length : 0;
108
- const token = generateToken(pattern.label, index);
109
- vault.addEntry(sessionId, {
110
- token,
111
- original: match,
112
- patternLabel: pattern.label,
113
- category: ('category' in pattern ? pattern.category : 'identity'),
114
- createdAt: new Date().toISOString(),
115
- });
116
- return token;
117
- }
118
- case 'mask':
119
- return maskValue(match, pattern.label);
120
- case 'hash':
121
- return `[${pattern.label}:${hashValue(match)}]`;
122
- case 'redact':
123
- return `[REDACTED]`;
124
- case 'blackout':
125
- return '\u2588'.repeat(match.length);
126
- case 'remove':
127
- return '';
128
- case 'pseudonymize': {
129
- if (dedup) {
130
- const existing = vault.findByOriginal(sessionId, match);
131
- if (existing)
132
- return existing.token;
133
- }
134
- const pseudonym = (0, pseudonymize_1.generatePseudonym)(sessionId, match, pattern.label);
135
- vault.addEntry(sessionId, {
136
- token: pseudonym,
137
- original: match,
138
- patternLabel: pattern.label,
139
- category: ('category' in pattern ? pattern.category : 'identity'),
140
- createdAt: new Date().toISOString(),
141
- });
142
- return pseudonym;
100
+ function storeReversible(match, pattern, vault, sessionId, dedup, makeToken) {
101
+ if (dedup) {
102
+ const existing = vault.findByOriginal(sessionId, match);
103
+ if (existing && !existing.numeric)
104
+ return existing.token;
105
+ }
106
+ const token = makeToken();
107
+ vault.addEntry(sessionId, {
108
+ token,
109
+ original: match,
110
+ patternLabel: pattern.label,
111
+ category: ('category' in pattern ? pattern.category : 'identity'),
112
+ createdAt: new Date().toISOString(),
113
+ });
114
+ return token;
115
+ }
116
+ const IRREVERSIBLE = {
117
+ mask: (match, label) => maskValue(match, label),
118
+ hash: (match, label) => `[${label}:${hashValue(match)}]`,
119
+ redact: () => '[REDACTED]',
120
+ blackout: (match) => '\u2588'.repeat(match.length),
121
+ remove: () => '',
122
+ };
123
+ function applyRedaction(match, pattern, mode, vault, sessionId, dedup, reserved) {
124
+ if (mode === 'token') {
125
+ return storeReversible(match, pattern, vault, sessionId, dedup, () => freshToken(pattern.label, vault, sessionId, reserved));
126
+ }
127
+ if (mode === 'pseudonymize') {
128
+ return storeReversible(match, pattern, vault, sessionId, dedup, () => uniquePseudonym(vault, sessionId, match, pattern.label));
129
+ }
130
+ const irreversible = IRREVERSIBLE[mode];
131
+ return irreversible ? irreversible(match, pattern.label) : match;
132
+ }
133
+ function freshToken(label, vault, sessionId, reserved) {
134
+ var _a, _b;
135
+ const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
136
+ let index = Object.keys(entries).length;
137
+ let token = generateToken(label, index);
138
+ while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
139
+ token = generateToken(label, ++index);
140
+ return token;
141
+ }
142
+ const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
143
+ function collectTokenLike(value, into = new Set()) {
144
+ const scan = (text) => {
145
+ for (const m of text.matchAll(TOKEN_LIKE_RE))
146
+ into.add(m[0]);
147
+ };
148
+ if (typeof value === 'string')
149
+ scan(value);
150
+ else if (Array.isArray(value))
151
+ value.forEach((v) => collectTokenLike(v, into));
152
+ else if (value !== null && typeof value === 'object') {
153
+ for (const [k, v] of Object.entries(value)) {
154
+ scan(k);
155
+ collectTokenLike(v, into);
143
156
  }
144
- default:
145
- return match;
146
157
  }
158
+ return into;
159
+ }
160
+ function uniquePseudonym(vault, sessionId, original, label) {
161
+ var _a, _b;
162
+ const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
163
+ for (let attempt = 0; attempt < 32; attempt++) {
164
+ const seed = attempt === 0 ? original : `${original}\u0000${attempt}`;
165
+ const candidate = (0, pseudonymize_1.generatePseudonym)(sessionId, seed, label);
166
+ const taken = entries[candidate];
167
+ if (candidate === original)
168
+ continue;
169
+ if (!taken || taken.original === original)
170
+ return candidate;
171
+ }
172
+ return generateToken(label, Object.keys(entries).length);
147
173
  }
148
174
  function shouldProcessField(fieldPath, fieldMode, fieldRules) {
149
175
  if (fieldMode === 'all')
@@ -169,17 +195,7 @@ function fieldPathMatches(actual, pattern) {
169
195
  }
170
196
  }
171
197
  function isSafeRegex(regexStr) {
172
- if (/(\+|\*|\?|\{[^}]+\})\s*(\+|\*|\?|\{)/.test(regexStr))
173
- return false;
174
- if (regexStr.length > 500)
175
- return false;
176
- try {
177
- new RegExp(regexStr);
178
- return true;
179
- }
180
- catch {
181
- return false;
182
- }
198
+ return (0, regexSafety_1.checkRegex)(regexStr) === null;
183
199
  }
184
200
  const SEMANTIC_FIELD_MAP = [
185
201
  {
@@ -386,279 +402,342 @@ function getSemanticLabel(fieldKey, siblingKeys) {
386
402
  }
387
403
  return null;
388
404
  }
389
- function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath = '', siblingKeys) {
390
- var _a, _b;
405
+ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
406
+ var _a;
407
+ const semantic = getSemanticLabel(path, siblingKeys);
408
+ if (!semantic || !semantic.label || raw.trim().length === 0)
409
+ return null;
410
+ if (isAllowListed(raw, ctx.allowList))
411
+ return null;
412
+ const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
413
+ if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
414
+ return null;
415
+ const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
416
+ hits.push({
417
+ token: replacement, original: '***',
418
+ patternName: `semantic_${semantic.label}`,
419
+ patternLabel: semantic.label,
420
+ category: semantic.category,
421
+ field: display, itemIndex,
422
+ confidence: semConf,
423
+ });
424
+ return replacement;
425
+ }
426
+ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
427
+ var _a;
428
+ const text = String(value);
429
+ const numCtx = { ...ctx, dedup: false };
430
+ let out = null;
431
+ if (!ctx.skipSemantic)
432
+ out = redactSemantic(text, numCtx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
433
+ if (out === null && ctx.scanNumbers)
434
+ out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
435
+ if (out === null || out === text)
436
+ return value;
437
+ const entry = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries[out];
438
+ if (entry && entry.original === text)
439
+ entry.numeric = true;
440
+ return out;
441
+ }
442
+ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
443
+ var _a;
444
+ const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
445
+ if (out === key)
446
+ return key;
447
+ const clashes = (k) => k === '' || k in taken || sourceKeys.has(k);
448
+ if (!clashes(out))
449
+ return out;
450
+ if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
451
+ const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...sourceKeys, ...Object.keys(taken)]));
452
+ vault.addEntry(sessionId, { token, original: key, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
453
+ return token;
454
+ }
455
+ let suffixed = out;
456
+ for (let n = 1; clashes(suffixed); n++)
457
+ suffixed = `${out}_${n}`;
458
+ return suffixed;
459
+ }
460
+ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
461
+ var _a;
462
+ const entries = Object.entries(value);
463
+ const siblingKeys = entries.map(([k]) => k);
464
+ const sourceKeys = new Set(siblingKeys);
465
+ const origins = new Map();
466
+ const obj = {};
467
+ for (const [k, v] of entries) {
468
+ const fieldPath = path ? `${path}.${k}` : k;
469
+ const keyHits = [];
470
+ const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
471
+ ? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
472
+ : k;
473
+ origins.set(outKey, k);
474
+ const childDisplay = display ? `${display}.${outKey}` : outKey;
475
+ for (const h of keyHits)
476
+ hits.push({ ...h, field: childDisplay });
477
+ obj[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
478
+ }
479
+ (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
480
+ return obj;
481
+ }
482
+ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath = '', siblingKeys, displayPath = currentPath) {
483
+ const processed = shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules);
391
484
  if (typeof value === 'string') {
392
- if (!shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules)) {
485
+ if (!processed)
393
486
  return value;
394
- }
395
487
  if (!ctx.skipSemantic) {
396
- const semantic = getSemanticLabel(currentPath, siblingKeys);
397
- if (semantic && semantic.label && value.trim().length > 0) {
398
- if (isAllowListed(value, ctx.allowList)) {
399
- }
400
- else {
401
- const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
402
- if (semConf >= ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0)) {
403
- const replacement = applyRedaction(value, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup);
404
- hits.push({
405
- token: replacement, original: '***',
406
- patternName: `semantic_${semantic.label}`,
407
- patternLabel: semantic.label,
408
- category: semantic.category,
409
- field: currentPath, itemIndex,
410
- confidence: semConf,
411
- });
412
- return replacement;
413
- }
414
- }
415
- }
488
+ const semantic = redactSemantic(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
489
+ if (semantic !== null)
490
+ return semantic;
416
491
  }
417
- return redactText(value, ctx, vault, sessionId, hits, itemIndex, currentPath);
492
+ return redactText(value, ctx, vault, sessionId, hits, itemIndex, displayPath);
418
493
  }
419
- if (typeof value === 'number' && !ctx.skipSemantic) {
420
- if (!shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules)) {
494
+ if (typeof value === 'number') {
495
+ if (!processed)
421
496
  return value;
422
- }
423
- const semantic = getSemanticLabel(currentPath, siblingKeys);
424
- if (semantic && semantic.label) {
425
- const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
426
- if (semConf >= ((_b = ctx.confidenceThreshold) !== null && _b !== void 0 ? _b : 0)) {
427
- const strValue = String(value);
428
- const replacement = applyRedaction(strValue, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup);
429
- hits.push({
430
- token: replacement, original: '***',
431
- patternName: `semantic_${semantic.label}`,
432
- patternLabel: semantic.label,
433
- category: semantic.category,
434
- field: currentPath, itemIndex,
435
- confidence: semConf,
436
- });
437
- return replacement;
438
- }
439
- }
497
+ return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
440
498
  }
441
499
  if (Array.isArray(value)) {
442
- return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`));
500
+ return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`));
443
501
  }
444
502
  if (value !== null && typeof value === 'object') {
445
- const entries = Object.entries(value);
446
- const siblingKeys = entries.map(([k]) => k);
447
- const obj = {};
448
- for (const [k, v] of entries) {
449
- const fieldPath = currentPath ? `${currentPath}.${k}` : k;
450
- obj[k] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys);
451
- }
452
- return obj;
503
+ return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
453
504
  }
454
505
  return value;
455
506
  }
456
- function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
507
+ const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
508
+ function segments(text, produced) {
457
509
  var _a;
458
- let result = text;
459
- const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
460
- if (ctx.denyList && ctx.denyList.length > 0) {
461
- for (const entry of ctx.denyList) {
462
- try {
463
- let re;
464
- switch (entry.type) {
465
- case 'regex':
466
- if (!isSafeRegex(entry.value))
467
- continue;
468
- re = new RegExp(entry.value, 'gi');
469
- break;
470
- case 'contains':
471
- re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
472
- break;
473
- default:
474
- re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
475
- }
476
- result = result.replace(re, (match) => {
477
- if (TOKEN_RE.test(match))
478
- return match;
479
- const replacement = applyRedaction(match, { label: 'DENY_LIST', category: 'other' }, ctx.mode, vault, sessionId, ctx.dedup);
480
- hits.push({
481
- token: replacement, original: '***',
482
- patternName: 'denyList', patternLabel: 'DENY_LIST',
483
- category: 'other', field: fieldPath, itemIndex,
484
- confidence: context_1.DENY_LIST_CONFIDENCE,
485
- });
486
- return replacement;
487
- });
488
- }
489
- catch { }
490
- }
491
- }
492
- const enabled = (0, patterns_1.getPatternsByNames)(ctx.enabledPatterns);
493
- const activePatterns = [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)];
494
- for (const pattern of activePatterns) {
495
- const re = new RegExp(pattern.regex.source, pattern.regex.flags);
496
- result = result.replace(re, (match, ...args) => {
497
- if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
498
- return match;
499
- const validatorPassed = pattern.validate ? pattern.validate(match) : true;
500
- if (pattern.validate && !validatorPassed)
501
- return match;
502
- if (isAllowListed(match, ctx.allowList))
503
- return match;
504
- const matchOffset = typeof args[args.length - 2] === 'number' ? args[args.length - 2] : 0;
505
- const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, text, matchOffset, matchOffset + match.length);
506
- if (confidence < threshold)
507
- return match;
508
- const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup);
509
- hits.push({
510
- token: replacement, original: '***',
511
- patternName: pattern.name, patternLabel: pattern.label,
512
- category: pattern.category, field: fieldPath, itemIndex,
513
- confidence,
514
- });
515
- return replacement;
516
- });
510
+ if (produced.size === 0)
511
+ return [{ text, locked: false }];
512
+ const guard = new RegExp([...produced].sort((a, b) => b.length - a.length).map(escapeRe).join('|'), 'g');
513
+ const out = [];
514
+ let last = 0;
515
+ for (const m of text.matchAll(guard)) {
516
+ const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
517
+ if (at > last)
518
+ out.push({ text: text.slice(last, at), locked: false });
519
+ out.push({ text: m[0], locked: true });
520
+ last = at + m[0].length;
517
521
  }
518
- if (!ctx.skipSemantic) {
519
- const nameMatches = (0, names_1.detectNamesInText)(result);
520
- for (const nm of nameMatches) {
521
- if (nm.name.includes('[') && nm.name.includes(']'))
522
- continue;
523
- if (isAllowListed(nm.name, ctx.allowList))
524
- continue;
525
- let nameConfidence = 0.65;
526
- if ((0, context_1.hasContextWords)(text, nm.start, nm.end, names_1.NAME_CONTEXT_WORDS)) {
527
- nameConfidence = 0.80;
528
- }
529
- if (nameConfidence < threshold)
522
+ if (last < text.length)
523
+ out.push({ text: text.slice(last), locked: false });
524
+ return out;
525
+ }
526
+ function replaceOutside(text, produced, re, fn) {
527
+ return segments(text, produced)
528
+ .map((seg) => (seg.locked ? seg.text : seg.text.replace(new RegExp(re.source, re.flags), fn)))
529
+ .join('');
530
+ }
531
+ function occursOutside(text, produced, needle) {
532
+ return segments(text, produced).some((seg) => !seg.locked && seg.text.includes(needle));
533
+ }
534
+ function redactMatch(pass, match, pattern, detail) {
535
+ const { ctx, vault, sessionId } = pass;
536
+ const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
537
+ pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
538
+ if (replacement)
539
+ pass.produced.add(replacement);
540
+ return replacement;
541
+ }
542
+ function denyListRegex(entry) {
543
+ if (entry.type === 'regex')
544
+ return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
545
+ return new RegExp(escapeRe(entry.value), 'gi');
546
+ }
547
+ function denyListStage(pass) {
548
+ var _a;
549
+ for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
550
+ try {
551
+ const re = denyListRegex(entry);
552
+ if (!re)
530
553
  continue;
531
- const replacement = applyRedaction(nm.name, { label: 'PERSON_NAME', category: 'identity' }, ctx.mode, vault, sessionId, ctx.dedup);
532
- result = result.split(nm.name).join(replacement);
533
- hits.push({
534
- token: replacement, original: '***',
535
- patternName: 'personNameDict', patternLabel: 'PERSON_NAME',
536
- category: 'identity', field: fieldPath, itemIndex,
537
- confidence: nameConfidence,
538
- });
554
+ pass.result = replaceOutside(pass.result, pass.produced, re, (match) => TOKEN_RE.test(match)
555
+ ? match
556
+ : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
557
+ patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
558
+ }));
539
559
  }
560
+ catch { }
540
561
  }
541
- for (const cp of ctx.customPatterns) {
542
- if (!cp.label || !cp.regex)
562
+ }
563
+ function builtInMatch(pass, pattern, match, args) {
564
+ if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
565
+ return match;
566
+ const validatorPassed = pattern.validate ? pattern.validate(match) : true;
567
+ if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
568
+ return match;
569
+ const offsetArg = args[args.length - 2];
570
+ const offset = typeof offsetArg === 'number' ? offsetArg : 0;
571
+ const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
572
+ if (confidence < pass.threshold)
573
+ return match;
574
+ return redactMatch(pass, match, pattern, {
575
+ patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
576
+ });
577
+ }
578
+ function builtInStage(pass) {
579
+ const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
580
+ for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
581
+ const re = new RegExp(pattern.regex.source, pattern.regex.flags);
582
+ pass.result = replaceOutside(pass.result, pass.produced, re, (match, ...args) => builtInMatch(pass, pattern, match, args));
583
+ }
584
+ }
585
+ function nameStage(pass) {
586
+ if (pass.ctx.skipSemantic)
587
+ return;
588
+ for (const nm of (0, names_1.detectNamesInText)(pass.result)) {
589
+ if ((nm.name.includes('[') && nm.name.includes(']')) || isAllowListed(nm.name, pass.ctx.allowList))
590
+ continue;
591
+ const confidence = (0, context_1.hasContextWords)(pass.source, nm.start, nm.end, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
592
+ if (confidence < pass.threshold || !occursOutside(pass.result, pass.produced, nm.name))
543
593
  continue;
544
- if (!isSafeRegex(cp.regex))
594
+ const before = new Set(pass.produced);
595
+ const replacement = redactMatch(pass, nm.name, { label: 'PERSON_NAME', category: 'identity' }, {
596
+ patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
597
+ });
598
+ pass.result = replaceOutside(pass.result, before, new RegExp(escapeRe(nm.name), 'g'), () => replacement);
599
+ }
600
+ }
601
+ function customStage(pass) {
602
+ if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
603
+ return;
604
+ for (const cp of pass.ctx.customPatterns) {
605
+ if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
545
606
  continue;
607
+ const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
546
608
  try {
547
- const re = new RegExp(cp.regex, 'g');
548
- result = result.replace(re, (match) => {
549
- if (TOKEN_RE.test(match))
550
- return match;
551
- if (isAllowListed(match, ctx.allowList))
552
- return match;
553
- if (context_1.CUSTOM_PATTERN_CONFIDENCE < threshold)
554
- return match;
555
- const pseudoPattern = {
556
- label: cp.label.toUpperCase(),
557
- category: cp.category || 'identity',
558
- };
559
- const replacement = applyRedaction(match, pseudoPattern, ctx.mode, vault, sessionId, ctx.dedup);
560
- hits.push({
561
- token: replacement, original: '***',
562
- patternName: `custom_${cp.label}`, patternLabel: pseudoPattern.label,
563
- category: pseudoPattern.category, field: fieldPath, itemIndex,
609
+ pass.result = replaceOutside(pass.result, pass.produced, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
610
+ ? match
611
+ : redactMatch(pass, match, pattern, {
612
+ patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
564
613
  confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
565
- });
566
- return replacement;
567
- });
614
+ }));
568
615
  }
569
- catch {
616
+ catch { }
617
+ }
618
+ }
619
+ function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
620
+ var _a;
621
+ const pass = {
622
+ source: text, result: text, produced: new Set(), threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
623
+ ctx, vault, sessionId, hits, itemIndex, fieldPath,
624
+ };
625
+ denyListStage(pass);
626
+ builtInStage(pass);
627
+ nameStage(pass);
628
+ customStage(pass);
629
+ return pass.result;
630
+ }
631
+ function makeRestorer(entries) {
632
+ const tokens = Object.keys(entries).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
633
+ if (tokens.length === 0)
634
+ return (text) => text;
635
+ const re = new RegExp(tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
636
+ return (text) => text.replace(re, (token) => entries[token].original);
637
+ }
638
+ function restoreWith(value, restore, entries) {
639
+ if (typeof value === 'string') {
640
+ const exact = entries[value];
641
+ if ((exact === null || exact === void 0 ? void 0 : exact.numeric) && Number.isFinite(Number(exact.original)))
642
+ return Number(exact.original);
643
+ return restore(value);
644
+ }
645
+ if (Array.isArray(value))
646
+ return value.map((v) => restoreWith(v, restore, entries));
647
+ if (value !== null && typeof value === 'object') {
648
+ const obj = {};
649
+ for (const [k, v] of Object.entries(value)) {
650
+ obj[restore(k)] = restoreWith(v, restore, entries);
570
651
  }
652
+ return obj;
571
653
  }
572
- return result;
654
+ return value;
573
655
  }
574
656
  function restoreValue(value, vault, sessionId) {
575
657
  const session = vault.getSession(sessionId);
576
658
  if (!session)
577
659
  return value;
660
+ return restoreWith(value, makeRestorer(session.entries), session.entries);
661
+ }
662
+ function toUtf16Spans(text, entities) {
663
+ const offsets = [0];
664
+ for (const ch of text)
665
+ offsets.push(offsets[offsets.length - 1] + ch.length);
666
+ return entities
667
+ .filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
668
+ .map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
669
+ }
670
+ function resolveOverlaps(spans) {
671
+ const ranked = [...spans].sort((a, b) => b.score - a.score || (b.end - b.start) - (a.end - a.start) || a.start - b.start);
672
+ const kept = [];
673
+ for (const span of ranked) {
674
+ if (!kept.some((k) => span.start < k.end && k.start < span.end))
675
+ kept.push(span);
676
+ }
677
+ return kept.sort((a, b) => b.start - a.start);
678
+ }
679
+ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
680
+ var _a;
681
+ if (value.trim().length === 0 || TOKEN_RE.test(value))
682
+ return value;
683
+ const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
684
+ const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
685
+ const spans = resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold));
686
+ let text = value;
687
+ for (const span of spans) {
688
+ const matchedText = value.slice(span.start, span.end);
689
+ if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
690
+ continue;
691
+ if (isAllowListed(matchedText, ctx.allowList))
692
+ continue;
693
+ const label = (0, presidio_1.getRedactorLabel)(span.entityType);
694
+ const category = (0, presidio_1.getRedactorCategory)(span.entityType);
695
+ const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
696
+ text = text.slice(0, span.start) + replacement + text.slice(span.end);
697
+ hits.push({
698
+ token: replacement, original: '***',
699
+ patternName: `presidio_${span.entityType}`, patternLabel: label,
700
+ category, field: path, itemIndex, confidence: span.score,
701
+ });
702
+ }
703
+ return text;
704
+ }
705
+ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
706
+ var _a, _b;
578
707
  if (typeof value === 'string') {
579
- let result = value;
580
- for (const [token, e] of Object.entries(session.entries)) {
581
- result = result.split(token).join(e.original);
708
+ if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
709
+ return value;
710
+ try {
711
+ return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
712
+ }
713
+ catch {
714
+ return value;
582
715
  }
583
- return result;
584
716
  }
585
717
  if (Array.isArray(value)) {
586
- return value.map((v) => restoreValue(v, vault, sessionId));
718
+ const out = [];
719
+ for (let i = 0; i < value.length; i++) {
720
+ out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
721
+ }
722
+ return out;
587
723
  }
588
724
  if (value !== null && typeof value === 'object') {
589
- const obj = {};
725
+ const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
726
+ const out = {};
590
727
  for (const [k, v] of Object.entries(value)) {
591
- obj[k] = restoreValue(v, vault, sessionId);
728
+ const original = (_b = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _b !== void 0 ? _b : k;
729
+ const child = path ? `${path}.${k}` : k;
730
+ const childPolicy = policyPath ? `${policyPath}.${original}` : original;
731
+ out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
592
732
  }
593
- return obj;
733
+ return out;
594
734
  }
595
735
  return value;
596
736
  }
597
737
  async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex) {
598
- var _a;
599
738
  if (!ctx.presidioUrl)
600
739
  return data;
601
- const result = { ...data };
602
- for (const [key, value] of Object.entries(result)) {
603
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
604
- result[key] = await enhanceWithPresidio(value, ctx, vault, sessionId, hits, itemIndex);
605
- continue;
606
- }
607
- if (Array.isArray(value)) {
608
- const arr = [];
609
- for (const item of value) {
610
- if (item !== null && typeof item === 'object') {
611
- arr.push(await enhanceWithPresidio(item, ctx, vault, sessionId, hits, itemIndex));
612
- }
613
- else {
614
- arr.push(item);
615
- }
616
- }
617
- result[key] = arr;
618
- continue;
619
- }
620
- if (typeof value !== 'string')
621
- continue;
622
- if (value.trim().length === 0)
623
- continue;
624
- if (TOKEN_RE.test(value))
625
- continue;
626
- try {
627
- const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
628
- if (entities.length === 0)
629
- continue;
630
- const sorted = entities.sort((a, b) => b.start - a.start);
631
- let text = value;
632
- for (const entity of sorted) {
633
- const matchedText = text.slice(entity.start, entity.end);
634
- if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
635
- continue;
636
- if (isAllowListed(matchedText, ctx.allowList))
637
- continue;
638
- if (entity.score < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
639
- continue;
640
- const label = (0, presidio_1.getRedactorLabel)(entity.entity_type);
641
- const category = (0, presidio_1.getRedactorCategory)(entity.entity_type);
642
- const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup);
643
- text = text.slice(0, entity.start) + replacement + text.slice(entity.end);
644
- hits.push({
645
- token: replacement,
646
- original: '***',
647
- patternName: `presidio_${entity.entity_type}`,
648
- patternLabel: label,
649
- category,
650
- field: key,
651
- itemIndex,
652
- confidence: entity.score,
653
- });
654
- }
655
- result[key] = text;
656
- }
657
- catch {
658
- continue;
659
- }
660
- }
661
- return result;
740
+ return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, '', ''));
662
741
  }
663
742
  function buildReport(sessionId, hits) {
664
743
  const hitsByCategory = {};