@privacyscrubber/sdk 2.4.0 → 2.4.2

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.
Files changed (29) hide show
  1. package/benchmark.js +33 -25
  2. package/browser.js +274 -37
  3. package/cli.js +7 -1
  4. package/examples/python-bridge/bridge_daemon.cjs +2 -2
  5. package/examples/python-bridge/fastapi_example.py +1 -1
  6. package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
  7. package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +1 -1
  8. package/examples/python-bridge/privacyscrubber/cli.py +1 -1
  9. package/examples/python-bridge/privacyscrubber/client.py +11 -5
  10. package/examples/python-bridge/pyproject.toml +1 -1
  11. package/examples/python-bridge/setup.py +1 -1
  12. package/examples/python-bridge/test_python_bridge.py +1 -1
  13. package/fastmcp.cjs +29 -18
  14. package/index.cjs +31 -13
  15. package/index.js +35 -17
  16. package/package.json +5 -1
  17. package/ps-license-manager.js +261 -11
  18. package/ps-pii-engine.cjs +3611 -8
  19. package/scrubber-core.cjs +1801 -8
  20. package/vercel-ai.cjs +1 -1
  21. package/vercel-ai.js +1 -1
  22. package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
  23. package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
  24. package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +0 -311
  25. package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +0 -18
  26. package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +0 -1
  27. package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +0 -3
  28. package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +0 -18
  29. package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +0 -1
