@privacyscrubber/sdk 2.4.8 → 2.4.9
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 +7 -1
- package/cli.js +1 -1
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +1 -1
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/index.cjs +89 -24
- package/index.d.ts +8 -0
- package/index.js +89 -24
- package/package.json +1 -1
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +2 -2
- package/scrubber-core.cjs +1 -1
package/README.md
CHANGED
|
@@ -207,7 +207,13 @@ const { scrubbedText, tokenMap } = sanitize(rawCrmPayload, {
|
|
|
207
207
|
],
|
|
208
208
|
|
|
209
209
|
// Exclude specific domain terms or false-positive nouns (case-insensitive; also prevents multi-word NAME matches containing these terms):
|
|
210
|
-
ignoreList: ['Materialmangel', 'Störgrund', 'Prüfstand', 'Linie 3']
|
|
210
|
+
ignoreList: ['Materialmangel', 'Störgrund', 'Prüfstand', 'Linie 3'],
|
|
211
|
+
|
|
212
|
+
// Optional: Entity-type allowlist filter (e.g., ['NAME'] or ['PERSON'] to mask ONLY person names while keeping batch numbers, order IDs, and phone/date fields readable):
|
|
213
|
+
// entities: ['NAME'],
|
|
214
|
+
|
|
215
|
+
// Optional: Enable higher-recall person-name detection in free text:
|
|
216
|
+
// highRecallNames: true
|
|
211
217
|
});
|
|
212
218
|
```
|
|
213
219
|
|
package/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "privacyscrubber"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.9"
|
|
8
8
|
description = "Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -13,7 +13,7 @@ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="privacyscrubber",
|
|
16
|
-
version="2.4.
|
|
16
|
+
version="2.4.9",
|
|
17
17
|
description="Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)",
|
|
18
18
|
long_description=long_description,
|
|
19
19
|
long_description_content_type="text/markdown",
|
package/index.cjs
CHANGED
|
@@ -39,7 +39,7 @@ const SDK_VERSION = (() => {
|
|
|
39
39
|
return pkgJson.version;
|
|
40
40
|
}
|
|
41
41
|
} catch (_) {}
|
|
42
|
-
return '2.4.
|
|
42
|
+
return '2.4.9';
|
|
43
43
|
})();
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -142,8 +142,9 @@ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
142
142
|
if (!t) continue;
|
|
143
143
|
const tokenStr = typeof t === 'string' ? t : ((t && (t.token || t.mask)) ? String(t.token || t.mask) : '');
|
|
144
144
|
if (!tokenStr) continue;
|
|
145
|
-
const match = tokenStr.match(/\[([A-
|
|
146
|
-
const
|
|
145
|
+
const match = tokenStr.match(/\[([A-Za-z0-9_]+)_\d+\]/);
|
|
146
|
+
const rawBase = match ? match[1] : tokenStr.replace(/\[|\]|<|>|\{|\}|__/g, '').replace(/_[0-9]+$/, '');
|
|
147
|
+
const baseType = (rawBase.replace(/[^A-Za-z0-9_]/g, '') || 'CUSTOM').toUpperCase();
|
|
147
148
|
entities[baseType] = (entities[baseType] || 0) + 1;
|
|
148
149
|
totalCount++;
|
|
149
150
|
}
|
|
@@ -153,7 +154,7 @@ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
153
154
|
let riskLevel = 'LOW EXPOSURE';
|
|
154
155
|
|
|
155
156
|
if (totalCount > 0) {
|
|
156
|
-
const hasHighRisk = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
157
|
+
const hasHighRisk = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'PHI', 'PRIVILEGE', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
157
158
|
if (hasHighRisk || types.length >= 3 || totalCount >= 10) {
|
|
158
159
|
riskLevel = 'CRITICAL (HIGH EXPOSURE)';
|
|
159
160
|
} else if (types.length >= 2 || totalCount >= 3) {
|
|
@@ -178,10 +179,10 @@ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
178
179
|
frameworksSet.add('PCI DSS v4.0');
|
|
179
180
|
frameworksSet.add('IRC § 7216 & GLBA');
|
|
180
181
|
}
|
|
181
|
-
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
182
|
+
if (types.some(t => ['PHI', 'MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
182
183
|
frameworksSet.add('HIPAA §164.514');
|
|
183
184
|
}
|
|
184
|
-
if (types.some(t => ['CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
185
|
+
if (types.some(t => ['PRIVILEGE', 'CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
185
186
|
frameworksSet.add('FRE 502 (Privilege)');
|
|
186
187
|
}
|
|
187
188
|
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN', 'CREDENTIAL'].includes(t))) {
|
|
@@ -191,6 +192,26 @@ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
191
192
|
frameworksSet.add('EU AI Act (Art. 50)');
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
const frameworksList = Array.from(frameworksSet);
|
|
196
|
+
const getFrameworkSlug = (name) => {
|
|
197
|
+
const lower = name.toLowerCase();
|
|
198
|
+
if (lower.includes('gdpr')) return 'gdpr';
|
|
199
|
+
if (lower.includes('ccpa')) return 'ccpa';
|
|
200
|
+
if (lower.includes('soc')) return 'soc2';
|
|
201
|
+
if (lower.includes('iso')) return 'iso27001';
|
|
202
|
+
if (lower.includes('pci')) return 'pci';
|
|
203
|
+
if (lower.includes('hipaa')) return 'hipaa';
|
|
204
|
+
if (lower.includes('nist')) return 'nist';
|
|
205
|
+
if (lower.includes('irc') || lower.includes('7216')) return 'irc7216';
|
|
206
|
+
if (lower.includes('fre') || lower.includes('502')) return 'fre502';
|
|
207
|
+
if (lower.includes('ai act')) return 'euaiact';
|
|
208
|
+
return lower.replace(/[^a-z0-9]/g, '');
|
|
209
|
+
};
|
|
210
|
+
const triggeredFrameworks = frameworksList.map(name => ({
|
|
211
|
+
name,
|
|
212
|
+
id: getFrameworkSlug(name)
|
|
213
|
+
}));
|
|
214
|
+
|
|
194
215
|
const ipHoldings = {
|
|
195
216
|
standard: 'IETF draft-sibiryakov-ztds-protocol-01',
|
|
196
217
|
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
@@ -262,10 +283,12 @@ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
262
283
|
|
|
263
284
|
return {
|
|
264
285
|
totalCount,
|
|
286
|
+
totalDetected: totalCount,
|
|
265
287
|
entities,
|
|
266
288
|
types,
|
|
267
289
|
riskLevel,
|
|
268
|
-
frameworksList
|
|
290
|
+
frameworksList,
|
|
291
|
+
triggeredFrameworks,
|
|
269
292
|
auditScope,
|
|
270
293
|
statutoryCitations,
|
|
271
294
|
ipHoldings,
|
|
@@ -515,7 +538,7 @@ We request approval to acquire a **Developer SDK License** (\$299/month flat or
|
|
|
515
538
|
*/
|
|
516
539
|
const SDK_VERTICAL_DETECTORS = [
|
|
517
540
|
{ profile: 'medical', label: 'Medical PHI (HIPAA)', regex: /\b(?:Patient\s*(?:Name|ID|DOB|Record)|MRN\b|Medical\s+Record|Attending\s+Physician|Chief\s+Complaint|Diagnosis\s*:|ICD-10|HIPAA|Clinical\s+Intake|Discharge\s+Summary|MedPod|NPI\s*(?:#|No|:)?\s*\d{10}|Medicare\s*(?:ID|Beneficiary|Claim)|Medicaid\s*ID|Prescription|Rx\s*#|Blood\s+Type|Vital\s+Signs|Radiology|Lab\s+Results|EHR\b|EMR\b)\b/gi },
|
|
518
|
-
{ profile: 'accounting', label: 'Tax & Accounting', regex: /\b(?:Form\s*1040|Form\s*W-2|Form\s*1099|Adjusted\s+Gross\s+Income|Employer\s+Identification\s+Number|Schedule\s+C\b|Steuer-ID|USt-IdNr|Tax\s+Return|General\s+Ledger|Withholding\s+Allowance|Total\s+Tax\s+Liability)\b/gi },
|
|
541
|
+
{ profile: 'accounting', label: 'Tax & Accounting', regex: /\b(?:Form\s*1040|Form\s*W-2|Form\s*1099|Adjusted\s+Gross\s+Income|Assessable\s+Income|Employer\s+Identification\s+Number|Schedule\s+C\b|Steuer-ID|USt-IdNr|Tax\s+Return|General\s+Ledger|Withholding\s+Allowance|Total\s+Tax\s+Liability|ABN\b|TFN\b|Tax\s+File\s+Number|Australian\s+Business\s+Number|Business\s+Activity\s+Statement|PAYG\s+(?:Withholding|Instalment)|Franking\s+Credits?)\b/gi },
|
|
519
542
|
{ profile: 'wealthmgmt', label: 'Wealth Management', regex: /\b(?:Revocable\s+Living\s+Trust|Irrevocable\s+(?:Generation-Skipping\s+)?Trust|Grantor\s*:|Settlor\s*:|Trustee\s*:|Beneficiary\s+Designation|Roth\s+IRA|Asset\s+Allocation|FBO\b|CUSIP\s*:|Grantor\s+Retained\s+Annuity\s+Trust|AUM\s*:)\b/gi },
|
|
520
543
|
{ profile: 'legal', label: 'Legal & Contracts', regex: /\b(?:Non-Disclosure\s+Agreement|Attorney-Client\s+Privilege|Docket\s+No|Hereinafter\s+referred|Indemnification\s+Clause|Governing\s+Law\s+and\s+Jurisdiction|Plaintiff\b|Defendant\b|Subpoena\b|Deposition\b|State\s+Bar\s*#)\b/gi },
|
|
521
544
|
{ profile: 'finance', label: 'Finance & Banking', regex: /\b(?:Wire\s+Transfer\s+Authorization|SWIFT\s*\/?\s*BIC\s*:|IBAN\s*:\s*[A-Z]{2}\d{2}|ABA\s+Routing|ACH\s+Transfer|PCI-DSS|Primary\s+Account\s+Number)\b/gi },
|
|
@@ -616,7 +639,9 @@ function sanitize(text, options = {}) {
|
|
|
616
639
|
throwOnLimit = true,
|
|
617
640
|
onLimit = 'throw',
|
|
618
641
|
detectSecrets = false,
|
|
619
|
-
ignoreList = null
|
|
642
|
+
ignoreList = null,
|
|
643
|
+
entities = options.entityTypes || options.enabledEntities || null,
|
|
644
|
+
highRecallNames = Boolean(options.highRecallNames || options.nameRecall === 'high')
|
|
620
645
|
} = options;
|
|
621
646
|
|
|
622
647
|
const isAutoProfile = Boolean(
|
|
@@ -732,8 +757,23 @@ function sanitize(text, options = {}) {
|
|
|
732
757
|
}
|
|
733
758
|
}
|
|
734
759
|
|
|
760
|
+
let normalizedEntities = null;
|
|
761
|
+
if (entities) {
|
|
762
|
+
const rawArr = entities instanceof Set ? Array.from(entities) : (Array.isArray(entities) ? entities : [entities]);
|
|
763
|
+
normalizedEntities = rawArr.filter(Boolean).map(e => {
|
|
764
|
+
const u = String(e).trim().toUpperCase();
|
|
765
|
+
if (u === 'PERSON' || u === 'PERSONS' || u === 'PEOPLE' || u === 'NAMES') return 'NAME';
|
|
766
|
+
if (u === 'EMAILS') return 'EMAIL';
|
|
767
|
+
if (u === 'PHONES' || u === 'TELEPHONE') return 'PHONE';
|
|
768
|
+
if (u === 'IDS' || u === 'IDENTIFIER' || u === 'IDENTIFIERS') return 'ID';
|
|
769
|
+
if (u === 'ADDRESSES' || u === 'LOCATION' || u === 'LOCATIONS') return 'ADDRESS';
|
|
770
|
+
return u;
|
|
771
|
+
});
|
|
772
|
+
if (normalizedEntities.length === 0) normalizedEntities = null;
|
|
773
|
+
}
|
|
774
|
+
|
|
735
775
|
const activeRules = [...effectiveCustomRules];
|
|
736
|
-
if (effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) {
|
|
776
|
+
if ((effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) && (!normalizedEntities || normalizedEntities.includes('SECRET') || normalizedEntities.includes('CUSTOM'))) {
|
|
737
777
|
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
738
778
|
activeRules.push({
|
|
739
779
|
name: rule.name,
|
|
@@ -757,7 +797,9 @@ function sanitize(text, options = {}) {
|
|
|
757
797
|
effectiveProfile,
|
|
758
798
|
existingSessionMap,
|
|
759
799
|
isPro,
|
|
760
|
-
effectiveIgnoreList
|
|
800
|
+
effectiveIgnoreList,
|
|
801
|
+
normalizedEntities,
|
|
802
|
+
Boolean(highRecallNames)
|
|
761
803
|
);
|
|
762
804
|
|
|
763
805
|
const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
@@ -1085,6 +1127,8 @@ class PrivacyScrubberEngine {
|
|
|
1085
1127
|
this.generateReceipt = config.generateReceipt !== false;
|
|
1086
1128
|
this.sessionMap = { ...(config.initialSessionMap || {}) };
|
|
1087
1129
|
this.ignoreList = new Set(config.ignoreList || []);
|
|
1130
|
+
this.entities = config.entities || config.entityTypes || config.enabledEntities || null;
|
|
1131
|
+
this.highRecallNames = Boolean(config.highRecallNames || config.nameRecall === 'high');
|
|
1088
1132
|
this.maxSessionTokens = config.maxSessionTokens || 10000;
|
|
1089
1133
|
|
|
1090
1134
|
this.license = validateLicense(this.licenseKey);
|
|
@@ -1126,7 +1170,9 @@ class PrivacyScrubberEngine {
|
|
|
1126
1170
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
1127
1171
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
1128
1172
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
1129
|
-
ignoreList: mergedIgnoreList.size > 0 ? mergedIgnoreList : null
|
|
1173
|
+
ignoreList: mergedIgnoreList.size > 0 ? mergedIgnoreList : null,
|
|
1174
|
+
entities: options.entities || options.entityTypes || options.enabledEntities || this.entities,
|
|
1175
|
+
highRecallNames: options.highRecallNames !== undefined ? Boolean(options.highRecallNames) : (options.nameRecall === 'high' ? true : this.highRecallNames)
|
|
1130
1176
|
};
|
|
1131
1177
|
|
|
1132
1178
|
const result = sanitize(text, mergedOptions);
|
|
@@ -2666,13 +2712,13 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
2666
2712
|
|
|
2667
2713
|
for (const k of sortedKeys) {
|
|
2668
2714
|
const val = tokenMap[k];
|
|
2669
|
-
const inner = k.replace(
|
|
2715
|
+
const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
|
|
2670
2716
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
2671
2717
|
|
|
2672
2718
|
if (match) {
|
|
2673
2719
|
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
2674
2720
|
const idx = match[2];
|
|
2675
|
-
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
2721
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
2676
2722
|
patterns.push(patternStr);
|
|
2677
2723
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
2678
2724
|
lookup.set(normKey, val);
|
|
@@ -2702,8 +2748,8 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
2702
2748
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
2703
2749
|
} else {
|
|
2704
2750
|
const inner = coreMatch
|
|
2705
|
-
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
2706
|
-
.replace(/^[
|
|
2751
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
2752
|
+
.replace(/^[*`_~\s]+|[.,;:!?*`_~\s]+$/g, '')
|
|
2707
2753
|
.trim();
|
|
2708
2754
|
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
2709
2755
|
if (m) {
|
|
@@ -2816,18 +2862,37 @@ class StreamingDetokenizer {
|
|
|
2816
2862
|
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
2817
2863
|
// Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
2818
2864
|
const tailLen = 160;
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2865
|
+
const tailRegex = /(?:(?:\[|【|[|〔|「|『|<|\{\{?|__|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?|_)$/;
|
|
2866
|
+
let tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
2867
|
+
let offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
2868
|
+
let m = tail.match(tailRegex);
|
|
2822
2869
|
|
|
2823
2870
|
if (m) {
|
|
2824
2871
|
const candidate = m[0];
|
|
2825
2872
|
// Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2873
|
+
const uClose = candidate.startsWith('__') ? candidate.indexOf('__', 2) : -1;
|
|
2874
|
+
const uniMatch = candidate.match(/\\u005[Dd]/);
|
|
2875
|
+
const closeLen = uClose !== -1 ? uClose + 2 : (uniMatch ? uniMatch.index + 6 : -1);
|
|
2876
|
+
if (closeLen !== -1) {
|
|
2877
|
+
const closedToken = candidate.slice(0, closeLen);
|
|
2878
|
+
if (this.replacer(closedToken) !== closedToken) {
|
|
2879
|
+
const splitAfterToken = offset + m.index + closeLen;
|
|
2880
|
+
const prefixEmitted = this.replacer(this.pendingBuffer.slice(0, splitAfterToken));
|
|
2881
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitAfterToken);
|
|
2882
|
+
if (!this.pendingBuffer) return prefixEmitted;
|
|
2883
|
+
tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
2884
|
+
offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
2885
|
+
m = tail.match(tailRegex);
|
|
2886
|
+
if (!m) {
|
|
2887
|
+
const rest = this.replacer(this.pendingBuffer);
|
|
2888
|
+
this.pendingBuffer = '';
|
|
2889
|
+
return prefixEmitted + rest;
|
|
2890
|
+
}
|
|
2891
|
+
const sIdx = offset + m.index;
|
|
2892
|
+
const safe = this.pendingBuffer.slice(0, sIdx);
|
|
2893
|
+
this.pendingBuffer = this.pendingBuffer.slice(sIdx);
|
|
2894
|
+
return prefixEmitted + (safe ? this.replacer(safe) : '');
|
|
2895
|
+
}
|
|
2831
2896
|
}
|
|
2832
2897
|
const splitIdx = offset + m.index;
|
|
2833
2898
|
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
package/index.d.ts
CHANGED
|
@@ -74,6 +74,14 @@ export interface SanitizeOptions {
|
|
|
74
74
|
returnFullSessionMap?: boolean;
|
|
75
75
|
/** Set or array of strings or exact terms to preserve unmasked in cleartext. */
|
|
76
76
|
ignoreList?: string[] | Set<string>;
|
|
77
|
+
/** Optional entity-type allowlist filter (e.g. ['NAME'] or ['PERSON'] to mask only person names while keeping batch/order numbers, dates, and IDs readable). */
|
|
78
|
+
entities?: string[];
|
|
79
|
+
/** Alias for entities. */
|
|
80
|
+
entityTypes?: string[];
|
|
81
|
+
/** Enable higher-recall person-name detection in free text (Default: false). */
|
|
82
|
+
highRecallNames?: boolean;
|
|
83
|
+
/** Alias for highRecallNames ('default' | 'high'). */
|
|
84
|
+
nameRecall?: 'default' | 'high';
|
|
77
85
|
/** Whether to generate a CISO Markdown audit receipt (Default: true). */
|
|
78
86
|
generateReceipt?: boolean;
|
|
79
87
|
/** Throw PrivacyScrubberLicenseError when free tier limits are reached (Default: true). */
|
package/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export const SDK_VERSION = (() => {
|
|
|
41
41
|
return pkgJson.version;
|
|
42
42
|
}
|
|
43
43
|
} catch (_) {}
|
|
44
|
-
return '2.4.
|
|
44
|
+
return '2.4.9';
|
|
45
45
|
})();
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -144,8 +144,9 @@ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
144
144
|
if (!t) continue;
|
|
145
145
|
const tokenStr = typeof t === 'string' ? t : ((t && (t.token || t.mask)) ? String(t.token || t.mask) : '');
|
|
146
146
|
if (!tokenStr) continue;
|
|
147
|
-
const match = tokenStr.match(/\[([A-
|
|
148
|
-
const
|
|
147
|
+
const match = tokenStr.match(/\[([A-Za-z0-9_]+)_\d+\]/);
|
|
148
|
+
const rawBase = match ? match[1] : tokenStr.replace(/\[|\]|<|>|\{|\}|__/g, '').replace(/_[0-9]+$/, '');
|
|
149
|
+
const baseType = (rawBase.replace(/[^A-Za-z0-9_]/g, '') || 'CUSTOM').toUpperCase();
|
|
149
150
|
entities[baseType] = (entities[baseType] || 0) + 1;
|
|
150
151
|
totalCount++;
|
|
151
152
|
}
|
|
@@ -155,7 +156,7 @@ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
155
156
|
let riskLevel = 'LOW EXPOSURE';
|
|
156
157
|
|
|
157
158
|
if (totalCount > 0) {
|
|
158
|
-
const hasHighRisk = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
159
|
+
const hasHighRisk = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'PHI', 'PRIVILEGE', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
159
160
|
if (hasHighRisk || types.length >= 3 || totalCount >= 10) {
|
|
160
161
|
riskLevel = 'CRITICAL (HIGH EXPOSURE)';
|
|
161
162
|
} else if (types.length >= 2 || totalCount >= 3) {
|
|
@@ -180,10 +181,10 @@ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
180
181
|
frameworksSet.add('PCI DSS v4.0');
|
|
181
182
|
frameworksSet.add('IRC § 7216 & GLBA');
|
|
182
183
|
}
|
|
183
|
-
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
184
|
+
if (types.some(t => ['PHI', 'MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
184
185
|
frameworksSet.add('HIPAA §164.514');
|
|
185
186
|
}
|
|
186
|
-
if (types.some(t => ['CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
187
|
+
if (types.some(t => ['PRIVILEGE', 'CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
187
188
|
frameworksSet.add('FRE 502 (Privilege)');
|
|
188
189
|
}
|
|
189
190
|
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN', 'CREDENTIAL'].includes(t))) {
|
|
@@ -193,6 +194,26 @@ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
193
194
|
frameworksSet.add('EU AI Act (Art. 50)');
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
const frameworksList = Array.from(frameworksSet);
|
|
198
|
+
const getFrameworkSlug = (name) => {
|
|
199
|
+
const lower = name.toLowerCase();
|
|
200
|
+
if (lower.includes('gdpr')) return 'gdpr';
|
|
201
|
+
if (lower.includes('ccpa')) return 'ccpa';
|
|
202
|
+
if (lower.includes('soc')) return 'soc2';
|
|
203
|
+
if (lower.includes('iso')) return 'iso27001';
|
|
204
|
+
if (lower.includes('pci')) return 'pci';
|
|
205
|
+
if (lower.includes('hipaa')) return 'hipaa';
|
|
206
|
+
if (lower.includes('nist')) return 'nist';
|
|
207
|
+
if (lower.includes('irc') || lower.includes('7216')) return 'irc7216';
|
|
208
|
+
if (lower.includes('fre') || lower.includes('502')) return 'fre502';
|
|
209
|
+
if (lower.includes('ai act')) return 'euaiact';
|
|
210
|
+
return lower.replace(/[^a-z0-9]/g, '');
|
|
211
|
+
};
|
|
212
|
+
const triggeredFrameworks = frameworksList.map(name => ({
|
|
213
|
+
name,
|
|
214
|
+
id: getFrameworkSlug(name)
|
|
215
|
+
}));
|
|
216
|
+
|
|
196
217
|
const ipHoldings = {
|
|
197
218
|
standard: 'IETF draft-sibiryakov-ztds-protocol-01',
|
|
198
219
|
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
@@ -264,10 +285,12 @@ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
|
264
285
|
|
|
265
286
|
return {
|
|
266
287
|
totalCount,
|
|
288
|
+
totalDetected: totalCount,
|
|
267
289
|
entities,
|
|
268
290
|
types,
|
|
269
291
|
riskLevel,
|
|
270
|
-
frameworksList
|
|
292
|
+
frameworksList,
|
|
293
|
+
triggeredFrameworks,
|
|
271
294
|
auditScope,
|
|
272
295
|
statutoryCitations,
|
|
273
296
|
ipHoldings,
|
|
@@ -517,7 +540,7 @@ We request approval to acquire a **Developer SDK License** (\$299/month flat or
|
|
|
517
540
|
*/
|
|
518
541
|
const SDK_VERTICAL_DETECTORS = [
|
|
519
542
|
{ profile: 'medical', label: 'Medical PHI (HIPAA)', regex: /\b(?:Patient\s*(?:Name|ID|DOB|Record)|MRN\b|Medical\s+Record|Attending\s+Physician|Chief\s+Complaint|Diagnosis\s*:|ICD-10|HIPAA|Clinical\s+Intake|Discharge\s+Summary|MedPod|NPI\s*(?:#|No|:)?\s*\d{10}|Medicare\s*(?:ID|Beneficiary|Claim)|Medicaid\s*ID|Prescription|Rx\s*#|Blood\s+Type|Vital\s+Signs|Radiology|Lab\s+Results|EHR\b|EMR\b)\b/gi },
|
|
520
|
-
{ profile: 'accounting', label: 'Tax & Accounting', regex: /\b(?:Form\s*1040|Form\s*W-2|Form\s*1099|Adjusted\s+Gross\s+Income|Employer\s+Identification\s+Number|Schedule\s+C\b|Steuer-ID|USt-IdNr|Tax\s+Return|General\s+Ledger|Withholding\s+Allowance|Total\s+Tax\s+Liability)\b/gi },
|
|
543
|
+
{ profile: 'accounting', label: 'Tax & Accounting', regex: /\b(?:Form\s*1040|Form\s*W-2|Form\s*1099|Adjusted\s+Gross\s+Income|Assessable\s+Income|Employer\s+Identification\s+Number|Schedule\s+C\b|Steuer-ID|USt-IdNr|Tax\s+Return|General\s+Ledger|Withholding\s+Allowance|Total\s+Tax\s+Liability|ABN\b|TFN\b|Tax\s+File\s+Number|Australian\s+Business\s+Number|Business\s+Activity\s+Statement|PAYG\s+(?:Withholding|Instalment)|Franking\s+Credits?)\b/gi },
|
|
521
544
|
{ profile: 'wealthmgmt', label: 'Wealth Management', regex: /\b(?:Revocable\s+Living\s+Trust|Irrevocable\s+(?:Generation-Skipping\s+)?Trust|Grantor\s*:|Settlor\s*:|Trustee\s*:|Beneficiary\s+Designation|Roth\s+IRA|Asset\s+Allocation|FBO\b|CUSIP\s*:|Grantor\s+Retained\s+Annuity\s+Trust|AUM\s*:)\b/gi },
|
|
522
545
|
{ profile: 'legal', label: 'Legal & Contracts', regex: /\b(?:Non-Disclosure\s+Agreement|Attorney-Client\s+Privilege|Docket\s+No|Hereinafter\s+referred|Indemnification\s+Clause|Governing\s+Law\s+and\s+Jurisdiction|Plaintiff\b|Defendant\b|Subpoena\b|Deposition\b|State\s+Bar\s*#)\b/gi },
|
|
523
546
|
{ profile: 'finance', label: 'Finance & Banking', regex: /\b(?:Wire\s+Transfer\s+Authorization|SWIFT\s*\/?\s*BIC\s*:|IBAN\s*:\s*[A-Z]{2}\d{2}|ABA\s+Routing|ACH\s+Transfer|PCI-DSS|Primary\s+Account\s+Number)\b/gi },
|
|
@@ -618,7 +641,9 @@ export function sanitize(text, options = {}) {
|
|
|
618
641
|
throwOnLimit = true,
|
|
619
642
|
onLimit = 'throw',
|
|
620
643
|
detectSecrets = false,
|
|
621
|
-
ignoreList = null
|
|
644
|
+
ignoreList = null,
|
|
645
|
+
entities = options.entityTypes || options.enabledEntities || null,
|
|
646
|
+
highRecallNames = Boolean(options.highRecallNames || options.nameRecall === 'high')
|
|
622
647
|
} = options;
|
|
623
648
|
|
|
624
649
|
const isAutoProfile = Boolean(
|
|
@@ -734,8 +759,23 @@ export function sanitize(text, options = {}) {
|
|
|
734
759
|
}
|
|
735
760
|
}
|
|
736
761
|
|
|
762
|
+
let normalizedEntities = null;
|
|
763
|
+
if (entities) {
|
|
764
|
+
const rawArr = entities instanceof Set ? Array.from(entities) : (Array.isArray(entities) ? entities : [entities]);
|
|
765
|
+
normalizedEntities = rawArr.filter(Boolean).map(e => {
|
|
766
|
+
const u = String(e).trim().toUpperCase();
|
|
767
|
+
if (u === 'PERSON' || u === 'PERSONS' || u === 'PEOPLE' || u === 'NAMES') return 'NAME';
|
|
768
|
+
if (u === 'EMAILS') return 'EMAIL';
|
|
769
|
+
if (u === 'PHONES' || u === 'TELEPHONE') return 'PHONE';
|
|
770
|
+
if (u === 'IDS' || u === 'IDENTIFIER' || u === 'IDENTIFIERS') return 'ID';
|
|
771
|
+
if (u === 'ADDRESSES' || u === 'LOCATION' || u === 'LOCATIONS') return 'ADDRESS';
|
|
772
|
+
return u;
|
|
773
|
+
});
|
|
774
|
+
if (normalizedEntities.length === 0) normalizedEntities = null;
|
|
775
|
+
}
|
|
776
|
+
|
|
737
777
|
const activeRules = [...effectiveCustomRules];
|
|
738
|
-
if (effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) {
|
|
778
|
+
if ((effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) && (!normalizedEntities || normalizedEntities.includes('SECRET') || normalizedEntities.includes('CUSTOM'))) {
|
|
739
779
|
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
740
780
|
activeRules.push({
|
|
741
781
|
name: rule.name,
|
|
@@ -759,7 +799,9 @@ export function sanitize(text, options = {}) {
|
|
|
759
799
|
effectiveProfile,
|
|
760
800
|
existingSessionMap,
|
|
761
801
|
isPro,
|
|
762
|
-
effectiveIgnoreList
|
|
802
|
+
effectiveIgnoreList,
|
|
803
|
+
normalizedEntities,
|
|
804
|
+
Boolean(highRecallNames)
|
|
763
805
|
);
|
|
764
806
|
|
|
765
807
|
const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
@@ -1087,6 +1129,8 @@ export class PrivacyScrubberEngine {
|
|
|
1087
1129
|
this.generateReceipt = config.generateReceipt !== false;
|
|
1088
1130
|
this.sessionMap = { ...(config.initialSessionMap || {}) };
|
|
1089
1131
|
this.ignoreList = new Set(config.ignoreList || []);
|
|
1132
|
+
this.entities = config.entities || config.entityTypes || config.enabledEntities || null;
|
|
1133
|
+
this.highRecallNames = Boolean(config.highRecallNames || config.nameRecall === 'high');
|
|
1090
1134
|
this.maxSessionTokens = config.maxSessionTokens || 10000;
|
|
1091
1135
|
|
|
1092
1136
|
this.license = validateLicense(this.licenseKey);
|
|
@@ -1128,7 +1172,9 @@ export class PrivacyScrubberEngine {
|
|
|
1128
1172
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
1129
1173
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
1130
1174
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
1131
|
-
ignoreList: mergedIgnoreList.size > 0 ? mergedIgnoreList : null
|
|
1175
|
+
ignoreList: mergedIgnoreList.size > 0 ? mergedIgnoreList : null,
|
|
1176
|
+
entities: options.entities || options.entityTypes || options.enabledEntities || this.entities,
|
|
1177
|
+
highRecallNames: options.highRecallNames !== undefined ? Boolean(options.highRecallNames) : (options.nameRecall === 'high' ? true : this.highRecallNames)
|
|
1132
1178
|
};
|
|
1133
1179
|
|
|
1134
1180
|
const result = sanitize(text, mergedOptions);
|
|
@@ -2668,13 +2714,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
2668
2714
|
|
|
2669
2715
|
for (const k of sortedKeys) {
|
|
2670
2716
|
const val = tokenMap[k];
|
|
2671
|
-
const inner = k.replace(
|
|
2717
|
+
const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
|
|
2672
2718
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
2673
2719
|
|
|
2674
2720
|
if (match) {
|
|
2675
2721
|
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
2676
2722
|
const idx = match[2];
|
|
2677
|
-
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
2723
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
2678
2724
|
patterns.push(patternStr);
|
|
2679
2725
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
2680
2726
|
lookup.set(normKey, val);
|
|
@@ -2704,8 +2750,8 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
2704
2750
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
2705
2751
|
} else {
|
|
2706
2752
|
const inner = coreMatch
|
|
2707
|
-
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
2708
|
-
.replace(/^[
|
|
2753
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
2754
|
+
.replace(/^[*`_~\s]+|[.,;:!?*`_~\s]+$/g, '')
|
|
2709
2755
|
.trim();
|
|
2710
2756
|
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
2711
2757
|
if (m) {
|
|
@@ -2818,18 +2864,37 @@ export class StreamingDetokenizer {
|
|
|
2818
2864
|
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
2819
2865
|
// Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
2820
2866
|
const tailLen = 160;
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2867
|
+
const tailRegex = /(?:(?:\[|【|[|〔|「|『|<|\{\{?|__|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?|_)$/;
|
|
2868
|
+
let tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
2869
|
+
let offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
2870
|
+
let m = tail.match(tailRegex);
|
|
2824
2871
|
|
|
2825
2872
|
if (m) {
|
|
2826
2873
|
const candidate = m[0];
|
|
2827
2874
|
// Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2875
|
+
const uClose = candidate.startsWith('__') ? candidate.indexOf('__', 2) : -1;
|
|
2876
|
+
const uniMatch = candidate.match(/\\u005[Dd]/);
|
|
2877
|
+
const closeLen = uClose !== -1 ? uClose + 2 : (uniMatch ? uniMatch.index + 6 : -1);
|
|
2878
|
+
if (closeLen !== -1) {
|
|
2879
|
+
const closedToken = candidate.slice(0, closeLen);
|
|
2880
|
+
if (this.replacer(closedToken) !== closedToken) {
|
|
2881
|
+
const splitAfterToken = offset + m.index + closeLen;
|
|
2882
|
+
const prefixEmitted = this.replacer(this.pendingBuffer.slice(0, splitAfterToken));
|
|
2883
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitAfterToken);
|
|
2884
|
+
if (!this.pendingBuffer) return prefixEmitted;
|
|
2885
|
+
tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
2886
|
+
offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
2887
|
+
m = tail.match(tailRegex);
|
|
2888
|
+
if (!m) {
|
|
2889
|
+
const rest = this.replacer(this.pendingBuffer);
|
|
2890
|
+
this.pendingBuffer = '';
|
|
2891
|
+
return prefixEmitted + rest;
|
|
2892
|
+
}
|
|
2893
|
+
const sIdx = offset + m.index;
|
|
2894
|
+
const safe = this.pendingBuffer.slice(0, sIdx);
|
|
2895
|
+
this.pendingBuffer = this.pendingBuffer.slice(sIdx);
|
|
2896
|
+
return prefixEmitted + (safe ? this.replacer(safe) : '');
|
|
2897
|
+
}
|
|
2833
2898
|
}
|
|
2834
2899
|
const splitIdx = offset + m.index;
|
|
2835
2900
|
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/sdk",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.9",
|
|
4
4
|
"description": "In-process consumer privacy defense and pre-emptive PII interception SDK for Node.js, Next.js, and AI pipelines. Zero data loss, zero third parties, sub-millisecond in-memory execution.",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"module": "index.js",
|