@privacyscrubber/sdk 2.4.6 → 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/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.6';
44
+ return '2.4.8';
45
45
  })();
46
46
 
47
47
  /**
@@ -90,7 +90,10 @@ export class PrivacyScrubberLicenseError extends Error {
90
90
  this.currentLength = currentLength;
91
91
  this.limit = limit;
92
92
  this.profile = profile;
93
- this.upgradeUrl = 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell';
93
+ const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
94
+ this.upgradeUrl = `https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
95
+ this.trialUrl = `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
96
+ this.trialCommand = 'npx @privacyscrubber/sdk trial';
94
97
  }
95
98
  }
96
99
 
@@ -131,7 +134,7 @@ export function validateLicenseKey(key) {
131
134
  * @param {Record<string, string>|string[]} currentTokenMap
132
135
  * @returns {object}
133
136
  */
134
- export function buildAuditTelemetry(currentTokenMap = {}) {
137
+ export function buildAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
135
138
  const entities = {};
136
139
  let totalCount = 0;
137
140
 
@@ -197,11 +200,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
197
200
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
198
201
  };
199
202
 
203
+ const agenticProtocol = {
204
+ protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
205
+ agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
206
+ ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
207
+ regulatorySafeHarbor: [
208
+ 'GDPR-Art-4(5)-Pseudonymization',
209
+ 'GDPR-Art-25-Privacy-by-Design',
210
+ 'GDPR-Art-32-TOMs',
211
+ 'EU-AI-Act-Art-50',
212
+ 'EU-AI-Act-Art-17-EN-18286-QMS',
213
+ 'CNIL-ICO-Agentic-Memory-Compartmentalization',
214
+ 'IL-PPL-Amendment-13-Sec-17'
215
+ ],
216
+ zeroCleartextEgress: true
217
+ };
218
+
200
219
  const statutoryCitations = [
201
220
  {
202
- statute: 'EU AI Act (Art. 50)',
203
- scope: 'AI Transparency & Data Minimization',
204
- rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
221
+ statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
222
+ scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
223
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
205
224
  },
206
225
  {
207
226
  statute: 'GDPR (Art. 4, 25, 28, 32)',
@@ -225,14 +244,34 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
225
244
  }
226
245
  ];
227
246
 
247
+ const rawLicenseTier = String(extraStats?.licenseTier || '').trim().toUpperCase();
248
+ const isOrganizationalScope = ['TEAMS', 'SDK', 'OEM', 'ENTERPRISE', 'ENTERPRISE_AIRGAP'].includes(rawLicenseTier);
249
+ const auditScope = isOrganizationalScope ? {
250
+ tier: rawLicenseTier,
251
+ isOrganizational: true,
252
+ badge: 'ORGANIZATIONAL MULTI-SEAT COMPLIANCE SCOPE',
253
+ coverage: 'Covers department, clinic, or firm personnel under unified ZTDS policy governance.',
254
+ upgradeRecommended: false,
255
+ upgradeUrl: null
256
+ } : {
257
+ tier: rawLicenseTier || 'SINGLE_OPERATOR',
258
+ isOrganizational: false,
259
+ badge: 'SINGLE-OPERATOR PERSONAL SCOPE',
260
+ coverage: 'Valid for individual workstation only. Multi-employee clinics, CPA firms, and departments require TEAMS ($99/mo flat, unlimited seats) for organizational audit coverage.',
261
+ upgradeRecommended: true,
262
+ upgradeUrl: 'https://privacyscrubber.com/pricing/?tier=teams&utm_source=audit_receipt&utm_medium=scope_upsell'
263
+ };
264
+
228
265
  return {
229
266
  totalCount,
230
267
  entities,
231
268
  types,
232
269
  riskLevel,
233
270
  frameworksList: Array.from(frameworksSet),
271
+ auditScope,
234
272
  statutoryCitations,
235
273
  ipHoldings,
274
+ agenticProtocol,
236
275
  multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
237
276
  };
238
277
  }
@@ -283,6 +322,19 @@ export function _resetCommunityNoticeForTesting() {
283
322
  communityNoticeLogged = false;
284
323
  }
285
324
 
