@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
@@ -89,14 +89,16 @@ class PrivacyScrubberClient:
89
89
  self.daemon_path,
90
90
  "--profile", self.default_profile
91
91
  ]
92
+ proc_env = os.environ.copy()
92
93
  if self.license_key:
93
- cmd.extend(["--license-key", self.license_key])
94
+ proc_env["PRIVACYSCRUBBER_LICENSE_KEY"] = self.license_key
94
95
 
95
96
  self._process = subprocess.Popen(
96
97
  cmd,
97
98
  stdin=subprocess.PIPE,
98
99
  stdout=subprocess.PIPE,
99
100
  stderr=subprocess.PIPE,
101
+ env=proc_env,
100
102
  text=True,
101
103
  bufsize=1
102
104
  )
@@ -341,14 +343,16 @@ class AsyncPrivacyScrubberClient:
341
343
  self.daemon_path,
342
344
  "--profile", self.default_profile
343
345
  ]
346
+ proc_env = os.environ.copy()
344
347
  if self.license_key:
345
- cmd.extend(["--license-key", self.license_key])
348
+ proc_env["PRIVACYSCRUBBER_LICENSE_KEY"] = self.license_key
346
349
 
347
350
  self._process = await asyncio.create_subprocess_exec(
348
351
  *cmd,
349
352
  stdin=asyncio.subprocess.PIPE,
350
353
  stdout=asyncio.subprocess.PIPE,
351
- stderr=asyncio.subprocess.PIPE
354
+ stderr=asyncio.subprocess.PIPE,
355
+ env=proc_env
352
356
  )
353
357
 
354
358
  async def close(self) -> None:
@@ -387,13 +391,15 @@ class AsyncPrivacyScrubberClient:
387
391
  self.daemon_path,
388
392
  "--profile", self.default_profile
389
393
  ]
394
+ proc_env = os.environ.copy()
390
395
  if self.license_key:
391
- cmd.extend(["--license-key", self.license_key])
396
+ proc_env["PRIVACYSCRUBBER_LICENSE_KEY"] = self.license_key
392
397
  self._process = await asyncio.create_subprocess_exec(
393
398
  *cmd,
394
399
  stdin=asyncio.subprocess.PIPE,
395
400
  stdout=asyncio.subprocess.PIPE,
396
- stderr=asyncio.subprocess.PIPE
401
+ stderr=asyncio.subprocess.PIPE,
402
+ env=proc_env
397
403
  )
398
404
 
399
405
  self._req_id += 1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "privacyscrubber"
7
- version = "2.4.0"
7
+ version = "2.4.2"
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.0",
16
+ version="2.4.2",
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",
@@ -312,7 +312,7 @@ class TestPrivacyScrubberBridge(unittest.TestCase):
312
312
  cwd=bridge_dir,
313
313
  env=sub_env
314
314
  )
315
- self.assertIn("2.4.0", ver_out)
315
+ self.assertIn("2.4.2", ver_out)
316
316
 
317
317
 
318
318
  if __name__ == "__main__":
package/fastmcp.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @privacyscrubber/fastmcp-guard
3
3
  * Zero-Trust Data Sanitization (ZTDS) In-Memory Middleware for FastMCP
4
- * Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-02
4
+ * Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-01
5
5
  *
6
6
  * Core Protocol Invariants:
7
7
  * 1. Zero External Egress Prior to Sanitization
@@ -14,7 +14,7 @@
14
14
  const crypto = require("node:crypto");
15
15
 
16
16
  const ZTDS_PATTERNS = {
17
- API_SECRET: /\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{20,}|(?:ghp|gho|ghu|ghs|ghr|github_pat_)[A-Za-z0-9_]{20,}|eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,})\b/g,
17
+ API_SECRET: /\b(?:sk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}|(?:ghp|gho|ghu|ghs|ghr|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{16,}|(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}|eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,})\b/g,
18
18
  AWS_KEY: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g,
19
19
  CREDIT_CARD: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
20
20
  EMAIL: /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}/g,
@@ -24,7 +24,7 @@ const ZTDS_PATTERNS = {
24
24
  SSN: /\b\d{3}-\d{2}-\d{4}\b/g,
25
25
  };
26
26
 
