@privacyscrubber/mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/index.js +322 -0
- package/package.json +31 -0
- package/scrubber-core.cjs +1532 -0
- package/test-mcp.js +114 -0
|
@@ -0,0 +1,1532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrivacyScrubber Core Engine — Shared Module
|
|
3
|
+
* Pure JavaScript, zero DOM dependencies.
|
|
4
|
+
* Used by both the Chrome Extension and (via extraction) the main website.
|
|
5
|
+
*
|
|
6
|
+
* Zero-server rule: No fetch(), no XMLHttpRequest, no external calls.
|
|
7
|
+
* Airplane Mode Verified: works with no network after load.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Regex Rules ─────────────────────────────────────────────────────────────
|
|
11
|
+
(function() {
|
|
12
|
+
if (typeof window !== 'undefined' && window.PrivacyScrubberCore && window.PrivacyScrubberCore.isInitialized) return;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
let REGEX_RULES = JSON.parse('[{"type":"EMAIL","regex":"/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}/g"},{"type":"FINANCIAL","regex":"/\\\\$[0-9,.]+\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b[A-Z]{4}(?:AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b\\\\d{9}\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}[a-zA-Z0-9]{0,16}\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\bPORTFOLIO[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:\\\\d[ -]?){13,19}\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b(?:1|3|bc1)[a-zA-HJ-NP-Z0-9]{25,39}\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b0x[a-fA-F0-9]{40}\\\\b/g"},{"type":"LEGAL","regex":"/\\\\bCASE[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"LEGAL","regex":"/\\\\bMATTER[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"LEGAL","regex":"/\\\\b[A-Z]{2,4}[- ]?\\\\d{2}[- ]?\\\\d{4,}\\\\b/g"},{"type":"PRIVILEGE","regex":"/ATTORNEY[- ]CLIENT[- ]PRIVILEGE/gi"},{"type":"ID","regex":"/\\\\bEEID[ -]?\\\\d{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bRESUME[-_]?[A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bLEAD[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bCAMPAIGN[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bDEAL[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bENTITY[-_][0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bOPPORTUNITY[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bPROSPECT[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bTICKET[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bZENDESK[-_][0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bEMP[-_]\\\\d{3,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bLIS[-]?\\\\d{6,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bPARCEL[-]?\\\\d{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bAGENT[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bTASK[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bSTUDENT[-_][0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bCOURSE[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bINSTANCE[-_]ID[-_][a-z0-9-]{10,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bENV[-_][A-Z0-9]{3,}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\bTENANT[-_]ID[-_][0-9]{4,}\\\\b/gi"},{"type":"ADDRESS","regex":"/\\\\b\\\\d{1,6}\\\\s+[A-Z][a-zA-Z0-9.-]*\\\\s+(?:St|Street|Ave|Avenue|Blvd|Boulevard|Rd|Road|Ln|Lane|Dr|Drive|Way|Ct|Court|Pl|Place|Terrace|Pkwy|Parkway|Sq|Square)\\\\b/gi"},{"type":"LOCATION","regex":"/\\\\b[A-Z][a-zA-Z\\\\s.-]{2,25},\\\\s*[A-Z]{2}\\\\b/g"},{"type":"PHI","regex":"/\\\\bMRN[ -]?\\\\d{6,}\\\\b/gi"},{"type":"PHI","regex":"/\\\\b[A-TV-Z]\\\\d{2}[. ]?\\\\d[A-Z0-9]?\\\\b/g"},{"type":"PHI","regex":"/\\\\b[A-Z]{2,3}\\\\d{6,8}\\\\b/g"},{"type":"PHI","regex":"/\\\\bNHS[ -]?\\\\d{3}[ -]?\\\\d{3}[ -]?\\\\d{4}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\\\\b/g"},{"type":"SECRET","regex":"/\\\\beyJ[a-zA-Z0-9_-]+\\\\.[a-zA-Z0-9_-]+\\\\.[a-zA-Z0-9_-]+\\\\b/g"},{"type":"SECRET","regex":"/\\\\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\\\\b/g"},{"type":"SECRET","regex":"/\\\\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\\\\b/g"},{"type":"SECRET","regex":"/\\\\b(sk|pk|secret|key|token|auth)(?:[-_][a-zA-Z0-9_-]{5,}|(?=[a-zA-Z0-9_-]{5,}\\\\b)(?=[a-zA-Z_-]*[0-9])[a-zA-Z0-9_-]{5,})\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b[a-fA-F0-9]{32,64}\\\\b/g"},{"type":"SECRET","regex":"/\\\\bCVE-\\\\d{4}-\\\\d{4,}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(MD5|SHA1|SHA256)[:\\\\s][a-f0-9]{32,64}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\\\\s*[:=]\\\\s*[^\\\\s\\"\']+\\\\b/gi"},{"type":"COPYRIGHT","regex":"/\\\\bPROJECT[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"COPYRIGHT","regex":"/\\\\b(DRAFT|ASSET|SCRIPT)[-_][0-9]{4,}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\b(GDPR|HIPAA|CCPA|SOC2)[-_]AUDIT[-_]\\\\d{4}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\bPOLICY[-_][A-Z0-9]{5,}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\bGRADE[S]?[:\\\\s]*[A-DF][+-]?\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\b(DOB|BIRTHDAY)[:\\\\s]*[0-9./-]{6,10}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\b(PASSWORD|PWD|SECRET)[:\\\\s]*[\\\\S]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\b\\\\d{3}-\\\\d{2}-\\\\d{4}\\\\b/g"},{"type":"ID","regex":"/(?:\\\\/|[A-Za-z]:\\\\\\\\)[\\\\w\\\\-. ]+(?:[\\\\/\\\\\\\\][\\\\w\\\\-. ]+)+/g"},{"type":"ID","regex":"/\\\\b[A-CEGHJ-PR-TW-Z]{1}[A-CEGHJ-NPR-TW-Z]{1}[0-9]{6}[A-DFM]{1}\\\\b/gi"},{"type":"ID","regex":"/\\\\b[A-Z]{2}[0-9]{6,12}\\\\b/gi"},{"type":"ID","regex":"/[A-Z0-9<]{30,44}/gi"},{"type":"IP","regex":"/\\\\b(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\b/g"},{"type":"IP","regex":"/\\\\b(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}\\\\b/g"},{"type":"ID","regex":"/\\\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\\\b/g"},{"type":"ID","regex":"/(?:\\\\B\\\\/|\\\\b[a-zA-Z]:\\\\\\\\)(?:[\\\\w.-]+[\\\\/\\\\\\\\]?)+\\\\b/g"},{"type":"ID","regex":"/\\\\b\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(?:\\\\.\\\\d+)?(?:Z|[+-]\\\\d{2}(?::\\\\d{2})?)?\\\\b/g"},{"type":"ID","regex":"/\\\\b\\\\d{4}-\\\\d{2}-\\\\d{2}\\\\b/g"},{"type":"ID","regex":"/\\\\b\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{4}\\\\b/g"},{"type":"PHONE","regex":"/\\\\b(?:\\\\+?\\\\d{1,3}[-.\\\\s]?)?\\\\(?\\\\d{3}\\\\)?[-.\\\\s]?\\\\d{3}[-.\\\\s]?\\\\d{4}\\\\b/g"},{"type":"PHONE","regex":"/(?:\\\\+?1[-.\\\\s]?)?\\\\(?\\\\d{3}\\\\)?[-.\\\\s]\\\\d{3}[-.\\\\s]\\\\d{4}/g"},{"type":"PHONE","regex":"/\\\\+?[1-9]\\\\d{1,3}[\\\\s.-]\\\\(?\\\\d{1,4}\\\\)?[\\\\s.-]\\\\d{2,4}[\\\\s.-]\\\\d{4}/g"},{"type":"PHONE","regex":"/(?:\\\\+44\\\\s?7\\\\d{3}|\\\\(?07\\\\d{3}\\\\)?)\\\\s?\\\\d{3}\\\\s?\\\\d{3}\\\\b/g"},{"type":"PHONE","regex":"/\\\\b(?:\\\\d{3}[-.\\\\s]??\\\\d{4}|\\\\(\\\\d{3}\\\\)\\\\s??\\\\d{3}[-.\\\\s]??\\\\d{4}|\\\\d{3}[-.\\\\s]??\\\\d{3}[-.\\\\s]??\\\\d{4})\\\\b/g"},{"type":"LOCATION","regex":"/\\\\b-?\\\\d{1,3}\\\\.\\\\d{4,6}[° ]?[NSns],\\\\s*-?\\\\d{1,3}\\\\.\\\\d{4,6}[° ]?[EWew]\\\\b/g"},{"type":"LOCATION","regex":"/\\\\b(LATITUDE|LONGITUDE)[:\\\\s]-?\\\\d{1,3}\\\\.\\\\d{4,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\bDL[ -]?\\\\d{6,12}\\\\b/gi"},{"type":"ID","regex":"/\\\\bDRIVER[S]?\\\\s+LICENSE[ -]?\\\\d{6,15}\\\\b/gi"},{"type":"NAME","regex":"/(?<=^|[^\\\\p{L}\\\\p{N}_])\\\\p{Lu}[\\\\p{Ll}\'-]*(?:\\\\p{Lu}[\\\\p{Ll}\'-]*)?\\\\p{L}(?:[ \\\\t\\\\xA0]+(?:\\\\p{Lu}[\\\\p{Ll}\'-]{1,}(?:\\\\p{Lu}[\\\\p{Ll}\'-]*)?\\\\p{L}|\\\\p{Lu}\\\\.?|van|von|de|di|da|la|le|del|du|der|van[ \\\\t\\\\xA0]+de)){0,1}[ \\\\t\\\\xA0]+\\\\p{Lu}[\\\\p{Ll}\'-]*(?:\\\\p{Lu}[\\\\p{Ll}\'-]*)?\\\\p{L}(?:\'s)?(?=[^\\\\p{L}\\\\p{N}_]|$)/gu"},{"type":"NAME","regex":"/(?<=^|[^\\\\p{L}\\\\p{N}_])\\\\p{Lu}{3,}(?:[\\\\p{Lu}\'-]*\\\\p{Lu})?(?:[ \\\\t\\\\xA0]+\\\\p{Lu}{3,}(?:[\\\\p{Lu}\'-]*\\\\p{Lu})?){1,2}(?:\'s)?(?=[^\\\\p{L}\\\\p{N}_]|$)/gu"},{"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}_]|$)/gu"},{"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{Lu}[\\\\p{Ll}\'-]*)?\\\\p{L}(?=[^\\\\p{L}\\\\p{N}_]|$)/gu"}]').map(r => { const ls=r.regex.lastIndexOf('/'); return {...r, regex: new RegExp(r.regex.substring(1,ls), r.regex.substring(ls+1)) }; });
|
|
16
|
+
let PROFILE_RULES = (function(){ const p = JSON.parse('{"general":[],"legal":[{"type":"LEGAL","regex":"/\\\\bCASE[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"LEGAL","regex":"/\\\\bMATTER[-_][A-Z0-9]{4,}\\\\b/gi"},{"type":"LEGAL","regex":"/\\\\b[A-Z]{2,4}[- ]?\\\\d{2}[- ]?\\\\d{4,}\\\\b/g"},{"type":"PRIVILEGE","regex":"/ATTORNEY[- ]CLIENT[- ]PRIVILEGE/gi"}],"hr":[{"type":"ID","regex":"/\\\\bEEID[ -]?\\\\d{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bEMP[-_]\\\\d{3,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bRESUME[-_]?[A-Z0-9]{4,}\\\\b/gi"},{"type":"PRIVACY","regex":"/\\\\b(DOB|BIRTHDAY)[:\\\\s]*[0-9./-]{6,10}\\\\b/gi"},{"type":"ADDRESS","regex":"/\\\\b\\\\d{1,6}\\\\s+[A-Z][a-zA-Z0-9.-]*\\\\s+(?:St|Street|Ave|Avenue|Blvd|Boulevard|Rd|Road|Ln|Lane|Dr|Drive|Way|Ct|Court|Pl|Place|Terrace|Pkwy|Parkway|Sq|Square)\\\\b/gi"}],"finance":[{"type":"FINANCIAL","regex":"/\\\\b[A-Z]{4}(?:AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\b[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}[a-zA-Z0-9]{0,16}\\\\b/g"},{"type":"FINANCIAL","regex":"/\\\\bPORTFOLIO[-_][A-Z0-9]{5,}\\\\b/gi"}],"medical":[{"type":"PHI","regex":"/\\\\bMRN[ -]?\\\\d{6,}\\\\b/gi"},{"type":"PHI","regex":"/\\\\b[A-TV-Z]\\\\d{2}[. ]?\\\\d[A-Z0-9]?\\\\b/g"},{"type":"PHI","regex":"/\\\\b[A-Z]{2,3}\\\\d{6,8}\\\\b/g"},{"type":"PHI","regex":"/\\\\bNHS[ -]?\\\\d{3}[ -]?\\\\d{3}[ -]?\\\\d{4}\\\\b/gi"}],"security":[{"type":"SECRET","regex":"/\\\\bCVE-\\\\d{4}-\\\\d{4,}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(MD5|SHA1|SHA256)[:\\\\s][a-f0-9]{32,64}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:INCIDENT|BREACH)[-_: ]?ID[-_: ]?[0-9]{4,10}\\\\b/gi"}],"marketing":[{"type":"ID","regex":"/\\\\b(?:LEAD|PROSPECT)[-_: ]*[A-Z0-9_-]*[0-9][A-Z0-9_-]*\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:CAMPAIGN|CLID|GCLID|FBCLID)[-_:=]*[A-Za-z0-9_-]{10,}\\\\b/gi"},{"type":"FINANCIAL","regex":"/(?<=\\\\b(?:LTV|CAC)[\\\\s:]*)\\\\$[0-9,.]+\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:SEGMENT|COHORT)[-_: ]*[0-9]{4,8}\\\\b/gi"}],"bizops":[{"type":"ID","regex":"/\\\\b(?:DEAL|KPI|METRIC)[-_: ]?[A-Z0-9]{4,}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:ENTITY|VENDOR|PARTNER)[-_: ]?[0-9]{4,10}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:REVENUE|EBITDA|PROFIT|MARGIN)[-_: ]?\\\\$?[0-9,.]+[KM]?\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:NDA|M&A|MERGER)[-_: ]?[A-Z0-9]{4,}\\\\b/gi"}],"sales":[{"type":"ID","regex":"/\\\\bOPPORTUNITY[-_: ]?[A-Z0-9]{5,}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:DOCUSIGN|CONTRACT)[-_: ]?[0-9A-F]{8,32}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:ARR|MRR|QUOTA)[\\\\s:]?\\\\$?[0-9,.]+[KM]?\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:SFDC|HUBSPOT)[-_: ]?[0-9A-Z]{15,18}\\\\b/gi"}],"support":[{"type":"ID","regex":"/\\\\bTICKET[-_:# ]?[A-Z0-9]{5,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:ZENDESK|INTERCOM|JIRA)[-_:# ]?[0-9]{4,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:REFUND|RETURN|RMA)[-_:# ]?[A-Z0-9]{6,12}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:LOYALTY|REWARDS)[-_:# ]?\\\\d{8,12}\\\\b/gi"}],"realestate":[{"type":"ID","regex":"/\\\\b(?:MLS|LIS)[- ]?\\\\d{6,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\bPARCEL[- ]?\\\\d{5,15}\\\\b/gi"},{"type":"ID","regex":"/\\\\bTENANT[-_]ID[-_][0-9]{4,}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:RENT|LEASE|ESCROW)[\\\\s:]?\\\\$[0-9,]{3,}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:GATE|DOOR|LOBBY)[-_ ](?:CODE|PIN)[\\\\s:]?\\\\d{4,6}\\\\b/gi"}],"compliance":[{"type":"SECRET","regex":"/\\\\b(?:GDPR|HIPAA|CCPA|SOC2|ISO27001)[-_: ]?AUDIT[-_: ]?\\\\d{4}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:DPA|POLICY)[-_: ]?[A-Z0-9]{5,15}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:SAR|DSAR)[-_: ]?\\\\d{4,10}\\\\b/gi"}],"ccpa":[{"type":"ID","regex":"/\\\\bDL[ -]?\\\\d{6,12}\\\\b/gi"},{"type":"LOCATION","regex":"/\\\\b-?\\\\d{1,3}\\\\.\\\\d{4,6}[° ]?[NSns],\\\\s*-?\\\\d{1,3}\\\\.\\\\d{4,6}[° ]?[EWew]\\\\b/g"},{"type":"PRIVACY","regex":"/\\\\b(?:CCPA|CPRA)[-_: ]?OPT[-_ ]OUT\\\\b/gi"},{"type":"ID","regex":"/\\\\bACCOUNT[ -]?(?:ID|NUM|NUMBER)[:\\\\s][A-Z0-9]{6,20}\\\\b/gi"}],"engineering":[{"type":"SECRET","regex":"/\\\\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\\\\s*[:=]\\\\s*[\\"\']?[A-Za-z0-9_-]{10,}[\\"\']?\\\\b/gi"},{"type":"ID","regex":"/\\\\b[a-z0-9._-]+\\\\/[a-z0-9._-]+:[a-z0-9._-]+\\\\b/g"},{"type":"ID","regex":"/\\\\b[a-fA-F0-9]{40}\\\\b/g"},{"type":"ID","regex":"/\\\\b[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\\\.svc\\\\.cluster\\\\.local\\\\b/g"}],"agents":[{"type":"ID","regex":"/\\\\b(?:AGENT|VECTOR|EMBEDDING)[-_: ]?[A-Z0-9]{8,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bTASK[-_: ]?[A-Z0-9]{5,15}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:SYS_PROMPT|SYSTEM_PROMPT|OPENAI_API_KEY)[-_: ]?[A-Za-z0-9_-]{10,}\\\\b/gi"}],"academic":[{"type":"ID","regex":"/\\\\b(?:STUDENT|ALUMNI)[-_:# ]?[0-9]{5,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\bCOURSE[-_:# ]?[A-Z]{3,4}[ ]?[0-9]{3,4}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:FERPA|IRB)[-_:# ]?[A-Z0-9]{5,10}\\\\b/gi"},{"type":"ID","regex":"/\\\\bGRADE[S]?[\\\\s:][A-DF][+-]?\\\\b/gi"}],"creative":[{"type":"SECRET","regex":"/\\\\b(?:PROJECT|DRAFT|ASSET|SCRIPT|IP)[-_: ]?[0-9]{4,10}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:SPOILER|UNRELEASED|EMBARGOED)\\\\b/gi"},{"type":"NAME","regex":"/\\\\b(?:GHOSTWRITER|SOURCE)[:\\\\s][A-Z][a-z]+ [A-Z][a-z]+\\\\b/g"}],"tech":[{"type":"ID","regex":"/\\\\b(?:INSTANCE|NODE|CLUSTER)[-_]ID[-_][a-z0-9-]{10,}\\\\b/gi"},{"type":"ID","regex":"/\\\\bENV[-_: ]?(?:PROD|STAGING|DEV|TEST|QA)\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:CONFIG|KUBECONFIG|TFSTATE)[-_: ]?[A-Z0-9]{6,15}\\\\b/gi"}],"personal":[{"type":"ID","regex":"/\\\\b(?:DOB|BIRTHDAY)[\\\\s:]*[0-9./-]{6,10}\\\\b/gi"},{"type":"SECRET","regex":"/\\\\b(?:PASSWORD|PWD|SECRET|PIN)[\\\\s:]*[\\\\S]{4,20}\\\\b/gi"},{"type":"PHONE","regex":"/\\\\b(?:WIFE|HUSBAND|PARTNER|MOM|DAD)[\\\\s:]+(?:\\\\+?1[-.\\\\s]?)?\\\\(?\\\\d{3}\\\\)?[-.\\\\s]\\\\d{3}[-.\\\\s]\\\\d{4}\\\\b/gi"}],"wealthmgmt":[{"type":"LEGAL","regex":"/\\\\b(?:THE\\\\s+)?[A-Z][A-Z\\\\s.\'-]{4,}\\\\s+(?:(?:REVOCABLE|IRREVOCABLE|LIVING|FAMILY|TESTAMENTARY|CHARITABLE|GENERATION-SKIPPING)\\\\s+)*TRUST\\\\b/g"},{"type":"NAME","regex":"/(?<=^|[^\\\\p{L}\\\\p{N}_])\\\\p{Lu}{3,}(?:[\\\\p{Lu}\'-]*\\\\p{Lu})?(?:[ \\\\t\\\\xA0]+\\\\p{Lu}\\\\.)*[ \\\\t\\\\xA0]+\\\\p{Lu}\\\\p{Ll}[\\\\p{Ll}\'-]*(?:\\\\p{Lu}[\\\\p{Ll}\'-]*)?\\\\p{L}(?:\'s)?(?=[^\\\\p{L}\\\\p{N}_]|$)/gu"},{"type":"FINANCIAL","regex":"/\\\\b(?:ABA|Routing(?:\\\\s+No)?|RTN)[:\\\\s#]*\\\\d{9}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Account|Acct\\\\.?)[:\\\\s#]*\\\\d{4}[-\\\\s]?\\\\d{4}[-\\\\s]?\\\\d{2,6}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Policy|Contract)\\\\s*(?:No\\\\.?|Number|#)[:\\\\s]+[A-Z0-9][A-Z0-9\\\\-]{3,14}\\\\b/gi"},{"type":"ID","regex":"/\\\\bCRD\\\\s*#?\\\\s*\\\\d{4,8}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Annual\\\\s+(?:Distribution|Withdrawal)|RMD|Required\\\\s+Minimum\\\\s+Distribution)[:\\\\s]+[$][\\\\d,]+(?:[.][\\\\d]{2})?\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Portfolio|Market\\\\s+Value|Net\\\\s+Worth|AUM|Total\\\\s+Assets)[:\\\\s]+[$][\\\\d,.]+[KMB]?\\\\b/gi"},{"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"}],"insurance":[{"type":"ID","regex":"/\\\\b(?:Claim|CLM)[:\\\\s#-]*[A-Z0-9]{6,15}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Policy|POL)[:\\\\s#-]*[A-Z]{0,4}[-]?\\\\d{6,12}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Loss|Claim\\\\s+Amount|Settlement|Reserve|Indemnity)[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Adjuster\\\\s+(?:ID|No)|Claim\\\\s+Rep(?:\\\\.|resentative)?)[:\\\\s#]*[A-Z0-9]{4,12}\\\\b/gi"},{"type":"ID","regex":"/\\\\bNAIC[:\\\\s#]*\\\\d{5}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Deductible|Premium|Coverage\\\\s+Amount)[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"ID","regex":"/\\\\b[A-HJ-NPR-Z0-9]{17}\\\\b/g"},{"type":"ID","regex":"/\\\\b(?:Insured|Named\\\\s+Insured)[:\\\\s]+[A-Z][a-z]+(?:\\\\s+[A-Z][a-z]+)+/g"},{"type":"ID","regex":"/\\\\b(?:Agent|Producer)\\\\s*(?:Code|No|ID)[:\\\\s#]*[A-Z0-9]{4,12}\\\\b/gi"}],"accounting":[{"type":"ID","regex":"/\\\\b(?:EIN|FEIN|Tax\\\\s+ID)[:\\\\s#]*\\\\d{2}-\\\\d{7}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:AGI|Adjusted\\\\s+Gross\\\\s+Income|Taxable\\\\s+Income|Total\\\\s+Income)[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\bBox\\\\s+\\\\d{1,2}[a-z]?[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Form|Schedule)\\\\s+(?:1040|1040-SR|W-2|W-4|1099-[A-Z]{1,4}|K-1|941|990|4562)\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Federal|State)\\\\s+(?:Tax\\\\s+)?(?:Refund|Amount\\\\s+Owed|Balance\\\\s+Due)[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:State\\\\s+Tax\\\\s+ID|SUI|UI\\\\s+Account\\\\s+No)[:\\\\s#]*[A-Z]{0,3}[-]?\\\\d{4,15}\\\\b/gi"},{"type":"FINANCIAL","regex":"/\\\\b(?:Net\\\\s+Pay|Gross\\\\s+Pay|Taxable\\\\s+Wages)[:\\\\s]+[$][\\\\d,]+(?:[.]\\\\d{2})?\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:CAF|Practitioner\\\\s+(?:PIN|ID)|PTIN)[:\\\\s#]*[A-Z]?\\\\d{6,9}\\\\b/gi"}],"pharma":[{"type":"ID","regex":"/\\\\b(?:Subject|Patient|Participant)\\\\s+(?:ID|No)[:\\\\s#]*[A-Z0-9]{3,12}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Protocol|Study)\\\\s*(?:No|Number|ID)[:\\\\s#]*[A-Z0-9][-A-Z0-9]{3,15}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:IND|NDA|BLA|ANDA)\\\\s*(?:No|Number|#)?[:\\\\s]*\\\\d{3,6}\\\\b/gi"},{"type":"ID","regex":"/\\\\bIRB[:\\\\s#]*[A-Z0-9]{4,12}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Site\\\\s+(?:No|ID)|Investigator\\\\s+Site)[:\\\\s#]*\\\\d{3,6}\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:Batch|Lot|Serial)\\\\s*(?:No[.]?|Number|#)?[:\\\\s#]*[A-Z0-9][A-Z0-9\\\\-]{3,14}\\\\b/gi"},{"type":"PHI","regex":"/\\\\b(?:Dose|Dosage)[:\\\\s]+\\\\d+(?:\\\\.\\\\d+)?\\\\s*(?:mg|mcg|mL|IU|units?)\\\\b/gi"},{"type":"ID","regex":"/\\\\b(?:CRF|eCRF|Case\\\\s+Report\\\\s+Form)\\\\s*(?:No|Page|ID)?[:\\\\s#]*[A-Z0-9]{2,10}\\\\b/gi"}]}'); for(let k in p){ p[k] = p[k].map(r => { const ls=r.regex.lastIndexOf('/'); return {...r, regex: new RegExp(r.regex.substring(1,ls), r.regex.substring(ls+1)) }; }); } return p; })();
|
|
17
|
+
let NAME_STOP_LIST = new Set(JSON.parse('["case no","account no","client no","ref no","matter no","affected user","incident date","incident type","incident report","review period","review date","salary band","salary range","personal email","work email","business email","lead source","account exec","account executive","next appt","next appointment","last appt","chief complaint","portfolio value","portfolio manager","tax id","account number","employee id","social security","date of","date of birth","place of birth","hiring manager","direct report","team lead","team leader","job title","job function","pay grade","review type","performance review","annual review","invoice number","purchase order","order number","oak street","main street","high street","park avenue","first street","second street","third street","north avenue","south avenue","east side","west side","soc team","hr team","it team","qa team","ux team","new york","los angeles","san francisco","las vegas","united states","united kingdom","north america","south america","senior analyst","senior engineer","senior manager","senior consultant","junior analyst","junior engineer","junior developer","vice president","chief executive","chief officer","account manager","project manager","product manager","security officer","compliance officer","general hospital","medical center","urgent care","primary care","read more","learn more","click here","sign up","log in","privacy policy","terms of","terms of service","true positive","false positive","open source","private identifiers","data privacy","system instruction","system instructions","critical","instruction","user data","protected user","privacy scrubber","end of","thank you","best regards","kind regards","warm regards","yours truly","sincerely yours","good morning","good afternoon","good evening","hello world","hello there","supreme court","high court","district court","civil court","non disclosure","data protection","intellectual property","trade secret","force majeure","habeas corpus","amicus curiae","board member","board meeting","general assembly","first name","last name","middle name","full name","email address","phone number","cell phone","home phone","zip code","postal code","page number","section one","table contents","table of","figure one","marketing department","sales department","engineering team","product team","customer support","human resources","public relations","artificial intelligence","machine learning","deep learning","large language","operating system","source code","user interface","web browser","pull request","merge request","commit message","code review","cloud computing","database schema","blood pressure","heart rate","chief physician","treating physician","health care","healthcare provider","medical record","grade a","grade b","grade c","grade d","grade f","version 1","version 2","version 3","version 4","version 5","step 1","step 2","step 3","step 4","step 5","page 1","page 2","page 3","page 4","page 5","cs101","course cs101","january","february","march","april","may","june","july","august","september","october","november","december","monday","tuesday","wednesday","thursday","friday","saturday","sunday","yesterday","tomorrow","today","last week","next month","early morning","late night","microsoft office","google workspace","slack channel","zoom meeting","teams call","amazon prime","netflix show","youtube video","twitter post","linkedin profile","central park","white house","grand canyon","mount everest","pacific ocean","atlantic ocean","silicon valley","wall street","fifth avenue","times square","golden gate","empire state","statue liberty","tower bridge","big ben","eiffel tower","great wall","london bridge","san jose","las vegas","mexico city","hong kong","san diego","software development","user experience","front end","back end","full stack","web application","mobile app","desktop app","cloud services","data analytics","cyber security","network security","information security","incident response","disaster recovery","business continuity","risk management","quality assurance","user acceptance","beta test","release candidate","version control","continuous integration","continuous deployment","agile scrum","kanban board","sprint planning","daily standup","product backlog","user story","acceptance criteria","technical debt","codebase","repository","branch name","tag version","hot fix","patch note","legal counsel","general counsel","human capital","talent acquisition","employee benefits","payroll services","stock options","health insurance","retirement plan","vacation time","sick leave","performance bonus","commission structure","sales target","market share","brand identity","marketing campaign","ad spend","click rate","conversion rate","customer lifetime","churn rate","user engagement","social media","content strategy","search engine","local search","organic traffic","paid search","email marketing","press release","case study","white paper","user guide","help center","frequently asked","support ticket","service level","response time","resolution time","customer satisfaction","net promoter","user feedback","product feature","roadmap item","beta program","early access","invite only","project status","meeting notes","call transcript","action items","follow up","best practices","success stories","class name","function name","variable name","database table","schema name","index name","query result","error message","warning message","log entry","debug log","stack trace","staff member","team member","board member","board meeting","committee member","executive board","email us","contact us","about us","sign in","sign out","quarterly results","strategic planning","market research","customer base","privacy settings","account settings","security settings","download now","free trial","limited time","copyright protected","all rights","rights reserved","credit score","monthly rent","lease application","property address","reference number","additional identifier","lease agreement","hiring review","candidate name","lighting","keyboard","creating","building","training","planning","starting","painting","printing","returned","released","required","accepted","imported","services","products","accounts","settings","partners","keywords","keystone","keyspace","keynotes","keychain"]'));
|
|
18
|
+
let JARGON_WORDS = new Set(JSON.parse('["step","page","grade","version","course","class","follow","chapter","lesson","unit","marketing","manager","specialist","science","administration","university","skills","leadership","communication","working","proficiency","decision","driven","experience","summary","bachelor","ads","solutions","positioning","acquisition","strategy","research","database","forecast","interest","prepared","merchant","document","feedback","template","campaign","partners","settings","keystone"]'));
|
|
19
|
+
let NOT_NAME_WORDS = new Set(JSON.parse('["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","hello","hi","hey","dear","greetings","summary","experience","education","skills","languages","project","history","background","objective","profile","awards","honors","certifications","publications","interests","references","manager","director","specialist","analyst","engineer","developer","consultant","officer","representative","agent","lead","leader","president","coordinator","admin","administrator","executive","founder","partner","intern","trainee","advisor","head","vp","chief","marketing","sales","engineering","finance","accounting","legal","operations","support","recruiting","talent","acquisition","compliance","security","technical","development","product","design","creative","strategy","planning","analytics","science","business","administration","google","ads","analytics","meta","hubspot","crm","salesforce","wordpress","mailchimp","adobe","figma","canva","slack","zoom","teams","microsoft","office","excel","word","powerpoint","notion","jira","confluence","github","gitlab","aws","azure","cloud","database","sql","python","java","javascript","html","css","react","node","api","saas","b2b","b2c","url","domain","website","app","application","software","email","phone","contact","address","bachelor","master","doctor","associate","degree","university","college","school","institute","academy","graduated","major","minor","gpa","cum","laude","honors","deans","list","scholarship","results","driven","oriented","expert","professional","proven","track","record","creative","excellent","communication","verbal","written","native","fluent","bilingual","working","proficiency","strategic","interpersonal","teamwork","organizational","detail","analytical","results-driven","data-driven","customer-centric","detail-oriented","cross-functional","self-motivated","time-management","problem-solving","fast-paced","year-over-year","trust","trustee","co-trustee","settlor","grantor","beneficiary","agreement","will","estate","witness","declaration","signatory","testator","notary","commission","county","state","court","article","section","paragraph","schedule","exhibit","amendment","addendum","power","attorney","guardian","executor","administrator","survivor","predecessor","successor","whereof","hereby","thereby","herein","therein","witnesseth","whereas","therefore","now","dated","effective","california","texas","florida","york","illinois","pennsylvania","ohio","georgia","michigan","carolina","virginia","washington","arizona","massachusetts","tennessee","indiana","maryland","missouri","wisconsin","colorado","minnesota","alabama","louisiana","kentucky","oregon","oklahoma","connecticut","utah","iowa","nevada","arkansas","mississippi","kansas","new mexico","nebraska","idaho","hawaii","maine","new hampshire","rhode island","montana","delaware","south dakota","north dakota","alaska","vermont","wyoming"]'));
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runtime Initialization for dynamic rule synchronization.
|
|
24
|
+
* @param {Object} config - { regexes, profiles, names }
|
|
25
|
+
*/
|
|
26
|
+
function init(config) {
|
|
27
|
+
if (!config) return;
|
|
28
|
+
|
|
29
|
+
// Re-hydrate regex objects if they came from JSON storage as strings
|
|
30
|
+
const hydrateRegex = (rule) => {
|
|
31
|
+
if (rule && typeof rule.regex === 'string' && rule.regex.startsWith('/')) {
|
|
32
|
+
try {
|
|
33
|
+
const lastSlash = rule.regex.lastIndexOf('/');
|
|
34
|
+
const pattern = rule.regex.substring(1, lastSlash);
|
|
35
|
+
const flags = rule.regex.substring(lastSlash + 1);
|
|
36
|
+
return { ...rule, regex: new RegExp(pattern, flags) };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('PS: Failed to hydrate regex:', rule.regex, e);
|
|
39
|
+
return rule;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return rule;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (config.regexes) {
|
|
46
|
+
REGEX_RULES = config.regexes.map(hydrateRegex);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.profiles) {
|
|
50
|
+
PROFILE_RULES = {};
|
|
51
|
+
for (const [profile, rules] of Object.entries(config.profiles)) {
|
|
52
|
+
PROFILE_RULES[profile] = rules.map(hydrateRegex);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.names) NAME_STOP_LIST = new Set(config.names);
|
|
57
|
+
|
|
58
|
+
if (typeof window !== 'undefined' && window.PrivacyScrubberCore) {
|
|
59
|
+
window.PrivacyScrubberCore.isInitialized = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Stitcher runs for ALL profiles — LEGAL_STARTERS guard prevents false positives.
|
|
64
|
+
|
|
65
|
+
// Signal A: line ends with an ALL-CAPS name fragment (caps word or initial+dot)
|
|
66
|
+
// e.g. "KKKKK I." or "JOHNSON" or "KKKKK"
|
|
67
|
+
const RE_LINE_ENDS_AS_NAME = /(?:^|[ ,])[A-Z]{2,}(?:[A-Z'-]*[A-Z])?(?:\s+[A-Z]\.)*\s*$/m;
|
|
68
|
+
|
|
69
|
+
// Signal B: next line starts with a surname — either:
|
|
70
|
+
// Mixed-case: e.g. "Mmmmmmm, of Branford" (PDF mixed-caps rendering)
|
|
71
|
+
// ALL-CAPS: e.g. "MMMMMM, of Branford" (PDF all-caps rendering)
|
|
72
|
+
const RE_LINE_STARTS_AS_SURNAME = /^(?:[A-Z][a-z]{2,}(?:[A-Za-z'-]*)?|[A-Z]{3,}(?:[A-Z'-]*)?)(?:[,\s]|$)/;
|
|
73
|
+
|
|
74
|
+
// Guard: if next line starts with one of these words, it is a legal/structural paragraph
|
|
75
|
+
// starter — NOT a surname. Skip stitching to avoid false positives.
|
|
76
|
+
const LEGAL_STARTERS = new Set([
|
|
77
|
+
// Mixed-case legal clause starters
|
|
78
|
+
'Wherein','That','This','The','Said','Dated','Whereas','Now','Therefore',
|
|
79
|
+
'Hereby','Herein','Hereto','Herewith','Hereafter','Upon','Pursuant',
|
|
80
|
+
'Between','Among','Under','Within','Without','During','After','Before',
|
|
81
|
+
'Each','Any','All','Such','Both','Either','Neither','No','Not','If','When',
|
|
82
|
+
// ALL-CAPS equivalents (trust/deed documents often use all-caps paragraphs)
|
|
83
|
+
'WHEREIN','THAT','THIS','WHEREAS','NOW','THEREFORE','HEREBY','HEREIN',
|
|
84
|
+
'PURSUANT','BETWEEN','AMONG','UNDER','DURING','AFTER','BEFORE',
|
|
85
|
+
'TRUST','AGREEMENT','DEED','SCHEDULE','EXHIBIT','ARTICLE','SECTION'
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
function stitchOrphanedNameLines(text, profile) {
|
|
89
|
+
// Only run on multi-line text (single-line paste has nothing to stitch)
|
|
90
|
+
if (!text.includes('\n')) return text;
|
|
91
|
+
const lines = text.split('\n');
|
|
92
|
+
if (lines.length < 2) return text;
|
|
93
|
+
const result = [];
|
|
94
|
+
let i = 0;
|
|
95
|
+
while (i < lines.length) {
|
|
96
|
+
const line = lines[i];
|
|
97
|
+
const next = lines[i + 1];
|
|
98
|
+
if (next !== undefined && RE_LINE_ENDS_AS_NAME.test(line) && RE_LINE_STARTS_AS_SURNAME.test(next.trimStart())) {
|
|
99
|
+
// Guard: do not stitch if next line starts with a legal clause word
|
|
100
|
+
const firstWord = (next.trimStart().match(/^[A-Za-z]+/) || [''])[0];
|
|
101
|
+
if (LEGAL_STARTERS.has(firstWord)) {
|
|
102
|
+
result.push(line);
|
|
103
|
+
i++;
|
|
104
|
+
} else {
|
|
105
|
+
// Stitch the two lines — replace \n with a space
|
|
106
|
+
result.push(line.trimEnd() + ' ' + next.trimStart());
|
|
107
|
+
i += 2;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
result.push(line);
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Protect PII from plain text.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} text - Raw input text
|
|
121
|
+
* @param {Array<{label: string, pattern: string}>} [customRules=[]] - Optional PRO regex rules or exact text
|
|
122
|
+
* @returns {{ scrubbedText: string, tokenMap: Object, count: number }}
|
|
123
|
+
* scrubbedText: text with PII replaced by tokens like [NAME_1]
|
|
124
|
+
* tokenMap: { "[NAME_1]": "John Doe", ... } — for data reveal
|
|
125
|
+
* count: total number of items protected
|
|
126
|
+
* uniqueUnmasked: Set of unmasked values
|
|
127
|
+
*/
|
|
128
|
+
function scrubText(text, customRules = [], tokenLabelMap = {}, profile = 'General', existingSessionMap = {}) {
|
|
129
|
+
if (!text) return { scrubbedText: "", tokenMap: {}, count: 0, uniqueUnmasked: new Set() };
|
|
130
|
+
|
|
131
|
+
// Stitch PDF-split name fragments across line breaks (all profiles)
|
|
132
|
+
text = stitchOrphanedNameLines(text, profile);
|
|
133
|
+
|
|
134
|
+
// Protect system prompt from being scrubbed or counted
|
|
135
|
+
let extractedSystemPrompt = "";
|
|
136
|
+
let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
|
|
137
|
+
|
|
138
|
+
// Split attached table labels/headers (e.g. Sarah MitchellEmail: -> Sarah Mitchell Email:)
|
|
139
|
+
// Specifically matches a letter followed directly by field names and a colon
|
|
140
|
+
textToProcess = textToProcess.replace(/([a-zA-Z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
|
|
141
|
+
|
|
142
|
+
const sysMarker = "[Privacy Scrubber Mode]";
|
|
143
|
+
const oldMarker = "[SYSTEM INSTRUCTION: DATA PRIVACY MODE]";
|
|
144
|
+
const newMarker = "[Context: identifiers";
|
|
145
|
+
if (textToProcess.includes(sysMarker) || textToProcess.includes(oldMarker) || textToProcess.includes(newMarker)) {
|
|
146
|
+
const sysPromptRegex = /(?:----------------------\s*)?(?:\[SYSTEM INSTRUCTION: DATA PRIVACY MODE\]|\[Privacy Scrubber Mode\]|\[Context: identifiers)[\s\S]*/;
|
|
147
|
+
const match = textToProcess.match(sysPromptRegex);
|
|
148
|
+
if (match) {
|
|
149
|
+
extractedSystemPrompt = match[0];
|
|
150
|
+
textToProcess = textToProcess.replace(sysPromptRegex, '');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const sessionMap = {};
|
|
154
|
+
const counters = { NAME: 0, EMAIL: 0, PHONE: 0, ID: 0, FINANCIAL: 0, SECRET: 0, ADDRESS: 0, CUSTOM: 0, SSN: 0, DATE: 0, PHI: 0 };
|
|
155
|
+
const customCounters = {};
|
|
156
|
+
const plaintextToTokenAtlas = {};
|
|
157
|
+
|
|
158
|
+
// Seed counters and atlas from existing session map to prevent overwrites
|
|
159
|
+
if (existingSessionMap && typeof existingSessionMap === 'object') {
|
|
160
|
+
Object.entries(existingSessionMap).forEach(([token, value]) => {
|
|
161
|
+
const match = token.match(/^\[([A-Z_a-z0-9]+)_(\d+)\]$/);
|
|
162
|
+
if (match) {
|
|
163
|
+
const type = match[1];
|
|
164
|
+
const idx = parseInt(match[2], 10);
|
|
165
|
+
if (counters[type] !== undefined) {
|
|
166
|
+
counters[type] = Math.max(counters[type], idx);
|
|
167
|
+
} else {
|
|
168
|
+
customCounters[type] = Math.max(customCounters[type] || 0, idx);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
plaintextToTokenAtlas[value.toLowerCase()] = token;
|
|
172
|
+
plaintextToTokenAtlas[value] = token;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// Default labels
|
|
178
|
+
const labels = {
|
|
179
|
+
NAME: 'NAME', EMAIL: 'EMAIL', PHONE: 'PHONE', ID: 'ID',
|
|
180
|
+
FINANCIAL: 'FINANCIAL', SECRET: 'SECRET', ADDRESS: 'ADDRESS', CUSTOM: 'CUSTOM',
|
|
181
|
+
SSN: 'SSN', DATE: 'DATE', PHI: 'PHI',
|
|
182
|
+
...tokenLabelMap
|
|
183
|
+
};
|
|
184
|
+
let matches = [];
|
|
185
|
+
|
|
186
|
+
// Custom rules (PRO) — sorted longest pattern first to prevent partial matches
|
|
187
|
+
if (customRules && customRules.length > 0) {
|
|
188
|
+
const sorted = [...customRules].sort((a, b) => {
|
|
189
|
+
const patternA = typeof a === 'string' ? a : a.pattern;
|
|
190
|
+
const patternB = typeof b === 'string' ? b : b.pattern;
|
|
191
|
+
return patternB.length - patternA.length;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
sorted.forEach(cr => {
|
|
195
|
+
const pattern = typeof cr === 'string' ? cr : cr.pattern;
|
|
196
|
+
const label = typeof cr === 'string' ? 'CUSTOM' : (cr.label || 'CUSTOM');
|
|
197
|
+
|
|
198
|
+
let rx;
|
|
199
|
+
try {
|
|
200
|
+
// If it looks like exact text (no regex boundaries/quantifiers), escape it
|
|
201
|
+
const isExact = !/(\^|\$|\\[bBdDwWsS]|\[|\(|\{|\*|\+|\|)/.test(pattern);
|
|
202
|
+
if (isExact) {
|
|
203
|
+
const safe = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
204
|
+
rx = new RegExp(`(?:^|\\b)${safe}(?:\\b|$)`, 'gi');
|
|
205
|
+
} else {
|
|
206
|
+
rx = new RegExp(pattern, 'gi');
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.warn('Invalid custom regex pattern:', pattern);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let m;
|
|
214
|
+
// Prevent infinite loops from 0-length regex matches
|
|
215
|
+
if (rx.test('')) return;
|
|
216
|
+
rx.lastIndex = 0;
|
|
217
|
+
|
|
218
|
+
while ((m = rx.exec(textToProcess)) !== null) {
|
|
219
|
+
matches.push({ start: m.index, end: m.index + m[0].length, value: m[0], type: 'CUSTOM', customLabel: label });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Combine built-in regex rules with profile rules.
|
|
225
|
+
// PROFILE_ALIAS_MAP: normalizes popup.html dropdown values to PROFILE_RULES keys.
|
|
226
|
+
// Popup has display names like 'Medical', 'Engineering', 'Bizops' etc. — these must
|
|
227
|
+
// map to the canonical keys used in PROFILE_RULES (loaded from ps_rules_cache).
|
|
228
|
+
// Any unrecognized profile silently falls back to General (base REGEX_RULES only).
|
|
229
|
+
const PROFILE_ALIAS_MAP = {
|
|
230
|
+
// Healthcare
|
|
231
|
+
'medical': 'medical',
|
|
232
|
+
'healthcare': 'medical',
|
|
233
|
+
'health': 'medical',
|
|
234
|
+
'pharma': 'pharma',
|
|
235
|
+
// Engineering / Dev
|
|
236
|
+
'engineering': 'engineering',
|
|
237
|
+
'dev': 'engineering',
|
|
238
|
+
'tech': 'tech',
|
|
239
|
+
// Finance
|
|
240
|
+
'finance': 'finance',
|
|
241
|
+
'bizops': 'bizops',
|
|
242
|
+
'sales': 'sales',
|
|
243
|
+
'wealthmgmt': 'wealthmgmt',
|
|
244
|
+
'insurance': 'insurance',
|
|
245
|
+
'accounting': 'accounting',
|
|
246
|
+
// Legal
|
|
247
|
+
'legal': 'legal',
|
|
248
|
+
'compliance': 'compliance',
|
|
249
|
+
'ccpa': 'ccpa',
|
|
250
|
+
// HR
|
|
251
|
+
'hr': 'hr',
|
|
252
|
+
// Security
|
|
253
|
+
'security': 'security',
|
|
254
|
+
// Marketing / Support
|
|
255
|
+
'marketing': 'marketing',
|
|
256
|
+
'support': 'support',
|
|
257
|
+
};
|
|
258
|
+
let activeRules = [...REGEX_RULES];
|
|
259
|
+
if (profile && profile.toLowerCase() !== 'general') {
|
|
260
|
+
const rawKey = profile.toLowerCase();
|
|
261
|
+
// Resolve alias (undefined = key not in map at all → treat as General)
|
|
262
|
+
const resolvedKey = PROFILE_ALIAS_MAP.hasOwnProperty(rawKey)
|
|
263
|
+
? PROFILE_ALIAS_MAP[rawKey]
|
|
264
|
+
: rawKey; // pass-through for direct keys like 'health', 'finance', 'hr', 'legal', 'dev'
|
|
265
|
+
if (resolvedKey && PROFILE_RULES[resolvedKey]) {
|
|
266
|
+
activeRules = [...PROFILE_RULES[resolvedKey], ...activeRules];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
activeRules.forEach(rule => {
|
|
272
|
+
// Apply Detection Profiles Logic
|
|
273
|
+
const isDevProfile = (profile === 'Dev' || profile.toLowerCase() === 'engineering' || profile.toLowerCase() === 'security');
|
|
274
|
+
if (isDevProfile && rule.type === 'NAME') return; // Dev/Security profiles don't protect function names
|
|
275
|
+
|
|
276
|
+
rule.regex.lastIndex = 0;
|
|
277
|
+
let m;
|
|
278
|
+
|
|
279
|
+
while ((m = rule.regex.exec(textToProcess)) !== null) {
|
|
280
|
+
let matchedText = m[0];
|
|
281
|
+
let start = m.index;
|
|
282
|
+
let end = m.index + matchedText.length;
|
|
283
|
+
|
|
284
|
+
if (rule.type === 'NAME') {
|
|
285
|
+
let words = matchedText.split(/[ \t\xA0]+/);
|
|
286
|
+
|
|
287
|
+
// Trim leading stop words/jargon words
|
|
288
|
+
while (words.length >= 2 && (NOT_NAME_WORDS.has(words[0].toLowerCase()) || JARGON_WORDS.has(words[0].toLowerCase()))) {
|
|
289
|
+
const removedWord = words.shift();
|
|
290
|
+
const prefixLength = removedWord.length + (matchedText.slice(removedWord.length).match(/^[ \t\xA0]+/)?.[0]?.length || 0);
|
|
291
|
+
start += prefixLength;
|
|
292
|
+
matchedText = matchedText.slice(prefixLength);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Trim trailing stop words/jargon words
|
|
296
|
+
while (words.length >= 2 && (NOT_NAME_WORDS.has(words[words.length - 1].toLowerCase()) || JARGON_WORDS.has(words[words.length - 1].toLowerCase()))) {
|
|
297
|
+
const removedWord = words.pop();
|
|
298
|
+
const suffixLength = removedWord.length + (matchedText.slice(0, matchedText.length - removedWord.length).match(/[ \t\xA0]+$/)?.[0]?.length || 0);
|
|
299
|
+
end -= suffixLength;
|
|
300
|
+
matchedText = matchedText.slice(0, matchedText.length - suffixLength);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (words.length < 2) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Final validation: check if any remaining words are stop/jargon words
|
|
308
|
+
if (words.some(w => NOT_NAME_WORDS.has(w.toLowerCase()) || JARGON_WORDS.has(w.toLowerCase()))) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const val = matchedText.toLowerCase().trim();
|
|
312
|
+
if (NAME_STOP_LIST.has(val)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
matches.push({ start, end, value: matchedText, type: rule.type });
|
|
317
|
+
} else {
|
|
318
|
+
const val = matchedText.toLowerCase().trim();
|
|
319
|
+
if (NAME_STOP_LIST.has(val)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const words = val.split(/\s+/);
|
|
323
|
+
if (words.some(w => JARGON_WORDS.has(w))) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
matches.push({ start, end, value: matchedText, type: rule.type });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/* --- Dynamic Name Learning Phase --- */
|
|
332
|
+
const learnedNames = new Set();
|
|
333
|
+
matches.forEach(m => {
|
|
334
|
+
if (m.type === 'NAME') {
|
|
335
|
+
const nameWords = m.value.split(/[^\p{L}'-]+/u);
|
|
336
|
+
nameWords.forEach(w => {
|
|
337
|
+
if (w && w.length >= 2 && /^\p{Lu}/u.test(w)) {
|
|
338
|
+
const wl = w.toLowerCase();
|
|
339
|
+
if (!NOT_NAME_WORDS.has(wl) && !JARGON_WORDS.has(wl) && !NAME_STOP_LIST.has(wl)) {
|
|
340
|
+
learnedNames.add(w);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (learnedNames.size > 0) {
|
|
348
|
+
learnedNames.forEach(name => {
|
|
349
|
+
const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
350
|
+
const rx = new RegExp(`(?<=^|[^\\p{L}\\p{N}_])${safeName}(?=[^\\p{L}\\p{N}_]|$)`, 'gu');
|
|
351
|
+
let m;
|
|
352
|
+
while ((m = rx.exec(text)) !== null) {
|
|
353
|
+
const exists = matches.some(existing => existing.start <= m.index && existing.end >= m.index + m[0].length);
|
|
354
|
+
if (!exists) {
|
|
355
|
+
matches.push({ start: m.index, end: m.index + m[0].length, value: m[0], type: 'NAME' });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Sort by start, prefer longer matches, remove overlaps
|
|
362
|
+
|
|
363
|
+
matches.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
364
|
+
let filtered = [];
|
|
365
|
+
let lastEnd = 0;
|
|
366
|
+
matches.forEach(m => {
|
|
367
|
+
if (m.start >= lastEnd) { filtered.push(m); lastEnd = m.end; }
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Assign tokens in left-to-right order
|
|
371
|
+
|
|
372
|
+
const uniqueUnmasked = new Set();
|
|
373
|
+
|
|
374
|
+
filtered.forEach(m => {
|
|
375
|
+
// Deduplication Check: Reuse tokens for identical values (case-insensitive for core types)
|
|
376
|
+
const matchKey = m.type === 'CUSTOM' || m.type === 'ID' || m.type === 'SECRET' ? m.value : m.value.toLowerCase();
|
|
377
|
+
|
|
378
|
+
uniqueUnmasked.add(matchKey);
|
|
379
|
+
|
|
380
|
+
if (plaintextToTokenAtlas[matchKey]) {
|
|
381
|
+
m.token = plaintextToTokenAtlas[matchKey];
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Generate unique token using custom labels if provided
|
|
386
|
+
const label = m.customLabel || labels[m.type] || m.type;
|
|
387
|
+
const isBuiltIn = counters[m.type] !== undefined;
|
|
388
|
+
|
|
389
|
+
if (isBuiltIn && !m.customLabel) {
|
|
390
|
+
// Built-in type (NAME, EMAIL, PHONE, etc.) — use seeded counters[] for collision prevention
|
|
391
|
+
counters[m.type]++;
|
|
392
|
+
m.token = `[${label}_${counters[m.type]}]`;
|
|
393
|
+
} else {
|
|
394
|
+
// Custom label (PRO rule) — use customCounters[]
|
|
395
|
+
customCounters[label] = (customCounters[label] || 0) + 1;
|
|
396
|
+
m.token = `[${label}_${customCounters[label]}]`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
plaintextToTokenAtlas[matchKey] = m.token;
|
|
400
|
+
sessionMap[m.token] = m.value;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Replace from end → start (preserves string indices while mutating result)
|
|
404
|
+
const rightToLeft = [...filtered].sort((a, b) => b.start - a.start);
|
|
405
|
+
let result = textToProcess;
|
|
406
|
+
rightToLeft.forEach(m => {
|
|
407
|
+
result = result.substring(0, m.start) + m.token + result.substring(m.end);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
scrubbedText: extractedSystemPrompt + result,
|
|
412
|
+
tokenMap: sessionMap,
|
|
413
|
+
count: Object.keys(sessionMap).length,
|
|
414
|
+
uniqueUnmasked: uniqueUnmasked
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const LABEL_ALIASES = {
|
|
419
|
+
NAME: ['NAME', 'NAMES', 'USERNAME', 'USER_NAME', 'CLIENTNAME', 'CLIENT_NAME', 'CANDIDATE_NAME', 'FULL_NAME', 'FIRSTNAME', 'FIRST_NAME', 'LASTNAME', 'LAST_NAME', 'SURNAME', 'ИМЯ', 'ИМЕНА', 'ПОЛЬЗОВАТЕЛЬ', 'ФИО', 'КЛИЕНТ', 'NOMBRE', 'NOMBRES', 'USUARIO', 'CLIENTE', 'NOM', 'NOMS', 'UTILISATEUR', 'NAME', 'NAMEN', 'BENUTZER', 'KUNDE', 'NOME', 'COGNOME', 'UTENTE', 'NAAM', 'GEBRUIKER', 'KLANT'],
|
|
420
|
+
EMAIL: ['EMAIL', 'EMAILS', 'EMAILADDR', 'EMAIL_ADDR', 'EMAILADDRESS', 'EMAIL_ADDRESS', 'EMAIL_ADR', 'MAIL', 'MAILS', 'ПОЧТА', 'ЭЛ_ПОЧТА', 'АДРЕС_ПОЧТЫ', 'МЕЙЛ', 'МАЙЛ', 'CORREO', 'COURRIEL', 'CORREO_ELECTRONICO', 'MEL'],
|
|
421
|
+
PHONE: ['PHONE', 'PHONES', 'PHONENUM', 'PHONE_NUM', 'PHONENUMBER', 'PHONE_NUMBER', 'TEL', 'TELS', 'TELEPHONE', 'TELEPHONES', 'MOBILE', 'CELL', 'ТЕЛЕФОН', 'ТЕЛЕФОНЫ', 'НОМЕР_ТЕЛЕФОНА', 'НОМЕР', 'MOVIL', 'PORTABLE', 'HANDY', 'TELEFONI', 'CELLULARE'],
|
|
422
|
+
ID: ['ID', 'IDS', 'IDNUM', 'ID_NUM', 'IDNUMBER', 'ID_NUMBER', 'IDENTIFIER', 'IDENTIFIERS', 'PASSPORT', 'SSN', 'EIN', 'TAXID', 'TAX_ID', 'LICENSE', 'LICENSE_PLATE', 'ИД', 'ИДЕНТИФИКАТОР', 'ПАСПОРТ', 'СНИЛС', 'ИНН', 'IDENTIFICADOR', 'PASAPORTE', 'IDENTIFIANT', 'PASSEPORT', 'IDENTIFIKATOR', 'PASS', 'IDENTIFICATORE', 'PASSAPORTO'],
|
|
423
|
+
FINANCIAL: ['FINANCIAL', 'FINANCIALS', 'MONEY', 'AMOUNT', 'PRICE', 'COST', 'CARD', 'CREDITCARD', 'DEBITCARD', 'ACCOUNT', 'IBAN', 'BIC', 'ДЕНЬГИ', 'СУММА', 'КАРТА', 'СЧЕТ', 'БАНК', 'DINERO', 'CANTIDAD', 'TARJETA', 'CUENTA', 'ARGENT', 'MONTANT', 'COMPTE', 'GELD', 'BETRAG', 'KONTO'],
|
|
424
|
+
ADDRESS: ['ADDRESS', 'ADDRESSES', 'STREET', 'STREET_ADDRESS', 'CITY', 'STATE', 'ZIP', 'ZIPCODE', 'ZIP_CODE', 'COUNTRY', 'LOCATION', 'АДРЕС', 'АДРЕСА', 'УЛИЦА', 'ГОРОД', 'СТРАНА', 'DIRECCION', 'DIRECCIONES', 'CALLE', 'CIUDAD', 'PAIS', 'ADRESSE', 'ADRESSES', 'RUE', 'VILLE', 'STRASSE', 'STADT', 'LAND'],
|
|
425
|
+
DATE: ['DATE', 'DATES', 'BIRTHDAY', 'DOB', 'ДАТА', 'ДАТЫ', 'ДЕНЬ_РОЖДЕНИЯ', 'FECHA', 'FECHAS', 'CUMPLEANOS', 'ANNIVERSAIRE', 'DATUM', 'DATEN', 'GEBURTSTAG'],
|
|
426
|
+
PHI: ['PHI', 'MRN', 'NHS', 'HEALTH', 'MEDICAL', 'PATIENT', 'МЕД', 'ПАЦИЕНТ', 'PACIENTE'],
|
|
427
|
+
SECRET: ['SECRET', 'SECRETS', 'KEY', 'KEYS', 'TOKEN', 'TOKENS', 'PASSWORD', 'PASSWORDS', 'AUTH', 'APIKEY', 'API_KEY', 'КЛЮЧ', 'КЛЮЧИ', 'ПАРОЛЬ', 'ПАРОЛИ', 'ТОКЕН', 'CLAVE', 'CONTRASENA', 'CLE', 'MOT_DE_PASSE', 'SCHLUESSEL', 'PASSWORT'],
|
|
428
|
+
CUSTOM: ['CUSTOM', 'CUSTOMS', 'RULE', 'RULES', 'КАСТОМ', 'ПРАВИЛО']
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
function getLabelAliases(label) {
|
|
432
|
+
const upper = label.toUpperCase();
|
|
433
|
+
if (LABEL_ALIASES[upper]) {
|
|
434
|
+
return LABEL_ALIASES[upper];
|
|
435
|
+
}
|
|
436
|
+
const aliases = new Set([label, upper, label.toLowerCase()]);
|
|
437
|
+
aliases.add(label.replace(/_/g, ' '));
|
|
438
|
+
aliases.add(label.replace(/_/g, '-'));
|
|
439
|
+
aliases.add(label.replace(/ /g, '_'));
|
|
440
|
+
aliases.add(label.replace(/-/g, '_'));
|
|
441
|
+
return Array.from(aliases);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function buildRestorationRegexAndRules(tokenMap) {
|
|
445
|
+
const keys = Object.keys(tokenMap);
|
|
446
|
+
if (keys.length === 0) {
|
|
447
|
+
return { compositeRegex: null, looseRules: [] };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const sortedKeys = [...keys].sort((a, b) => {
|
|
451
|
+
const innerA = a.replace(/^\[|\]$/g, '');
|
|
452
|
+
const innerB = b.replace(/^\[|\]$/g, '');
|
|
453
|
+
const matchA = innerA.match(/^([A-Za-z_0-9]+?)[-_]?(\d+)$/);
|
|
454
|
+
const matchB = innerB.match(/^([A-Za-z_0-9]+?)[-_]?(\d+)$/);
|
|
455
|
+
|
|
456
|
+
if (matchA && matchB) {
|
|
457
|
+
const idxA = parseInt(matchA[2], 10);
|
|
458
|
+
const idxB = parseInt(matchB[2], 10);
|
|
459
|
+
const labelA = matchA[1];
|
|
460
|
+
const labelB = matchB[1];
|
|
461
|
+
|
|
462
|
+
if (idxA !== idxB) {
|
|
463
|
+
return idxB - idxA;
|
|
464
|
+
}
|
|
465
|
+
if (labelA.length !== labelB.length) {
|
|
466
|
+
return labelB.length - labelA.length;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return b.length - a.length;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const looseRules = [];
|
|
473
|
+
const regexParts = [];
|
|
474
|
+
|
|
475
|
+
sortedKeys.forEach(k => {
|
|
476
|
+
const inner = k.replace(/^\[|\]$/g, '');
|
|
477
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_]?(\d+)$/);
|
|
478
|
+
if (match) {
|
|
479
|
+
const label = match[1];
|
|
480
|
+
const baseIndex = parseInt(match[2], 10);
|
|
481
|
+
const aliases = getLabelAliases(label);
|
|
482
|
+
|
|
483
|
+
const escapedAliases = aliases.map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
484
|
+
const aliasesGroup = `(?:${escapedAliases.join('|')})`;
|
|
485
|
+
|
|
486
|
+
const looseRegex = '\\[?\\s*' + aliasesGroup + '[-_\\s]*0*' + baseIndex + '\\s*\\]?';
|
|
487
|
+
looseRules.push({ patternStr: looseRegex, originalKey: k });
|
|
488
|
+
regexParts.push(looseRegex);
|
|
489
|
+
} else {
|
|
490
|
+
const safe = k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
491
|
+
looseRules.push({ patternStr: safe, originalKey: k });
|
|
492
|
+
regexParts.push(safe);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const compositeRegex = new RegExp('(?<=^|[^a-zA-Z0-9_А-Яа-яЁё])(' + regexParts.join('|') + ')(?=$|[^a-zA-Z0-9_А-Яа-яЁё])', 'gi');
|
|
497
|
+
|
|
498
|
+
return { compositeRegex, looseRules };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Clean AI prompt prefix (e.g., "Claude responded:", "ChatGPT:") from the text.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} text
|
|
505
|
+
* @returns {string}
|
|
506
|
+
*/
|
|
507
|
+
function cleanAIPromptPrefix(text) {
|
|
508
|
+
if (!text) return "";
|
|
509
|
+
let cleaned = text.replace(/^\s*(Claude responded|Claude|ChatGPT|Gemini|Grok|DeepSeek|Kimi|Copilot|Assistant|User)\s*(?::|\bsaid\b|\bresponded\b)\s*/i, "");
|
|
510
|
+
cleaned = cleaned.replace(/^\s*Edit\s*\n+/i, "");
|
|
511
|
+
cleaned = cleaned.replace(/\s*\bEdit\s+in\s+a\s+page\b\s*$/i, "");
|
|
512
|
+
return cleaned;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Reverse-protect: replace tokens in AI response with originals from tokenMap.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} aiResponse - Text containing tokens like [NAME_1]
|
|
519
|
+
* @param {Object} tokenMap - { "[NAME_1]": "John Doe", ... }
|
|
520
|
+
* @returns {{ restoredText: string, restoredCount: number }}
|
|
521
|
+
*/
|
|
522
|
+
function unscrubText(aiResponse, tokenMap) {
|
|
523
|
+
let text = cleanAIPromptPrefix(aiResponse);
|
|
524
|
+
let restoredCount = 0;
|
|
525
|
+
|
|
526
|
+
const { compositeRegex, looseRules } = buildRestorationRegexAndRules(tokenMap);
|
|
527
|
+
if (compositeRegex) {
|
|
528
|
+
text = text.replace(compositeRegex, (match) => {
|
|
529
|
+
restoredCount++;
|
|
530
|
+
let origKey = match;
|
|
531
|
+
for (const rule of looseRules) {
|
|
532
|
+
if (new RegExp('^' + rule.patternStr + '$', 'i').test(match)) {
|
|
533
|
+
origKey = rule.originalKey;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return tokenMap[origKey] || match;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { restoredText: text, restoredCount };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Reverse-protect with HTML highlighting: replace tokens in AI response with originals wrapped in span.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} aiResponse - Text containing tokens like [NAME_1]
|
|
548
|
+
* @param {Object} tokenMap - { "[NAME_1]": "John Doe", ... }
|
|
549
|
+
* @returns {{ restoredHTML: string, restoredCount: number }}
|
|
550
|
+
*/
|
|
551
|
+
function unscrubTextAsHTML(aiResponse, tokenMap) {
|
|
552
|
+
let restoredCount = 0;
|
|
553
|
+
|
|
554
|
+
const cleanResponse = cleanAIPromptPrefix(aiResponse);
|
|
555
|
+
// ALWAYS escape HTML first — even with empty tokenMap — to prevent XSS from AI-generated content
|
|
556
|
+
let text = cleanResponse.replace(/[&<>'"]/g, c => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c] || c));
|
|
557
|
+
|
|
558
|
+
const { compositeRegex, looseRules } = buildRestorationRegexAndRules(tokenMap);
|
|
559
|
+
if (compositeRegex) {
|
|
560
|
+
text = text.replace(compositeRegex, (match) => {
|
|
561
|
+
restoredCount++;
|
|
562
|
+
let origKey = match;
|
|
563
|
+
for (const rule of looseRules) {
|
|
564
|
+
if (new RegExp('^' + rule.patternStr + '$', 'i').test(match)) {
|
|
565
|
+
origKey = rule.originalKey;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const rawVal = tokenMap[origKey] || match;
|
|
570
|
+
const safeVal = rawVal.replace(/[&<>'"]/g, c => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c] || c));
|
|
571
|
+
return `<span class="ps-restored-data" title="Restored by PrivacyScrubber" style="border-bottom: 2px dashed #10b981; color: inherit; cursor: help; padding-bottom: 1px; font-weight: 500; text-shadow: 0 0 5px rgba(16, 185, 129, 0.2);">${safeVal}</span>`;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { restoredHTML: text, restoredCount };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Expose standalone hydrateRegex for unit testing
|
|
579
|
+
function hydrateRegex(rule) {
|
|
580
|
+
if (rule && typeof rule.regex === 'string' && rule.regex.startsWith('/')) {
|
|
581
|
+
try {
|
|
582
|
+
const lastSlash = rule.regex.lastIndexOf('/');
|
|
583
|
+
const pattern = rule.regex.substring(1, lastSlash);
|
|
584
|
+
const flags = rule.regex.substring(lastSlash + 1);
|
|
585
|
+
return { ...rule, regex: new RegExp(pattern, flags) };
|
|
586
|
+
} catch (e) {
|
|
587
|
+
console.error('PS: Failed to hydrate regex:', rule.regex, e);
|
|
588
|
+
return rule;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return rule;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function showTeamsPassphraseModal(onSaveCallback) {
|
|
595
|
+
const existing = document.getElementById('ps-teams-modal');
|
|
596
|
+
if (existing) existing.remove();
|
|
597
|
+
|
|
598
|
+
const overlay = document.createElement('div');
|
|
599
|
+
overlay.id = 'ps-teams-modal';
|
|
600
|
+
Object.assign(overlay.style, {
|
|
601
|
+
position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
|
|
602
|
+
background: 'rgba(2,6,23,0.8)', backdropFilter: 'blur(10px)',
|
|
603
|
+
zIndex: '2147483647', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
604
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const modal = document.createElement('div');
|
|
608
|
+
Object.assign(modal.style, {
|
|
609
|
+
background: 'rgba(15,23,42,0.9)', border: '1px solid rgba(59,130,246,0.3)',
|
|
610
|
+
borderRadius: '16px', padding: '24px', width: '380px', maxWidth: '90%',
|
|
611
|
+
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5), 0 0 20px rgba(59,130,246,0.1)',
|
|
612
|
+
color: '#f8fafc', display: 'flex', flexDirection: 'column', gap: '16px',
|
|
613
|
+
animation: 'ps-slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1)'
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (!document.getElementById('ps-modal-styles')) {
|
|
617
|
+
const style = document.createElement('style');
|
|
618
|
+
style.id = 'ps-modal-styles';
|
|
619
|
+
style.textContent = `
|
|
620
|
+
@keyframes ps-slide-up { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
621
|
+
.ps-modal-input { width: 100%; box-sizing: border-box; background: #020617; border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 12px; border-radius: 8px; font-size: 14px; outline: none; transition: all 0.2s; }
|
|
622
|
+
.ps-modal-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.2); }
|
|
623
|
+
.ps-modal-btn { flex: 1; padding: 10px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; }
|
|
624
|
+
.ps-modal-btn.primary { background: linear-gradient(135deg, #06b6d4, #3b82f6); color: white; box-shadow: 0 4px 12px rgba(59,130,246,0.3); }
|
|
625
|
+
.ps-modal-btn.primary:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
|
626
|
+
.ps-modal-btn.primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; filter: none; }
|
|
627
|
+
.ps-modal-btn.secondary { background: rgba(255,255,255,0.05); color: #cbd5e1; border: 1px solid rgba(255,255,255,0.1); }
|
|
628
|
+
.ps-modal-btn.secondary:hover { background: rgba(255,255,255,0.1); }
|
|
629
|
+
`;
|
|
630
|
+
document.head.appendChild(style);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const header = document.createElement('div');
|
|
634
|
+
Object.assign(header.style, { display: 'flex', alignItems: 'center', gap: '12px' });
|
|
635
|
+
|
|
636
|
+
const headerIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
637
|
+
headerIcon.setAttribute("width", "24"); headerIcon.setAttribute("height", "24"); headerIcon.setAttribute("viewBox", "0 0 24 24");
|
|
638
|
+
headerIcon.setAttribute("fill", "none"); headerIcon.setAttribute("stroke", "#06b6d4"); headerIcon.setAttribute("stroke-width", "2");
|
|
639
|
+
const hp1 = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
640
|
+
hp1.setAttribute("d", "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2");
|
|
641
|
+
headerIcon.appendChild(hp1);
|
|
642
|
+
const hc1 = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
643
|
+
hc1.setAttribute("cx", "9"); hc1.setAttribute("cy", "7"); hc1.setAttribute("r", "4");
|
|
644
|
+
headerIcon.appendChild(hc1);
|
|
645
|
+
|
|
646
|
+
const title = document.createElement('h3');
|
|
647
|
+
title.textContent = 'TEAMS Passphrase';
|
|
648
|
+
Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: '600' });
|
|
649
|
+
|
|
650
|
+
header.appendChild(headerIcon);
|
|
651
|
+
header.appendChild(title);
|
|
652
|
+
|
|
653
|
+
const desc = document.createElement('p');
|
|
654
|
+
desc.textContent = 'Secure Session Sharing requires a shared key. Set your team\'s passphrase to encrypt the data locally before sharing.';
|
|
655
|
+
Object.assign(desc.style, { margin: '0', fontSize: '13px', color: '#94a3b8', lineHeight: '1.5' });
|
|
656
|
+
|
|
657
|
+
const inputContainer = document.createElement('div');
|
|
658
|
+
const passInput = document.createElement('input');
|
|
659
|
+
passInput.type = 'password';
|
|
660
|
+
passInput.id = 'ps-teams-pass-input';
|
|
661
|
+
passInput.className = 'ps-modal-input';
|
|
662
|
+
passInput.placeholder = 'Configure Passphrase (min 8 chars)';
|
|
663
|
+
|
|
664
|
+
const strengthBar = document.createElement('div');
|
|
665
|
+
strengthBar.id = 'ps-teams-strength-bar';
|
|
666
|
+
Object.assign(strengthBar.style, { height: '4px', borderRadius: '2px', background: 'rgba(255,255,255,0.05)', marginTop: '8px', overflow: 'hidden' });
|
|
667
|
+
const strengthFill = document.createElement('div');
|
|
668
|
+
strengthFill.id = 'ps-teams-strength-fill';
|
|
669
|
+
Object.assign(strengthFill.style, { height: '100%', width: '0%', transition: 'all 0.3s' });
|
|
670
|
+
strengthBar.appendChild(strengthFill);
|
|
671
|
+
|
|
672
|
+
const strengthLabel = document.createElement('div');
|
|
673
|
+
strengthLabel.id = 'ps-teams-strength-label';
|
|
674
|
+
Object.assign(strengthLabel.style, { fontSize: '10px', textTransform: 'uppercase', fontWeight: 'bold', marginTop: '4px', textAlign: 'center' });
|
|
675
|
+
strengthLabel.textContent = '\u00A0';
|
|
676
|
+
|
|
677
|
+
const errorMsg = document.createElement('div');
|
|
678
|
+
errorMsg.id = 'ps-teams-error';
|
|
679
|
+
Object.assign(errorMsg.style, { color: '#ef4444', fontSize: '12px', marginTop: '4px', display: 'none', textAlign: 'center' });
|
|
680
|
+
errorMsg.textContent = 'Passphrase is too weak. Mix characters & letters.';
|
|
681
|
+
|
|
682
|
+
inputContainer.appendChild(passInput);
|
|
683
|
+
inputContainer.appendChild(strengthBar);
|
|
684
|
+
inputContainer.appendChild(strengthLabel);
|
|
685
|
+
inputContainer.appendChild(errorMsg);
|
|
686
|
+
|
|
687
|
+
const btnGroup = document.createElement('div');
|
|
688
|
+
Object.assign(btnGroup.style, { display: 'flex', gap: '10px', marginTop: '4px' });
|
|
689
|
+
const cancelBtn = document.createElement('button');
|
|
690
|
+
cancelBtn.id = 'ps-teams-cancel';
|
|
691
|
+
cancelBtn.className = 'ps-modal-btn secondary';
|
|
692
|
+
cancelBtn.textContent = 'Cancel';
|
|
693
|
+
const saveBtn = document.createElement('button');
|
|
694
|
+
saveBtn.id = 'ps-teams-save';
|
|
695
|
+
saveBtn.className = 'ps-modal-btn primary';
|
|
696
|
+
saveBtn.disabled = true;
|
|
697
|
+
saveBtn.textContent = 'Save & Encrypt';
|
|
698
|
+
btnGroup.appendChild(cancelBtn);
|
|
699
|
+
btnGroup.appendChild(saveBtn);
|
|
700
|
+
|
|
701
|
+
// SECURITY: Opt-in persistence removed to strictly enforce ZTDS (no local storage for plaintext keys).
|
|
702
|
+
|
|
703
|
+
const footer = document.createElement('div');
|
|
704
|
+
Object.assign(footer.style, { textAlign: 'center', marginTop: '4px' });
|
|
705
|
+
const learnLink = document.createElement('a');
|
|
706
|
+
learnLink.href = 'https://privacyscrubber.com/teams';
|
|
707
|
+
learnLink.target = '_blank';
|
|
708
|
+
Object.assign(learnLink.style, { color: '#60a5fa', fontSize: '12px', textDecoration: 'none' });
|
|
709
|
+
learnLink.textContent = 'Learn about TEAMS Cryptography →';
|
|
710
|
+
footer.appendChild(learnLink);
|
|
711
|
+
|
|
712
|
+
modal.appendChild(header);
|
|
713
|
+
modal.appendChild(desc);
|
|
714
|
+
modal.appendChild(inputContainer);
|
|
715
|
+
modal.appendChild(btnGroup);
|
|
716
|
+
modal.appendChild(footer);
|
|
717
|
+
|
|
718
|
+
overlay.appendChild(modal);
|
|
719
|
+
document.body.appendChild(overlay);
|
|
720
|
+
|
|
721
|
+
passInput.focus();
|
|
722
|
+
|
|
723
|
+
passInput.addEventListener('input', () => {
|
|
724
|
+
const p = passInput.value;
|
|
725
|
+
let score = 0;
|
|
726
|
+
if (p.length >= 8) score++;
|
|
727
|
+
if (p.length >= 14) score++;
|
|
728
|
+
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) score++;
|
|
729
|
+
if (/[0-9]/.test(p)) score++;
|
|
730
|
+
if (/[^A-Za-z0-9]/.test(p)) score++;
|
|
731
|
+
score = Math.min(score, 4);
|
|
732
|
+
|
|
733
|
+
const levels = [
|
|
734
|
+
{ pct: '0%', color: 'transparent', text: '' },
|
|
735
|
+
{ pct: '20%', color: '#ef4444', text: 'Weak' },
|
|
736
|
+
{ pct: '40%', color: '#fbbf24', text: 'Fair' },
|
|
737
|
+
{ pct: '60%', color: '#facc15', text: 'Good' },
|
|
738
|
+
{ pct: '80%', color: '#4ade80', text: 'Strong' },
|
|
739
|
+
{ pct: '100%', color: '#10b981', text: 'Great' },
|
|
740
|
+
];
|
|
741
|
+
|
|
742
|
+
const level = (p.length === 0) ? levels[0] : levels[score];
|
|
743
|
+
strengthFill.style.width = level.pct;
|
|
744
|
+
strengthFill.style.background = level.color;
|
|
745
|
+
strengthLabel.textContent = level.text || '\u00A0';
|
|
746
|
+
strengthLabel.style.color = level.color;
|
|
747
|
+
|
|
748
|
+
if (score >= 2) {
|
|
749
|
+
saveBtn.disabled = false;
|
|
750
|
+
errorMsg.style.display = 'none';
|
|
751
|
+
} else {
|
|
752
|
+
saveBtn.disabled = true;
|
|
753
|
+
if (p.length > 0) errorMsg.style.display = 'block';
|
|
754
|
+
else errorMsg.style.display = 'none';
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const close = () => {
|
|
759
|
+
overlay.style.opacity = '0';
|
|
760
|
+
setTimeout(() => overlay.remove(), 200);
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
cancelBtn.addEventListener('click', close);
|
|
764
|
+
saveBtn.addEventListener('click', () => {
|
|
765
|
+
if (!saveBtn.disabled) {
|
|
766
|
+
const val = passInput.value.trim();
|
|
767
|
+
close();
|
|
768
|
+
if (onSaveCallback) onSaveCallback({
|
|
769
|
+
passphrase: val,
|
|
770
|
+
shouldClearSession: false,
|
|
771
|
+
shouldSaveLocal: false
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
passInput.addEventListener('keydown', (e) => {
|
|
776
|
+
if (e.key === 'Enter' && !saveBtn.disabled) {
|
|
777
|
+
saveBtn.click();
|
|
778
|
+
} else if (e.key === 'Escape') {
|
|
779
|
+
close();
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
overlay.addEventListener('click', (e) => {
|
|
783
|
+
if (e.target === overlay) close();
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function showInPageToast(text, variant = 'success') {
|
|
787
|
+
const existing = document.getElementById('ps-ext-toast');
|
|
788
|
+
if (existing) existing.remove();
|
|
789
|
+
|
|
790
|
+
const toast = document.createElement('div');
|
|
791
|
+
toast.id = 'ps-ext-toast';
|
|
792
|
+
toast.textContent = text;
|
|
793
|
+
|
|
794
|
+
const colors = {
|
|
795
|
+
success: { bg: 'rgba(16,185,129,0.12)', border: 'rgba(16,185,129,0.35)', color: '#10b981' },
|
|
796
|
+
info: { bg: 'rgba(59,130,246,0.12)', border: 'rgba(59,130,246,0.35)', color: '#60a5fa' },
|
|
797
|
+
warning: { bg: 'rgba(251,191,36,0.1)', border: 'rgba(251,191,36,0.3)', color: '#fbbf24' },
|
|
798
|
+
error: { bg: 'rgba(239,68,68,0.12)', border: 'rgba(239,68,68,0.35)', color: '#ef4444' },
|
|
799
|
+
};
|
|
800
|
+
const { bg, border, color } = colors[variant] || colors.info;
|
|
801
|
+
|
|
802
|
+
Object.assign(toast.style, {
|
|
803
|
+
position: 'fixed', top: '24px', left: '50%', transform: 'translateX(-50%)', zIndex: '2147483647',
|
|
804
|
+
background: bg, border: `1px solid ${border}`, color,
|
|
805
|
+
padding: '9px 18px', borderRadius: '12px', fontSize: '13px',
|
|
806
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
807
|
+
fontWeight: '500', boxShadow: '0 8px 30px rgba(0,0,0,0.3)',
|
|
808
|
+
backdropFilter: 'blur(8px)', transition: 'opacity 0.3s ease', opacity: '1',
|
|
809
|
+
textAlign: 'center'
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
document.body.appendChild(toast);
|
|
813
|
+
setTimeout(() => {
|
|
814
|
+
toast.style.opacity = '0';
|
|
815
|
+
setTimeout(() => toast.remove(), 350);
|
|
816
|
+
}, 2500);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Natively walks DOM to replace Tokens with Original text natively inside SPAs.
|
|
821
|
+
* Guaranteed safe for React/Angular since we only manipulate target TextNodes.
|
|
822
|
+
*/
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Natively walks DOM to replace Original Text with Tokens natively inside SPAs.
|
|
826
|
+
* Guaranteed safe for React/Angular input fields via MutationObserver syncs.
|
|
827
|
+
*/
|
|
828
|
+
function scrubDataInDOM(rootElement, customRules, globalTokenLabels, activeProfile, existingSessionMap, detectOnly = false) {
|
|
829
|
+
if (!rootElement) return null;
|
|
830
|
+
|
|
831
|
+
// ── TEXTAREA / INPUT fast path ────────────────────────────────────────
|
|
832
|
+
// Native <textarea> and <input> store their content in .value — it is
|
|
833
|
+
// NOT a child text node, so createTreeWalker finds nothing and always
|
|
834
|
+
// returns count=0. Handle them directly here.
|
|
835
|
+
if (rootElement.tagName === 'TEXTAREA' || rootElement.tagName === 'INPUT') {
|
|
836
|
+
const text = rootElement.value || '';
|
|
837
|
+
if (!text.trim()) return { count: 0, tokenMap: existingSessionMap || {} };
|
|
838
|
+
const result = scrubText(text, customRules, globalTokenLabels, activeProfile, existingSessionMap || {});
|
|
839
|
+
if (result.uniqueUnmasked.size > 0 && !detectOnly) {
|
|
840
|
+
rootElement.value = result.scrubbedText;
|
|
841
|
+
rootElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
842
|
+
}
|
|
843
|
+
return { count: result.uniqueUnmasked.size, tokenMap: { ...(existingSessionMap || {}), ...result.tokenMap } };
|
|
844
|
+
}
|
|
845
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
846
|
+
|
|
847
|
+
// ── DETECT ONLY fast path ─────────────────────────────────────────────
|
|
848
|
+
// If we only need to update the badge counter (no DOM mutation), evaluate
|
|
849
|
+
// the full text string at once. This bypasses text node splitting issues
|
|
850
|
+
// in ProseMirror/contenteditable and guarantees a 100% accurate count.
|
|
851
|
+
if (detectOnly) {
|
|
852
|
+
const fullText = rootElement.innerText || rootElement.textContent || '';
|
|
853
|
+
if (!fullText.trim()) return { count: 0, tokenMap: existingSessionMap || {} };
|
|
854
|
+
const fullResult = scrubText(fullText, customRules, globalTokenLabels, activeProfile, existingSessionMap || {});
|
|
855
|
+
return { count: fullResult.uniqueUnmasked.size, tokenMap: existingSessionMap || {} };
|
|
856
|
+
}
|
|
857
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
858
|
+
|
|
859
|
+
// ── ACTUAL DOM MUTATION (TreeWalker for AutoScrub) ────────────────────
|
|
860
|
+
|
|
861
|
+
const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, null, false);
|
|
862
|
+
const nodesToProcess = [];
|
|
863
|
+
let node;
|
|
864
|
+
while(node = walker.nextNode()) {
|
|
865
|
+
if (!node.nodeValue.trim()) continue;
|
|
866
|
+
nodesToProcess.push(node);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let globalTokenMap = { ...existingSessionMap };
|
|
870
|
+
let allUniqueUnmasked = new Set();
|
|
871
|
+
|
|
872
|
+
nodesToProcess.forEach(node => {
|
|
873
|
+
const originalText = node.nodeValue;
|
|
874
|
+
const result = scrubText(originalText, customRules, globalTokenLabels, activeProfile, globalTokenMap);
|
|
875
|
+
|
|
876
|
+
if (result.uniqueUnmasked.size > 0) {
|
|
877
|
+
if (!detectOnly && result.scrubbedText !== originalText) {
|
|
878
|
+
node.nodeValue = result.scrubbedText;
|
|
879
|
+
}
|
|
880
|
+
if (result.count > 0) {
|
|
881
|
+
globalTokenMap = { ...globalTokenMap, ...result.tokenMap };
|
|
882
|
+
}
|
|
883
|
+
result.uniqueUnmasked.forEach(k => allUniqueUnmasked.add(k));
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// Force React/ProseMirror to notice the change
|
|
888
|
+
if (allUniqueUnmasked.size > 0 && !detectOnly) {
|
|
889
|
+
rootElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
count: allUniqueUnmasked.size,
|
|
894
|
+
tokenMap: globalTokenMap
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const GLOBAL_ASSISTANT_SELECTORS = [
|
|
899
|
+
// ChatGPT
|
|
900
|
+
'[data-message-author-role="assistant"]',
|
|
901
|
+
'[data-message-author-role="model"]',
|
|
902
|
+
'[data-testid="chat-message-text"]',
|
|
903
|
+
'[data-testid="assistant-message"]',
|
|
904
|
+
'[data-is-streaming]',
|
|
905
|
+
'.agent-turn',
|
|
906
|
+
'div[role="article"].agent-turn',
|
|
907
|
+
// Claude
|
|
908
|
+
'model-response',
|
|
909
|
+
'.font-claude-message',
|
|
910
|
+
'.claude-artifact',
|
|
911
|
+
'[data-testid="artifact-renderer"]',
|
|
912
|
+
// Gemini
|
|
913
|
+
'.model-response-text__content',
|
|
914
|
+
'.response-content',
|
|
915
|
+
'message-content[is-response]',
|
|
916
|
+
// Grok (x.com)
|
|
917
|
+
'[data-testid="messageblock"]',
|
|
918
|
+
'[class*="GrokResponseMessage"]',
|
|
919
|
+
// DeepSeek
|
|
920
|
+
'.ds-markdown',
|
|
921
|
+
// Copilot — NEW React UI (copilot.microsoft.com as of 2025)
|
|
922
|
+
'[data-testid="ai-message"]',
|
|
923
|
+
'[class*="AIMessageContent"]',
|
|
924
|
+
'[class*="ResponseMessage"]',
|
|
925
|
+
'[class*="CopilotMessage"]',
|
|
926
|
+
'[class*="BotMessage"]',
|
|
927
|
+
// Copilot — legacy cib-serp Shadow DOM + Adaptive Cards
|
|
928
|
+
'.ac-textBlock',
|
|
929
|
+
'.cib-message-text',
|
|
930
|
+
'.cib-message',
|
|
931
|
+
'[class*="bot-message"]',
|
|
932
|
+
'[class*="copilot-message"]',
|
|
933
|
+
// Qwen / Tongyi
|
|
934
|
+
'.output-area',
|
|
935
|
+
// Perplexity
|
|
936
|
+
'[data-testid="answer"]',
|
|
937
|
+
// Generic article-based message wrappers (used by many platforms)
|
|
938
|
+
// Excluded from sidebar via nav/aside filter in gatherUniversalAIContext()
|
|
939
|
+
'div[role="article"]',
|
|
940
|
+
// Conservative generic fallbacks — nav/aside exclusion filter prevents sidebar matches
|
|
941
|
+
'.prose',
|
|
942
|
+
'.whitespace-pre-wrap',
|
|
943
|
+
'.message-content:not([contenteditable])',
|
|
944
|
+
'[class*="message-row"]',
|
|
945
|
+
'[class*="assistant"]',
|
|
946
|
+
'.markdown'
|
|
947
|
+
].join(', ');
|
|
948
|
+
|
|
949
|
+
function revealDataInDOM(tokenMap) {
|
|
950
|
+
const { compositeRegex, looseRules } = buildRestorationRegexAndRules(tokenMap);
|
|
951
|
+
if (!compositeRegex) {
|
|
952
|
+
if (typeof showInPageToast === 'function') showInPageToast("No protected session data to reveal.", "info");
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
let restoredCount = 0;
|
|
957
|
+
const nodesToReplace = [];
|
|
958
|
+
|
|
959
|
+
const ASSISTANT_SELECTORS = GLOBAL_ASSISTANT_SELECTORS;
|
|
960
|
+
|
|
961
|
+
function isPromptInput(el) {
|
|
962
|
+
if (!el) return false;
|
|
963
|
+
if (el.closest) {
|
|
964
|
+
return !!el.closest('[data-ps-attached="true"]');
|
|
965
|
+
}
|
|
966
|
+
let parent = el;
|
|
967
|
+
while (parent) {
|
|
968
|
+
if (parent.dataset && parent.dataset.psAttached === 'true') {
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
parent = parent.parentNode || parent.host;
|
|
972
|
+
}
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Shadow-DOM-aware assistant container detection:
|
|
977
|
+
// .closest() cannot cross shadow boundaries, so if it returns null
|
|
978
|
+
// we check whether the node lives inside a ShadowRoot whose host
|
|
979
|
+
// (or host ancestor) matches an AI response selector.
|
|
980
|
+
function isInsideAssistant(el) {
|
|
981
|
+
if (!el || !el.closest) return false;
|
|
982
|
+
if (el.closest(ASSISTANT_SELECTORS)) return true;
|
|
983
|
+
// Shadow DOM fallback
|
|
984
|
+
const root = el.getRootNode();
|
|
985
|
+
if (root instanceof ShadowRoot && root.host) {
|
|
986
|
+
// Check if the shadow host itself matches, or has an ancestor that does
|
|
987
|
+
if (root.host.matches && root.host.matches(ASSISTANT_SELECTORS)) return true;
|
|
988
|
+
if (root.host.closest && root.host.closest(ASSISTANT_SELECTORS)) return true;
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function walkTextNodes(root) {
|
|
994
|
+
if (!root) return;
|
|
995
|
+
|
|
996
|
+
// Handle elements with shadow roots if the root itself has one
|
|
997
|
+
if (root.shadowRoot) {
|
|
998
|
+
walkTextNodes(root.shadowRoot);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Standard node iteration
|
|
1002
|
+
let child = root.firstChild;
|
|
1003
|
+
while (child) {
|
|
1004
|
+
const next = child.nextSibling;
|
|
1005
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
1006
|
+
if (child.parentElement) {
|
|
1007
|
+
const tag = child.parentElement.tagName;
|
|
1008
|
+
if (!['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'NOSCRIPT'].includes(tag)) {
|
|
1009
|
+
// Determine if node is inside an AI assistant response container.
|
|
1010
|
+
// If so, ALWAYS process it — skip both inPromptInput and isInstructionNode checks.
|
|
1011
|
+
const isAssistant = isInsideAssistant(child.parentElement);
|
|
1012
|
+
|
|
1013
|
+
let shouldSkip = false;
|
|
1014
|
+
if (!isAssistant) {
|
|
1015
|
+
// Ignore user input areas where user types prompts actively.
|
|
1016
|
+
// Precise check via isPromptInput helper (looks for data-ps-attached attribute).
|
|
1017
|
+
const inPromptInput = isPromptInput(child.parentElement);
|
|
1018
|
+
if (inPromptInput) {
|
|
1019
|
+
shouldSkip = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
// Check if inside the instruction block itself.
|
|
1022
|
+
const parentTc = child.parentElement.textContent;
|
|
1023
|
+
if (parentTc && parentTc.length < 8000 && (parentTc.includes('[Privacy Scrubber Mode]') || parentTc.includes('[SYSTEM INSTRUCTION: DATA PRIVACY MODE]'))) {
|
|
1024
|
+
shouldSkip = true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
if (!shouldSkip) {
|
|
1031
|
+
compositeRegex.lastIndex = 0;
|
|
1032
|
+
const hasMatch = compositeRegex.test(child.nodeValue);
|
|
1033
|
+
if (hasMatch) {
|
|
1034
|
+
if (['PRE', 'CODE'].includes(tag) || (child.parentElement.closest && child.parentElement.closest('pre, code'))) {
|
|
1035
|
+
const { restoredText, restoredCount: count } = unscrubText(child.nodeValue, tokenMap);
|
|
1036
|
+
if (count > 0) {
|
|
1037
|
+
child.nodeValue = restoredText;
|
|
1038
|
+
restoredCount += count;
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
nodesToReplace.push(child);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
1048
|
+
const tag = child.tagName;
|
|
1049
|
+
if (!['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'NOSCRIPT'].includes(tag)) {
|
|
1050
|
+
if (tag === 'IFRAME') {
|
|
1051
|
+
try {
|
|
1052
|
+
const iframeDoc = child.contentDocument || child.contentWindow?.document;
|
|
1053
|
+
if (iframeDoc && iframeDoc.body) {
|
|
1054
|
+
walkTextNodes(iframeDoc.body);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
// ignore cross-origin
|
|
1058
|
+
}
|
|
1059
|
+
} else {
|
|
1060
|
+
// Check if inside an AI assistant response — always walk those.
|
|
1061
|
+
const isAssistantEl = isInsideAssistant(child);
|
|
1062
|
+
|
|
1063
|
+
let shouldSkipEl = false;
|
|
1064
|
+
if (!isAssistantEl) {
|
|
1065
|
+
const inPromptInput = isPromptInput(child);
|
|
1066
|
+
if (inPromptInput) {
|
|
1067
|
+
shouldSkipEl = true;
|
|
1068
|
+
}
|
|
1069
|
+
// NOTE: We intentionally do NOT check textContent for
|
|
1070
|
+
// '[Privacy Scrubber Mode]' at the element level because
|
|
1071
|
+
// textContent aggregates ALL descendant text. A parent
|
|
1072
|
+
// container (like <main>) would match the user prompt's
|
|
1073
|
+
// instruction block and skip the entire subtree including
|
|
1074
|
+
// AI responses. The instruction check is only safe at the
|
|
1075
|
+
// TEXT_NODE level where parentElement scope is narrow.
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (!shouldSkipEl) {
|
|
1079
|
+
walkTextNodes(child);
|
|
1080
|
+
if (child.shadowRoot) {
|
|
1081
|
+
walkTextNodes(child.shadowRoot);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
child = next;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
walkTextNodes(document.documentElement);
|
|
1092
|
+
|
|
1093
|
+
nodesToReplace.forEach(node => {
|
|
1094
|
+
const text = node.nodeValue;
|
|
1095
|
+
const fragment = document.createDocumentFragment();
|
|
1096
|
+
let lastIndex = 0;
|
|
1097
|
+
let match;
|
|
1098
|
+
|
|
1099
|
+
compositeRegex.lastIndex = 0;
|
|
1100
|
+
while ((match = compositeRegex.exec(text)) !== null) {
|
|
1101
|
+
if (match.index > lastIndex) {
|
|
1102
|
+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
|
|
1103
|
+
}
|
|
1104
|
+
let origKey = match[0];
|
|
1105
|
+
for (const rule of looseRules) {
|
|
1106
|
+
if (new RegExp('^' + rule.patternStr + '$', 'i').test(match[0])) {
|
|
1107
|
+
origKey = rule.originalKey;
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const rawVal = tokenMap[origKey] || match[0];
|
|
1112
|
+
|
|
1113
|
+
const span = document.createElement('span');
|
|
1114
|
+
span.className = 'ps-restored-data';
|
|
1115
|
+
span.title = 'Decrypted Locally. LLMs cannot see this.';
|
|
1116
|
+
Object.assign(span.style, {
|
|
1117
|
+
borderBottom: '2px dashed #10b981',
|
|
1118
|
+
color: '#10b981',
|
|
1119
|
+
cursor: 'help',
|
|
1120
|
+
paddingBottom: '1px',
|
|
1121
|
+
fontWeight: '500',
|
|
1122
|
+
position: 'relative',
|
|
1123
|
+
zIndex: '1',
|
|
1124
|
+
textShadow: '0 0 5px rgba(16, 185, 129, 0.2)'
|
|
1125
|
+
});
|
|
1126
|
+
span.textContent = rawVal;
|
|
1127
|
+
|
|
1128
|
+
fragment.appendChild(span);
|
|
1129
|
+
restoredCount++;
|
|
1130
|
+
lastIndex = compositeRegex.lastIndex;
|
|
1131
|
+
}
|
|
1132
|
+
if (lastIndex < text.length) {
|
|
1133
|
+
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (node.parentElement) {
|
|
1137
|
+
try {
|
|
1138
|
+
node.parentElement.replaceChild(fragment, node);
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
// Fail silently
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
if (restoredCount > 0) {
|
|
1146
|
+
if (typeof showInPageToast === 'function') showInPageToast(`✅ Decrypted Locally! Your data is now safe to copy.`, "success");
|
|
1147
|
+
} else {
|
|
1148
|
+
if (typeof showInPageToast === 'function') showInPageToast("No active tokens found on the screen.", "info");
|
|
1149
|
+
}
|
|
1150
|
+
return restoredCount;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function gatherUniversalAIContext() {
|
|
1154
|
+
try {
|
|
1155
|
+
// Helper function to recursively find elements matching selectors across shadow DOMs and same-origin iframes
|
|
1156
|
+
function querySelectorAllRecursive(root, selector) {
|
|
1157
|
+
// Single depth-first traversal that crosses shadow DOM boundaries.
|
|
1158
|
+
// Does NOT use root.querySelectorAll() to avoid duplicating shadow DOM results.
|
|
1159
|
+
const results = [];
|
|
1160
|
+
if (!root) return results;
|
|
1161
|
+
|
|
1162
|
+
function walk(node) {
|
|
1163
|
+
if (!node) return;
|
|
1164
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1165
|
+
try {
|
|
1166
|
+
if (node.matches && node.matches(selector)) results.push(node);
|
|
1167
|
+
} catch (e) {}
|
|
1168
|
+
// Recurse into shadow DOM (depth-first before light children)
|
|
1169
|
+
if (node.shadowRoot) walk(node.shadowRoot);
|
|
1170
|
+
// Recurse into same-origin iframes
|
|
1171
|
+
if (node.tagName === 'IFRAME') {
|
|
1172
|
+
try {
|
|
1173
|
+
const iframeDoc = node.contentDocument || node.contentWindow?.document;
|
|
1174
|
+
if (iframeDoc) walk(iframeDoc);
|
|
1175
|
+
} catch (e) {}
|
|
1176
|
+
return; // children handled above
|
|
1177
|
+
}
|
|
1178
|
+
} else if (node.nodeType !== Node.DOCUMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
|
|
1179
|
+
return; // text node, comment, etc
|
|
1180
|
+
}
|
|
1181
|
+
// Walk light DOM / document children
|
|
1182
|
+
let child = node.firstChild;
|
|
1183
|
+
while (child) { walk(child); child = child.nextSibling; }
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
walk(root);
|
|
1187
|
+
return results;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
// 1. Gather Input Text
|
|
1192
|
+
const inputs = querySelectorAllRecursive(document, 'textarea, div[contenteditable="true"], div.ProseMirror');
|
|
1193
|
+
// Find the active visible input, ignoring our own injected toolbar
|
|
1194
|
+
const activeInput = inputs.find(el => el.offsetParent !== null && el.offsetHeight > 0 && !el.closest('.ps-toolbar-container'));
|
|
1195
|
+
const inputText = activeInput ? (activeInput.value || activeInput.innerText || activeInput.textContent || "") : "";
|
|
1196
|
+
|
|
1197
|
+
// 2. Gather Output Text (Last AI Response)
|
|
1198
|
+
let outputText = "";
|
|
1199
|
+
|
|
1200
|
+
// Universal selectors covering major AI models + Claude Artifacts
|
|
1201
|
+
const knownSelectors = GLOBAL_ASSISTANT_SELECTORS;
|
|
1202
|
+
|
|
1203
|
+
const candidateNodes = querySelectorAllRecursive(document, knownSelectors);
|
|
1204
|
+
|
|
1205
|
+
// Filter out nodes that obviously belong to the user or sidebar/navigation
|
|
1206
|
+
const validResponses = Array.from(candidateNodes).filter(el => {
|
|
1207
|
+
// If it is inside a shadow DOM, offsetParent might be null, check offsetHeight/getBoundingClientRect
|
|
1208
|
+
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
1209
|
+
const isVisible = (el.offsetParent !== null && el.offsetHeight > 0) || (rect && rect.height > 0 && rect.width > 0);
|
|
1210
|
+
if (!isVisible) return false;
|
|
1211
|
+
|
|
1212
|
+
// SIDEBAR/NAV EXCLUSION: exclude elements inside navigation or sidebar areas.
|
|
1213
|
+
// This prevents Kimi sidebar, Grok left panel, and similar from being captured.
|
|
1214
|
+
if (el.closest) {
|
|
1215
|
+
if (el.closest('nav, aside, header')) return false;
|
|
1216
|
+
if (el.closest('[role="navigation"], [role="complementary"], [role="banner"]')) return false;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// QWEN THINKING FILTER: exclude Qwen/DeepSeek "Thinking completed" reasoning blocks.
|
|
1220
|
+
// These are collapsible sections shown before the actual response.
|
|
1221
|
+
if (el.closest) {
|
|
1222
|
+
if (el.closest('[class*="thinking"], [class*="reasoning"], [class*="Thinking"], [class*="chain-of-thought"]')) return false;
|
|
1223
|
+
if (el.classList.contains('thinking') || el.classList.contains('reasoning')) return false;
|
|
1224
|
+
}
|
|
1225
|
+
// Also exclude by text heuristic: element whose ONLY content is the thinking header
|
|
1226
|
+
{
|
|
1227
|
+
const rawText = (el.innerText || el.textContent || '').trim();
|
|
1228
|
+
if (rawText.toLowerCase() === 'thinking completed' || rawText.toLowerCase() === 'thinking...') return false;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// KEY EXCLUSION: elements that contain the active textarea are page/chat wrappers,
|
|
1232
|
+
// never AI responses. This is the definitive fix for Kimi (and similar platforms)
|
|
1233
|
+
// where a large wrapper div matches a generic selector and includes the entire
|
|
1234
|
+
// sidebar + chat area + user input field.
|
|
1235
|
+
if (activeInput && el.contains(activeInput)) return false;
|
|
1236
|
+
|
|
1237
|
+
// Exclude user messages that share generic classes like .message-content
|
|
1238
|
+
const role = el.getAttribute('data-message-author-role');
|
|
1239
|
+
if (role === 'user') return false;
|
|
1240
|
+
|
|
1241
|
+
// Exclude explicit user classes
|
|
1242
|
+
if (el.closest) {
|
|
1243
|
+
if (el.closest('.user-message') || el.classList.contains('user-message')) return false;
|
|
1244
|
+
if (el.closest('[data-testid="user-message"]')) return false;
|
|
1245
|
+
if (el.closest('[data-message-author-role="user"]')) return false;
|
|
1246
|
+
} else {
|
|
1247
|
+
let parent = el.parentNode;
|
|
1248
|
+
while (parent) {
|
|
1249
|
+
if (parent.classList && (parent.classList.contains('user-message') || parent.getAttribute('data-testid') === 'user-message')) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
parent = parent.parentNode || parent.host;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return true;
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
if (validResponses.length > 0) {
|
|
1260
|
+
// Return the text of the very last valid response on the page.
|
|
1261
|
+
// De-ancestor: if element A contains element B (both matched), prefer B (more specific).
|
|
1262
|
+
const leafResponses = validResponses.filter((el, _, arr) =>
|
|
1263
|
+
!arr.some(other => other !== el && el.contains(other))
|
|
1264
|
+
);
|
|
1265
|
+
const candidates = leafResponses.length > 0 ? leafResponses : validResponses;
|
|
1266
|
+
const lastLeaf = candidates[candidates.length - 1];
|
|
1267
|
+
|
|
1268
|
+
// MULTI-LEAF AGGREGATION (fixes Copilot partial response):
|
|
1269
|
+
// When multiple leaf elements exist, check if they share a close ancestor
|
|
1270
|
+
// within 2 DOM levels of the last leaf. If yes, use the ancestor's full text
|
|
1271
|
+
// (groups same-message paragraphs). Depth limit of 2 prevents merging Kimi's
|
|
1272
|
+
// separate user + AI message elements whose common ancestor is 4+ levels up.
|
|
1273
|
+
let found = false;
|
|
1274
|
+
if (candidates.length > 1) {
|
|
1275
|
+
let probe = lastLeaf.parentElement;
|
|
1276
|
+
for (let depth = 0; depth < 2 && probe; depth++) {
|
|
1277
|
+
if (activeInput && probe.contains(activeInput)) break;
|
|
1278
|
+
if (probe.tagName === 'BODY') break;
|
|
1279
|
+
const contained = candidates.filter(el => probe.contains(el));
|
|
1280
|
+
if (contained.length >= 2) {
|
|
1281
|
+
outputText = probe.innerText || probe.textContent || '';
|
|
1282
|
+
found = true;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
probe = probe.parentElement;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (!found) {
|
|
1289
|
+
outputText = lastLeaf.innerText || lastLeaf.textContent || '';
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
if (!outputText.trim()) {
|
|
1295
|
+
// Fallback: look for any element containing tokens, excluding prompt inputs and nav/sidebar areas
|
|
1296
|
+
const allElements = querySelectorAllRecursive(document, 'div, p, span, li, td, input, textarea, [role="textbox"]');
|
|
1297
|
+
const tokenContainingElements = allElements.filter(el => {
|
|
1298
|
+
// Exclude navigation/sidebar areas (same as primary filter)
|
|
1299
|
+
if (el.closest && el.closest('nav, aside, header, [role="navigation"], [role="complementary"], [role="banner"]')) return false;
|
|
1300
|
+
// Exclude page/chat wrappers that contain the active input
|
|
1301
|
+
if (activeInput && el.contains(activeInput)) return false;
|
|
1302
|
+
// Exclude prompt inputs (active ones with data-ps-attached attribute)
|
|
1303
|
+
let parent = el;
|
|
1304
|
+
while (parent) {
|
|
1305
|
+
if (parent.dataset && parent.dataset.psAttached === 'true') return false;
|
|
1306
|
+
parent = parent.parentNode || parent.host;
|
|
1307
|
+
}
|
|
1308
|
+
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
|
|
1309
|
+
const text = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') ? (el.value || "") : (el.innerText || el.textContent || "");
|
|
1310
|
+
return /\[[A-Z]+_[0-9]+\]/.test(text);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
let bestEl = null;
|
|
1314
|
+
let maxTokens = 0;
|
|
1315
|
+
let bestTextLength = 0;
|
|
1316
|
+
|
|
1317
|
+
for (const el of tokenContainingElements) {
|
|
1318
|
+
const text = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') ? (el.value || "") : (el.innerText || el.textContent || "");
|
|
1319
|
+
const matches = text.match(/\[[A-Z]+_[0-9]+\]/g) || [];
|
|
1320
|
+
const uniqueTokens = new Set(matches).size;
|
|
1321
|
+
if (uniqueTokens > maxTokens || (uniqueTokens === maxTokens && uniqueTokens > 0 && text.length > bestTextLength)) {
|
|
1322
|
+
maxTokens = uniqueTokens;
|
|
1323
|
+
bestTextLength = text.length;
|
|
1324
|
+
bestEl = el;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (bestEl) {
|
|
1328
|
+
outputText = (bestEl.tagName === 'INPUT' || bestEl.tagName === 'TEXTAREA') ? (bestEl.value || "") : (bestEl.innerText || bestEl.textContent || "");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return {
|
|
1333
|
+
inputText: inputText.trim(),
|
|
1334
|
+
outputText: cleanAIPromptPrefix(outputText).trim()
|
|
1335
|
+
};
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
console.error("[PrivacyScrubber] Universal Context gather failed:", e);
|
|
1338
|
+
return { inputText: "", outputText: "" };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Export to window for content scripts
|
|
1343
|
+
// Expose for OCR and advanced usage
|
|
1344
|
+
function getRules(profile = 'General') {
|
|
1345
|
+
let activeRules = [...REGEX_RULES];
|
|
1346
|
+
if (profile && profile.toLowerCase() !== 'general') {
|
|
1347
|
+
// Apply same alias map as scrubText() for consistency
|
|
1348
|
+
const ALIAS = {
|
|
1349
|
+
'medical': 'medical', 'healthcare': 'medical',
|
|
1350
|
+
'engineering': 'engineering', 'security': 'security',
|
|
1351
|
+
'bizops': 'finance', 'sales': 'finance',
|
|
1352
|
+
'compliance': 'legal',
|
|
1353
|
+
};
|
|
1354
|
+
const rawKey = profile.toLowerCase();
|
|
1355
|
+
const resolvedKey = ALIAS[rawKey] || rawKey;
|
|
1356
|
+
if (resolvedKey && PROFILE_RULES[resolvedKey]) {
|
|
1357
|
+
activeRules = [...PROFILE_RULES[resolvedKey], ...activeRules];
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return activeRules;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Show a right-click context menu for Profile Switching
|
|
1365
|
+
*/
|
|
1366
|
+
function showProfileMenu(e, anchorElement, onProfileChange) {
|
|
1367
|
+
// Remove existing menu if any
|
|
1368
|
+
let existing = document.getElementById('ps-profile-menu');
|
|
1369
|
+
if (existing) {
|
|
1370
|
+
existing.remove();
|
|
1371
|
+
return; // Act as a toggle
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const PROFILES = [
|
|
1375
|
+
{ id: 'General', label: 'General' },
|
|
1376
|
+
{ id: 'Engineering', label: 'Engineering' },
|
|
1377
|
+
{ id: 'Finance', label: 'Finance' },
|
|
1378
|
+
{ id: 'Legal', label: 'Legal' },
|
|
1379
|
+
{ id: 'Medical', label: 'Healthcare' },
|
|
1380
|
+
{ id: 'HR', label: 'HR' }
|
|
1381
|
+
];
|
|
1382
|
+
|
|
1383
|
+
chrome.storage.local.get(['ps_active_profile', 'ps_is_pro', 'ps_is_teams'], (data) => {
|
|
1384
|
+
const activeProfile = data.ps_active_profile || 'General';
|
|
1385
|
+
const isPro = data.ps_is_pro || data.ps_is_teams || false;
|
|
1386
|
+
|
|
1387
|
+
const menu = document.createElement('div');
|
|
1388
|
+
menu.id = 'ps-profile-menu';
|
|
1389
|
+
menu.className = 'ps-profile-menu';
|
|
1390
|
+
|
|
1391
|
+
// Positioning
|
|
1392
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
1393
|
+
// Try to position it below and to the left of the button
|
|
1394
|
+
menu.style.top = (rect.bottom + window.scrollY + 5) + 'px';
|
|
1395
|
+
menu.style.left = (rect.right + window.scrollX - 150) + 'px';
|
|
1396
|
+
|
|
1397
|
+
const header = document.createElement('div');
|
|
1398
|
+
header.className = 'ps-profile-menu-header';
|
|
1399
|
+
header.innerText = 'Detection Profile';
|
|
1400
|
+
menu.appendChild(header);
|
|
1401
|
+
|
|
1402
|
+
PROFILES.forEach(p => {
|
|
1403
|
+
const item = document.createElement('div');
|
|
1404
|
+
item.className = 'ps-profile-menu-item';
|
|
1405
|
+
if (p.id === activeProfile) item.classList.add('active');
|
|
1406
|
+
|
|
1407
|
+
let labelText = p.label;
|
|
1408
|
+
item.innerText = labelText;
|
|
1409
|
+
|
|
1410
|
+
if (!isPro && p.id !== 'General') {
|
|
1411
|
+
item.style.opacity = '0.5';
|
|
1412
|
+
item.style.cursor = 'not-allowed';
|
|
1413
|
+
item.title = 'PRO Feature';
|
|
1414
|
+
item.innerText = labelText + ' 🔒';
|
|
1415
|
+
} else if (p.id === activeProfile) {
|
|
1416
|
+
item.innerText = labelText + ' ✓';
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
item.addEventListener('click', (ev) => {
|
|
1420
|
+
ev.stopPropagation();
|
|
1421
|
+
if (!isPro && p.id !== 'General') {
|
|
1422
|
+
if (typeof showInPageToast === 'function') {
|
|
1423
|
+
showInPageToast('Specialized Profiles require PRO upgrade.', 'warning');
|
|
1424
|
+
}
|
|
1425
|
+
menu.remove();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
chrome.storage.local.set({ ps_active_profile: p.id }, () => {
|
|
1430
|
+
if (typeof showInPageToast === 'function') {
|
|
1431
|
+
showInPageToast(`Profile Switched to ${p.label}`, 'success');
|
|
1432
|
+
}
|
|
1433
|
+
menu.remove();
|
|
1434
|
+
|
|
1435
|
+
// Trigger immediate re-scrub if callback provided
|
|
1436
|
+
if (typeof onProfileChange === 'function') {
|
|
1437
|
+
onProfileChange(p.id);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
menu.appendChild(item);
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
document.body.appendChild(menu);
|
|
1446
|
+
|
|
1447
|
+
// Close on click outside
|
|
1448
|
+
const closeMenu = (ev) => {
|
|
1449
|
+
if (!menu.contains(ev.target) && ev.target !== anchorElement) {
|
|
1450
|
+
menu.remove();
|
|
1451
|
+
document.removeEventListener('click', closeMenu);
|
|
1452
|
+
document.removeEventListener('contextmenu', closeMenu);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
setTimeout(() => {
|
|
1457
|
+
document.addEventListener('click', closeMenu);
|
|
1458
|
+
document.addEventListener('contextmenu', closeMenu);
|
|
1459
|
+
}, 50);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* exportSessionToFile — Downloads the current session map as a JSON file
|
|
1465
|
+
*/
|
|
1466
|
+
function exportSessionToFile(sessionMap, filename = 'ps-session', extraContext = null) {
|
|
1467
|
+
if (!sessionMap || Object.keys(sessionMap).length === 0) {
|
|
1468
|
+
if (window.showInPageToast) window.showInPageToast("No active session data to export.", "info");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
try {
|
|
1472
|
+
const data = {
|
|
1473
|
+
version: "1.5.2",
|
|
1474
|
+
timestamp: new Date().toISOString(),
|
|
1475
|
+
sessionMap: sessionMap,
|
|
1476
|
+
context: extraContext
|
|
1477
|
+
};
|
|
1478
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
1479
|
+
const url = URL.createObjectURL(blob);
|
|
1480
|
+
const a = document.createElement('a');
|
|
1481
|
+
a.href = url;
|
|
1482
|
+
a.download = `${filename}-${Date.now()}.json`;
|
|
1483
|
+
document.body.appendChild(a);
|
|
1484
|
+
a.click();
|
|
1485
|
+
document.body.removeChild(a);
|
|
1486
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
1487
|
+
if (window.showInPageToast) window.showInPageToast("✓ Session exported successfully", "success");
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
console.error("PrivacyScrubber Export Error:", e);
|
|
1490
|
+
if (window.showInPageToast) window.showInPageToast("❌ Export failed.", "error");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
if (typeof window !== 'undefined') {
|
|
1495
|
+
window.PrivacyScrubberCore = {
|
|
1496
|
+
init,
|
|
1497
|
+
scrubText,
|
|
1498
|
+
unscrubText,
|
|
1499
|
+
unscrubTextAsHTML,
|
|
1500
|
+
showTeamsPassphraseModal,
|
|
1501
|
+
scrubDataInDOM,
|
|
1502
|
+
revealDataInDOM,
|
|
1503
|
+
gatherUniversalAIContext,
|
|
1504
|
+
exportSessionToFile,
|
|
1505
|
+
getRules,
|
|
1506
|
+
showProfileMenu,
|
|
1507
|
+
cleanAIPromptPrefix,
|
|
1508
|
+
isInitialized: false
|
|
1509
|
+
};
|
|
1510
|
+
window.showInPageToast = showInPageToast;
|
|
1511
|
+
|
|
1512
|
+
// v1.4.4: Automatic re-hydration on boot from local storage cache
|
|
1513
|
+
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
|
1514
|
+
chrome.storage.local.get(['ps_rules_cache'], (data) => {
|
|
1515
|
+
if (data.ps_rules_cache) {
|
|
1516
|
+
init(data.ps_rules_cache);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
} else if (typeof module !== 'undefined' && module.exports) {
|
|
1521
|
+
module.exports = {
|
|
1522
|
+
init,
|
|
1523
|
+
scrubText,
|
|
1524
|
+
unscrubText,
|
|
1525
|
+
getRules,
|
|
1526
|
+
unscrubTextAsHTML,
|
|
1527
|
+
hydrateRegex,
|
|
1528
|
+
cleanAIPromptPrefix
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
})();
|
|
1532
|
+
|