325
+ export const FREE_PROD_SPECIALIZED_CALL_LIMIT = 250;
326
+ let _freeProdSpecializedCalls = 0;
327
+ let _prodProfileFallbackNoticeLogged = false;
328
+
329
+ /**
330
+ * Resets the production specialized call quota state for testing purposes.
331
+ * @private
332
+ */
333
+ export function _resetProdQuotaForTesting() {
334
+ _freeProdSpecializedCalls = 0;
335
+ _prodProfileFallbackNoticeLogged = false;
336
+ }
337
+
286
338
  let devSecretNoticeLogged = false;
287
339
 
288
340
  function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
@@ -463,6 +515,82 @@ We request approval to acquire a **Developer SDK License** (\$299/month flat or
463
515
  * auditReceipt: string
464
516
  * }}
465
517
  */
518
+ const SDK_VERTICAL_DETECTORS = [
519
+ { 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 },
521
+ { 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
+ { 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
+ { 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 },
524
+ { 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 },
525
+ { 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 },
526
+ { 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 },
527
+ { 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 },
528
+ { 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 },
529
+ { 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 },
530
+ { 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 },
531
+ { 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 },
532
+ { 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 },
533
+ { 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 },
534
+ { 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 },
535
+ { 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 },
536
+ { 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 },
537
+ { 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 },
538
+ { 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 },
539
+ { 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 },
540
+ { 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 },
541
+ { 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 },
542
+ { 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 },
543
+ { 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 }
544
+ ];
545
+
546
+ /**
547
+ * Automatically detects the most relevant industry compliance profile from text content.
548
+ * Executes in <0.05ms in local RAM with zero network egress.
549
+ *
550
+ * @param {string} text - Input document or prompt text.
551
+ * @returns {{
552
+ * detectedProfile: string,
553
+ * label: string,
554
+ * confidence: 'high'|'medium'|'low',
555
+ * score: number,
556
+ * alternatives: Array<{ profile: string, label: string, score: number }>
557
+ * }}
558
+ */
559
+ export function detectProfile(text) {
560
+ if (!text || typeof text !== 'string') {
561
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
562
+ }
563
+ const sampleSlice = text.length > 16384 ? text.slice(0, 16384) : text;
564
+ const matches = [];
565
+ for (let i = 0; i < SDK_VERTICAL_DETECTORS.length; i++) {
566
+ const c = SDK_VERTICAL_DETECTORS[i];
567
+ c.regex.lastIndex = 0;
568
+ const found = sampleSlice.match(c.regex);
569
+ if (found && found.length > 0) {
570
+ matches.push({
571
+ profile: c.profile,
572
+ label: c.label,
573
+ score: found.length,
574
+ priority: i
575
+ });
576
+ }
577
+ }
578
+ if (matches.length === 0) {
579
+ return { detectedProfile: 'general', label: 'General PII', confidence: 'low', score: 0, alternatives: [] };
580
+ }
581
+ matches.sort((a, b) => (b.score !== a.score) ? (b.score - a.score) : (a.priority - b.priority));
582
+ const primary = matches[0];
583
+ const alternatives = matches.slice(1, 4).map(({ profile, label, score }) => ({ profile, label, score }));
584
+ const confidence = primary.score >= 3 ? 'high' : (primary.score >= 2 ? 'medium' : 'low');
585
+ return {
586
+ detectedProfile: primary.profile,
587
+ label: primary.label,
588
+ confidence,
589
+ score: primary.score,
590
+ alternatives
591
+ };
592
+ }
593
+
466
594
  export function sanitize(text, options = {}) {
467
595
  if (!text || typeof text !== 'string') {
468
596
  return {
@@ -481,7 +609,7 @@ export function sanitize(text, options = {}) {
481
609
  }
482
610
 
483
611
  const {
484
- profile = 'General',
612
+ profile: rawProfileOption = 'General',
485
613
  licenseKey = (typeof process !== 'undefined' && (process.env?.PRIVACYSCRUBBER_KEY || process.env?.PRIVACYSCRUBBER_LICENSE_KEY)) || '',
486
614
  customRules = [],
487
615
  tokenLabelMap = {},
@@ -493,6 +621,12 @@ export function sanitize(text, options = {}) {
493
621
  ignoreList = null
494
622
  } = options;
495
623
 
624
+ const isAutoProfile = Boolean(
625
+ (typeof rawProfileOption === 'string' && rawProfileOption.trim().toLowerCase() === 'auto') ||
626
+ options.autoDetectProfile === true
627
+ );
628
+ const autoDetected = isAutoProfile ? detectProfile(text) : null;
629
+
496
630
  const licenseCheck = validateLicense(licenseKey);
497
631
  const rawTier = licenseCheck.valid ? (licenseCheck.tier || 'PRO') : 'FREE';
498
632
  const isExpiredKey = !licenseCheck.valid && typeof licenseCheck.error === 'string' && /expired/i.test(licenseCheck.error);
@@ -514,6 +648,21 @@ export function sanitize(text, options = {}) {
514
648
  const isSdkTier = licenseCheck.valid && ['SDK', 'OEM', 'ENTERPRISE'].includes(rawTier);
515
649
  const isSeatTier = licenseCheck.valid && ['PRO', 'TEAMS'].includes(rawTier);
516
650
 
651
+ const isPro = isSdkTier;
652
+ const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
653
+
654
+ let autoSuggestedOnly = false;
655
+ let profile = rawProfileOption;
656
+ if (isAutoProfile && autoDetected) {
657
+ if (!isPro && text.length > 5000 && text.length <= 15000 && autoDetected.detectedProfile !== 'general') {
658
+ // Non-blocking PLG fallback: keep General for Free 5k-15k texts so auto-mode never breaks free calls
659
+ profile = 'General';
660
+ autoSuggestedOnly = true;
661
+ } else {
662
+ profile = autoDetected.detectedProfile;
663
+ }
664
+ }
665
+
517
666
  if (isSeatTier) {
518
667
  const keyPreview = licenseKey ? `${licenseKey.slice(0, 12)}...` : '';
519
668
  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`;
@@ -527,9 +676,6 @@ export function sanitize(text, options = {}) {
527
676
  }
528
677
  }
529
678
 
530
- const isPro = isSdkTier;
531
- const tier = isPro ? rawTier : (isSeatTier ? rawTier : 'FREE');
532
-
533
679
  emitCommunityNoticeIfNeeded(isPro);
534
680
 
535
681
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
@@ -541,7 +687,8 @@ export function sanitize(text, options = {}) {
541
687
  if (!isPro && text.length > limit) {
542
688
  if (shouldThrow) {
543
689
  const expiredPrefix = isExpiredKey ? `[PILOT/TRIAL EXPIRED: ${licenseCheck.error}] ` : '';
544
- const errorMsg = `[PrivacyScrubber] ${expiredPrefix}Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to Developer SDK ($299/mo) for unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&src=${isExpiredKey ? 'pilot_expired' : 'dev_limit'} (Run 'npx @privacyscrubber/sdk memo' for CISO approval memo)`;
690
+ const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
691
+ const errorMsg = `[PrivacyScrubber] ${expiredPrefix}Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to Developer SDK ($299/mo) for unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&src=${isExpiredKey ? 'pilot_expired' : 'dev_limit'} (Start 7-day B2B trial: 'npx @privacyscrubber/sdk trial' or 'npx @privacyscrubber/sdk memo' for CISO memo)`;
545
692
  throw new PrivacyScrubberLicenseError(errorMsg, {
546
693
  tier: 'FREE',
547
694
  currentLength: text.length,
@@ -562,8 +709,33 @@ export function sanitize(text, options = {}) {
562
709
  });
563
710
 
564
711
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
565
- const activeRules = [...safeCustomRules];
566
- if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
712
+ const isProdEnv = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
713
+ const hasCustomEnterpriseConfig = isSpecialized || safeCustomRules.length > 0 || (Array.isArray(ignoreList) && ignoreList.length > 0);
714
+ let profileFallback = false;
715
+ let effectiveProfile = profile;
716
+ let effectiveCustomRules = safeCustomRules;
717
+ let effectiveIgnoreList = ignoreList;
718
+
719
+ if (!isPro && isProdEnv && hasCustomEnterpriseConfig) {
720
+ _freeProdSpecializedCalls++;
721
+ if (_freeProdSpecializedCalls > FREE_PROD_SPECIALIZED_CALL_LIMIT) {
722
+ profileFallback = true;
723
+ effectiveProfile = 'General';
724
+ effectiveCustomRules = [];
725
+ effectiveIgnoreList = null;
726
+ if (!_prodProfileFallbackNoticeLogged && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
727
+ _prodProfileFallbackNoticeLogged = true;
728
+ process.stderr.write(
729
+ `\n[@privacyscrubber/sdk] Free Production Quota (${FREE_PROD_SPECIALIZED_CALL_LIMIT} specialized calls) reached for profile '${profile}'.\n` +
730
+ `[@privacyscrubber/sdk] Gracefully falling back to 'General' profile (Core PII remains protected, 0 downtime).\n` +
731
+ `[@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`
732
+ );
733
+ }
734
+ }
735
+ }
736
+
737
+ const activeRules = [...effectiveCustomRules];
738
+ if (effectiveProfile.toLowerCase() === 'dev' || detectSecrets === true) {
567
739
  DEVOPS_SECRETS_DETECTOR.forEach(rule => {
568
740
  activeRules.push({
569
741
  name: rule.name,
@@ -584,10 +756,10 @@ export function sanitize(text, options = {}) {
584
756
  textToScrub,
585
757
  activeRules,
586
758
  tokenLabelMap,
587
- profile,
759
+ effectiveProfile,
588
760
  existingSessionMap,
589
761
  isPro,
590
- ignoreList
762
+ effectiveIgnoreList
591
763
  );
592
764
 
593
765
  const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
@@ -595,7 +767,7 @@ export function sanitize(text, options = {}) {
595
767
  : Date.now();
596
768
 
597
769
  const executionMs = Math.round((endTime - startTime) * 100) / 100;
598
- const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
770
+ const telemetry = buildAuditTelemetry(rawResult.tokenMap || {}, { licenseTier: tier });
599
771
  const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
600
772
 
601
773
  emitDevSecretNoticeIfNeeded(isPro, rawResult.tokenMap, limit);
@@ -622,15 +794,32 @@ export function sanitize(text, options = {}) {
622
794
  tier: tier,
623
795
  limit: limit,
624
796
  wasTruncated: wasTruncated,
797
+ truncated: wasTruncated,
798
+ profileFallback: profileFallback,
799
+ effectiveProfile: effectiveProfile,
800
+ specializedCallsUsed: _freeProdSpecializedCalls,
801
+ specializedCallsLimit: isPro ? Infinity : FREE_PROD_SPECIALIZED_CALL_LIMIT,
625
802
  ...(isSeatTier && {
626
- upgradeUrl: 'https://privacyscrubber.com/pricing?tier=sdk&src=seat_tier_mismatch',
803
+ upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
804
+ trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
805
+ trialCommand: 'npx @privacyscrubber/sdk trial',
627
806
  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`
628
807
  }),
629
808
  ...(!isPro && !isSeatTier && {
630
- upgradeUrl: 'https://privacyscrubber.com/pricing?tier=sdk&src=dev_upsell',
809
+ upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
810
+ trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=${profileFallback ? 'prod_profile_fallback' : 'dev_upsell'}`,
811
+ trialCommand: 'npx @privacyscrubber/sdk trial',
631
812
  recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited backend text processing, custom regex rules, and specialized compliance profiles.'
632
813
  })
633
814
  },
815
+ profileDetection: {
816
+ autoDetected: isAutoProfile,
817
+ detectedProfile: autoDetected ? autoDetected.detectedProfile : String(effectiveProfile || 'General').toLowerCase(),
818
+ label: autoDetected ? autoDetected.label : effectiveProfile,
819
+ confidence: autoDetected ? autoDetected.confidence : 'high',
820
+ suggestedOnly: autoSuggestedOnly,
821
+ alternatives: autoDetected ? autoDetected.alternatives : []
822
+ },
634
823
  telemetry: telemetry,
635
824
  auditReceipt: auditReceipt
636
825
  };
@@ -3862,6 +4051,7 @@ import {
3862
4051
 
3863
4052
  export default {
3864
4053
  SDK_VERSION,
4054
+ detectProfile,
3865
4055
  sanitize,
3866
4056
  sanitizeObject,
3867
4057
  scrub,
@@ -3914,6 +4104,8 @@ export default {
3914
4104
  scrubText,
3915
4105
  unscrubText,
3916
4106
  DEVOPS_SECRETS_DETECTOR,
4107
+ FREE_PROD_SPECIALIZED_CALL_LIMIT,
4108
+ _resetProdQuotaForTesting,
3917
4109
  stripExifMetadata,
3918
4110
  redactImageBoundingBoxes,
3919
4111
  generateSvgRedactionOverlay
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
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",