27
- const TOKEN_PATTERN = /\[([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)\]/g;
27
+ const TOKEN_PATTERN = /(?:%5[Bb]|\\u005[Bb]|&(?:amp;)?#0*91;|\[|<|\{\{?|__|【|[)[*`_~\s]*([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)[*`_~.,;:!?\s]*(?:%5[Dd]|\\u005[Dd]|&(?:amp;)?#0*93;|\]|>|\}\}?|__|】|])/gi;
28
28
 
29
29
  function escapeRegex(s) {
30
30
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -41,15 +41,18 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
41
41
 
42
42
  for (const k of sortedKeys) {
43
43
  const val = tokenMap[k];
44
- const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
45
- const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
44
+ const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
45
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]([a-zA-Z0-9]+)$/);
46
46
 
47
47
  if (match) {
48
48
  const label = escapeRegex(match[1]);
49
- const idx = match[2];
50
- 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)';
49
+ const idx = escapeRegex(match[2]);
50
+ const idxPat = /^\d+$/.test(match[2]) ? '0*' + idx : idx;
51
+ const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\s]*' + idxPat + '[*`_~.,;:!?\\s]*';
52
+ const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+' + idxPat + '\\b)';
51
53
  patterns.push(patternStr);
52
- const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
54
+ const normSuffix = /^\d+$/.test(match[2]) ? String(parseInt(match[2], 10)) : match[2].toUpperCase();
55
+ const normKey = (match[1] + '_' + normSuffix).toUpperCase();
53
56
  lookup.set(normKey, val);
54
57
  lookup.set(k, val);
55
58
  lookup.set(k.toUpperCase(), val);
@@ -76,10 +79,15 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
76
79
  } else if (lookup.has(coreMatch.toUpperCase())) {
77
80
  restored = lookup.get(coreMatch.toUpperCase());
78
81
  } else {
79
- const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
80
- const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
82
+ const inner = coreMatch
83
+ .replace(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
84
+ .replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
85
+ .replace(/[*`_~.,;:!?\s]+/g, ' ')
86
+ .trim();
87
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]([a-zA-Z0-9]+)$/);
81
88
  if (m) {
82
- const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
89
+ const normSuffix = /^\d+$/.test(m[2]) ? String(parseInt(m[2], 10)) : m[2].toUpperCase();
90
+ const normKey = (m[1] + '_' + normSuffix).toUpperCase();
83
91
  if (lookup.has(normKey)) {
84
92
  restored = lookup.get(normKey);
85
93
  }
@@ -158,12 +166,12 @@ class StreamingDetokenizer {
158
166
  const tailLen = 160;
159
167
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
160
168
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
161
- const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
169
+ 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)?)?)?)?)?)?)$/);
162
170
 
163
171
  if (m) {
164
172
  const candidate = m[0];
165
- const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
166
- if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
173
+ const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
174
+ if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
167
175
  const toEmit = this.pendingBuffer;
168
176
  this.pendingBuffer = '';
169
177
  return this.replacer(toEmit);
@@ -268,7 +276,9 @@ class ZTDSFastMCPGuard {
268
276
 
269
277
  const tokenMap = this._sessionMaps.get(sessionId);
270
278
  const entityMap = this._entityMaps.get(sessionId);
271
- let sanitized = text;
279
+ let sanitized = text
280
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u00AD]/g, "")
281
+ .replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
272
282
 
273
283
  for (const entityType of this.enabledEntities) {
274
284
  const pattern = ZTDS_PATTERNS[entityType];
@@ -317,8 +327,9 @@ class ZTDSFastMCPGuard {
317
327
  return text;
318
328
  }
319
329
 
320
- return text.replace(TOKEN_PATTERN, (match) => {
321
- return tokenMap.get(match) || match;
330
+ return text.replace(TOKEN_PATTERN, (match, entityType, tokenId) => {
331
+ const canonical = `[${entityType.toUpperCase()}_TOKEN_${tokenId}]`;
332
+ return tokenMap.get(canonical) || tokenMap.get(match) || match;
322
333
  });
323
334
  }
324
335
 
@@ -475,7 +486,7 @@ class ZTDSFastMCPGuard {
475
486
 
476
487
  const ztdsMeta = {
477
488
  sessionId,
478
- standard: "IETF draft-sibiryakov-ztds-protocol-02",
489
+ standard: "IETF draft-sibiryakov-ztds-protocol-01",
479
490
  zeroEgress: true,
480
491
  };
481
492
 
package/index.cjs CHANGED
@@ -179,7 +179,7 @@ function buildAuditTelemetry(currentTokenMap = {}) {
179
179
  }
180
180
 
181
181
  const ipHoldings = {
182
- standard: 'IETF draft-sibiryakov-ztds-protocol-00',
182
+ standard: 'IETF draft-sibiryakov-ztds-protocol-01',
183
183
  standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
184
184
  patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
185
185
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
@@ -189,27 +189,27 @@ function buildAuditTelemetry(currentTokenMap = {}) {
189
189
  {
190
190
  statute: 'EU AI Act (Art. 50)',
191
191
  scope: 'AI Transparency & Data Minimization',
192
- rule: '0-byte external network transmission; prevents unauthorized ingestion into frontier models.'
192
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
193
193
  },
194
194
  {
195
195
  statute: 'GDPR (Art. 4, 25, 28, 32)',
196
196
  scope: 'Pseudonymization & Processor Liability',
197
- rule: 'Volatile client-side tokenization eliminates GDPR Article 28 data processor liability.'
197
+ rule: 'Client-side pseudonymization is a technical measure supporting Art. 25 and 32. It does not by itself remove controller or processor obligations.'
198
198
  },
199
199
  {
200
200
  statute: 'HIPAA 45 CFR § 164.514(b)',
201
201
  scope: 'Safe Harbor De-Identification',
202
- rule: 'Deterministic removal of all 18 PHI identifiers prior to AI analysis.'
202
+ rule: 'Pattern-based masking of direct identifiers supports Safe Harbor de-identification workflows. Review output for residual identifiers before sharing.'
203
203
  },
204
204
  {
205
205
  statute: 'IRC § 7216 & IRS Pub. 1075',
206
206
  scope: 'Tax Return Information Confidentiality',
207
- rule: 'Strict criminal penalty safeguard against unauthorized disclosure of tax records to cloud LLMs.'
207
+ rule: 'Masking identifiers before AI use can reduce exposure of tax return information. Informational mapping, not legal advice.'
208
208
  },
209
209
  {
210
210
  statute: 'FRE 502',
211
211
  scope: 'Attorney-Client Privilege Preservation',
212
- rule: 'Automated in-memory redaction prevents inadvertent waiver of legal privilege in AI workflows.'
212
+ rule: 'In-memory redaction can reduce disclosure of privileged details in AI workflows. Privilege determinations remain a legal judgment.'
213
213
  }
214
214
  ];
215
215
 
@@ -238,7 +238,7 @@ const buildCisoAuditTelemetry = buildAuditTelemetry;
238
238
  function formatAuditReceipt(telemetry) {
239
239
  if (!telemetry || telemetry.totalCount === 0) {
240
240
  return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
241
- "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
241
+ "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **7-Day B2B Trial:** `npx @privacyscrubber/sdk trial` | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=audit_receipt)\n";
242
242
  }
243
243
  const entitiesList = Object.entries(telemetry.entities)
244
244
  .map(([type, count]) => `${count} ${type}`)
@@ -250,7 +250,7 @@ function formatAuditReceipt(telemetry) {
250
250
  `> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
251
251
  `> * **Framework References (informational):** ${telemetry.frameworksList.join(', ')}\n` +
252
252
  `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
253
- `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
253
+ `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **7-Day B2B Trial:** \`npx @privacyscrubber/sdk trial\` | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=audit_receipt)\n`;
254
254
  }
255
255
 
256
256
  let communityNoticeLogged = false;
@@ -259,7 +259,7 @@ function emitCommunityNoticeIfNeeded(isPro) {
259
259
  if (communityNoticeLogged || isPro) return;
260
260
  if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
261
261
  communityNoticeLogged = true;
262
- console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 25 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk');
262
+ console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 30 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=prod_notice (7-Day Trial: npx @privacyscrubber/sdk trial | CISO Memo: npx @privacyscrubber/sdk memo)');
263
263
  }
264
264
  }
265
265
 
@@ -291,7 +291,8 @@ function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
291
291
  process.stderr.write(
292
292
  `\n[PrivacyScrubber] Intercepted ${count} API secret(s) before LLM egress: ${secretSample}\n` +
293
293
  `[PrivacyScrubber] Protected in local RAM (0 byte network egress). Free Tier active (${limit.toLocaleString()} char cap).\n` +
294
- `[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
294
+ `[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n` +
295
+ `[PrivacyScrubber] Instant 7-Day Free B2B Trial: npx @privacyscrubber/sdk trial | CISO Approval Memo: npx @privacyscrubber/sdk memo\n\n`
295
296
  );
296
297
  }
297
298
  }
@@ -478,6 +479,18 @@ function sanitize(text, options = {}) {
478
479
 
479
480
  const licenseCheck = validateLicense(licenseKey);
480
481
  const rawTier = licenseCheck.valid ? (licenseCheck.tier || 'PRO') : 'FREE';
482
+ const isExpiredKey = !licenseCheck.valid && typeof licenseCheck.error === 'string' && /expired/i.test(licenseCheck.error);
483
+ const isExpiringSoon = licenseCheck.valid && typeof licenseCheck.expires === 'number' && (licenseCheck.expires - Date.now() <= 48 * 3600 * 1000);
484
+
485
+ if (isExpiringSoon && typeof process !== 'undefined' && process.stderr && !globalThis.__psSdkPreExpiryLogged) {
486
+ globalThis.__psSdkPreExpiryLogged = true;
487
+ const hrs = Math.max(0, Math.round((licenseCheck.expires - Date.now()) / 3600000));
488
+ process.stderr.write(
489
+ `\n[@privacyscrubber/sdk] ⏳ 7-Day Evaluation Key expires in ~${hrs}h. Prevent pipeline interruption:\n` +
490
+ ` - Commercial SDK ($299/mo flat): https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=pilot_expiring_48h\n` +
491
+ ` - CISO Approval Memo: npx @privacyscrubber/sdk memo\n\n`
492
+ );
493
+ }
481
494
 
482
495
  // Anti-Cannibalization Barrier (SSOT):
483
496
  // PRO ($15/mo) and TEAMS ($99/mo) licenses are strictly human-seat licenses for Web App & Chrome Extension.
@@ -511,7 +524,8 @@ function sanitize(text, options = {}) {
511
524
 
512
525
  if (!isPro && text.length > limit) {
513
526
  if (shouldThrow) {
514
- const errorMsg = `[PrivacyScrubber] 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=dev_limit`;
527
+ const expiredPrefix = isExpiredKey ? `[PILOT/TRIAL EXPIRED: ${licenseCheck.error}] ` : '';
528
+ 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)`;
515
529
  throw new PrivacyScrubberLicenseError(errorMsg, {
516
530
  tier: 'FREE',
517
531
  currentLength: text.length,
@@ -1917,7 +1931,7 @@ function formatCefEvent(eventData = {}) {
1917
1931
  const {
1918
1932
  vendor = 'PrivacyScrubber',
1919
1933
  product = 'ZTDS',
1920
- version = '2.4.0',
1934
+ version = '2.4.2',
1921
1935
  eventClassId = 'PII_REDACTION',
1922
1936
  eventName = 'Zero-Trust PII Sanitization Executed',
1923
1937
  riskLevel = 'LOW EXPOSURE',
@@ -1986,7 +2000,7 @@ function formatSyslogEvent(eventData = {}) {
1986
2000
  function formatJsonlEvent(eventData = {}) {
1987
2001
  const {
1988
2002
  timestamp = new Date().toISOString(),
1989
- version = '2.4.0',
2003
+ version = '2.4.2',
1990
2004
  protocol = 'ZTDS',
1991
2005
  event = 'sanitization',
1992
2006
  riskLevel = 'LOW EXPOSURE',
@@ -2717,6 +2731,10 @@ const _defaultExport = {
2717
2731
  buildCisoAuditTelemetry,
2718
2732
  formatAuditReceipt,
2719
2733
  generateAuditReport,
2734
+ PSR2_VERSION,
2735
+ PSR2_STATEMENTS,
2736
+ buildPsr2Receipt,
2737
+ psr2Lines,
2720
2738
  formatCefEvent,
2721
2739
  formatSyslogEvent,
2722
2740
  formatJsonlEvent,
package/index.js CHANGED
@@ -181,7 +181,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
181
181
  }
182
182
 
183
183
  const ipHoldings = {
184
- standard: 'IETF draft-sibiryakov-ztds-protocol-00',
184
+ standard: 'IETF draft-sibiryakov-ztds-protocol-01',
185
185
  standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
186
186
  patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
187
187
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
@@ -191,27 +191,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
191
191
  {
192
192
  statute: 'EU AI Act (Art. 50)',
193
193
  scope: 'AI Transparency & Data Minimization',
194
- rule: '0-byte external network transmission; prevents unauthorized ingestion into frontier models.'
194
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
195
195
  },
196
196
  {
197
197
  statute: 'GDPR (Art. 4, 25, 28, 32)',
198
198
  scope: 'Pseudonymization & Processor Liability',
199
- rule: 'Volatile client-side tokenization eliminates GDPR Article 28 data processor liability.'
199
+ rule: 'Client-side pseudonymization is a technical measure supporting Art. 25 and 32. It does not by itself remove controller or processor obligations.'
200
200
  },
201
201
  {
202
202
  statute: 'HIPAA 45 CFR § 164.514(b)',
203
203
  scope: 'Safe Harbor De-Identification',
204
- rule: 'Deterministic removal of all 18 PHI identifiers prior to AI analysis.'
204
+ rule: 'Pattern-based masking of direct identifiers supports Safe Harbor de-identification workflows. Review output for residual identifiers before sharing.'
205
205
  },
206
206
  {
207
207
  statute: 'IRC § 7216 & IRS Pub. 1075',
208
208
  scope: 'Tax Return Information Confidentiality',
209
- rule: 'Strict criminal penalty safeguard against unauthorized disclosure of tax records to cloud LLMs.'
209
+ rule: 'Masking identifiers before AI use can reduce exposure of tax return information. Informational mapping, not legal advice.'
210
210
  },
211
211
  {
212
212
  statute: 'FRE 502',
213
213
  scope: 'Attorney-Client Privilege Preservation',
214
- rule: 'Automated in-memory redaction prevents inadvertent waiver of legal privilege in AI workflows.'
214
+ rule: 'In-memory redaction can reduce disclosure of privileged details in AI workflows. Privilege determinations remain a legal judgment.'
215
215
  }
216
216
  ];
217
217
 
@@ -240,7 +240,7 @@ export const buildCisoAuditTelemetry = buildAuditTelemetry;
240
240
  export function formatAuditReceipt(telemetry) {
241
241
  if (!telemetry || telemetry.totalCount === 0) {
242
242
  return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
243
- "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
243
+ "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **7-Day B2B Trial:** `npx @privacyscrubber/sdk trial` | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=audit_receipt)\n";
244
244
  }
245
245
  const entitiesList = Object.entries(telemetry.entities)
246
246
  .map(([type, count]) => `${count} ${type}`)
@@ -252,7 +252,7 @@ export function formatAuditReceipt(telemetry) {
252
252
  `> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
253
253
  `> * **Framework References (informational):** ${telemetry.frameworksList.join(', ')}\n` +
254
254
  `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
255
- `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
255
+ `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **7-Day B2B Trial:** \`npx @privacyscrubber/sdk trial\` | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=audit_receipt)\n`;
256
256
  }
257
257
 
258
258
  let communityNoticeLogged = false;
@@ -261,7 +261,7 @@ function emitCommunityNoticeIfNeeded(isPro) {
261
261
  if (communityNoticeLogged || isPro) return;
262
262
  if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
263
263
  communityNoticeLogged = true;
264
- console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 25 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk');
264
+ console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 30 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=prod_notice (7-Day Trial: npx @privacyscrubber/sdk trial | CISO Memo: npx @privacyscrubber/sdk memo)');
265
265
  }
266
266
  }
267
267
 
@@ -293,7 +293,8 @@ function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
293
293
  process.stderr.write(
294
294
  `\n[PrivacyScrubber] Intercepted ${count} API secret(s) before LLM egress: ${secretSample}\n` +
295
295
  `[PrivacyScrubber] Protected in local RAM (0 byte network egress). Free Tier active (${limit.toLocaleString()} char cap).\n` +
296
- `[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
296
+ `[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n` +
297
+ `[PrivacyScrubber] Instant 7-Day Free B2B Trial: npx @privacyscrubber/sdk trial | CISO Approval Memo: npx @privacyscrubber/sdk memo\n\n`
297
298
  );
298
299
  }
299
300
  }
@@ -480,6 +481,18 @@ export function sanitize(text, options = {}) {
480
481
 
481
482
  const licenseCheck = validateLicense(licenseKey);
482
483
  const rawTier = licenseCheck.valid ? (licenseCheck.tier || 'PRO') : 'FREE';
484
+ const isExpiredKey = !licenseCheck.valid && typeof licenseCheck.error === 'string' && /expired/i.test(licenseCheck.error);
485
+ const isExpiringSoon = licenseCheck.valid && typeof licenseCheck.expires === 'number' && (licenseCheck.expires - Date.now() <= 48 * 3600 * 1000);
486
+
487
+ if (isExpiringSoon && typeof process !== 'undefined' && process.stderr && !globalThis.__psSdkPreExpiryLogged) {
488
+ globalThis.__psSdkPreExpiryLogged = true;
489
+ const hrs = Math.max(0, Math.round((licenseCheck.expires - Date.now()) / 3600000));
490
+ process.stderr.write(
491
+ `\n[@privacyscrubber/sdk] ⏳ 7-Day Evaluation Key expires in ~${hrs}h. Prevent pipeline interruption:\n` +
492
+ ` - Commercial SDK ($299/mo flat): https://privacyscrubber.com/pricing/?tier=sdk&utm_source=npm_sdk&utm_medium=pilot_expiring_48h\n` +
493
+ ` - CISO Approval Memo: npx @privacyscrubber/sdk memo\n\n`
494
+ );
495
+ }
483
496
 
484
497
  // Anti-Cannibalization Barrier (SSOT):
485
498
  // PRO ($15/mo) and TEAMS ($99/mo) licenses are strictly human-seat licenses for Web App & Chrome Extension.
@@ -513,7 +526,8 @@ export function sanitize(text, options = {}) {
513
526
 
514
527
  if (!isPro && text.length > limit) {
515
528
  if (shouldThrow) {
516
- const errorMsg = `[PrivacyScrubber] 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=dev_limit`;
529
+ const expiredPrefix = isExpiredKey ? `[PILOT/TRIAL EXPIRED: ${licenseCheck.error}] ` : '';
530
+ 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)`;
517
531
  throw new PrivacyScrubberLicenseError(errorMsg, {
518
532
  tier: 'FREE',
519
533
  currentLength: text.length,
@@ -1919,7 +1933,7 @@ export function formatCefEvent(eventData = {}) {
1919
1933
  const {
1920
1934
  vendor = 'PrivacyScrubber',
1921
1935
  product = 'ZTDS',
1922
- version = '2.4.0',
1936
+ version = '2.4.2',
1923
1937
  eventClassId = 'PII_REDACTION',
1924
1938
  eventName = 'Zero-Trust PII Sanitization Executed',
1925
1939
  riskLevel = 'LOW EXPOSURE',
@@ -1988,7 +2002,7 @@ export function formatSyslogEvent(eventData = {}) {
1988
2002
  export function formatJsonlEvent(eventData = {}) {
1989
2003
  const {
1990
2004
  timestamp = new Date().toISOString(),
1991
- version = '2.4.0',
2005
+ version = '2.4.2',
1992
2006
  protocol = 'ZTDS',
1993
2007
  event = 'sanitization',
1994
2008
  riskLevel = 'LOW EXPOSURE',
@@ -2152,8 +2166,8 @@ export function createAuditLogger(options = {}) {
2152
2166
  }
2153
2167
 
2154
2168
  // ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
2155
- const PSR2_VERSION = 'PSR-2';
2156
- const PSR2_STATEMENTS = {
2169
+ export const PSR2_VERSION = 'PSR-2';
2170
+ export const PSR2_STATEMENTS = {
2157
2171
  measured: [
2158
2172
  'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
2159
2173
  'Entity counts per type.',
@@ -2173,7 +2187,7 @@ const PSR2_STATEMENTS = {
2173
2187
  function psr2Sha256(input) {
2174
2188
  return require('crypto').createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
2175
2189
  }
2176
- function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2190
+ export function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2177
2191
  const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
2178
2192
  const counts = {};
2179
2193
  for (const t of labels) {
@@ -2186,7 +2200,7 @@ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2186
2200
  const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
2187
2201
  return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
2188
2202
  }
2189
- function psr2Lines(r) {
2203
+ export function psr2Lines(r) {
2190
2204
  return [
2191
2205
  `Receipt Version: ${r.version}`,
2192
2206
  `Timestamp (UTC): ${r.timestamp}`,
@@ -2724,6 +2738,10 @@ export default {
2724
2738
  buildCisoAuditTelemetry,
2725
2739
  formatAuditReceipt,
2726
2740
  generateAuditReport,
2741
+ PSR2_VERSION,
2742
+ PSR2_STATEMENTS,
2743
+ buildPsr2Receipt,
2744
+ psr2Lines,
2727
2745
  formatCefEvent,
2728
2746
  formatSyslogEvent,
2729
2747
  formatJsonlEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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",
@@ -101,6 +101,10 @@
101
101
  "type": "git",
102
102
  "url": "git+https://github.com/moxno/privacyscrubber-mcp.git"
103
103
  },
104
+ "funding": {
105
+ "type": "github",
106
+ "url": "https://github.com/moxno/privacyscrubber-mcp"
107
+ },
104
108
  "engines": {
105
109
  "node": ">=18.0.0"
106
110
  }