@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 +24 -1
- package/index.js +62 -21
- package/manifest.json +1 -1
- package/package.json +5 -2
- package/pii-masking-run.js +8 -1
- package/ps-license-manager.cjs +257 -0
- package/ps-pii-engine.cjs +60 -27
- package/ps-pii-engine.js +60 -27
- package/scrubber-core.cjs +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
[](https://theresanaiforthat.com/ai/privacy-scrubber/)
|
|
15
15
|
[](https://privacyscrubber.com)
|
|
16
16
|
[](https://privacyscrubber.com)
|
|
17
|
+
[-06b6d4.svg)](https://privacyscrubber.com/patents/)
|
|
18
|
+
[](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
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/pii-masking-run.js
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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", "
|
|
113
|
-
{ type: 'NAME', regex: /\b[A-Z]
|
|
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
|
|
121
|
-
{ type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|
|
|
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:
|
|
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
|
-
|
|
152
|
-
{ type: '
|
|
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: '
|
|
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: /\
|
|
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
|
|
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:]+)(?:[$€£¥₪₽₹]
|
|
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:
|
|
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]/
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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", "
|
|
113
|
-
{ type: 'NAME', regex: /\b[A-Z]
|
|
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
|
|
121
|
-
{ type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|
|
|
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:
|
|
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
|
-
|
|
152
|
-
{ type: '
|
|
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: '
|
|
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: /\
|
|
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
|
|
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:]+)(?:[$€£¥₪₽₹]
|
|
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:
|
|
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]/
|
|
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
|
|
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
|
|
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]/
|
|
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.
|
|
9
|
+
"version": "2.2.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@privacyscrubber/mcp-server",
|
|
14
|
-
"version": "2.2.
|
|
14
|
+
"version": "2.2.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|