package/benchmark.js CHANGED
@@ -114,17 +114,8 @@ function benchmarkWorkload(label, targetBytes, warmupCount, measurementCount, pr
114
114
  const rawPayload = generateRealisticWorkload(targetBytes);
115
115
  const actualBytes = Buffer.byteLength(rawPayload, 'utf8');
116
116
 
117
- // Generate valid test license key to avoid community tier truncation
118
- const salt = "ZTDS_SALT_2026_!@#";
119
- const core = "PROD9876KEY5";
120
- const tier = "SDK";
121
- const inputStr = core + salt + tier;
122
- let sum = 0;
123
- for (let j = 0; j < inputStr.length; j++) {
124
- sum += inputStr.charCodeAt(j) * (j + 1);
125
- }
126
- const checksum = (sum % 9999).toString().padStart(4, '0');
127
- const licenseKey = `PS-${tier}-${core}-${checksum}`;
117
+ // Optional commercial evaluation license key from environment (defaults to Community tier)
118
+ const licenseKey = process.env.PRIVACYSCRUBBER_KEY || undefined;
128
119
 
129
120
  // Warm-up phase (V8 TurboFan JIT optimization)
130
121
  let warmupResult;
@@ -198,7 +189,15 @@ export function runBenchmarks(options = {}) {
198
189
  const profile = options.profile || REQUESTED_PROFILE;
199
190
  const itersOverride = options.iterations || ITERATIONS_OVERRIDE;
200
191
 
192
+ const hasCommercialKey = Boolean(process.env.PRIVACYSCRUBBER_KEY);
201
193
  const suites = [
194
+ {
195
+ label: '1 KB (Short Agentic Tool Call / Chat Turn)',
196
+ bytes: 1 * 1024,
197
+ warmup: 50,
198
+ iterations: itersOverride > 0 ? itersOverride : 500,
199
+ targetSlaMs: 0.8
200
+ },
202
201
  {
203
202
  label: '2 KB (Interactive LLM Chat Prompt)',
204
203
  bytes: 2 * 1024,
@@ -206,6 +205,13 @@ export function runBenchmarks(options = {}) {
206
205
  iterations: itersOverride > 0 ? itersOverride : 500,
207
206
  targetSlaMs: 1.0 // Sub-millisecond ZTDS SLA invariant
208
207
  },
208
+ {
209
+ label: '4 KB (Multi-Entity Solid Pod / Clinical Note)',
210
+ bytes: 4 * 1024,
211
+ warmup: 30,
212
+ iterations: itersOverride > 0 ? itersOverride : 300,
213
+ targetSlaMs: 2.5
214
+ },
209
215
  {
210
216
  label: '10 KB (Enterprise Support Ticket / Incident)',
211
217
  bytes: 10 * 1024,
@@ -213,20 +219,22 @@ export function runBenchmarks(options = {}) {
213
219
  iterations: itersOverride > 0 ? itersOverride : 250,
214
220
  targetSlaMs: 5.0
215
221
  },
216
- {
217
- label: '50 KB (RAG Context Chunk / Knowledge Retrieval)',
218
- bytes: 50 * 1024,
219
- warmup: 15,
220
- iterations: itersOverride > 0 ? itersOverride : 100,
221
- targetSlaMs: 25.0
222
- },
223
- {
224
- label: '250 KB (Batch Processing / Multi-Page Document)',
225
- bytes: 250 * 1024,
226
- warmup: 5,
227
- iterations: itersOverride > 0 ? itersOverride : 30,
228
- targetSlaMs: 150.0
229
- }
222
+ ...(hasCommercialKey ? [
223
+ {
224
+ label: '50 KB (RAG Context Chunk / Knowledge Retrieval)',
225
+ bytes: 50 * 1024,
226
+ warmup: 15,
227
+ iterations: itersOverride > 0 ? itersOverride : 100,
228
+ targetSlaMs: 25.0
229
+ },
230
+ {
231
+ label: '250 KB (Batch Processing / Multi-Page Document)',
232
+ bytes: 250 * 1024,
233
+ warmup: 5,
234
+ iterations: itersOverride > 0 ? itersOverride : 30,
235
+ targetSlaMs: 150.0
236
+ }
237
+ ] : [])
230
238
  ];
231
239
 
232
240
  const results = [];
package/browser.js CHANGED
@@ -28,16 +28,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
28
28
  * Built-in patterns for DevOps & Cloud Secrets detection.
29
29
  */
30
30
  export const DEVOPS_SECRETS_DETECTOR = [
31
- { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g, label: 'AWS_KEY' },
31
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
32
+ { name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
33
+ { name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g, label: 'API_TOKEN' },
34
+ { name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g, label: 'API_TOKEN' },
35
+ { name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
36
+ { name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
37
+ { name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g, label: 'API_TOKEN' },
38
+ { name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g, label: 'API_TOKEN' },
39
+ { name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
40
+ { name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
41
+ { name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g, label: 'API_TOKEN' },
42
+ { name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
43
+ { name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g, label: 'SECRET' },
32
44
  { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
33
- { name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
34
- { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g, label: 'STRIPE_KEY' },
45
+ { name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g, label: 'API_TOKEN' },
46
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
35
47
  { name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
48
+ { name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi, label: 'SECRET' },
49
+ { name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi, label: 'SECRET' },
36
50
  { name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
37
- { name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
51
+ { name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
38
52
  { name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
53
+ { name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi, label: 'SECRET' },
54
+ { name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi, label: 'SECRET' },
55
+ { name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
39
56
  { name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
40
- { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi, label: 'SECRET' },
57
+ { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi, label: 'SECRET' },
41
58
  { name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
42
59
  { name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
43
60
  ];
@@ -147,7 +164,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
147
164
  }
148
165
 
149
166
  const ipHoldings = {
150
- standard: 'IETF draft-sibiryakov-ztds-protocol-00',
167
+ standard: 'IETF draft-sibiryakov-ztds-protocol-01',
151
168
  standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
152
169
  patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
153
170
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
@@ -157,27 +174,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
157
174
  {
158
175
  statute: 'EU AI Act (Art. 50)',
159
176
  scope: 'AI Transparency & Data Minimization',
160
- rule: '0-byte external network transmission; prevents unauthorized ingestion into frontier models.'
177
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
161
178
  },
162
179
  {
163
180
  statute: 'GDPR (Art. 4, 25, 28, 32)',
164
181
  scope: 'Pseudonymization & Processor Liability',
165
- rule: 'Volatile client-side tokenization eliminates GDPR Article 28 data processor liability.'
182
+ rule: 'Client-side pseudonymization is a technical measure supporting Art. 25 and 32. It does not by itself remove controller or processor obligations.'
166
183
  },
167
184
  {
168
185
  statute: 'HIPAA 45 CFR § 164.514(b)',
169
186
  scope: 'Safe Harbor De-Identification',
170
- rule: 'Deterministic removal of all 18 PHI identifiers prior to AI analysis.'
187
+ rule: 'Pattern-based masking of direct identifiers supports Safe Harbor de-identification workflows. Review output for residual identifiers before sharing.'
171
188
  },
172
189
  {
173
190
  statute: 'IRC § 7216 & IRS Pub. 1075',
174
191
  scope: 'Tax Return Information Confidentiality',
175
- rule: 'Strict criminal penalty safeguard against unauthorized disclosure of tax records to cloud LLMs.'
192
+ rule: 'Masking identifiers before AI use can reduce exposure of tax return information. Informational mapping, not legal advice.'
176
193
  },
177
194
  {
178
195
  statute: 'FRE 502',
179
196
  scope: 'Attorney-Client Privilege Preservation',
180
- rule: 'Automated in-memory redaction prevents inadvertent waiver of legal privilege in AI workflows.'
197
+ rule: 'In-memory redaction can reduce disclosure of privileged details in AI workflows. Privilege determinations remain a legal judgment.'
181
198
  }
182
199
  ];
183
200
 
@@ -545,6 +562,10 @@ export class PrivacyScrubberEngine {
545
562
  delete this.sessionMap[key];
546
563
  }
547
564
  }
565
+
566
+ createStreamRestorer(options = {}) {
567
+ return createStreamTokenRestorer(this.sessionMap, options);
568
+ }
548
569
  }
549
570
 
550
571
  /**
@@ -576,13 +597,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
576
597
 
577
598
  for (const k of sortedKeys) {
578
599
  const val = tokenMap[k];
579
- const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
600
+ const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
580
601
  const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
581
602
 
582
603
  if (match) {
583
- const label = escapeRegex(match[1]);
604
+ const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
584
605
  const idx = match[2];
585
- const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
606
+ 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)';
586
607
  patterns.push(patternStr);
587
608
  const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
588
609
  lookup.set(normKey, val);
@@ -611,10 +632,14 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
611
632
  } else if (lookup.has(coreMatch.toUpperCase())) {
612
633
  restored = lookup.get(coreMatch.toUpperCase());
613
634
  } else {
614
- const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
615
- const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
635
+ const inner = coreMatch
636
+ .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, '')
637
+ .replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
638
+ .trim();
639
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
616
640
  if (m) {
617
- const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
641
+ const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
642
+ const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
618
643
  if (lookup.has(normKey)) {
619
644
  restored = lookup.get(normKey);
620
645
  }
@@ -720,18 +745,17 @@ export class StreamingDetokenizer {
720
745
  this.pendingBuffer += text;
721
746
 
722
747
  // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
723
- // Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
748
+ // Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity &#91; / &lbrack;
724
749
  const tailLen = 160;
725
750
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
726
751
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
727
- const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
752
+ const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{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)?)?)?)?)?)?)$/);
728
753
 
729
754
  if (m) {
730
755
  const candidate = m[0];
731
- // Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
732
- // Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
733
- const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
734
- if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
756
+ // Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
757
+ const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
758
+ if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
735
759
  const toEmit = this.pendingBuffer;
736
760
  this.pendingBuffer = '';
737
761
  return this.replacer(toEmit);
@@ -773,6 +797,49 @@ export class StreamingDetokenizer {
773
797
  this.pendingBuffer = '';
774
798
  return this.replacer(remaining);
775
799
  }
800
+
801
+ /**
802
+ * Wraps an async iterable (e.g. LLM streaming response) yielding restored chunks.
803
+ * @param {AsyncIterable<any>} asyncIterable
804
+ * @returns {AsyncGenerator<any>}
805
+ */
806
+ async* wrapStream(asyncIterable) {
807
+ for await (const chunk of asyncIterable) {
808
+ if (chunk === null || chunk === undefined) continue;
809
+ if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
810
+ const delta = chunk.choices[0].delta.content;
811
+ if (typeof delta === 'string') {
812
+ const restored = this.feed(delta);
813
+ if (restored) {
814
+ chunk.choices[0].delta.content = restored;
815
+ yield chunk;
816
+ }
817
+ } else {
818
+ yield chunk;
819
+ }
820
+ continue;
821
+ }
822
+
823
+ let text = '';
824
+ if (typeof chunk === 'string') {
825
+ text = chunk;
826
+ } else if (typeof chunk?.text === 'string') {
827
+ text = chunk.text;
828
+ } else if (typeof chunk?.content === 'string') {
829
+ text = chunk.content;
830
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
831
+ text = chunk;
832
+ } else {
833
+ text = String(chunk);
834
+ }
835
+
836
+ const emitted = this.feed(text);
837
+ if (emitted) yield emitted;
838
+ }
839
+
840
+ const finalChunk = this.flush();
841
+ if (finalChunk) yield finalChunk;
842
+ }
776
843
  }
777
844
 
778
845
  /**
@@ -803,33 +870,198 @@ export async function* wrapAiStream(stream, tokenMap = {}, options = {}) {
803
870
 
804
871
  export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
805
872
  const detok = new StreamingDetokenizer(tokenMap, options);
873
+ let stream;
806
874
  if (typeof TransformStream !== 'undefined') {
807
- const ts = new TransformStream({
875
+ stream = new TransformStream({
808
876
  transform(chunk, controller) {
809
- const str = typeof chunk === 'string' ? chunk : String(chunk);
810
- const out = detok.process(str);
877
+ const out = detok.feed(chunk);
811
878
  if (out) controller.enqueue(out);
812
879
  },
813
880
  flush(controller) {
814
- const rem = detok.flush();
815
- if (rem) controller.enqueue(rem);
881
+ const out = detok.flush();
882
+ if (out) controller.enqueue(out);
816
883
  }
817
884
  });
818
- ts.process = (c) => detok.process(c);
819
- ts.flush = () => detok.flush();
820
- ts.detokenizer = detok;
821
- return ts;
885
+ } else {
886
+ stream = {};
822
887
  }
823
- return {
824
- process: (c) => detok.process(c),
825
- flush: () => detok.flush(),
826
- detokenizer: detok
827
- };
888
+ stream.process = (chunk) => detok.feed(chunk);
889
+ stream.feed = (chunk) => detok.feed(chunk);
890
+ stream.flush = () => detok.flush();
891
+ stream.wrapStream = (asyncIterable) => detok.wrapStream(asyncIterable);
892
+ stream.detokenizer = detok;
893
+ return stream;
828
894
  }
829
895
 
830
896
  export const scrubText = scrubberPkg ? scrubberPkg.scrubText : null;
831
897
  export const unscrubText = scrubberPkg ? scrubberPkg.unscrubText : null;
832
898
 
899
+ // ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
900
+ export const PSR2_VERSION = 'PSR-2';
901
+ export const PSR2_STATEMENTS = {
902
+ measured: [
903
+ 'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
904
+ 'Entity counts per type.',
905
+ 'Receipt SHA-256: digest of the canonical field string, so edits to this receipt are detectable.'
906
+ ],
907
+ byDesign: [
908
+ 'The sanitization engine runs locally in process memory and does not transmit prompt content. This is an architectural property and is not measured by this receipt.',
909
+ 'Token-to-original mappings are held in volatile memory for the session. This receipt does not record their contents.'
910
+ ],
911
+ limits: [
912
+ 'This receipt is not a digital signature and does not prove who generated it or when.',
913
+ 'No final output digest is recorded by this export (Output SHA-256: not recorded).',
914
+ 'Detection is pattern-based. Free-text narrative can contain indirect identifiers that remain after sanitization; review output before sharing.',
915
+ 'Regulatory references are informational technical-safeguard mappings, not a legal determination of compliance.'
916
+ ]
917
+ };
918
+ function psr2Sha256(input) {
919
+ const bytes = new TextEncoder().encode(String(input == null ? '' : input));
920
+ const K = [
921
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
922
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
923
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
924
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
925
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
926
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
927
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
928
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
929
+ ];
930
+ const H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
931
+ const bitLen = bytes.length * 8;
932
+ const totalLen = (((bytes.length + 9 + 63) >> 6) << 6);
933
+ const msg = new Uint8Array(totalLen);
934
+ msg.set(bytes);
935
+ msg[bytes.length] = 0x80;
936
+ const view = new DataView(msg.buffer);
937
+ view.setUint32(totalLen - 8, Math.floor(bitLen / 0x100000000), false);
938
+ view.setUint32(totalLen - 4, bitLen >>> 0, false);
939
+ const W = new Uint32Array(64);
940
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
941
+ for (let off = 0; off < totalLen; off += 64) {
942
+ for (let i = 0; i < 16; i++) W[i] = view.getUint32(off + i * 4, false);
943
+ for (let i = 16; i < 64; i++) {
944
+ const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3);
945
+ const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10);
946
+ W[i] = (W[i - 16] + s0 + W[i - 7] + s1) >>> 0;
947
+ }
948
+ let [a, b, c, d, e, f, g, h] = H;
949
+ for (let i = 0; i < 64; i++) {
950
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
951
+ const ch = (e & f) ^ ((~e) & g);
952
+ const temp1 = (h + S1 + ch + K[i] + W[i]) >>> 0;
953
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
954
+ const maj = (a & b) ^ (a & c) ^ (b & c);
955
+ const temp2 = (S0 + maj) >>> 0;
956
+ h = g; g = f; f = e; e = (d + temp1) >>> 0;
957
+ d = c; c = b; b = a; a = (temp1 + temp2) >>> 0;
958
+ }
959
+ H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
960
+ H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
961
+ }
962
+ return H.map(x => x.toString(16).padStart(8, '0')).join('');
963
+ }
964
+ export function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
965
+ const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
966
+ const counts = {};
967
+ for (const t of labels) {
968
+ const m = t.match(/\[([A-Z_]+)_\d+\]/);
969
+ const type = m ? m[1] : 'CUSTOM';
970
+ counts[type] = (counts[type] || 0) + 1;
971
+ }
972
+ const entityCounts = Object.keys(counts).sort().map(k => `${k}=${counts[k]}`).join(',') || 'none';
973
+ const manifestSha256 = psr2Sha256(labels.join('\n'));
974
+ const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
975
+ return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
976
+ }
977
+ export function psr2Lines(r) {
978
+ return [
979
+ `Receipt Version: ${r.version}`,
980
+ `Timestamp (UTC): ${r.timestamp}`,
981
+ `Output SHA-256: not recorded`,
982
+ `Token Manifest SHA-256: ${r.manifestSha256}`,
983
+ `Entity Counts: ${r.entityCounts}`,
984
+ `Tokens In Final Output: not recorded`,
985
+ `Canonical: ${r.canonical}`,
986
+ `Receipt SHA-256: ${r.receiptSha256}`
987
+ ];
988
+ }
989
+ function formatPsr2Report({ format, receipt, telemetry, company_name, department, utmMedium }) {
990
+ const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
991
+ const teamsUrl = `https://privacyscrubber.com/teams?tier=teams&utm_source=sdk_audit_report&utm_medium=${utmMedium}&utm_campaign=teams_upgrade`;
992
+ const sdkUrl = `https://privacyscrubber.com/sdk/?tier=sdk&utm_source=sdk_audit_report&utm_medium=${utmMedium}&utm_campaign=sdk_pipeline`;
993
+ if (format === 'json') {
994
+ return JSON.stringify({
995
+ protocol: 'Zero-Trust Data Sanitization (ZTDS)',
996
+ receipt_version: receipt.version,
997
+ company: company_name,
998
+ department,
999
+ timestamp: receipt.timestamp,
1000
+ output_sha256: null,
1001
+ token_manifest_sha256: receipt.manifestSha256,
1002
+ entity_counts: receipt.entityCounts,
1003
+ canonical: receipt.canonical,
1004
+ receipt_sha256: receipt.receiptSha256,
1005
+ risk_level: telemetry.riskLevel,
1006
+ frameworks_referenced: telemetry.frameworksList,
1007
+ total_masked_tokens: telemetry.totalCount,
1008
+ entities_breakdown: telemetry.entities,
1009
+ scope: { measured: PSR2_STATEMENTS.measured, by_design_not_measured: PSR2_STATEMENTS.byDesign, limits: PSR2_STATEMENTS.limits },
1010
+ verification_url: 'https://privacyscrubber.com/features/audit-receipt/#verify',
1011
+ licensing_and_governance: { teams_governance_url: teamsUrl, sdk_pipeline_url: sdkUrl }
1012
+ }, null, 2);
1013
+ }
1014
+ if (format === 'summary') {
1015
+ return `[PrivacyScrubber SHA-256 Receipt] ${receipt.version} | Receipt SHA-256: ${receipt.receiptSha256.substring(0, 16)}... | Organization: ${company_name} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks (informational): ${telemetry.frameworksList.join(', ')}`;
1016
+ }
1017
+ return `# Zero-Trust Data Sanitization Receipt
1018
+ **Organization:** ${company_name} (${department})
1019
+ **Timestamp:** ${receipt.timestamp}
1020
+ **Receipt Format:** ${receipt.version} (SHA-256 digests; not a digital signature)
1021
+
1022
+ ---
1023
+
1024
+ ### Sanitization Metrics & Risk Assessment
1025
+ * **Overall Risk Rating:** **${telemetry.riskLevel}**
1026
+ * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
1027
+ * **Entity Breakdown:** ${entitySummary}
1028
+
1029
+ ### Informational Framework References
1030
+ ${telemetry.frameworksList.map(f => `- ${f}`).join('\n')}
1031
+
1032
+ These are technical-safeguard mappings, not a legal determination of compliance.
1033
+
1034
+ ### Receipt Integrity (${receipt.version})
1035
+ \`\`\`text
1036
+ ${psr2Lines(receipt).join('\n')}
1037
+ \`\`\`
1038
+
1039
+ ### Scope of This Receipt
1040
+ **Measured:** ${PSR2_STATEMENTS.measured.join(' ')}
1041
+
1042
+ **Stated by design, not measured:** ${PSR2_STATEMENTS.byDesign.join(' ')}
1043
+
1044
+ **Limits:** ${PSR2_STATEMENTS.limits.join(' ')}
1045
+
1046
+ ### Enterprise & Team Governance
1047
+ * **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules and encrypted session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](${teamsUrl})
1048
+ * **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](${sdkUrl})
1049
+
1050
+ *Generated locally by PrivacyScrubber Core SDK*
1051
+ *Verify locally (paste this receipt): https://privacyscrubber.com/features/audit-receipt/#verify*`;
1052
+ }
1053
+
1054
+ export function generateAuditReport(sessionMap = {}, options = {}) {
1055
+ const { companyName = 'PrivacyScrubber Client', department = 'SecOps / Compliance', format = 'markdown' } = options;
1056
+ const telemetry = buildAuditTelemetry(sessionMap);
1057
+ const timestamp = new Date().toISOString();
1058
+ const receipt = buildPsr2Receipt(sessionMap, timestamp);
1059
+ if (format === 'json') {
1060
+ return JSON.parse(formatPsr2Report({ format: 'json', receipt, telemetry, company_name: companyName, department, utmMedium: 'json' }));
1061
+ }
1062
+ return formatPsr2Report({ format: format === 'summary' ? 'summary' : 'markdown', receipt, telemetry, company_name: companyName, department, utmMedium: 'markdown' });
1063
+ }
1064
+
833
1065
  // Vision & Multimodal exports
834
1066
  export {
835
1067
  stripExifMetadata,
@@ -855,6 +1087,11 @@ export default {
855
1087
  buildAuditTelemetry,
856
1088
  buildCisoAuditTelemetry,
857
1089
  formatAuditReceipt,
1090
+ generateAuditReport,
1091
+ PSR2_VERSION,
1092
+ PSR2_STATEMENTS,
1093
+ buildPsr2Receipt,
1094
+ psr2Lines,
858
1095
  PrivacyScrubberEngine,
859
1096
  PrivacyScrubberLicenseError,
860
1097
  StreamingDetokenizer,
package/cli.js CHANGED
@@ -23,7 +23,7 @@ const __dirname = path.dirname(__filename);
23
23
 
24
24
  const args = process.argv.slice(2);
25
25
 
26
- let sdkVersion = '2.4.0';
26
+ let sdkVersion = '2.4.2';
27
27
  try {
28
28
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
29
29
  sdkVersion = pkg.version || sdkVersion;
@@ -389,6 +389,7 @@ fi
389
389
  try { fs.chmodSync(preCommitPath, 0o755); } catch (_) {}
390
390
  console.log('[PrivacyScrubber Hook] Successfully installed Zero-Trust pre-commit hook in .git/hooks/pre-commit.');
391
391
  console.log('[PrivacyScrubber Hook] Staged changes will now be audited for unmasked secrets before commit.');
392
+ console.log('[GitHub] Star the open-source repository: https://github.com/moxno/privacyscrubber-mcp (CLI: gh repo star moxno/privacyscrubber-mcp)');
392
393
  process.exit(0);
393
394
  }
394
395
 
@@ -404,10 +405,15 @@ fi
404
405
  fs.unlinkSync(preCommitPath);
405
406
  }
406
407
  console.log('[PrivacyScrubber Hook] Pre-commit hook removed successfully.');
408
+ console.log('[Feedback] Why did you remove the hook? (1-click instant log):');
409
+ console.log(' - False positive / Accuracy: https://privacyscrubber.com/uninstall/?src=sdk&reason=accuracy');
410
+ console.log(' - Workflow / Setup issue: https://privacyscrubber.com/uninstall/?src=sdk&reason=setup');
411
+ console.log(' - General feedback: https://privacyscrubber.com/uninstall/?mode=feedback&src=sdk');
407
412
  process.exit(0);
408
413
  }
409
414
  }
410
415
  console.log('[PrivacyScrubber Hook] Pre-commit hook was not found in active repository.');
416
+ console.log('[Feedback] Share feedback or report an issue: https://privacyscrubber.com/uninstall/?mode=feedback&src=sdk');
411
417
  process.exit(0);
412
418
  }
413
419
  }
@@ -24,6 +24,7 @@ const candidatePaths = [
24
24
  process.env.PRIVACYSCRUBBER_SDK_PATH,
25
25
  path.resolve(process.cwd(), 'npm-sdk/index.cjs'),
26
26
  path.resolve(process.cwd(), 'node_modules/@privacyscrubber/sdk'),
27
+ path.resolve(__dirname, '../../../index.cjs'),
27
28
  path.resolve(__dirname, '../../index.cjs'),
28
29
  path.resolve(__dirname, '../index.cjs'),
29
30
  path.resolve(__dirname, './index.cjs'),
@@ -115,7 +116,7 @@ rl.on('line', (line) => {
115
116
  sendResponse(id, {
116
117
  pong: true,
117
118
  engine: 'PrivacyScrubber ZTDS',
118
- version: '2.4.0',
119
+ version: '2.4.2',
119
120
  uptime: process.uptime()
120
121
  });
121
122
  break;
@@ -171,7 +172,6 @@ rl.on('line', (line) => {
171
172
  };
172
173
 
173
174
  const res = sdk.sanitizeObject(data, options);
174
- // Also merge into engine sessionMap if requested
175
175
  if (req.persist_session !== false && res.tokenMap) {
176
176
  Object.assign(engine.sessionMap, res.tokenMap);
177
177
  }
@@ -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.0"
62
+ version="2.4.2"
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.0"
27
+ __version__ = "2.4.2"
28
28
  __author__ = "Ilya Sibiryakov (BrandMeWeb)"
29
29
  __license__ = "Proprietary"
30
30
 
@@ -116,7 +116,7 @@ rl.on('line', (line) => {
116
116
  sendResponse(id, {
117
117
  pong: true,
118
118
  engine: 'PrivacyScrubber ZTDS',
119
- version: '2.4.0',
119
+ version: '2.4.2',
120
120
  uptime: process.uptime()
121
121
  });
122
122
  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.0")
53
+ print("PrivacyScrubber ZTDS Python SDK v2.4.2")
54
54
  sys.exit(0)
55
55
 
56
56
  if not args.command: