@privacyscrubber/sdk 2.4.7 → 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/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.7';
44
+ return '2.4.9';
45
45
  })();
46
46
 
47
47
  /**
@@ -134,7 +134,7 @@ export function validateLicenseKey(key) {
134
134
  * @param {Record<string, string>|string[]} currentTokenMap
135
135
  * @returns {object}
136
136
  */
137
- export function buildAuditTelemetry(currentTokenMap = {}) {
137
+ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
138
138
  const entities = {};
139
139
  let totalCount = 0;
140
140
 
@@ -144,8 +144,9 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
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-Z0-9_]+)_\d+\]/);
148
- const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
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 = {}) {
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 = {}) {
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 = {}) {
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/',
@@ -244,12 +265,33 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
244
265
  }
245
266
  ];
246
267
 
268
+ const rawLicenseTier = String(extraStats?.licenseTier || '').trim().toUpperCase();
269
+ const isOrganizationalScope = ['TEAMS', 'SDK', 'OEM', 'ENTERPRISE', 'ENTERPRISE_AIRGAP'].includes(rawLicenseTier);
270
+ const auditScope = isOrganizationalScope ? {
271
+ tier: rawLicenseTier,
272
+ isOrganizational: true,
273
+ badge: 'ORGANIZATIONAL MULTI-SEAT COMPLIANCE SCOPE',
274
+ coverage: 'Covers department, clinic, or firm personnel under unified ZTDS policy governance.',
275
+ upgradeRecommended: false,
276
+ upgradeUrl: null
277
+ } : {
278
+ tier: rawLicenseTier || 'SINGLE_OPERATOR',
279
+ isOrganizational: false,
280
+ badge: 'SINGLE-OPERATOR PERSONAL SCOPE',
281
+ coverage: 'Valid for individual workstation only. Multi-employee clinics, CPA firms, and departments require TEAMS ($99/mo flat, unlimited seats) for organizational audit coverage.',
282
+ upgradeRecommended: true,
283
+ upgradeUrl: 'https://privacyscrubber.com/pricing/?tier=teams&utm_source=audit_receipt&utm_medium=scope_upsell'
284
+ };
285
+
247
286
  return {
248
287
  totalCount,
288
+ totalDetected: totalCount,
249
289
  entities,
250
290
  types,
251
291
  riskLevel,
252
- frameworksList: Array.from(frameworksSet),
292
+ frameworksList,
293
+ triggeredFrameworks,
294
+ auditScope,
253
295
  statutoryCitations,
254
296
  ipHoldings,
255
297
  agenticProtocol,
@@ -303,6 +345,19 @@ export function _resetCommunityNoticeForTesting() {
303
345
  communityNoticeLogged = false;
304
346
  }
305
347
 
348
+ export const FREE_PROD_SPECIALIZED_CALL_LIMIT = 250;
349
+ let _freeProdSpecializedCalls = 0;
350
+ let _prodProfileFallbackNoticeLogged = false;
351
+
352
+ /**
353
+ * Resets the production specialized call quota state for testing purposes.
354
+ * @private
355
+ */
356
+ export function _resetProdQuotaForTesting() {
357
+ _freeProdSpecializedCalls = 0;
358
+ _prodProfileFallbackNoticeLogged = false;
359
+ }
360
+
306
361
  let devSecretNoticeLogged = false;
307
362
 
308
363
  function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
@@ -483,6 +538,82 @@ We request approval to acquire a **Developer SDK License** (\$299/month flat or
483
538
  * auditReceipt: string
484
539
  * }}
485
540
  */
541
+ const SDK_VERTICAL_DETECTORS = [
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 },
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 },
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 },
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 },
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 },
547
+ { profile: 'hr', label: 'HR & Resumes', regex: /\b(?:Employee\s*ID|Performance\s+Improvement\s+Plan|Payroll\s+Register|Gross\s+Pay\b|Net\s+Pay\b|Candidate\s+Evaluation|Compensation\s+Band|Sozialversicherungsnummer|Personalnummer|EEID\s*:)\b/gi },
548
+ { profile: 'realestate', label: 'Real Estate', regex: /\b(?:Assessor\s+Parcel\s+Number|APN\s*:|MLS\s*#|Closing\s+Disclosure|Escrow\s+(?:Account|Number|Officer)|Property\s+Deed|Title\s+Insurance|Lease\s+Application)\b/gi },
549
+ { profile: 'dev', label: 'API & Dev Logs', regex: /\b(?:BEGIN\s+(?:RSA|OPENSSH|EC|PRIVATE)\s+PRIVATE\s+KEY|AWS_SECRET_ACCESS_KEY|DATABASE_URL\s*=|GITHUB_TOKEN\s*=|Bearer\s+eyJ[A-Za-z0-9_-]+)\b/g },
550
+ { profile: 'sales', label: 'Sales & CRM', regex: /\b(?:CRM\s+(?:Opportunity|Export)|HubSpot\b|Salesforce\b|MEDDPICC\b|Primary\s+Buyer\s*:|MSA\s+Redlines|Deal\s+Stage|Pipeline\s+Forecast|Opportunity\s+ID|Account\s+Executive)\b/gi },
551
+ { profile: 'engineering', label: 'DevOps & Infra', regex: /\b(?:Incident\s+Postmortem|Kubeconfig|Kubernetes\s+Cluster|Helm\s+Chart|Terraform\s+State|DMAIC\b|Bill\s+of\s+Materials|BOM\s+Spec|CNC\s+Machining|Shop\s+Floor\s+Batch)\b/gi },
552
+ { profile: 'insurance', label: 'Insurance & Claims', regex: /\b(?:Claim\s+Settlement|Policyholder\s*:|Policy\s*#?POL-|Claim\s+Number|Adjuster\s*:|Loss\s+Location|Loss\s+Run|Subrogation\b|FNOL\b|Deductible\s*:)\b/gi },
553
+ { profile: 'pharma', label: 'Pharma & Trials', regex: /\b(?:Clinical\s+Trial(?:\s+Protocol)?|NCT\d{8}\b|IRB\s+Protocol|FDA\s+1572|Adverse\s+Event|Pharmacovigilance|Principal\s+Investigator|Cohort\s+[A-Z]\b|Blinded\s+Study|CDISC\b|IND\s*(?:#|Protocol))\b/gi },
554
+ { profile: 'underwriting', label: 'Loan Underwriting & Income', regex: /\b(?:Loan\s+Underwriting|Mortgage\s+Appraisal|Loan\s+Application\s+#?LOS|FICO\s*:\s*\d{3}|Appraised\s+Value\s*:|80%\s+LTV|Monthly\s+Qualifying\s*:|YTD\s+Earnings|Borrower\s*:)\b/gi },
555
+ { profile: 'security', label: 'Security & SIEM', regex: /\b(?:SIEM\s+Alert|CVE-\d{4}-\d{4,7}\b|Threat\s*:\s*CRITICAL|Unauthorized\s+S3\s+bucket|SOC\s+Incident|Firewall\s+Rule\s+Violation|Intrusion\s+Detection)\b/gi },
556
+ { profile: 'support', label: 'Support & Tickets', regex: /\b(?:Support\s+Ticket|Ticket\s*#INC-|Zendesk\b|Intercom\b|Billing\s+Discrepancy|SLA\s+Escalation|CSAT\s+Score|Customer\s+Support\s+Escalation)\b/gi },
557
+ { profile: 'compliance', label: 'Compliance Audit', regex: /\b(?:Suspicious\s+Activity\s+Report|FinCEN\s+SAR|BSA\s+Officer|31\s+CFR\b|ROPA\s+Register|Data\s+Protection\s+Impact\s+Assessment|SOC\s*2\s+Type\s+II|ISO\s*27001\s+Audit|CMMC\s+Level|ITAR\s+Controlled)\b/gi },
558
+ { profile: 'ccpa', label: 'CCPA / CPRA Privacy', regex: /\b(?:CCPA\s*\/?\s*CPRA|1798\.105|Right\s+to\s+Delete|Opt-out\s+of\s+Sale|Ticket\s*#?DSAR-)\b/gi },
559
+ { profile: 'bizops', label: 'Biz Ops & M&A', regex: /\b(?:Vendor\s+Master\s+Agreement|Bill\s+of\s+Lading|Customs\s+Declaration|Purchase\s+Order\s*#?PO-|Supply\s+Chain\s+Manifest)\b/gi },
560
+ { profile: 'biotech', label: 'Biotech & Genomics', regex: /\b(?:Sequencing\s+Platform|Illumina\s+NovaSeq|Flowcell\s*:|FastQ\s+Sample|ClinVar\b|HGVS\s+Variant|Genomic\s+Sequencing|Bioinformatics\s+Intake|BRCA[12]\s+c\.)\b/gi },
561
+ { profile: 'automotive', label: 'Automotive & EV', regex: /\b(?:Connected\s+Vehicle\s+Telematics|ECU\s+Serial|Telematics\s+Device|ASIL-D\b|DTC\s+P\d{4}\b|CAN-Bus\s+Log)\b/gi },
562
+ { profile: 'energy', label: 'Energy & Smart Grid', regex: /\b(?:Critical\s+Infrastructure\s+SCADA|Substation\s+ID|Feeder\s+ID|Smart\s+Meter\s*:|ESI\s+ID\s*:|NERC\s+CIP|BES\s+Cyber\s+Asset)\b/gi },
563
+ { profile: 'hospitality', label: 'Hospitality & Travel', regex: /\b(?:Airline\s+Passenger\s+Dossier|PNR\s*:\s*[A-Z0-9]{6}|e-Ticket\s*:|Boarding\s+Pass\s*:|Known\s+Traveler\s+KTN|Guest\s+Folio|Marriott\s+Bonvoy|SkyMiles)\b/gi },
564
+ { profile: 'telecom', label: 'Telecom & Call Center', regex: /\b(?:Telecom\s+Subscriber\s+CDR|MSISDN\s*:|IMSI\s*:\s*\d{14,15}|IMEI\s*:\s*\d{14,16}|Cell\s+Tower\s+ID|SIP\s+Signaling)\b/gi },
565
+ { profile: 'academic', label: 'Academic & Research', regex: /\b(?:FERPA\s+Student\s+Record|Student\s+ID\s*:\s*#?STU-|Disciplinary\s+Record|IEP\s+Evaluation|IRB\s+Approval)\b/gi },
566
+ { profile: 'agents', label: 'AI Agents & Prompts', regex: /\b(?:Autonomous\s+Financial\s+Procurement\s+Agent|AgentID\s*:\s*agt_|Encrypted\s+Tool\s+Token|Agent\s+Memory\s*>)\b/gi }
567
+ ];
568
+
569
+ /**
570
+ * Automatically detects the most relevant industry compliance profile from text content.
571
+ * Executes in <0.05ms in local RAM with zero network egress.
572
+ *
573
+ * @param {string} text - Input document or prompt text.
574
+ * @returns {{
575
+ * detectedProfile: string,
576
+ * label: string,
577
+ * confidence: 'high'|'medium'|'low',
578
+ * score: number,
579
+ * alternatives: Array<{ profile: string, label: string, score: number }>
580
+ * }}
581
+ */
582
+ export function detectProfile(text) {
583
+ if (!text || typeof text !== 'string') {
584
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
585
+ }
586
+ const sampleSlice = text.length > 16384 ? text.slice(0, 16384) : text;
587
+ const matches = [];
588
+ for (let i = 0; i < SDK_VERTICAL_DETECTORS.length; i++) {
589
+ const c = SDK_VERTICAL_DETECTORS[i];
590
+ c.regex.lastIndex = 0;
591
+ const found = sampleSlice.match(c.regex);
592
+ if (found && found.length > 0) {
593
+ matches.push({
594
+ profile: c.profile,
595
+ label: c.label,
596
+ score: found.length,
597
+ priority: i
598
+ });
599
+ }
600
+ }
601
+ if (matches.length === 0) {
602
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
603
+ }
604
+ matches.sort((a, b) => (b.score !== a.score) ? (b.score - a.score) : (a.priority - b.priority));
605
+ const primary = matches[0];
606
+ const alternatives = matches.slice(1, 4).map(({ profile, label, score }) => ({ profile, label, score }));
607
+ const confidence = primary.score >= 3 ? 'high' : (primary.score >= 2 ? 'medium' : 'low');
608
+ return {
609
+ detectedProfile: primary.profile,
610
+ label: primary.label,
611
+ confidence,
612
+ score: primary.score,
613
+ alternatives
614
+ };
615
+ }
616
+
486
617
  export function sanitize(text, options = {}) {
487
618
  if (!text || typeof text !== 'string') {
488
619
  return {
@@ -501,7 +632,7 @@ export function sanitize(text, options = {}) {
501
632
  }
502
633
 
503
634
  const {
504
- profile = 'General',
635
+ profile: rawProfileOption = 'General',
505
636
  licenseKey = (typeof process !== 'undefined' && (process.env?.PRIVACYSCRUBBER_KEY || process.env?.PRIVACYSCRUBBER_LICENSE_KEY)) || '',
506
637
  customRules = [],
507
638
  tokenLabelMap = {},
@@ -510,9 +641,17 @@ export function sanitize(text, options = {}) {
510
641
  throwOnLimit = true,
511
642
  onLimit = 'throw',
512
643
  detectSecrets = false,
513
- ignoreList = null
644
+ ignoreList = null,
645
+ entities = options.entityTypes || options.enabledEntities || null,
646
+ highRecallNames = Boolean(options.highRecallNames || options.nameRecall === 'high')
514
647
  } = options;
515
648
 
649
+ const isAutoProfile = Boolean(
650
+ (typeof rawProfileOption === 'string' && rawProfileOption.trim().toLowerCase() === 'auto') ||
651
+ options.autoDetectProfile === true
652
+ );
653
+ const autoDetected = isAutoProfile ? detectProfile(text) : null;
654
+
516
655
  const licenseCheck = validateLicense(licenseKey);
517
656
  const rawTier = licenseCheck.valid ? (licenseCheck.tier || 'PRO') : 'FREE';
518
657
  const isExpiredKey = !licenseCheck.valid && typeof licenseCheck.error === 'string' && /expired/i.test(licenseCheck.error);
@@ -534,6 +673,21 @@ export function sanitize(text, options = {}) {
534
673
  const isSdkTier = licenseCheck.valid && ['SDK', 'OEM', 'ENTERPRISE'].includes(rawTier);
535
674
  const isSeatTier = licenseCheck.valid && ['PRO', 'TEAMS'].includes(rawTier);
536
675
 
676
+ const isPro = isSdkTier;
677
+ const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
678
+
679
+ let autoSuggestedOnly = false;
680
+ let profile = rawProfileOption;
681
+ if (isAutoProfile && autoDetected) {
682
+ if (!isPro && text.length > 5000 && text.length <= 15000 && autoDetected.detectedProfile !== 'general') {
683
+ // Non-blocking PLG fallback: keep General for Free 5k-15k texts so auto-mode never breaks free calls
684
+ profile = 'General';
685
+ autoSuggestedOnly = true;
686
+ } else {
687
+ profile = autoDetected.detectedProfile;
688
+ }
689
+ }
690
+
537
691
  if (isSeatTier) {
538
692
  const keyPreview = licenseKey ? `${licenseKey.slice(0, 12)}...` : '';
539
693
  const errorMsg = `[PrivacyScrubber] License tier mismatch: ${rawTier} license (${keyPreview}) is licensed strictly for human seats (Web App & Chrome Extension). Backend automation, microservices, and CRM pipelines require a Developer SDK license ($299/mo). Upgrade: https://privacyscrubber.com/pricing?tier=sdk&src=seat_tier_mismatch`;
@@ -547,9 +701,6 @@ export function sanitize(text, options = {}) {
547
701
  }
548
702
  }
549
703
 
550
- const isPro = isSdkTier;
551
- const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
552
-
553
704
  emitCommunityNoticeIfNeeded(isPro);
554
705
 
555
706
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
@@ -583,8 +734,48 @@ export function sanitize(text, options = {}) {
583
734
  });
584
735
 
585
736
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
586
- const activeRules = [...safeCustomRules];
587
- if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
737
+ const isProdEnv = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
738
+ const hasCustomEnterpriseConfig = isSpecialized || safeCustomRules.length > 0 || (Array.isArray(ignoreList) && ignoreList.length > 0);
739
+ let profileFallback = false;
740
+ let effectiveProfile = profile;
741
+ let effectiveCustomRules = safeCustomRules;
742
+ let effectiveIgnoreList = ignoreList;
743
+
744
+ if (!isPro && isProdEnv && hasCustomEnterpriseConfig) {
745
+ _freeProdSpecializedCalls++;
746
+ if (_freeProdSpecializedCalls > FREE_PROD_SPECIALIZED_CALL_LIMIT) {
747
+ profileFallback = true;
748
+ effectiveProfile = 'General';
749
+ effectiveCustomRules = [];
750
+ effectiveIgnoreList = null;
751
+ if (!_prodProfileFallbackNoticeLogged && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
752
+ _prodProfileFallbackNoticeLogged = true;
753
+ process.stderr.write(
754
+ `\n[@privacyscrubber/sdk] Free Production Quota (${FREE_PROD_SPECIALIZED_CALL_LIMIT} specialized calls) reached for profile '${profile}'.\n` +
755
+ `[@privacyscrubber/sdk] Gracefully falling back to 'General' profile (Core PII remains protected, 0 downtime).\n` +
756
+ `[@privacyscrubber/sdk] Unlock unlimited specialized profiles, custom rules & ignoreLists in production ($299/mo): https://privacyscrubber.com/pricing?tier=sdk&src=prod_profile_fallback (7-Day Trial: npx @privacyscrubber/sdk trial)\n\n`
757
+ );
758
+ }
759
+ }
760
+ }
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
+
777
+ const activeRules = [...effectiveCustomRules];
778
+ if ((effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) && (!normalizedEntities || normalizedEntities.includes('SECRET') || normalizedEntities.includes('CUSTOM'))) {
588
779
  DEVOPS_SECRETS_DETECTOR.forEach(rule => {
589
780
  activeRules.push({
590
781
  name: rule.name,
@@ -605,10 +796,12 @@ export function sanitize(text, options = {}) {
605
796
  textToScrub,
606
797
  activeRules,
607
798
  tokenLabelMap,
608
- profile,
799
+ effectiveProfile,
609
800
  existingSessionMap,
610
801
  isPro,
611
- ignoreList
802
+ effectiveIgnoreList,
803
+ normalizedEntities,
804
+ Boolean(highRecallNames)
612
805
  );
613
806
 
614
807
  const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
@@ -616,7 +809,7 @@ export function sanitize(text, options = {}) {
616
809
  : Date.now();
617
810
 
618
811
  const executionMs = Math.round((endTime - startTime) * 100) / 100;
619
- const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
812
+ const telemetry = buildAuditTelemetry(rawResult.tokenMap || {}, { licenseTier: tier });
620
813
  const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
621
814
 
622
815
  emitDevSecretNoticeIfNeeded(isPro, rawResult.tokenMap, limit);
@@ -644,6 +837,10 @@ export function sanitize(text, options = {}) {
644
837
  limit: limit,
645
838
  wasTruncated: wasTruncated,
646
839
  truncated: wasTruncated,
840
+ profileFallback: profileFallback,
841
+ effectiveProfile: effectiveProfile,
842
+ specializedCallsUsed: _freeProdSpecializedCalls,
843
+ specializedCallsLimit: isPro ? Infinity : FREE_PROD_SPECIALIZED_CALL_LIMIT,
647
844
  ...(isSeatTier && {
648
845
  upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
649
846
  trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
@@ -651,12 +848,20 @@ export function sanitize(text, options = {}) {
651
848
  recommendation: `Your ${rawTier} license is valid for interactive browser seats only. Upgrade to Developer SDK ($299/mo) for unlimited backend server throughput: https://privacyscrubber.com/pricing?tier=sdk`
652
849
  }),
653
850
  ...(!isPro && !isSeatTier && {
654
- upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
655
- trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
851
+ upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
852
+ trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
656
853
  trialCommand: 'npx @privacyscrubber/sdk trial',
657
854
  recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited backend text processing, custom regex rules, and specialized compliance profiles.'
658
855
  })
659
856
  },
857
+ profileDetection: {
858
+ autoDetected: isAutoProfile,
859
+ detectedProfile: autoDetected ? autoDetected.detectedProfile : String(effectiveProfile || 'General').toLowerCase(),
860
+ label: autoDetected ? autoDetected.label : effectiveProfile,
861
+ confidence: autoDetected ? autoDetected.confidence : 'high',
862
+ suggestedOnly: autoSuggestedOnly,
863
+ alternatives: autoDetected ? autoDetected.alternatives : []
864
+ },
660
865
  telemetry: telemetry,
661
866
  auditReceipt: auditReceipt
662
867
  };
@@ -924,6 +1129,8 @@ export class PrivacyScrubberEngine {
924
1129
  this.generateReceipt = config.generateReceipt !== false;
925
1130
  this.sessionMap = { ...(config.initialSessionMap || {}) };
926
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');
927
1134
  this.maxSessionTokens = config.maxSessionTokens || 10000;
928
1135
 
929
1136
  this.license = validateLicense(this.licenseKey);
@@ -965,7 +1172,9 @@ export class PrivacyScrubberEngine {
965
1172
  generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
966
1173
  throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
967
1174
  detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
968
- 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)
969
1178
  };
970
1179
 
971
1180
  const result = sanitize(text, mergedOptions);
@@ -2505,13 +2714,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
2505
2714
 
2506
2715
  for (const k of sortedKeys) {
2507
2716
  const val = tokenMap[k];
2508
- const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
2717
+ const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
2509
2718
  const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
2510
2719
 
2511
2720
  if (match) {
2512
2721
  const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
2513
2722
  const idx = match[2];
2514
- 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)';
2515
2724
  patterns.push(patternStr);
2516
2725
  const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
2517
2726
  lookup.set(normKey, val);
@@ -2541,8 +2750,8 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
2541
2750
  restored = lookup.get(coreMatch.toUpperCase());
2542
2751
  } else {
2543
2752
  const inner = coreMatch
2544
- .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, '')
2545
- .replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
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, '')
2546
2755
  .trim();
2547
2756
  const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
2548
2757
  if (m) {
@@ -2655,18 +2864,37 @@ export class StreamingDetokenizer {
2655
2864
  // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
2656
2865
  // Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity &#91; / &lbrack;
2657
2866
  const tailLen = 160;
2658
- const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
2659
- const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
2660
- const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%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)?)?)?)?)?)?)$/);
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);
2661
2871
 
2662
2872
  if (m) {
2663
2873
  const candidate = m[0];
2664
2874
  // Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
2665
- const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
2666
- if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
2667
- const toEmit = this.pendingBuffer;
2668
- this.pendingBuffer = '';
2669
- return this.replacer(toEmit);
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
+ }
2670
2898
  }
2671
2899
  const splitIdx = offset + m.index;
2672
2900
  const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
@@ -3888,6 +4116,7 @@ import {
3888
4116
 
3889
4117
  export default {
3890
4118
  SDK_VERSION,
4119
+ detectProfile,
3891
4120
  sanitize,
3892
4121
  sanitizeObject,
3893
4122
  scrub,
@@ -3940,6 +4169,8 @@ export default {
3940
4169
  scrubText,
3941
4170
  unscrubText,
3942
4171
  DEVOPS_SECRETS_DETECTOR,
4172
+ FREE_PROD_SPECIALIZED_CALL_LIMIT,
4173
+ _resetProdQuotaForTesting,
3943
4174
  stripExifMetadata,
3944
4175
  redactImageBoundingBoxes,
3945
4176
  generateSvgRedactionOverlay
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.4.7",
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",