@privacyscrubber/mcp-server 2.2.0 → 2.2.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/README.md CHANGED
@@ -14,6 +14,8 @@
14
14
  [![There's An AI For That](https://img.shields.io/badge/There's_An_AI_For_That-Live-06b6d4.svg)](https://theresanaiforthat.com/ai/privacy-scrubber/)
15
15
  [![Security: 100% Local](https://img.shields.io/badge/Security-100%25%20Local-emerald)](https://privacyscrubber.com)
16
16
  [![Parity: 100% Core Match](https://img.shields.io/badge/Parity-100%25%20Core%20Match-blueviolet)](https://privacyscrubber.com)
17
+ [![Patent Pending](https://img.shields.io/badge/Patent-Pending%20(ILPO%20331905)-06b6d4.svg)](https://privacyscrubber.com/patents/)
18
+ [![GitHub Stars](https://img.shields.io/github/stars/moxno/privacyscrubber-mcp?style=social)](https://github.com/moxno/privacyscrubber-mcp)
17
19
 
18
20
  **CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop.**
19
21
  Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers to prevent API leaks and ensure HIPAA/SOC 2 compliance at the developer endpoint.
@@ -374,7 +376,16 @@ After adding the key, ask your AI agent to call `check_status`:
374
376
  Use the check_status tool from PrivacyScrubber MCP
375
377
  ```
376
378
 
377
- The dashboard should show **Tier: PRO** and all profiles unlocked.
379
+ ---
380
+
381
+ ## 🔗 Ecosystem & Production Architecture Guides
382
+
383
+ - 🌐 **Web App**: [https://privacyscrubber.com](https://privacyscrubber.com)
384
+ - 📦 **Node.js / TypeScript SDK**: [@privacyscrubber/sdk](https://www.npmjs.com/package/@privacyscrubber/sdk)
385
+ - 🧩 **Chrome Extension**: [Chrome Web Store](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-zero-tr/pimoejgefeilajmmbpghifdmhdlkgjol)
386
+ - 🛡️ **RAG & Vector Databases**: [Sanitizing PII Before Vector DB Ingestion (Pinecone, Chroma, Qdrant)](https://privacyscrubber.com/solutions/dev/rag-vector-database-pii-masking/)
387
+ - 🤖 **LangChain & LlamaIndex**: [In-Memory PII Middleware for AI Agent Pipelines](https://privacyscrubber.com/solutions/agents/langchain-llamaindex-pii-anonymizer/)
388
+ - ⚡ **AWS Comprehend Alternative**: [In-Memory Redaction Without Egress or Cloud Overhead](https://privacyscrubber.com/solutions/dev/aws-comprehend-pii-alternative/)
378
389
 
379
390
  ---
380
391
 
@@ -408,4 +419,16 @@ PrivacyScrubber and the Zero-Trust Data Sanitization (ZTDS) protocol are backed
408
419
 
409
420
  MIT © [Ilya Sibiryakov](https://privacyscrubber.com) (BrandMeWeb)
410
421
 
422
+ ---
423
+
424
+ ## ⚖️ Intellectual Property & Virtual Patent Marking
425
+
426
+ The Zero-Trust Data Sanitization (ZTDS) architecture, in-memory deterministic tokenization, cryptographic session handoff, and stdio execution methods implemented in this package are proprietary technology of Ilya Sibiryakov (BrandMeWeb) and are protected under **Patent Pending** status:
427
+
428
+ - **Patent Office:** State of Israel Ministry of Justice, Patent Office (ILPO)
429
+ - **Application Number:** `331905` (Tracking ID: `94221`)
430
+ - **Filing / Priority Date:** September 14, 2026 (Paris Convention Art. 4 & 35 U.S.C. § 119 Priority)
431
+ - **Official Title:** *SYSTEM AND METHOD FOR CLIENT-SIDE ZERO-TRUST DATA SANITIZATION AND CRYPTOGRAPHIC SESSION HANDOFF IN ARTIFICIAL INTELLIGENCE WORKFLOWS*
432
+ - **Virtual Patent Marking:** [privacyscrubber.com/patents/](https://privacyscrubber.com/patents/) in accordance with 35 U.S.C. § 287(a).
433
+
411
434
 
package/index.js CHANGED
@@ -38,7 +38,14 @@ const MCP_VERSION = require('./package.json').version;
38
38
  // Import the production core engine with 100% parity
39
39
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
40
40
  const PrivacyScrubberCore = require(scrubberCorePath);
41
- let LicenseManager = require('./ps-license-manager.js');
41
+ let LicenseManager;
42
+ try {
43
+ LicenseManager = require('./ps-license-manager.cjs');
44
+ } catch (e) {
45
+ try {
46
+ LicenseManager = require('./ps-license-manager.js');
47
+ } catch (err) {}
48
+ }
42
49
  if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
43
50
  LicenseManager = global.LicenseManager;
44
51
  }
@@ -62,15 +69,8 @@ const colors = {
62
69
  reset: '\x1b[0m'
63
70
  };
64
71
 
65
- // Helper to log to both standard terminal (with colors) and MCP logging protocol
72
+ // Helper to log to stderr (preserves stdout strictly for JSON-RPC MCP protocol messages)
66
73
  function mcpLog(msg) {
67
- const clean = msg.replace(/\x1b\[[0-9;]*m/g, '').trim();
68
- const level = clean.includes('⚠️') || clean.includes('Error') ? "warning" : "info";
69
- try {
70
- server.sendLoggingMessage({ level, data: clean });
71
- } catch(e) {
72
- // Ignore if server isn't fully connected yet
73
- }
74
74
  process.stderr.write(msg);
75
75
  }
76
76
 
@@ -222,9 +222,20 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
222
222
  };
223
223
  }
224
224
 
225
- function formatAuditReceipt(telemetry) {
225
+ function formatAuditReceipt(telemetry, compact = false) {
226
+ const isCompact = compact === true || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === '1' || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === 'true';
227
+ if (isCompact) {
228
+ if (telemetry.totalCount === 0) {
229
+ return "\n\n> [ZTDS: CLEAN (ZERO PII DETECTED)]\n";
230
+ }
231
+ const entitiesList = Object.entries(telemetry.entities)
232
+ .map(([type, count]) => `${count} ${type}`)
233
+ .join(', ');
234
+ return `\n\n> [ZTDS: ${telemetry.totalCount} masked (${entitiesList}) | Risk: ${telemetry.riskLevel} | Compliance: ${telemetry.frameworksList.join(', ')}]\n`;
235
+ }
236
+
226
237
  if (telemetry.totalCount === 0) {
227
- return "\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\\n";
238
+ return "\n\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
228
239
  }
229
240
 
230
241
  const entitiesList = Object.entries(telemetry.entities)
@@ -233,7 +244,7 @@ function formatAuditReceipt(telemetry) {
233
244
 
234
245
  const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
235
246
 
236
- return `\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**\\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\\n`;
247
+ return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
237
248
  }
238
249
 
239
250
  // Create the MCP server
@@ -270,6 +281,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
270
281
  type: "string",
271
282
  description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
272
283
  },
284
+ compact: {
285
+ type: "boolean",
286
+ description: "Optional. When true, returns a compact 1-line audit summary saving token overhead in AI IDEs (Cursor, Claude Desktop)."
287
+ },
273
288
  ignore_list: {
274
289
  type: "array",
275
290
  items: { type: "string" },
@@ -306,6 +321,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
306
321
  profile: {
307
322
  type: "string",
308
323
  description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
324
+ },
325
+ compact: {
326
+ type: "boolean",
327
+ description: "Optional. When true, returns a compact 1-line audit summary saving token overhead in AI IDEs (Cursor, Claude Desktop)."
309
328
  }
310
329
  },
311
330
  required: ["file_path"]
@@ -673,7 +692,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
673
692
 
674
693
  try {
675
694
  if (name === "sanitize_text") {
676
- const { text, profile = "General", ignore_list } = args || {};
695
+ const { text, profile = "General", ignore_list, compact = false } = args || {};
677
696
 
678
697
  // Merge per-call ignore_list into persistent sessionIgnoreList
679
698
  if (Array.isArray(ignore_list)) {
@@ -697,6 +716,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
697
716
  const isAdvanced = targetProfile.toLowerCase() !== "general";
698
717
  const license = checkLicenseStatus();
699
718
  let finalProfile = targetProfile;
719
+ let isAutoElevated = false;
720
+
721
+ // Heuristic auto-detection: if profile is General and text contains obvious DevOps credentials or code syntax, auto-activate Dev
722
+ const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
723
+
724
+ if (!isAdvanced && DEV_HEURISTIC_REGEX.test(text)) {
725
+ finalProfile = "Dev";
726
+ isAutoElevated = true;
727
+ mcpLog(`${colors.cyan}🔍 [PrivacyScrubber] Auto-detected code/DevOps secrets. Activated 'Dev' profile rules.${colors.reset}\n`);
728
+ }
700
729
 
701
730
  const limitStatus = checkFreeTierLimit(license.isPro);
702
731
  if (limitStatus.blocked) {
@@ -706,7 +735,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
706
735
  };
707
736
  }
708
737
 
709
- const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
738
+ const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
710
739
  if (isAdvanced && !license.isPro) {
711
740
  mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n`);
712
741
  }
@@ -716,7 +745,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
716
745
  const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList);
717
746
 
718
747
  const telemetry = buildCisoAuditTelemetry(newTokens);
719
- const receiptMd = formatAuditReceipt(telemetry);
748
+ const receiptMd = formatAuditReceipt(telemetry, compact);
720
749
 
721
750
  return {
722
751
  content: [
@@ -810,7 +839,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
810
839
  ]
811
840
  };
812
841
  }
813
- const { profile = "General" } = args;
842
+ const { profile = "General", compact = false } = args;
814
843
  const filePath = rawPath;
815
844
 
816
845
  const resolvedPath = path.resolve(filePath);
@@ -881,7 +910,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
881
910
  const sanitized = performSanitization(processedContent, finalProfile, sessionIgnoreList);
882
911
  const { scrubbedText, newTokens } = sanitized;
883
912
  const telemetry = buildCisoAuditTelemetry(newTokens);
884
- const receiptMarkdown = formatAuditReceipt(telemetry);
913
+ const receiptMarkdown = formatAuditReceipt(telemetry, compact);
885
914
  const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
886
915
 
887
916
  return {
@@ -927,7 +956,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
927
956
  const sanitized = performSanitization(content, finalProfile, sessionIgnoreList);
928
957
  const { scrubbedText, newTokens } = sanitized;
929
958
  const telemetry = buildCisoAuditTelemetry(newTokens);
930
- const receiptMarkdown = formatAuditReceipt(telemetry);
959
+ const receiptMarkdown = formatAuditReceipt(telemetry, compact);
931
960
  const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
932
961
 
933
962
  return {
@@ -960,7 +989,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
960
989
  const sanitized = performSanitization(content, finalProfile, sessionIgnoreList);
961
990
  const { scrubbedText, newTokens } = sanitized;
962
991
  const telemetry = buildCisoAuditTelemetry(newTokens);
963
- const receiptMarkdown = formatAuditReceipt(telemetry);
992
+ const receiptMarkdown = formatAuditReceipt(telemetry, compact);
964
993
  const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
965
994
 
966
995
  return {
@@ -1004,6 +1033,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1004
1033
  const isAdvanced = targetProfile.toLowerCase() !== "general";
1005
1034
  const license = checkLicenseStatus();
1006
1035
  let finalProfile = targetProfile;
1036
+ let isAutoElevated = false;
1037
+
1038
+ const ext = path.extname(resolvedPath).toLowerCase();
1039
+ const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.sh', '.bash', '.zsh', '.env', '.yaml', '.yml', '.toml', '.sql']);
1040
+ const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
1041
+
1042
+ if (!isAdvanced && (CODE_EXTENSIONS.has(ext) || DEV_HEURISTIC_REGEX.test(content))) {
1043
+ finalProfile = "Dev";
1044
+ isAutoElevated = true;
1045
+ mcpLog(`${colors.cyan}🔍 [PrivacyScrubber] Auto-detected code/DevOps file '${path.basename(resolvedPath)}'. Activated 'Dev' profile rules.${colors.reset}\n`);
1046
+ }
1007
1047
 
1008
1048
  const limitStatus = checkFreeTierLimit(license.isPro);
1009
1049
  if (limitStatus.blocked) {
@@ -1013,7 +1053,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1013
1053
  };
1014
1054
  }
1015
1055
 
1016
- const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
1056
+ const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
1017
1057
  if (isAdvanced && !license.isPro) {
1018
1058
  mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n`);
1019
1059
  }
@@ -1022,7 +1062,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1022
1062
 
1023
1063
  const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile, sessionIgnoreList);
1024
1064
  const telemetry = buildCisoAuditTelemetry(newTokens);
1025
- const receiptMd = formatAuditReceipt(telemetry);
1065
+ const receiptMd = formatAuditReceipt(telemetry, compact);
1026
1066
 
1027
1067
  return {
1028
1068
  content: [
@@ -1837,6 +1877,7 @@ server.connect(transport).then(() => {
1837
1877
  mcpLog(`${colors.greenBold}✅ PrivacyScrubber ZTDS MCP Server v${MCP_VERSION} started successfully.${colors.reset}\n`);
1838
1878
  mcpLog(`${colors.cyan}📦 Need programmatic in-code redaction? Try: npm install @privacyscrubber/sdk${colors.reset}\n`);
1839
1879
  mcpLog(`${colors.yellowBold}⭐ Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
1880
+ mcpLog(`${colors.cyan}👉 Developer SDK & Enterprise: https://privacyscrubber.com/pricing${colors.reset}\n`);
1840
1881
  }).catch((error) => {
1841
1882
  console.error("Failed to connect MCP server transport:", error);
1842
1883
  process.exit(1);
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "privacyscrubber/pii-masking-mcp",
4
- "version": "2.2.0",
4
+ "version": "2.2.1",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "author": {
7
7
  "name": "PrivacyScrubber",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "main": "index.js",
@@ -14,6 +14,7 @@
14
14
  "pii-masking-run.js",
15
15
  "scrubber-core.cjs",
16
16
  "ps-license-manager.js",
17
+ "ps-license-manager.cjs",
17
18
  "ps-pii-engine.cjs",
18
19
  "ps-pii-engine.js",
19
20
  "config-schema.json",
@@ -32,7 +33,9 @@
32
33
  "privacyscrubber-mcp-install": "install.js"
33
34
  },
34
35
  "scripts": {
35
- "start": "node index.js"
36
+ "start": "node index.js",
37
+ "build": "echo 'No build step required for vanilla JS'",
38
+ "test": "node test-mcp.js && node test-mcp-deep.js"
36
39
  },
37
40
  "dependencies": {
38
41
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -84,7 +84,14 @@ const DEVOPS_SECRETS_DETECTOR = [
84
84
  { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi }
85
85
  ];
86
86
 
87
- let LicenseManager = require('./ps-license-manager.js');
87
+ let LicenseManager;
88
+ try {
89
+ LicenseManager = require('./ps-license-manager.cjs');
90
+ } catch (e) {
91
+ try {
92
+ LicenseManager = require('./ps-license-manager.js');
93
+ } catch (err) {}
94
+ }
88
95
  if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
89
96
  LicenseManager = global.LicenseManager;
90
97
  }
@@ -0,0 +1,257 @@
1
+ (function(global) {
2
+ // ── START LICENSE CRYPTO ──
3
+ const PUBLIC_KEY_N = BigInt("21317979104050344678233452423803986868842680501941538057759536089609908024281729760556922188810565541201321990374637212776148857190553192389608364990754937991254608109727112734447704854882786946805547031142530865504430968628760311494129154950865158735816268612258108296781892583171421605668998979064782378729322077179022581884976242706169323540456286526420742434868559866211716347133681429395477256705746276962607312574959642065929462114149296066812527505837608212730874704915352119747022918247963886630350365119206627670591400728523813746184290857872958847087691236909900501703203494160206941648263514117706558791691");
4
+ const PUBLIC_KEY_E = 65537n;
5
+
6
+ function modPow(base, exp, mod) {
7
+ let res = 1n;
8
+ base = base % mod;
9
+ while (exp > 0n) {
10
+ if (exp % 2n === 1n) res = (res * base) % mod;
11
+ exp = exp / 2n;
12
+ base = (base * base) % mod;
13
+ }
14
+ return res;
15
+ }
16
+
17
+ function bigIntToString(bi) {
18
+ let hex = bi.toString(16);
19
+ if (hex.length % 2 !== 0) hex = '0' + hex;
20
+ let str = '';
21
+ for (let i = 0; i < hex.length; i += 2) {
22
+ str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
23
+ }
24
+ return str;
25
+ }
26
+
27
+ function stringToBigInt(str) {
28
+ let hex = '';
29
+ for (let i = 0; i < str.length; i++) {
30
+ hex += str.charCodeAt(i).toString(16).padStart(2, '0');
31
+ }
32
+ return BigInt('0x' + hex);
33
+ }
34
+
35
+ function sha256(ascii) {
36
+ function rightRotate(value, amount) { return (value>>>amount) | (value<<(32 - amount)); }
37
+ var mathPow = Math.pow; var maxWord = mathPow(2, 32);
38
+ var lengthProperty = 'length'; var i, j;
39
+ var result = ''; var words = []; var asciiBitLength = ascii[lengthProperty]*8;
40
+ var hash = sha256.h = sha256.h || [];
41
+ var k = sha256.k = sha256.k || [];
42
+ var primeCounter = k[lengthProperty];
43
+ var isComposite = {};
44
+ for (var candidate = 2; primeCounter < 64; candidate++) {
45
+ if (!isComposite[candidate]) {
46
+ for (i = 0; i < 313; i += candidate) { isComposite[i] = candidate; }
47
+ hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
48
+ k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
49
+ }
50
+ }
51
+ ascii += '\x80';
52
+ while (ascii[lengthProperty]%64 - 56) ascii += '\x00';
53
+ for (i = 0; i < ascii[lengthProperty]; i++) {
54
+ j = ascii.charCodeAt(i);
55
+ if (j>>8) return;
56
+ words[i>>2] |= j << ((3 - i)%4)*8;
57
+ }
58
+ words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
59
+ words[words[lengthProperty]] = (asciiBitLength)
60
+ for (j = 0; j < words[lengthProperty];) {
61
+ var w = words.slice(j, j += 16);
62
+ var oldHash = hash;
63
+ hash = hash.slice(0, 8);
64
+ for (i = 0; i < 64; i++) {
65
+ var w15 = w[i - 15], w2 = w[i - 2];
66
+ var a = hash[0], e = hash[4];
67
+ var temp1 = hash[7] + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) + ((e&hash[5])^((~e)&hash[6])) + k[i] + (w[i] = (i < 16) ? w[i] : (w[i - 16] + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) + w[i - 7] + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)))|0);
68
+ var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2]));
69
+ hash = [(temp1 + temp2)|0].concat(hash);
70
+ hash[4] = (hash[4] + temp1)|0;
71
+ }
72
+ for (i = 0; i < 8; i++) { hash[i] = (hash[i] + oldHash[i])|0; }
73
+ }
74
+ for (i = 0; i < 8; i++) {
75
+ for (j = 3; j + 1; j--) {
76
+ var b = (hash[i]>>(j*8))&255;
77
+ result += ((b < 16) ? 0 : '') + b.toString(16);
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ // ── END LICENSE CRYPTO ──
83
+
84
+ class LicenseManager {
85
+ /**
86
+ * Validates a license key universally across all environments.
87
+ * @param {string} key - The license key.
88
+ * @param {object} options - Options mapping to environment specifics.
89
+ * @returns {object} { valid: boolean, tier: string|null, reason?: string, expires?: number }
90
+ */
91
+ static validate(key, options = {}) {
92
+ if (!key) return { valid: false, tier: null, reason: "No key provided" };
93
+ const rawKey = key.trim();
94
+ const k = rawKey.toUpperCase();
95
+ const parts = k.split('-');
96
+
97
+ const now = options.mockTime !== undefined && options.mockTime !== null ? options.mockTime : Date.now();
98
+ let lastSeen = 0;
99
+
100
+ if (options.storage) {
101
+ lastSeen = parseInt(options.storage.getItem('ps_last_seen_time') || '0', 10);
102
+ if (options.mockTime === undefined || options.mockTime === null) {
103
+ options.storage.setItem('ps_last_seen_time', now.toString());
104
+ }
105
+ }
106
+
107
+ // Clock rollback guard
108
+ if ((options.mockTime === undefined || options.mockTime === null) && now < lastSeen) {
109
+ if (options.onAnomaly) options.onAnomaly();
110
+ return { valid: false, tier: null, reason: "Clock rollback anomaly detected" };
111
+ }
112
+
113
+ const _atob = typeof atob !== 'undefined' ? atob : (str) => Buffer.from(str, 'base64').toString('binary');
114
+
115
+ // 1. Process Pilot Keys (RSA-1024 Encrypted Timestamps)
116
+ if (k.startsWith('PS-PILOT-')) {
117
+ try {
118
+ const payload = rawKey.substring(9);
119
+ const binary = _atob(payload);
120
+ const bytes = new Uint8Array(binary.length);
121
+ for (let i = 0; i < binary.length; i++) {
122
+ bytes[i] = binary.charCodeAt(i);
123
+ }
124
+ let hex = '';
125
+ for (let i = 0; i < bytes.length; i++) {
126
+ hex += bytes[i].toString(16).padStart(2, '0');
127
+ }
128
+ const S = BigInt('0x' + hex);
129
+
130
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
131
+ const decryptedStr = bigIntToString(M);
132
+
133
+ if (decryptedStr.startsWith('ZTDS-PILOT-')) {
134
+ const expDate = parseInt(decryptedStr.substring(11), 10);
135
+ if (!isNaN(expDate)) {
136
+ if (now < expDate) return { valid: true, tier: 'TEAMS', expires: expDate };
137
+
138
+ if (options.onExpired) options.onExpired('Pilot');
139
+ return { valid: false, tier: null, reason: "Pilot expired" };
140
+ }
141
+ }
142
+ } catch(e) {
143
+ return { valid: false, tier: null, reason: "Pilot decryption failed" };
144
+ }
145
+ }
146
+
147
+ // 1.5. Process RSA Signed Keys (PS-RSA-...)
148
+ if (k.startsWith('PS-RSA-')) {
149
+ try {
150
+ const payload = rawKey.substring(7);
151
+ const binary = _atob(payload);
152
+ const bytes = new Uint8Array(binary.length);
153
+ for (let i = 0; i < binary.length; i++) {
154
+ bytes[i] = binary.charCodeAt(i);
155
+ }
156
+ let hex = '';
157
+ for (let i = 0; i < bytes.length; i++) {
158
+ hex += bytes[i].toString(16).padStart(2, '0');
159
+ }
160
+ const S = BigInt('0x' + hex);
161
+
162
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
163
+ const decryptedStr = bigIntToString(M);
164
+
165
+ if (decryptedStr.startsWith('ZTDS-LIC-')) {
166
+ const pieces = decryptedStr.split('-');
167
+ if (pieces.length >= 5) {
168
+ const tier = pieces[2];
169
+ const expires = parseInt(pieces[4], 10);
170
+ if (!isNaN(expires)) {
171
+ if (now < expires) return { valid: true, tier: tier, expires: expires };
172
+
173
+ if (options.onExpired) options.onExpired('RSA');
174
+ return { valid: false, tier: null, reason: "RSA key expired" };
175
+ }
176
+ }
177
+ }
178
+ } catch(e) {
179
+ return { valid: false, tier: null, reason: "RSA decryption failed" };
180
+ }
181
+ }
182
+
183
+ // 2. Process Modern Prefixed Keys (v1.4.2+ Salted Checksum)
184
+ if (parts.length === 4 && parts[0] === 'PS' && ['PRO', 'TEAMS', 'SDK', 'OEM', 'ENTERPRISE'].includes(parts[1])) {
185
+ const tier = parts[1];
186
+ const core = parts[2];
187
+ const checksum = parts[3];
188
+ const salt = "ZTDS_SALT_2026_!@#";
189
+ const input = core + salt + tier;
190
+ let sum = 0;
191
+ for (let i = 0; i < input.length; i++) {
192
+ sum += input.charCodeAt(i) * (i + 1);
193
+ }
194
+ const expectedChecksum = (sum % 9999).toString().padStart(4, '0');
195
+ if (checksum === expectedChecksum) {
196
+ return { valid: true, tier: tier };
197
+ }
198
+ }
199
+
200
+ // 3. Process Standard RS256 JWTs (eyJ...)
201
+ if (rawKey.startsWith('eyJ') && rawKey.split('.').length === 3) {
202
+ try {
203
+ const [b64Header, b64Payload, b64Signature] = rawKey.split('.');
204
+
205
+ let sigStr = b64Signature.replace(/-/g, '+').replace(/_/g, '/');
206
+ while (sigStr.length % 4) sigStr += '=';
207
+ const binarySig = _atob(sigStr);
208
+ let hexSig = '';
209
+ for (let i = 0; i < binarySig.length; i++) {
210
+ hexSig += binarySig.charCodeAt(i).toString(16).padStart(2, '0');
211
+ }
212
+
213
+ const S = BigInt('0x' + hexSig);
214
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
215
+
216
+ let mHex = M.toString(16);
217
+ if (mHex.length % 2 !== 0) mHex = '0' + mHex;
218
+
219
+ const expectedHash = sha256(b64Header + '.' + b64Payload);
220
+
221
+ if (mHex.endsWith(expectedHash)) {
222
+ const payloadJson = _atob(b64Payload.replace(/-/g, '+').replace(/_/g, '/'));
223
+ const payload = JSON.parse(payloadJson);
224
+
225
+ if (payload.exp && (now / 1000) < payload.exp) {
226
+ return { valid: true, tier: payload.tier || 'PRO', expires: payload.exp * 1000 };
227
+ } else {
228
+ if (options.onExpired) options.onExpired('JWT');
229
+ return { valid: false, tier: null, reason: "JWT expired" };
230
+ }
231
+ }
232
+ } catch(e) {
233
+ return { valid: false, tier: null, reason: "JWT validation failed" };
234
+ }
235
+ }
236
+
237
+ return { valid: false, tier: null, reason: "Invalid format" };
238
+ }
239
+ }
240
+
241
+ // Export module for ES modules, CommonJS, and Browser Global
242
+ if (typeof globalThis !== 'undefined') {
243
+ globalThis.LicenseManager = LicenseManager;
244
+ }
245
+ if (typeof global !== 'undefined') {
246
+ global.LicenseManager = LicenseManager;
247
+ }
248
+ if (typeof window !== 'undefined') {
249
+ window.LicenseManager = LicenseManager;
250
+ }
251
+ if (typeof exports !== 'undefined') {
252
+ if (typeof module !== 'undefined' && module.exports) {
253
+ module.exports = LicenseManager;
254
+ }
255
+ exports.LicenseManager = LicenseManager;
256
+ }
257
+ })(typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this));
package/ps-pii-engine.cjs CHANGED
@@ -47,9 +47,9 @@ let REGEX_RULES = [
47
47
 
48
48
  // Professional IDs & Organizations
49
49
  { type: 'NAME', isContextName: true, isCorporateName: true, regex: /\b(?:[A-Z][A-Za-z0-9&.,'-]*[ \t\xA0]+){1,5}(?:Inc\.?|LLC|Corp\.?|Corporation|Ltd\.?|Limited|Co\.?|Company|Group|Holdings|Solutions|Services|Technologies|Logistics|Industries|Capital|Bank|Partners|LLP|PLLC)(?:\s+(?:LLC|Inc\.?|Corp\.?|Ltd\.?|USA|Group))?\b/g },
50
- { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*[A-Z0-9-]{3,15}\b/gi },
50
+ { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*([A-Z0-9-]{3,15})\b/gi },
51
51
  { type: 'ID', regex: /\b(?:Pay\s+Group[:#\s]+[A-Za-z0-9_-]{2,30}|(?:Cost\s+Center|Dept|Department)[:#\s]+[A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)\b/gi },
52
- { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#)?|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
52
+ { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#|number|no\.?|num)|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
53
53
  { type: 'ID', regex: /\bEEID[ -]?\d{4,}\b/gi },
54
54
  { type: 'ID', regex: /\bEMP[-_]\d{3,}\b/gi },
55
55
 
@@ -92,7 +92,7 @@ let REGEX_RULES = [
92
92
  { type: 'ID', regex: /\b\d{2}\/\d{2}\/\d{4}\b/g },
93
93
 
94
94
  // Phone Numbers
95
- { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
95
+ { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?:\s*(?:ext\.?|x)\s*\d{1,5})?\b/gi },
96
96
  { type: 'PHONE', regex: /\+\d{1,3}[ \t.-]+(?:\(?\d{1,4}\)?[ \t.-]+)?\d{2,8}(?:[ \t.-]+\d{2,8}){0,4}\b/g },
97
97
  { type: 'PHONE', regex: /\+?[1-9]\d{1,3}[\s.-]\(?\d{1,4}\)?[\s.-]\d{2,4}[\s.-]\d{4}/g },
98
98
  { type: 'PHONE', regex: /(?:\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}\b/g },
@@ -109,16 +109,16 @@ let REGEX_RULES = [
109
109
  // Generalized Name Detection (First [Middle] Last) — max 1 middle word to avoid grabbing job titles
110
110
  // Middle word must be: a particle (van/de/etc.), a single initial (A. or A), or a capitalized word of ≥2 lowercase letters
111
111
  { type: 'NAME', isAggressiveName: true, regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*|\p{Lu}\.?|van|von|de|di|da|la|le|del|du|der|van[ \t\xA0]+de|van[ \t\xA0]+der)){0,1}[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
112
- // Payroll Format Names (e.g. "BARKER, KELLY", "BARKER, KELLY M", "DOE, JOHN M.")
113
- { type: 'NAME', regex: /\b[A-Z]{2,25},\s+[A-Z]{2,25}(?:\s+[A-Z]\.?|\s+[A-Z]{2,25})*\b/g },
112
+ // Payroll & Inverted Format Names (e.g. "BARKER, KELLY", "Vance-Soto, Eleanor", "Kaufman-Alvarez, Liam T.")
113
+ { type: 'NAME', regex: /\b[A-Z][a-zA-Z'’-]+(?:-[A-Za-z'’-]+)?,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?(?!\w)|\s+[A-Z][a-zA-Z'’-]+)*\b/g },
114
114
  // All-Caps Names (2–3 words, supporting single-letter middle initial e.g. "KELLY M BARKER", "JOHN M BARKER")
115
115
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]+(?:\p{Lu}\.?|[A-Z][a-z]+))?[ \t\xA0]+\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
116
116
  // ALL-CAPS first + middle initial(s) with optional spaces + ALL-CAPS last name
117
117
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
118
118
  // ALL-CAPS first name + optional middle initial(s) with optional spaces + Mixed-Case last name
119
119
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*|[ \t\xA0]+)(?:\p{Lu}\p{Ll}[\p{Ll}'-]*|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
- // Names with Honorifics (with or without period, supporting single or multi-word full names) (Unicode-safe)
121
- { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Hon\.)[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*))?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
+ // Names with Honorifics (with or without period, supporting middle initials and hyphenated surnames) (Unicode-safe)
121
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Rev|Judge|Atty|Doctor|Professor)(?:\.|\b)[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+)(?:[ \t\xA0]+(?:\p{Lu}\.?|\p{Lu}[\p{L}'-]+)){0,2}(?:[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+))(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
122
  // Names with Quoted or Parenthetical Nicknames (e.g. Judith "Judy" Diaz, Robert (Bob) Smith)
123
123
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]+)[ \t\xA0]+(?:"[\p{Lu}\p{Ll}'-]+"|'[\p{Lu}\p{Ll}'-]+'|[“‘][\p{Lu}\p{Ll}'-]+[”’]|\([\p{Lu}\p{Ll}'-]+\))[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]+)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
124
124
 
@@ -143,17 +143,29 @@ let REGEX_RULES = [
143
143
  let PROFILE_RULES = {
144
144
  general: [],
145
145
  legal: [
146
+ // Legal Party & Role-Prefixed Names (Claimant, Respondent, Plaintiff, Defendant, Petitioner, Appellant, Principal, Grantor, Attorney-in-Fact, Counsel)
147
+ { type: 'NAME', isContextName: true, regex: /\b(?:Registered\s+Representative|Claimant|Respondent|Plaintiff|Defendant|Petitioner|Appellant|Appellee|Principal|Grantor|Grantee|Trustee|Trustor|Fiduciary|Attorney-in-Fact|Designated\s+Attorney-in-Fact|Client\s*(?:\/\s*Principal)?|Lead\s+Counsel|Counsel|Managing\s+Partner|Escrow\s+(?:Trust\s+)?Officer)\s*[:#,]?\s*(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
146
148
  { type: 'LEGAL', regex: /\bCASE[-_][A-Z0-9_-]{4,}\b/gi },
147
149
  { type: 'LEGAL', regex: /\bMATTER[-_][A-Z0-9_-]{4,}\b/gi },
148
150
  { type: 'LEGAL', regex: /\b[A-Z]{2,4}[- ]?\d{2}[- ]?\d{4,}\b/g },
149
- { type: 'PRIVILEGE', regex: /ATTORNEY[- ]CLIENT[- ]PRIVILEGE/gi },
151
+ { type: 'PRIVILEGE', regex: /\bATTORNEY[- ]CLIENT[- ]PRIVILEGE[DS]?\b/gi },
150
152
  { type: 'LEGAL', regex: /\b(?:BATES|PROD|PLTF|DEFT|CONF|EXHIBIT)[-_ ]?[A-Z0-9]{2,10}[-_ ]?\d{4,10}\b/gi },
151
- { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR)[-_ ][A-Z0-9_-]{3,15}\b/gi },
152
- { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*\d{4,8}\b/gi },
153
+ // Case / Proceeding / Arbitration Numbers (including OCR homoglyphs O/0, I/1, l/1)
154
+ { type: 'LEGAL', regex: /\b(?:Case|Matter|Arbitration|Docket|Proceeding)\s*(?:(?:No\.?|Number|#)[:\s#]*|[:#]\s*)([A-Za-z0-9_-]{4,25})\b/gi },
155
+ { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR|AR)[-_ ][A-Za-z0-9_-]{3,15}\b/gi },
156
+ // Bar registration & CRD / Broker / Representative IDs (including OCR homoglyphs)
157
+ { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*([0-9OIloA-Za-z]{4,10})\b/gi },
158
+ { type: 'ID', regex: /\b(?:CRD|CIK|Broker\s*ID|Rep(?:resentative)?\s*ID|Advisor\s*ID|Registration\s*(?:No\.?|#)?)[:\s#]*([0-9OIloA-Za-z]{5,12})\b/gi },
153
159
  { type: 'LEGAL', regex: /\b(?:RETAINER|ENGAGEMENT|REPRESENTATION)[-_: ]*(?:AGREEMENT|ID|REF)?[:\s#-]*[A-Z0-9_-]{4,20}\b/gi },
154
160
  { type: 'LEGAL', regex: /\bECLI:[A-Z]{2}:[A-Za-z0-9]{1,7}:\d{4}:[A-Za-z0-9.]{1,25}\b/g },
155
161
  { type: 'ID', regex: /\bPCT\/[A-Z]{2}\d{4}\/\d{6}\b/g },
156
- { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g }
162
+ { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g },
163
+ // Escrow / Trust / Settlement / Disputed Account Numbers & ABA Routing
164
+ { type: 'FINANCIAL', regex: /\b(?:Escrow\s+(?:Account|Acct|Deposit)|Trust\s+Account|Settlement\s+Account|Disputed\s+Account)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9-]{5,25})\b/gi },
165
+ { type: 'FINANCIAL', regex: /\b(?:ABA\s+Routing|Routing\s+Transit|Fedwire\s+Routing)\s*(?:Number|Num|No\.?|#)?[:\s#]+(\d{9})\b/gi },
166
+ // Legal Financial Amounts (lookbehind + standalone)
167
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Settlement(?:\s+Amount)?|Retainer(?:\s+Fee)?|Hourly\s+Rate|Damages(?:\s+Claimed)?|Legal\s+Fees?|Sanctions?|Judgement|Judgment|Disgorgement|Penalty|(?:Prejudgment\s+)?Interest|Escrow(?:\s+Funds?|\s+Amount)?|Fine|Restitution)(?:[\s:]+(?:of)?[\s:]*|[:\s]+))(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
168
+ { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹]))/gi }
157
169
  ],
158
170
  hr: [
159
171
  { type: 'NAME', isContextName: true, regex: /(?:Candidate|Applicant|Employee|Reporting To|Manager|Mentored by|Direct Report)[\s:]+([A-Z][a-z]*(?:\s+[A-Z][a-z]*)?)/g },
@@ -188,18 +200,25 @@ let PROFILE_RULES = {
188
200
  { type: 'ID', regex: /\b(?:CRD|CIK|Advisor\s*ID|Broker\s*ID)[:\s#]*(\d{5,10})\b/gi },
189
201
  { type: 'ID', regex: /\b(?:LEI|LEGAL[\s-]ENTITY[\s-]IDENTIFIER)[:\s#-]*[0-9A-Z]{20}\b/gi },
190
202
  { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g },
191
- { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
192
- { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹]))/gi }
203
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
193
204
  ],
194
205
  medical: [
195
- { type: 'PHI', regex: /\b(?:MRN|Patient ID|Medical Record (?:No\.?|Num(?:ber)?|#)|Patient (?:No\.?|Num(?:ber)?|#))[\s:#]+([A-Za-z0-9-]+)/gi },
206
+ { type: 'NAME', isContextName: true, regex: /\b(?:PATIENT|Pt\.?|LEGAL\s+GUARDIAN(?:\/PROXY)?|PROXY|EMERGENCY\s+CONTACT|ATTENDING\s+PHYSICIAN|ATTENDING|SURGEON|PHYSICIAN|PROVIDER|SUBSCRIBER|GUARDIAN)\s*[:#|]?\s*(?!\b(?:NPI|MRN|DOB|ID|DATE|RECORD|PHONE)\b)(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
207
+ // HL7 v2 PID-5 Caret-Delimited Patient / Provider Names (e.g. "Guillermo^Maricela^C", "Vance-Soto^Eleanor^M")
208
+ { type: 'NAME', isContextName: true, isHL7Name: true, regex: /\b[A-Za-z'’-]{2,}\^[A-Za-z'’-]{2,}(?:\^[A-Za-z'’.-]+){0,4}\b/g },
209
+ // HL7 v2 Segment Delimited Dates (YYYYMMDD in PID-7, MSH-7, etc. e.g. "||19820412|F")
210
+ { type: 'DATE', regex: /(?<=[|:])(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?=[|:\s])/g },
211
+ { type: 'PHI', regex: /\b(?:MRN|Patient\s+(?:ID|Number|No\.?|#)|Medical\s+Record\s+(?:No\.?|Num(?:ber)?|#)|EHR(?:\s*(?:ID|Record|Number|No\.?|#))?)[\s:#.]+([A-Za-z0-9\/-]+)/gi },
196
212
  { type: 'DATE', regex: /\b(?:DOB|Date of Birth|BIRTHDAY)[\s:]+([0-9./-]{6,10})\b/gi },
197
- { type: 'PHI', regex: /\bMRN[-_ ]*[A-Za-z0-9_-]{4,}\b/gi },
213
+ { type: 'PHI', regex: /\b(?:MRN|EHR)[-_ .]*[A-Za-z0-9\/-]{4,}\b/gi },
198
214
  { type: 'ID', regex: /\b(?:Insurance\s+(?:ID|No\.?|Number|#)|Policy(?:\s*(?:ID|No\.?|Number|#)|[:#])|Member\s*(?:ID|No\.?|Number|#|[:#])|Subscriber\s*(?:ID|No\.?|Number|#|[:#])|Group\s*(?:ID|No\.?|Number|#|[:#])|Plan\s*(?:ID|No\.?|Number|#|[:#])|Health(?:\s+Plan)?\s*(?:ID|No\.?|Number|#)|Rx\s*(?:ID|No\.?|Number|Group|BIN|PCN|#))[:\s#]+([A-Za-z0-9-]{3,30})\b/gi },
199
215
  { type: 'ID', regex: /\b(?:Health\s+Plan(?:\s+Beneficiary)?|Beneficiary(?:\s+No\.?|\s+Number)?|HPN)[:\s#]+([A-Za-z0-9-]+)/gi },
200
216
  { type: 'ID', regex: /\bHPN[-_][A-Za-z0-9-]+\b/gi },
201
217
  { type: 'ID', regex: /\b(?:BCB|BCBS|AETNA|CIGNA|UHC|HUMANA|MEDICARE|MEDICAID)[-_A-Za-z0-9]+\b/gi },
202
- { type: 'ID', regex: /\b(?:NPI|National Provider Identifier)[:\s#]*(\d{10})\b/gi },
218
+ { type: 'ID', regex: /\b(?:NPI|National\s+Provider\s+(?:Identifier|ID)|Provider\s+(?:NPI|ID)|Attending\s+(?:NPI|Provider\s+ID)|Physician\s+NPI)[:\s#.]*([0-9OIlo]{10})\b/gi },
219
+ { type: 'ID', regex: /\(01\)\d{14}(?:\(\d{2}\)[A-Za-z0-9-]+)+/g },
220
+ { type: 'LOCATION', regex: /\b(?:Pavilion|Ward|Bed|Room|Rm\.?|Suite|Bldg|Building|Floor|Wing|Tower|Unit)\s+[A-Za-z0-9-]+\b/gi },
221
+ { type: 'DATE', regex: /\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember))\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\b/gi },
203
222
  { type: 'ID', regex: /\b(?:Device\s+(?:Identifier|ID|Serial|No\.?|Number)|UDI)[:\s#]+([A-Za-z0-9-]+)/gi },
204
223
  { type: 'ID', regex: /\bUDI[-_][A-Za-z0-9-]+\b/gi },
205
224
  { type: 'ID', regex: /\b(?:Vehicle\s+(?:Serial|ID|Identification(?:\s+Number)?|No\.?|Number)|VIN)[:\s#]+([A-Za-z0-9-]+)/gi },
@@ -293,7 +312,7 @@ let PROFILE_RULES = {
293
312
  { type: 'ID', regex: /\bPARCEL[- ]?\d{5,15}\b/gi },
294
313
  { type: 'ID', regex: /\b(?:APN|Assessor(?:'s)?\s*Parcel(?:\s*Number)?|Parcel\s*(?:ID|Number|No\.?|#))[:\s#-]*(\d{2,5}[-\s]\d{2,5}[-\s]\d{2,5}(?:[-\s]\d{1,4})?|\d{6,15})\b/gi },
295
314
  { type: 'ID', regex: /\bTENANT[-_]ID[-_][0-9]{4,}\b/gi },
296
- { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW)[\s:]+)(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[0-9,.'’]{3,}\b/gi },
315
+ { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW|Purchase\s+Price|Listing\s+Price|Asking\s+Price|Down\s+Payment|Mortgage(?:\s+Balance)?|Closing\s+Costs?|Appraisal(?:\s+Value)?)[\s:]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
297
316
  { type: 'SECRET', regex: /\b(?:GATE|DOOR|LOBBY)[-_ ](?:CODE|PIN)[\s:]*\d{4,6}\b/gi },
298
317
  { type: 'ID', regex: /\b(?:Escrow\s*(?:Number|No\.?|#|Account|File|Order)|Escrow)[:\s#]*([A-Za-z0-9-]{5,18})\b/gi },
299
318
  { type: 'ID', regex: /\b(?:Title\s*Policy|Title\s*Order|Title\s*Commitment)\s*(?:#|ID|No\.?)?[:\s#]*([A-Z0-9-]{6,20})\b/gi },
@@ -404,7 +423,7 @@ let PROFILE_RULES = {
404
423
  { type: 'FINANCIAL', regex: /\b(?:Policy|Contract)\s*(?:No\.?|Number|#)[:\s]+[A-Z0-9][A-Z0-9\-]{3,14}\b/gi },
405
424
  { type: 'ID', regex: /\bCRD\s*#?\s*\d{4,8}\b/gi },
406
425
  { type: 'FINANCIAL', regex: /\b(?:Annual\s+(?:Distribution|Withdrawal)|RMD|Required\s+Minimum\s+Distribution)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
407
- { type: 'FINANCIAL', regex: /\b(?:Portfolio|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+[KMB]?\b/gi },
426
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Portfolio(?:\s+Value)?|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
408
427
  { type: 'FINANCIAL', regex: /\b(?:Roth\s+)?(?:IRA|401k|401\(k\)|403b|403\(b\)|SEP|SIMPLE)\s*(?:Account|Acct|Plan)?\s*(?:No|Number|#)?[:\s#]*[A-Z0-9]{4,15}\b/gi },
409
428
  { type: 'ID', regex: /\bCUSIP[:\s#]*([0-9A-Z]{9})\b/gi },
410
429
  { type: 'ID', regex: /\bISIN[:\s#]*([A-Z]{2}[0-9A-Z]{9}[0-9])\b/gi },
@@ -437,7 +456,9 @@ let PROFILE_RULES = {
437
456
  { type: 'ID', regex: /\b(?:CAF(?:\s+ID)?|(?:Practitioner|Tax\s+Preparer)\s+(?:PIN|ID)|PTIN)[:\s#]*[A-Z0-9-]{6,12}\b/gi },
438
457
  { type: 'ID', regex: /\bEFIN[:\s#]*\d{6}\b/gi },
439
458
  { type: 'ID', regex: /\bGL[-_\s]*(?:Account|Acct|Code)?[:\s#]*\d{4,10}\b/gi },
440
- { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi }
459
+ { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi },
460
+ { type: 'ID', regex: /\b(?:Invoice|Inv|Purchase\s+Order|PO|Bill|Check|Voucher|Remittance)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9_-]{4,25})\b/gi },
461
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Invoice(?:\s+Total)?|Amount\s+Due|Subtotal|Total\s+Due|Payment(?:\s+Amount)?|Ledger\s+Balance|Credit|Debit|Net\s+Amount|Total\s+Paid|Billed|Remittance)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
441
462
  ],
442
463
  pharma: [
443
464
  { type: 'ID', regex: /\b(?:Subject|Patient|Participant)\s+(?:ID|No)[:\s#]*[-A-Z0-9]{3,15}\b/gi },
@@ -596,7 +617,7 @@ const CORPORATE_SUFFIXES = new Set([
596
617
  let NOT_NAME_WORDS = new Set([
597
618
  'uk', 'eu', 'us', 'usa', 'dpa', 'ico', 'dpo', 'ciso', 'ssrn', 'elsevier', 'gdpr', 'ccpa', 'cpra', 'hipaa', 'soc2', 'iso27001', 'governance', 'regulatory', 'framework', 'mandate', 'mandates', 'guidance', 'statute', 'statutes', 'jurisdiction', 'jurisdictions', 'recital', 'article', 'articles', 'treatise', 'workstation', 'device', 'endpoint', 'blueprint', 'personal', 'cross-border', 'cross', 'border', 'transfer', 'retrieval', 'augmented', 'generation', 'limitation', 'discrimination', 'imposes', 'heavy', 'identifiable', 'bypasses', 'author', 'authors', 'contact', 'contacts', 'keywords', 'keyword', 'keys', 'key', 'separation', 'pre-print', 'preprint', 'abstract', 'structured', 'background', 'methods', 'results', 'discussion', 'conclusion', 'conclusions', 'introduction', 'overview', 'workflow', 'paradigm', 'nomenclature', 'repository', 'classification', 'phenotypes', 'biomedical', 'bioinformatics', 'identifiers', 'de-identification', 'sanitization', 'ephemeral', 'validation', 'validating', 'genomic', 'genomics', 'clinical', 'phenotype', 'target', 'corresponding', 'subject', 'biorxiv', 'medrxiv', 'arxiv', 'privacyscrubber', 'research', 'lead', 'architect', 'chief', 'founder', 'symbols', 'gene', 'genes', 'sample', 'barcode', 'pipeline', 'downstream', 'pedigree', 'trees', 'exposes', 'violates', 'protecting', 'anonymization', 'redacted', 'redaction', 'zero-trust', 'ztds',
598
619
  'conventional', 'model', 'models', 'potential', 'multi', 'million', 'dollar', 'dollars', 'surrogate', 'surrogates', 'geographic', 'subdivisions', 'subdivision', 'inference', 'response', 'responses', 'latency', 'benchmarks', 'benchmark', 'experimental', 'methodology', 'informatics', 'ethics', 'accountability', 'civil', 'rights', 'perimeter', 'network', 'requirements', 'requirement', 'statutory', 'categories', 'category', 'regulations', 'regulation', 'penalties', 'penalty', 'disclosure', 'disclosures', 'clinician', 'terminal', 'transport', 'inpatient', 'discharge', 'summaries', 'operative', 'reports', 'outpatient', 'psychiatric', 'telehealth', 'consultation', 'pathology', 'sequencing', 'laboratory', 'sensitivity', 'specificity', 'proxy', 'gateways', 'gateway', 'packet', 'packets', 'inspection', 'declarations', 'availability', 'competing', 'interests', 'interventions', 'multicenter', 'multi-center', 'institution', 'institutional', 'protocol', 'deviations', 'adverse', 'events', 'abbreviations', 'agents', 'agent', 'committee', 'committees', 'board', 'boards', 'nih', 'pubmed', 'central', 'open', 'access', 'policy', 'trial', 'trials', 'crf', 'crfs', 'ehr', 'ehrs', 'baa', 'baas', 'hitech', 'safe', 'harbor', 'section',
599
- 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier',
620
+ 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier', 'tel', 'phone', 'fax', 'mobile', 'cell', 'email', 'dpoa', 'poa',
600
621
  // Grammatical & Sentence Starters
601
622
  'the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'your', 'his', 'her', 'their', 'our', 'its', 'it', 'he', 'she', 'they', 'we', 'i', 'you', 'who', 'whom', 'which', 'what', 'whose', 'why', 'how', 'when', 'where', 'with', 'for', 'from', 'by', 'to', 'at', 'in', 'on', 'of', 'about', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'and', 'but', 'or', 'so', 'yet', 'im', "i'm", "you're", "they're", "we're", "it's", "he's", "she's", "that's", "there's", "what's", "who's", "i've", "you've", "we've", "they've", "i'll", "you'll", "we'll", "they'll", "i'd", "you'd", "we'd", "they'd",
602
623
  // Verbs, Auxiliaries, Commands & Imperatives
@@ -735,7 +756,7 @@ const PROFILE_JARGON = {
735
756
  'insurance', 'bcbs', 'bronchitis', 'amoxicillin', 'penicillin', 'antibiotic', 'antibiotics', 'vital',
736
757
  'vitals', 'bp', 'hr', 'bpm', 'mmhg', 'allergies', 'dosage', 'parenchyma', 'ventricles', 'fazekas',
737
758
  'ischemia', 'lesion', 'cardiology', 'atherosclerotic', 'aortocoronary', 'bypass', 'graft', 'hyperlipidemia',
738
- 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10'
759
+ 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10', 'pt'
739
760
  ],
740
761
  realestate: [
741
762
  'escrow', 'tenant', 'landlord', 'lease', 'mortgage', 'appraisal', 'broker', 'property', 'zoning',
@@ -752,7 +773,13 @@ const PROFILE_JARGON = {
752
773
  'recitals', 'whereas', 'confidential', 'information', 'care', 'term', 'exhibit', 'party', 'parties',
753
774
  'disclosing', 'receiving', 'witnesseth', 'standard', 'indemnification', 'liability', 'governing',
754
775
  'law', 'docket', 'deponent', 'reporter', 'appearances', 'examination', 'services', 'scope', 'work',
755
- 'master', 'msa'
776
+ 'master', 'msa',
777
+ 'finra', 'sec', 'disciplinary', 'arbitration', 'proceeding', 'proceedings', 'dispute', 'resolution',
778
+ 'escrow', 'claimant', 'respondent', 'petitioner', 'appellant', 'appellee', 'trustee', 'trustor',
779
+ 'grantor', 'grantee', 'fiduciary', 'principal', 'agent', 'officer', 'counsel', 'lead', 'attorney',
780
+ 'partner', 'associate', 'representative', 'broker', 'primary', 'privilege', 'privileged', 'doctrine',
781
+ 'work-product', 'power', 'durable', 'evidence', 'rules', 'rule', 'statutory', 'disqualification',
782
+ 'acquisition', 'file', 'securities', 'exchange'
756
783
  ],
757
784
  academic: [
758
785
  'institutional', 'review', 'board', 'irb', 'protocol', 'national', 'institutes', 'health', 'nih',
@@ -866,7 +893,7 @@ const PROFILE_JARGON = {
866
893
  }
867
894
 
868
895
  function detectMatches(text, activeProfile = 'general', customRules = [], nlpNames = [], enabledEntities = null, mlEntities = []) {
869
- let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff]/g, '');
896
+ let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff\u00ad]/gi, '');
870
897
  textToProcess = textToProcess.replace(/([a-z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
871
898
 
872
899
  let matches = [];
@@ -944,9 +971,9 @@ const PROFILE_JARGON = {
944
971
  if (!val || NAME_STOP_LIST.has(val) || currentJargon.has(val) || NOT_NAME_WORDS.has(val)) continue;
945
972
 
946
973
  if (rule.type === 'NAME') {
947
- let words = val.split(/[ \t\xA0]+/);
974
+ let words = val.split(/[ \t\xA0\^]+/);
948
975
  if (!rule.isContextName) {
949
- let origWords = matchedText.split(/[ \t\xA0]+/);
976
+ let origWords = matchedText.split(/[ \t\xA0\^]+/);
950
977
  while (words.length > 2 && (currentJargon.has(words[0]) || (words[0].length > 1 && NOT_NAME_WORDS.has(words[0])) || (words[0].replace(/[^\p{L}]/gu, '').length > 1 && NOT_NAME_WORDS.has(words[0].replace(/[^\p{L}]/gu, ''))))) {
951
978
  origWords.shift();
952
979
  words.shift();
@@ -966,7 +993,7 @@ const PROFILE_JARGON = {
966
993
  if (cleanW.length <= 1) return false;
967
994
  return currentJargon.has(w) || NOT_NAME_WORDS.has(w) || NOT_NAME_WORDS.has(cleanW) || NAME_STOP_LIST.has(w) || NAME_STOP_LIST.has(cleanW);
968
995
  }) ||
969
- val.split(/[ \t\xA0-]+/).some(sw => {
996
+ val.split(/[ \t\xA0\^\-]+/).some(sw => {
970
997
  const cleanSW = sw.replace(/[^\p{L}]/gu, '');
971
998
  if (cleanSW.length <= 1) return false;
972
999
  return currentJargon.has(sw) || NOT_NAME_WORDS.has(sw) || NOT_NAME_WORDS.has(cleanSW) || NAME_STOP_LIST.has(sw) || NAME_STOP_LIST.has(cleanSW);
@@ -984,6 +1011,7 @@ const PROFILE_JARGON = {
984
1011
 
985
1012
  if (rule.isContextName && words.length > 0 && words.some(w => {
986
1013
  const cleanW = w.replace(/[^\p{L}]/gu, '');
1014
+ if (cleanW.length <= 1) return false;
987
1015
  if (rule.isCorporateName && (CORPORATE_SUFFIXES.has(cleanW) || US_STATES_SET.has(cleanW))) {
988
1016
  return false;
989
1017
  }
@@ -1178,7 +1206,12 @@ const PROFILE_JARGON = {
1178
1206
  cleaned = cleaned.replace(/^(?:Here (?:is|are) (?:the )?(?:redacted|scrubbed|sanitized|processed|clean|updated|modified) (?:text|output|version|data).*?[:\n]+|\*\*Scrubbed Text\*\*[:\n]+|### Scrubbed Text[:\n]+)/i, '');
1179
1207
  cleaned = cleaned.replace(/^\s*Edit\s*\n+/i, "");
1180
1208
  cleaned = cleaned.replace(/\s*\bEdit\s+in\s+a\s+page\b\s*$/i, "");
1181
- // 3. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1209
+ // 3. Strip echoed system instructions and PrivacyScrubber pseudonymization disclaimers
1210
+ cleaned = cleaned.replace(/^\s*(?:Understood|Acknowledged|Got it|Sure|OK|Okay|Note taken|Understood[.,!])[^\n]*\n+/i, "");
1211
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[\s\S]*?exactly as formatted in your response\.?\]\s*(?:\n*|$)/gi, "\n\n");
1212
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[^\]]*\]\s*(?:\n*|$)/gi, "\n\n");
1213
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:Direct identifiers in this prompt were pseudonymized|I will retain all bracketed tokens)[^\n]*(?:\n*|$)/gi, "\n\n");
1214
+ // 4. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1182
1215
  cleaned = cleaned.replace(/^[:;|\-\—\–]+(?=\n|$)/, "");
1183
1216
  cleaned = cleaned.replace(/^[:;]+\s*/, "");
1184
1217
  return cleaned.trim();
package/ps-pii-engine.js CHANGED
@@ -47,9 +47,9 @@ let REGEX_RULES = [
47
47
 
48
48
  // Professional IDs & Organizations
49
49
  { type: 'NAME', isContextName: true, isCorporateName: true, regex: /\b(?:[A-Z][A-Za-z0-9&.,'-]*[ \t\xA0]+){1,5}(?:Inc\.?|LLC|Corp\.?|Corporation|Ltd\.?|Limited|Co\.?|Company|Group|Holdings|Solutions|Services|Technologies|Logistics|Industries|Capital|Bank|Partners|LLP|PLLC)(?:\s+(?:LLC|Inc\.?|Corp\.?|Ltd\.?|USA|Group))?\b/g },
50
- { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*[A-Z0-9-]{3,15}\b/gi },
50
+ { type: 'ID', regex: /\b(?:Employee|Emp|EE|Worker|Staff|File|Badge|Member|Advisor|Producer|Agent|Borrower)\s*(?:#|ID|No\.?|Number)[:\s#]*([A-Z0-9-]{3,15})\b/gi },
51
51
  { type: 'ID', regex: /\b(?:Pay\s+Group[:#\s]+[A-Za-z0-9_-]{2,30}|(?:Cost\s+Center|Dept|Department)[:#\s]+[A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)\b/gi },
52
- { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#)?|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
52
+ { type: 'ID', regex: /(?:\b(?:Box\s+d\b|d\.\s*(?:Control|#|number|no\.?|num)|d\s+Control)\s*(?:Control\s+)?(?:number|no\.?|#|num)?[:\s#]+|\bControl\s*(?:number|no\.?|#|num)?[:\s#]+|\bControl[:#]\s*)([A-Za-z0-9-]{3,30})/gi },
53
53
  { type: 'ID', regex: /\bEEID[ -]?\d{4,}\b/gi },
54
54
  { type: 'ID', regex: /\bEMP[-_]\d{3,}\b/gi },
55
55
 
@@ -92,7 +92,7 @@ let REGEX_RULES = [
92
92
  { type: 'ID', regex: /\b\d{2}\/\d{2}\/\d{4}\b/g },
93
93
 
94
94
  // Phone Numbers
95
- { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
95
+ { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?:\s*(?:ext\.?|x)\s*\d{1,5})?\b/gi },
96
96
  { type: 'PHONE', regex: /\+\d{1,3}[ \t.-]+(?:\(?\d{1,4}\)?[ \t.-]+)?\d{2,8}(?:[ \t.-]+\d{2,8}){0,4}\b/g },
97
97
  { type: 'PHONE', regex: /\+?[1-9]\d{1,3}[\s.-]\(?\d{1,4}\)?[\s.-]\d{2,4}[\s.-]\d{4}/g },
98
98
  { type: 'PHONE', regex: /(?:\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}\b/g },
@@ -109,16 +109,16 @@ let REGEX_RULES = [
109
109
  // Generalized Name Detection (First [Middle] Last) — max 1 middle word to avoid grabbing job titles
110
110
  // Middle word must be: a particle (van/de/etc.), a single initial (A. or A), or a capitalized word of ≥2 lowercase letters
111
111
  { type: 'NAME', isAggressiveName: true, regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*|\p{Lu}\.?|van|von|de|di|da|la|le|del|du|der|van[ \t\xA0]+de|van[ \t\xA0]+der)){0,1}[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
112
- // Payroll Format Names (e.g. "BARKER, KELLY", "BARKER, KELLY M", "DOE, JOHN M.")
113
- { type: 'NAME', regex: /\b[A-Z]{2,25},\s+[A-Z]{2,25}(?:\s+[A-Z]\.?|\s+[A-Z]{2,25})*\b/g },
112
+ // Payroll & Inverted Format Names (e.g. "BARKER, KELLY", "Vance-Soto, Eleanor", "Kaufman-Alvarez, Liam T.")
113
+ { type: 'NAME', regex: /\b[A-Z][a-zA-Z'’-]+(?:-[A-Za-z'’-]+)?,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?(?!\w)|\s+[A-Z][a-zA-Z'’-]+)*\b/g },
114
114
  // All-Caps Names (2–3 words, supporting single-letter middle initial e.g. "KELLY M BARKER", "JOHN M BARKER")
115
115
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]+(?:\p{Lu}\.?|[A-Z][a-z]+))?[ \t\xA0]+\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
116
116
  // ALL-CAPS first + middle initial(s) with optional spaces + ALL-CAPS last name
117
117
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
118
118
  // ALL-CAPS first name + optional middle initial(s) with optional spaces + Mixed-Case last name
119
119
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*|[ \t\xA0]+)(?:\p{Lu}\p{Ll}[\p{Ll}'-]*|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
- // Names with Honorifics (with or without period, supporting single or multi-word full names) (Unicode-safe)
121
- { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Hon\.)[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*))?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
+ // Names with Honorifics (with or without period, supporting middle initials and hyphenated surnames) (Unicode-safe)
121
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Rev|Judge|Atty|Doctor|Professor)(?:\.|\b)[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+)(?:[ \t\xA0]+(?:\p{Lu}\.?|\p{Lu}[\p{L}'-]+)){0,2}(?:[ \t\xA0]+(?:\p{Lu}[\p{L}'-]+))(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
122
  // Names with Quoted or Parenthetical Nicknames (e.g. Judith "Judy" Diaz, Robert (Bob) Smith)
123
123
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]+)[ \t\xA0]+(?:"[\p{Lu}\p{Ll}'-]+"|'[\p{Lu}\p{Ll}'-]+'|[“‘][\p{Lu}\p{Ll}'-]+[”’]|\([\p{Lu}\p{Ll}'-]+\))[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]+)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
124
124
 
@@ -143,17 +143,29 @@ let REGEX_RULES = [
143
143
  let PROFILE_RULES = {
144
144
  general: [],
145
145
  legal: [
146
+ // Legal Party & Role-Prefixed Names (Claimant, Respondent, Plaintiff, Defendant, Petitioner, Appellant, Principal, Grantor, Attorney-in-Fact, Counsel)
147
+ { type: 'NAME', isContextName: true, regex: /\b(?:Registered\s+Representative|Claimant|Respondent|Plaintiff|Defendant|Petitioner|Appellant|Appellee|Principal|Grantor|Grantee|Trustee|Trustor|Fiduciary|Attorney-in-Fact|Designated\s+Attorney-in-Fact|Client\s*(?:\/\s*Principal)?|Lead\s+Counsel|Counsel|Managing\s+Partner|Escrow\s+(?:Trust\s+)?Officer)\s*[:#,]?\s*(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
146
148
  { type: 'LEGAL', regex: /\bCASE[-_][A-Z0-9_-]{4,}\b/gi },
147
149
  { type: 'LEGAL', regex: /\bMATTER[-_][A-Z0-9_-]{4,}\b/gi },
148
150
  { type: 'LEGAL', regex: /\b[A-Z]{2,4}[- ]?\d{2}[- ]?\d{4,}\b/g },
149
- { type: 'PRIVILEGE', regex: /ATTORNEY[- ]CLIENT[- ]PRIVILEGE/gi },
151
+ { type: 'PRIVILEGE', regex: /\bATTORNEY[- ]CLIENT[- ]PRIVILEGE[DS]?\b/gi },
150
152
  { type: 'LEGAL', regex: /\b(?:BATES|PROD|PLTF|DEFT|CONF|EXHIBIT)[-_ ]?[A-Z0-9]{2,10}[-_ ]?\d{4,10}\b/gi },
151
- { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR)[-_ ][A-Z0-9_-]{3,15}\b/gi },
152
- { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*\d{4,8}\b/gi },
153
+ // Case / Proceeding / Arbitration Numbers (including OCR homoglyphs O/0, I/1, l/1)
154
+ { type: 'LEGAL', regex: /\b(?:Case|Matter|Arbitration|Docket|Proceeding)\s*(?:(?:No\.?|Number|#)[:\s#]*|[:#]\s*)([A-Za-z0-9_-]{4,25})\b/gi },
155
+ { type: 'LEGAL', regex: /\b(?:\d{1,2}:)?\d{2,4}[-_ ]?(?:CV|CR|MDL|BK|MC|MISC|MJ|PO|CA|AP|PR|AR)[-_ ][A-Za-z0-9_-]{3,15}\b/gi },
156
+ // Bar registration & CRD / Broker / Representative IDs (including OCR homoglyphs)
157
+ { type: 'ID', regex: /\b(?:STATE\s+BAR|BAR\s+(?:NO\.?|#|ID)|SBN|ATTORNEY\s+REG(?:ISTRATION)?)[:\s#-]*([0-9OIloA-Za-z]{4,10})\b/gi },
158
+ { type: 'ID', regex: /\b(?:CRD|CIK|Broker\s*ID|Rep(?:resentative)?\s*ID|Advisor\s*ID|Registration\s*(?:No\.?|#)?)[:\s#]*([0-9OIloA-Za-z]{5,12})\b/gi },
153
159
  { type: 'LEGAL', regex: /\b(?:RETAINER|ENGAGEMENT|REPRESENTATION)[-_: ]*(?:AGREEMENT|ID|REF)?[:\s#-]*[A-Z0-9_-]{4,20}\b/gi },
154
160
  { type: 'LEGAL', regex: /\bECLI:[A-Z]{2}:[A-Za-z0-9]{1,7}:\d{4}:[A-Za-z0-9.]{1,25}\b/g },
155
161
  { type: 'ID', regex: /\bPCT\/[A-Z]{2}\d{4}\/\d{6}\b/g },
156
- { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g }
162
+ { type: 'ID', regex: /\b(?:TX|TXu|VA|VAu|PA|PAu|SR|SRu|SE|SEu|RE)\s*-?\d[-\d ]{5,9}\d\b/g },
163
+ // Escrow / Trust / Settlement / Disputed Account Numbers & ABA Routing
164
+ { type: 'FINANCIAL', regex: /\b(?:Escrow\s+(?:Account|Acct|Deposit)|Trust\s+Account|Settlement\s+Account|Disputed\s+Account)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9-]{5,25})\b/gi },
165
+ { type: 'FINANCIAL', regex: /\b(?:ABA\s+Routing|Routing\s+Transit|Fedwire\s+Routing)\s*(?:Number|Num|No\.?|#)?[:\s#]+(\d{9})\b/gi },
166
+ // Legal Financial Amounts (lookbehind + standalone)
167
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Settlement(?:\s+Amount)?|Retainer(?:\s+Fee)?|Hourly\s+Rate|Damages(?:\s+Claimed)?|Legal\s+Fees?|Sanctions?|Judgement|Judgment|Disgorgement|Penalty|(?:Prejudgment\s+)?Interest|Escrow(?:\s+Funds?|\s+Amount)?|Fine|Restitution)(?:[\s:]+(?:of)?[\s:]*|[:\s]+))(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
168
+ { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹]))/gi }
157
169
  ],
158
170
  hr: [
159
171
  { type: 'NAME', isContextName: true, regex: /(?:Candidate|Applicant|Employee|Reporting To|Manager|Mentored by|Direct Report)[\s:]+([A-Z][a-z]*(?:\s+[A-Z][a-z]*)?)/g },
@@ -188,18 +200,25 @@ let PROFILE_RULES = {
188
200
  { type: 'ID', regex: /\b(?:CRD|CIK|Advisor\s*ID|Broker\s*ID)[:\s#]*(\d{5,10})\b/gi },
189
201
  { type: 'ID', regex: /\b(?:LEI|LEGAL[\s-]ENTITY[\s-]IDENTIFIER)[:\s#-]*[0-9A-Z]{20}\b/gi },
190
202
  { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g },
191
- { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
192
- { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹]))/gi }
203
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
193
204
  ],
194
205
  medical: [
195
- { type: 'PHI', regex: /\b(?:MRN|Patient ID|Medical Record (?:No\.?|Num(?:ber)?|#)|Patient (?:No\.?|Num(?:ber)?|#))[\s:#]+([A-Za-z0-9-]+)/gi },
206
+ { type: 'NAME', isContextName: true, regex: /\b(?:PATIENT|Pt\.?|LEGAL\s+GUARDIAN(?:\/PROXY)?|PROXY|EMERGENCY\s+CONTACT|ATTENDING\s+PHYSICIAN|ATTENDING|SURGEON|PHYSICIAN|PROVIDER|SUBSCRIBER|GUARDIAN)\s*[:#|]?\s*(?!\b(?:NPI|MRN|DOB|ID|DATE|RECORD|PHONE)\b)(?:(?:Dr|Mr|Mrs|Ms|Prof)\.?\s+)?([A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?(?:,\s+[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?|\s+(?:[A-Z]\.?\s+)?[A-Z][a-zA-Z'’-]+(?:-[A-Z][a-zA-Z'’-]+)?))/gi },
207
+ // HL7 v2 PID-5 Caret-Delimited Patient / Provider Names (e.g. "Guillermo^Maricela^C", "Vance-Soto^Eleanor^M")
208
+ { type: 'NAME', isContextName: true, isHL7Name: true, regex: /\b[A-Za-z'’-]{2,}\^[A-Za-z'’-]{2,}(?:\^[A-Za-z'’.-]+){0,4}\b/g },
209
+ // HL7 v2 Segment Delimited Dates (YYYYMMDD in PID-7, MSH-7, etc. e.g. "||19820412|F")
210
+ { type: 'DATE', regex: /(?<=[|:])(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?=[|:\s])/g },
211
+ { type: 'PHI', regex: /\b(?:MRN|Patient\s+(?:ID|Number|No\.?|#)|Medical\s+Record\s+(?:No\.?|Num(?:ber)?|#)|EHR(?:\s*(?:ID|Record|Number|No\.?|#))?)[\s:#.]+([A-Za-z0-9\/-]+)/gi },
196
212
  { type: 'DATE', regex: /\b(?:DOB|Date of Birth|BIRTHDAY)[\s:]+([0-9./-]{6,10})\b/gi },
197
- { type: 'PHI', regex: /\bMRN[-_ ]*[A-Za-z0-9_-]{4,}\b/gi },
213
+ { type: 'PHI', regex: /\b(?:MRN|EHR)[-_ .]*[A-Za-z0-9\/-]{4,}\b/gi },
198
214
  { type: 'ID', regex: /\b(?:Insurance\s+(?:ID|No\.?|Number|#)|Policy(?:\s*(?:ID|No\.?|Number|#)|[:#])|Member\s*(?:ID|No\.?|Number|#|[:#])|Subscriber\s*(?:ID|No\.?|Number|#|[:#])|Group\s*(?:ID|No\.?|Number|#|[:#])|Plan\s*(?:ID|No\.?|Number|#|[:#])|Health(?:\s+Plan)?\s*(?:ID|No\.?|Number|#)|Rx\s*(?:ID|No\.?|Number|Group|BIN|PCN|#))[:\s#]+([A-Za-z0-9-]{3,30})\b/gi },
199
215
  { type: 'ID', regex: /\b(?:Health\s+Plan(?:\s+Beneficiary)?|Beneficiary(?:\s+No\.?|\s+Number)?|HPN)[:\s#]+([A-Za-z0-9-]+)/gi },
200
216
  { type: 'ID', regex: /\bHPN[-_][A-Za-z0-9-]+\b/gi },
201
217
  { type: 'ID', regex: /\b(?:BCB|BCBS|AETNA|CIGNA|UHC|HUMANA|MEDICARE|MEDICAID)[-_A-Za-z0-9]+\b/gi },
202
- { type: 'ID', regex: /\b(?:NPI|National Provider Identifier)[:\s#]*(\d{10})\b/gi },
218
+ { type: 'ID', regex: /\b(?:NPI|National\s+Provider\s+(?:Identifier|ID)|Provider\s+(?:NPI|ID)|Attending\s+(?:NPI|Provider\s+ID)|Physician\s+NPI)[:\s#.]*([0-9OIlo]{10})\b/gi },
219
+ { type: 'ID', regex: /\(01\)\d{14}(?:\(\d{2}\)[A-Za-z0-9-]+)+/g },
220
+ { type: 'LOCATION', regex: /\b(?:Pavilion|Ward|Bed|Room|Rm\.?|Suite|Bldg|Building|Floor|Wing|Tower|Unit)\s+[A-Za-z0-9-]+\b/gi },
221
+ { type: 'DATE', regex: /\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember))\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\b/gi },
203
222
  { type: 'ID', regex: /\b(?:Device\s+(?:Identifier|ID|Serial|No\.?|Number)|UDI)[:\s#]+([A-Za-z0-9-]+)/gi },
204
223
  { type: 'ID', regex: /\bUDI[-_][A-Za-z0-9-]+\b/gi },
205
224
  { type: 'ID', regex: /\b(?:Vehicle\s+(?:Serial|ID|Identification(?:\s+Number)?|No\.?|Number)|VIN)[:\s#]+([A-Za-z0-9-]+)/gi },
@@ -293,7 +312,7 @@ let PROFILE_RULES = {
293
312
  { type: 'ID', regex: /\bPARCEL[- ]?\d{5,15}\b/gi },
294
313
  { type: 'ID', regex: /\b(?:APN|Assessor(?:'s)?\s*Parcel(?:\s*Number)?|Parcel\s*(?:ID|Number|No\.?|#))[:\s#-]*(\d{2,5}[-\s]\d{2,5}[-\s]\d{2,5}(?:[-\s]\d{1,4})?|\d{6,15})\b/gi },
295
314
  { type: 'ID', regex: /\bTENANT[-_]ID[-_][0-9]{4,}\b/gi },
296
- { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW)[\s:]+)(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[0-9,.'’]{3,}\b/gi },
315
+ { type: 'FINANCIAL', regex: /(?<=\b(?:RENT|LEASE|ESCROW|Purchase\s+Price|Listing\s+Price|Asking\s+Price|Down\s+Payment|Mortgage(?:\s+Balance)?|Closing\s+Costs?|Appraisal(?:\s+Value)?)[\s:]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
297
316
  { type: 'SECRET', regex: /\b(?:GATE|DOOR|LOBBY)[-_ ](?:CODE|PIN)[\s:]*\d{4,6}\b/gi },
298
317
  { type: 'ID', regex: /\b(?:Escrow\s*(?:Number|No\.?|#|Account|File|Order)|Escrow)[:\s#]*([A-Za-z0-9-]{5,18})\b/gi },
299
318
  { type: 'ID', regex: /\b(?:Title\s*Policy|Title\s*Order|Title\s*Commitment)\s*(?:#|ID|No\.?)?[:\s#]*([A-Z0-9-]{6,20})\b/gi },
@@ -404,7 +423,7 @@ let PROFILE_RULES = {
404
423
  { type: 'FINANCIAL', regex: /\b(?:Policy|Contract)\s*(?:No\.?|Number|#)[:\s]+[A-Z0-9][A-Z0-9\-]{3,14}\b/gi },
405
424
  { type: 'ID', regex: /\bCRD\s*#?\s*\d{4,8}\b/gi },
406
425
  { type: 'FINANCIAL', regex: /\b(?:Annual\s+(?:Distribution|Withdrawal)|RMD|Required\s+Minimum\s+Distribution)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
407
- { type: 'FINANCIAL', regex: /\b(?:Portfolio|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+(?:[$€£¥₪₽₹]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+[KMB]?\b/gi },
426
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Portfolio(?:\s+Value)?|Market\s+Value|Net\s+Worth|AUM|Total\s+Assets)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi },
408
427
  { type: 'FINANCIAL', regex: /\b(?:Roth\s+)?(?:IRA|401k|401\(k\)|403b|403\(b\)|SEP|SIMPLE)\s*(?:Account|Acct|Plan)?\s*(?:No|Number|#)?[:\s#]*[A-Z0-9]{4,15}\b/gi },
409
428
  { type: 'ID', regex: /\bCUSIP[:\s#]*([0-9A-Z]{9})\b/gi },
410
429
  { type: 'ID', regex: /\bISIN[:\s#]*([A-Z]{2}[0-9A-Z]{9}[0-9])\b/gi },
@@ -437,7 +456,9 @@ let PROFILE_RULES = {
437
456
  { type: 'ID', regex: /\b(?:CAF(?:\s+ID)?|(?:Practitioner|Tax\s+Preparer)\s+(?:PIN|ID)|PTIN)[:\s#]*[A-Z0-9-]{6,12}\b/gi },
438
457
  { type: 'ID', regex: /\bEFIN[:\s#]*\d{6}\b/gi },
439
458
  { type: 'ID', regex: /\bGL[-_\s]*(?:Account|Acct|Code)?[:\s#]*\d{4,10}\b/gi },
440
- { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi }
459
+ { type: 'ID', regex: /\b(?:Cost\s+Center|CC)[-_\s]*(?:ID|Code|#)?[:\s#]*[A-Z0-9_-]*\d[A-Z0-9_-]*\b/gi },
460
+ { type: 'ID', regex: /\b(?:Invoice|Inv|Purchase\s+Order|PO|Bill|Check|Voucher|Remittance)\s*(?:Number|Num|No\.?|#)?[:\s#]+([A-Za-z0-9_-]{4,25})\b/gi },
461
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Invoice(?:\s+Total)?|Amount\s+Due|Subtotal|Total\s+Due|Payment(?:\s+Amount)?|Ledger\s+Balance|Credit|Debit|Net\s+Amount|Total\s+Paid|Billed|Remittance)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£¥₪₽₹]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£¥₪₽₹])|[0-9,.'’]{3,}\b)/gi }
441
462
  ],
442
463
  pharma: [
443
464
  { type: 'ID', regex: /\b(?:Subject|Patient|Participant)\s+(?:ID|No)[:\s#]*[-A-Z0-9]{3,15}\b/gi },
@@ -596,7 +617,7 @@ const CORPORATE_SUFFIXES = new Set([
596
617
  let NOT_NAME_WORDS = new Set([
597
618
  'uk', 'eu', 'us', 'usa', 'dpa', 'ico', 'dpo', 'ciso', 'ssrn', 'elsevier', 'gdpr', 'ccpa', 'cpra', 'hipaa', 'soc2', 'iso27001', 'governance', 'regulatory', 'framework', 'mandate', 'mandates', 'guidance', 'statute', 'statutes', 'jurisdiction', 'jurisdictions', 'recital', 'article', 'articles', 'treatise', 'workstation', 'device', 'endpoint', 'blueprint', 'personal', 'cross-border', 'cross', 'border', 'transfer', 'retrieval', 'augmented', 'generation', 'limitation', 'discrimination', 'imposes', 'heavy', 'identifiable', 'bypasses', 'author', 'authors', 'contact', 'contacts', 'keywords', 'keyword', 'keys', 'key', 'separation', 'pre-print', 'preprint', 'abstract', 'structured', 'background', 'methods', 'results', 'discussion', 'conclusion', 'conclusions', 'introduction', 'overview', 'workflow', 'paradigm', 'nomenclature', 'repository', 'classification', 'phenotypes', 'biomedical', 'bioinformatics', 'identifiers', 'de-identification', 'sanitization', 'ephemeral', 'validation', 'validating', 'genomic', 'genomics', 'clinical', 'phenotype', 'target', 'corresponding', 'subject', 'biorxiv', 'medrxiv', 'arxiv', 'privacyscrubber', 'research', 'lead', 'architect', 'chief', 'founder', 'symbols', 'gene', 'genes', 'sample', 'barcode', 'pipeline', 'downstream', 'pedigree', 'trees', 'exposes', 'violates', 'protecting', 'anonymization', 'redacted', 'redaction', 'zero-trust', 'ztds',
598
619
  'conventional', 'model', 'models', 'potential', 'multi', 'million', 'dollar', 'dollars', 'surrogate', 'surrogates', 'geographic', 'subdivisions', 'subdivision', 'inference', 'response', 'responses', 'latency', 'benchmarks', 'benchmark', 'experimental', 'methodology', 'informatics', 'ethics', 'accountability', 'civil', 'rights', 'perimeter', 'network', 'requirements', 'requirement', 'statutory', 'categories', 'category', 'regulations', 'regulation', 'penalties', 'penalty', 'disclosure', 'disclosures', 'clinician', 'terminal', 'transport', 'inpatient', 'discharge', 'summaries', 'operative', 'reports', 'outpatient', 'psychiatric', 'telehealth', 'consultation', 'pathology', 'sequencing', 'laboratory', 'sensitivity', 'specificity', 'proxy', 'gateways', 'gateway', 'packet', 'packets', 'inspection', 'declarations', 'availability', 'competing', 'interests', 'interventions', 'multicenter', 'multi-center', 'institution', 'institutional', 'protocol', 'deviations', 'adverse', 'events', 'abbreviations', 'agents', 'agent', 'committee', 'committees', 'board', 'boards', 'nih', 'pubmed', 'central', 'open', 'access', 'policy', 'trial', 'trials', 'crf', 'crfs', 'ehr', 'ehrs', 'baa', 'baas', 'hitech', 'safe', 'harbor', 'section',
599
- 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier',
620
+ 'retirement', 'roth', 'ira', 'acct', 'custodial', 'brokerage', 'portfolio', 'annuity', 'dividend', 'distribution', 'reconciliation', 'withholding', 'remuneration', 'pin', 'lobby', 'passcode', 'code', 'gate', 'door', 'access', 'tenant', 'landlord', 'escrow', 'cadastral', 'claim', 'claims', 'adjuster', 'insured', 'carrier', 'indemnity', 'underwriter', 'underwriting', 'deductible', 'premium', 'settlement', 'loss', 'cargo', 'writers', 'guild', 'ghostwriter', 'actor', 'screenplay', 'audiovisual', 'cinematic', 'co-production', 'alumnus', 'alumni', 'registrar', 'transcript', 'diploma', 'orchestration', 'runtime', 'helpdesk', 'freshdesk', 'intercom', 'zendesk', 'investigational', 'pharmacovigilance', 'reaction', 'medication', 'manufacturing', 'lot', 'industrial', 'electronics', 'driver', 'drivers', 'license', 'licence', 'clearing', 'house', 'automated', 'central', 'securities', 'depository', 'arbitration', 'tribunal', 'instrument', 'infrastructure', 'talent', 'dossier', 'tel', 'phone', 'fax', 'mobile', 'cell', 'email', 'dpoa', 'poa',
600
621
  // Grammatical & Sentence Starters
601
622
  'the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'your', 'his', 'her', 'their', 'our', 'its', 'it', 'he', 'she', 'they', 'we', 'i', 'you', 'who', 'whom', 'which', 'what', 'whose', 'why', 'how', 'when', 'where', 'with', 'for', 'from', 'by', 'to', 'at', 'in', 'on', 'of', 'about', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'and', 'but', 'or', 'so', 'yet', 'im', "i'm", "you're", "they're", "we're", "it's", "he's", "she's", "that's", "there's", "what's", "who's", "i've", "you've", "we've", "they've", "i'll", "you'll", "we'll", "they'll", "i'd", "you'd", "we'd", "they'd",
602
623
  // Verbs, Auxiliaries, Commands & Imperatives
@@ -735,7 +756,7 @@ const PROFILE_JARGON = {
735
756
  'insurance', 'bcbs', 'bronchitis', 'amoxicillin', 'penicillin', 'antibiotic', 'antibiotics', 'vital',
736
757
  'vitals', 'bp', 'hr', 'bpm', 'mmhg', 'allergies', 'dosage', 'parenchyma', 'ventricles', 'fazekas',
737
758
  'ischemia', 'lesion', 'cardiology', 'atherosclerotic', 'aortocoronary', 'bypass', 'graft', 'hyperlipidemia',
738
- 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10'
759
+ 'diverticulitis', 'resection', 'colon', 'superbill', 'encounter', 'cpt', 'hcpcs', 'icd-10', 'pt'
739
760
  ],
740
761
  realestate: [
741
762
  'escrow', 'tenant', 'landlord', 'lease', 'mortgage', 'appraisal', 'broker', 'property', 'zoning',
@@ -752,7 +773,13 @@ const PROFILE_JARGON = {
752
773
  'recitals', 'whereas', 'confidential', 'information', 'care', 'term', 'exhibit', 'party', 'parties',
753
774
  'disclosing', 'receiving', 'witnesseth', 'standard', 'indemnification', 'liability', 'governing',
754
775
  'law', 'docket', 'deponent', 'reporter', 'appearances', 'examination', 'services', 'scope', 'work',
755
- 'master', 'msa'
776
+ 'master', 'msa',
777
+ 'finra', 'sec', 'disciplinary', 'arbitration', 'proceeding', 'proceedings', 'dispute', 'resolution',
778
+ 'escrow', 'claimant', 'respondent', 'petitioner', 'appellant', 'appellee', 'trustee', 'trustor',
779
+ 'grantor', 'grantee', 'fiduciary', 'principal', 'agent', 'officer', 'counsel', 'lead', 'attorney',
780
+ 'partner', 'associate', 'representative', 'broker', 'primary', 'privilege', 'privileged', 'doctrine',
781
+ 'work-product', 'power', 'durable', 'evidence', 'rules', 'rule', 'statutory', 'disqualification',
782
+ 'acquisition', 'file', 'securities', 'exchange'
756
783
  ],
757
784
  academic: [
758
785
  'institutional', 'review', 'board', 'irb', 'protocol', 'national', 'institutes', 'health', 'nih',
@@ -866,7 +893,7 @@ const PROFILE_JARGON = {
866
893
  }
867
894
 
868
895
  function detectMatches(text, activeProfile = 'general', customRules = [], nlpNames = [], enabledEntities = null, mlEntities = []) {
869
- let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff]/g, '');
896
+ let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff\u00ad]/gi, '');
870
897
  textToProcess = textToProcess.replace(/([a-z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
871
898
 
872
899
  let matches = [];
@@ -944,9 +971,9 @@ const PROFILE_JARGON = {
944
971
  if (!val || NAME_STOP_LIST.has(val) || currentJargon.has(val) || NOT_NAME_WORDS.has(val)) continue;
945
972
 
946
973
  if (rule.type === 'NAME') {
947
- let words = val.split(/[ \t\xA0]+/);
974
+ let words = val.split(/[ \t\xA0\^]+/);
948
975
  if (!rule.isContextName) {
949
- let origWords = matchedText.split(/[ \t\xA0]+/);
976
+ let origWords = matchedText.split(/[ \t\xA0\^]+/);
950
977
  while (words.length > 2 && (currentJargon.has(words[0]) || (words[0].length > 1 && NOT_NAME_WORDS.has(words[0])) || (words[0].replace(/[^\p{L}]/gu, '').length > 1 && NOT_NAME_WORDS.has(words[0].replace(/[^\p{L}]/gu, ''))))) {
951
978
  origWords.shift();
952
979
  words.shift();
@@ -966,7 +993,7 @@ const PROFILE_JARGON = {
966
993
  if (cleanW.length <= 1) return false;
967
994
  return currentJargon.has(w) || NOT_NAME_WORDS.has(w) || NOT_NAME_WORDS.has(cleanW) || NAME_STOP_LIST.has(w) || NAME_STOP_LIST.has(cleanW);
968
995
  }) ||
969
- val.split(/[ \t\xA0-]+/).some(sw => {
996
+ val.split(/[ \t\xA0\^\-]+/).some(sw => {
970
997
  const cleanSW = sw.replace(/[^\p{L}]/gu, '');
971
998
  if (cleanSW.length <= 1) return false;
972
999
  return currentJargon.has(sw) || NOT_NAME_WORDS.has(sw) || NOT_NAME_WORDS.has(cleanSW) || NAME_STOP_LIST.has(sw) || NAME_STOP_LIST.has(cleanSW);
@@ -984,6 +1011,7 @@ const PROFILE_JARGON = {
984
1011
 
985
1012
  if (rule.isContextName && words.length > 0 && words.some(w => {
986
1013
  const cleanW = w.replace(/[^\p{L}]/gu, '');
1014
+ if (cleanW.length <= 1) return false;
987
1015
  if (rule.isCorporateName && (CORPORATE_SUFFIXES.has(cleanW) || US_STATES_SET.has(cleanW))) {
988
1016
  return false;
989
1017
  }
@@ -1178,7 +1206,12 @@ const PROFILE_JARGON = {
1178
1206
  cleaned = cleaned.replace(/^(?:Here (?:is|are) (?:the )?(?:redacted|scrubbed|sanitized|processed|clean|updated|modified) (?:text|output|version|data).*?[:\n]+|\*\*Scrubbed Text\*\*[:\n]+|### Scrubbed Text[:\n]+)/i, '');
1179
1207
  cleaned = cleaned.replace(/^\s*Edit\s*\n+/i, "");
1180
1208
  cleaned = cleaned.replace(/\s*\bEdit\s+in\s+a\s+page\b\s*$/i, "");
1181
- // 3. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1209
+ // 3. Strip echoed system instructions and PrivacyScrubber pseudonymization disclaimers
1210
+ cleaned = cleaned.replace(/^\s*(?:Understood|Acknowledged|Got it|Sure|OK|Okay|Note taken|Understood[.,!])[^\n]*\n+/i, "");
1211
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[\s\S]*?exactly as formatted in your response\.?\]\s*(?:\n*|$)/gi, "\n\n");
1212
+ cleaned = cleaned.replace(/(?:^|\n*)\s*\[(?:Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[^\]]*\]\s*(?:\n*|$)/gi, "\n\n");
1213
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:Direct identifiers in this prompt were pseudonymized|I will retain all bracketed tokens)[^\n]*(?:\n*|$)/gi, "\n\n");
1214
+ // 4. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
1182
1215
  cleaned = cleaned.replace(/^[:;|\-\—\–]+(?=\n|$)/, "");
1183
1216
  cleaned = cleaned.replace(/^[:;]+\s*/, "");
1184
1217
  return cleaned.trim();
package/scrubber-core.cjs CHANGED
@@ -66,7 +66,7 @@ function scrubText(text, customRules = [], tokenLabelMap = {}, profile = 'Genera
66
66
 
67
67
  // Protect system prompt from being scrubbed or counted
68
68
  let extractedSystemPrompt = "";
69
- let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
69
+ let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff\u00ad]/gi, '');
70
70
 
71
71
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
72
72
 
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/moxno/privacyscrubber-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.2.0",
9
+ "version": "2.2.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@privacyscrubber/mcp-server",
14
- "version": "2.2.0",
14
+ "version": "2.2.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },