@privacyscrubber/sdk 2.4.7 → 2.4.8

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/cli.js CHANGED
@@ -32,7 +32,7 @@ for (const a of rawArgs) {
32
32
  }
33
33
  }
34
34
 
35
- let sdkVersion = '2.4.7';
35
+ let sdkVersion = '2.4.8';
36
36
  try {
37
37
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
38
38
  sdkVersion = pkg.version || sdkVersion;
@@ -59,7 +59,7 @@ if FASTAPI_AVAILABLE:
59
59
  app = FastAPI(
60
60
  title="PrivacyScrubber ZTDS Sidecar Proxy",
61
61
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for LLM Pipelines",
62
- version="2.4.7"
62
+ version="2.4.8"
63
63
  )
64
64
 
65
65
  @app.on_event("startup")
@@ -24,7 +24,7 @@ from privacyscrubber.exceptions import (
24
24
  PrivacyScrubberLicenseError,
25
25
  )
26
26
 
27
- __version__ = "2.4.7"
27
+ __version__ = "2.4.8"
28
28
  __author__ = "Ilya Sibiryakov (BrandMeWeb)"
29
29
  __license__ = "Proprietary"
30
30
 
@@ -115,7 +115,7 @@ rl.on('line', (line) => {
115
115
  sendResponse(id, {
116
116
  pong: true,
117
117
  engine: 'PrivacyScrubber ZTDS',
118
- version: sdk.SDK_VERSION || '2.4.7',
118
+ version: sdk.SDK_VERSION || '2.4.8',
119
119
  uptime: process.uptime()
120
120
  });
121
121
  break;
@@ -50,7 +50,7 @@ def main() -> None:
50
50
  args = parser.parse_args()
51
51
 
52
52
  if args.version:
53
- print("PrivacyScrubber ZTDS Python SDK v2.4.7")
53
+ print("PrivacyScrubber ZTDS Python SDK v2.4.8")
54
54
  sys.exit(0)
55
55
 
56
56
  if not args.command:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "privacyscrubber"
7
- version = "2.4.7"
7
+ version = "2.4.8"
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.7",
16
+ version="2.4.8",
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.7';
42
+ return '2.4.8';
43
43
  })();
44
44
 
45
45
  /**
@@ -132,7 +132,7 @@ function validateLicenseKey(key) {
132
132
  * @param {Record<string, string>|string[]} currentTokenMap
133
133
  * @returns {object}
134
134
  */
135
- function buildAuditTelemetry(currentTokenMap = {}) {
135
+ function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
136
136
  const entities = {};
137
137
  let totalCount = 0;
138
138
 
@@ -242,12 +242,31 @@ function buildAuditTelemetry(currentTokenMap = {}) {
242
242
  }
243
243
  ];
244
244
 
245
+ const rawLicenseTier = String(extraStats?.licenseTier || '').trim().toUpperCase();
246
+ const isOrganizationalScope = ['TEAMS', 'SDK', 'OEM', 'ENTERPRISE', 'ENTERPRISE_AIRGAP'].includes(rawLicenseTier);
247
+ const auditScope = isOrganizationalScope ? {
248
+ tier: rawLicenseTier,
249
+ isOrganizational: true,
250
+ badge: 'ORGANIZATIONAL MULTI-SEAT COMPLIANCE SCOPE',
251
+ coverage: 'Covers department, clinic, or firm personnel under unified ZTDS policy governance.',
252
+ upgradeRecommended: false,
253
+ upgradeUrl: null
254
+ } : {
255
+ tier: rawLicenseTier || 'SINGLE_OPERATOR',
256
+ isOrganizational: false,
257
+ badge: 'SINGLE-OPERATOR PERSONAL SCOPE',
258
+ coverage: 'Valid for individual workstation only. Multi-employee clinics, CPA firms, and departments require TEAMS ($99/mo flat, unlimited seats) for organizational audit coverage.',
259
+ upgradeRecommended: true,
260
+ upgradeUrl: 'https://privacyscrubber.com/pricing/?tier=teams&utm_source=audit_receipt&utm_medium=scope_upsell'
261
+ };
262
+
245
263
  return {
246
264
  totalCount,
247
265
  entities,
248
266
  types,
249
267
  riskLevel,
250
268
  frameworksList: Array.from(frameworksSet),
269
+ auditScope,
251
270
  statutoryCitations,
252
271
  ipHoldings,
253
272
  agenticProtocol,
@@ -301,6 +320,19 @@ function _resetCommunityNoticeForTesting() {
301
320
  communityNoticeLogged = false;
302
321
  }
303
322
 
323
+ const FREE_PROD_SPECIALIZED_CALL_LIMIT = 250;
324
+ let _freeProdSpecializedCalls = 0;
325
+ let _prodProfileFallbackNoticeLogged = false;
326
+
327
+ /**
328
+ * Resets the production specialized call quota state for testing purposes.
329
+ * @private
330
+ */
331
+ function _resetProdQuotaForTesting() {
332
+ _freeProdSpecializedCalls = 0;
333
+ _prodProfileFallbackNoticeLogged = false;
334
+ }
335
+
304
336
  let devSecretNoticeLogged = false;
305
337
 
306
338
  function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
@@ -481,6 +513,82 @@ We request approval to acquire a **Developer SDK License** (\$299/month flat or
481
513
  * auditReceipt: string
482
514
  * }}
483
515
  */
516
+ const SDK_VERTICAL_DETECTORS = [
517
+ { 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 },
519
+ { 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
+ { 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
+ { 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 },
522
+ { 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 },
523
+ { 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 },
524
+ { 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 },
525
+ { 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 },
526
+ { 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 },
527
+ { 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 },
528
+ { 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 },
529
+ { 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 },
530
+ { 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 },
531
+ { 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 },
532
+ { 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 },
533
+ { 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 },
534
+ { 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 },
535
+ { 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 },
536
+ { 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 },
537
+ { 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 },
538
+ { 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 },
539
+ { 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 },
540
+ { 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 },
541
+ { 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 }
542
+ ];
543
+
544
+ /**
545
+ * Automatically detects the most relevant industry compliance profile from text content.
546
+ * Executes in <0.05ms in local RAM with zero network egress.
547
+ *
548
+ * @param {string} text - Input document or prompt text.
549
+ * @returns {{
550
+ * detectedProfile: string,
551
+ * label: string,
552
+ * confidence: 'high'|'medium'|'low',
553
+ * score: number,
554
+ * alternatives: Array<{ profile: string, label: string, score: number }>
555
+ * }}
556
+ */
557
+ function detectProfile(text) {
558
+ if (!text || typeof text !== 'string') {
559
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
560
+ }
561
+ const sampleSlice = text.length > 16384 ? text.slice(0, 16384) : text;
562
+ const matches = [];
563
+ for (let i = 0; i < SDK_VERTICAL_DETECTORS.length; i++) {
564
+ const c = SDK_VERTICAL_DETECTORS[i];
565
+ c.regex.lastIndex = 0;
566
+ const found = sampleSlice.match(c.regex);
567
+ if (found && found.length > 0) {
568
+ matches.push({
569
+ profile: c.profile,
570
+ label: c.label,
571
+ score: found.length,
572
+ priority: i
573
+ });
574
+ }
575
+ }
576
+ if (matches.length === 0) {
577
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
578
+ }
579
+ matches.sort((a, b) => (b.score !== a.score) ? (b.score - a.score) : (a.priority - b.priority));
580
+ const primary = matches[0];
581
+ const alternatives = matches.slice(1, 4).map(({ profile, label, score }) => ({ profile, label, score }));
582
+ const confidence = primary.score >= 3 ? 'high' : (primary.score >= 2 ? 'medium' : 'low');
583
+ return {
584
+ detectedProfile: primary.profile,
585
+ label: primary.label,
586
+ confidence,
587
+ score: primary.score,
588
+ alternatives
589
+ };
590
+ }
591
+
484
592
  function sanitize(text, options = {}) {
485
593
  if (!text || typeof text !== 'string') {
486
594
  return {
@@ -499,7 +607,7 @@ function sanitize(text, options = {}) {
499
607
  }
500
608
 
501
609
  const {
502
- profile = 'General',
610
+ profile: rawProfileOption = 'General',
503
611
  licenseKey = (typeof process !== 'undefined' && (process.env?.PRIVACYSCRUBBER_KEY || process.env?.PRIVACYSCRUBBER_LICENSE_KEY)) || '',
504
612
  customRules = [],
505
613
  tokenLabelMap = {},
@@ -511,6 +619,12 @@ function sanitize(text, options = {}) {
511
619
  ignoreList = null
512
620
  } = options;
513
621
 
622
+ const isAutoProfile = Boolean(
623
+ (typeof rawProfileOption === 'string' && rawProfileOption.trim().toLowerCase() === 'auto') ||
624
+ options.autoDetectProfile === true
625
+ );
626
+ const autoDetected = isAutoProfile ? detectProfile(text) : null;
627
+
514
628
  const licenseCheck = validateLicense(licenseKey);
515
629
  const rawTier = licenseCheck.valid ? (licenseCheck.tier || 'PRO') : 'FREE';
516
630
  const isExpiredKey = !licenseCheck.valid && typeof licenseCheck.error === 'string' && /expired/i.test(licenseCheck.error);
@@ -532,6 +646,21 @@ function sanitize(text, options = {}) {
532
646
  const isSdkTier = licenseCheck.valid && ['SDK', 'OEM', 'ENTERPRISE'].includes(rawTier);
533
647
  const isSeatTier = licenseCheck.valid && ['PRO', 'TEAMS'].includes(rawTier);
534
648
 
649
+ const isPro = isSdkTier;
650
+ const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
651
+
652
+ let autoSuggestedOnly = false;
653
+ let profile = rawProfileOption;
654
+ if (isAutoProfile && autoDetected) {
655
+ if (!isPro && text.length > 5000 && text.length <= 15000 && autoDetected.detectedProfile !== 'general') {
656
+ // Non-blocking PLG fallback: keep General for Free 5k-15k texts so auto-mode never breaks free calls
657
+ profile = 'General';
658
+ autoSuggestedOnly = true;
659
+ } else {
660
+ profile = autoDetected.detectedProfile;
661
+ }
662
+ }
663
+
535
664
  if (isSeatTier) {
536
665
  const keyPreview = licenseKey ? `${licenseKey.slice(0, 12)}...` : '';
537
666
  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`;
@@ -545,9 +674,6 @@ function sanitize(text, options = {}) {
545
674
  }
546
675
  }
547
676
 
548
- const isPro = isSdkTier;
549
- const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
550
-
551
677
  emitCommunityNoticeIfNeeded(isPro);
552
678
 
553
679
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
@@ -581,8 +707,33 @@ function sanitize(text, options = {}) {
581
707
  });
582
708
 
583
709
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
584
- const activeRules = [...safeCustomRules];
585
- if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
710
+ const isProdEnv = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
711
+ const hasCustomEnterpriseConfig = isSpecialized || safeCustomRules.length > 0 || (Array.isArray(ignoreList) && ignoreList.length > 0);
712
+ let profileFallback = false;
713
+ let effectiveProfile = profile;
714
+ let effectiveCustomRules = safeCustomRules;
715
+ let effectiveIgnoreList = ignoreList;
716
+
717
+ if (!isPro && isProdEnv && hasCustomEnterpriseConfig) {
718
+ _freeProdSpecializedCalls++;
719
+ if (_freeProdSpecializedCalls > FREE_PROD_SPECIALIZED_CALL_LIMIT) {
720
+ profileFallback = true;
721
+ effectiveProfile = 'General';
722
+ effectiveCustomRules = [];
723
+ effectiveIgnoreList = null;
724
+ if (!_prodProfileFallbackNoticeLogged && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
725
+ _prodProfileFallbackNoticeLogged = true;
726
+ process.stderr.write(
727
+ `\n[@privacyscrubber/sdk] Free Production Quota (${FREE_PROD_SPECIALIZED_CALL_LIMIT} specialized calls) reached for profile '${profile}'.\n` +
728
+ `[@privacyscrubber/sdk] Gracefully falling back to 'General' profile (Core PII remains protected, 0 downtime).\n` +
729
+ `[@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`
730
+ );
731
+ }
732
+ }
733
+ }
734
+
735
+ const activeRules = [...effectiveCustomRules];
736
+ if (effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) {
586
737
  DEVOPS_SECRETS_DETECTOR.forEach(rule => {
587
738
  activeRules.push({
588
739
  name: rule.name,
@@ -603,10 +754,10 @@ function sanitize(text, options = {}) {
603
754
  textToScrub,
604
755
  activeRules,
605
756
  tokenLabelMap,
606
- profile,
757
+ effectiveProfile,
607
758
  existingSessionMap,
608
759
  isPro,
609
- ignoreList
760
+ effectiveIgnoreList
610
761
  );
611
762
 
612
763
  const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
@@ -614,7 +765,7 @@ function sanitize(text, options = {}) {
614
765
  : Date.now();
615
766
 
616
767
  const executionMs = Math.round((endTime - startTime) * 100) / 100;
617
- const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
768
+ const telemetry = buildAuditTelemetry(rawResult.tokenMap || {}, { licenseTier: tier });
618
769
  const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
619
770
 
620
771
  emitDevSecretNoticeIfNeeded(isPro, rawResult.tokenMap, limit);
@@ -642,6 +793,10 @@ function sanitize(text, options = {}) {
642
793
  limit: limit,
643
794
  wasTruncated: wasTruncated,
644
795
  truncated: wasTruncated,
796
+ profileFallback: profileFallback,
797
+ effectiveProfile: effectiveProfile,
798
+ specializedCallsUsed: _freeProdSpecializedCalls,
799
+ specializedCallsLimit: isPro ? Infinity : FREE_PROD_SPECIALIZED_CALL_LIMIT,
645
800
  ...(isSeatTier && {
646
801
  upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
647
802
  trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
@@ -649,12 +804,20 @@ function sanitize(text, options = {}) {
649
804
  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`
650
805
  }),
651
806
  ...(!isPro && !isSeatTier && {
652
- upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
653
- trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
807
+ upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
808
+ trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
654
809
  trialCommand: 'npx @privacyscrubber/sdk trial',
655
810
  recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited backend text processing, custom regex rules, and specialized compliance profiles.'
656
811
  })
657
812
  },
813
+ profileDetection: {
814
+ autoDetected: isAutoProfile,
815
+ detectedProfile: autoDetected ? autoDetected.detectedProfile : String(effectiveProfile || 'General').toLowerCase(),
816
+ label: autoDetected ? autoDetected.label : effectiveProfile,
817
+ confidence: autoDetected ? autoDetected.confidence : 'high',
818
+ suggestedOnly: autoSuggestedOnly,
819
+ alternatives: autoDetected ? autoDetected.alternatives : []
820
+ },
658
821
  telemetry: telemetry,
659
822
  auditReceipt: auditReceipt
660
823
  };
@@ -3881,6 +4044,7 @@ const {
3881
4044
 
3882
4045
  const _defaultExport = {
3883
4046
  SDK_VERSION,
4047
+ detectProfile,
3884
4048
  sanitize,
3885
4049
  sanitizeObject,
3886
4050
  scrub,
@@ -3933,6 +4097,8 @@ const _defaultExport = {
3933
4097
  scrubText,
3934
4098
  unscrubText,
3935
4099
  DEVOPS_SECRETS_DETECTOR,
4100
+ FREE_PROD_SPECIALIZED_CALL_LIMIT,
4101
+ _resetProdQuotaForTesting,
3936
4102
  stripExifMetadata,
3937
4103
  redactImageBoundingBoxes,
3938
4104
  generateSvgRedactionOverlay