@privacyscrubber/sdk 2.3.7 โ†’ 2.4.0

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 (45) hide show
  1. package/README.md +86 -28
  2. package/benchmark.js +347 -0
  3. package/browser.js +2 -2
  4. package/cli.js +54 -2
  5. package/examples/python-bridge/MANIFEST.in +5 -0
  6. package/examples/python-bridge/README.md +265 -0
  7. package/examples/python-bridge/bridge_daemon.cjs +241 -0
  8. package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
  9. package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
  10. package/examples/python-bridge/fastapi_example.py +206 -0
  11. package/examples/python-bridge/langchain_adapter.py +20 -0
  12. package/examples/python-bridge/langchain_example.py +126 -0
  13. package/examples/python-bridge/llamaindex_example.py +103 -0
  14. package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
  15. package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
  16. package/examples/python-bridge/privacyscrubber/cli.py +138 -0
  17. package/examples/python-bridge/privacyscrubber/client.py +542 -0
  18. package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
  19. package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
  20. package/examples/python-bridge/privacyscrubber/types.py +52 -0
  21. package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
  22. package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
  23. package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
  24. package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
  25. package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
  26. package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
  27. package/examples/python-bridge/privacyscrubber.py +36 -0
  28. package/examples/python-bridge/pyproject.toml +62 -0
  29. package/examples/python-bridge/setup.py +66 -0
  30. package/examples/python-bridge/test_python_bridge.py +320 -0
  31. package/examples/vercel-ai-stream.js +195 -0
  32. package/fastmcp.cjs +291 -2
  33. package/fastmcp.d.ts +18 -0
  34. package/fastmcp.js +4 -0
  35. package/index.cjs +211 -134
  36. package/index.js +211 -134
  37. package/package.json +15 -3
  38. package/ps-license-manager.js +1 -1
  39. package/ps-pii-engine.cjs +1 -1
  40. package/scrubber-core.cjs +1 -1
  41. package/vercel-ai.cjs +400 -0
  42. package/vercel-ai.d.ts +156 -0
  43. package/vercel-ai.js +36 -0
  44. package/vision.cjs +13 -5
  45. package/vision.js +13 -5
package/index.js CHANGED
@@ -38,16 +38,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
38
38
  * Built-in patterns for DevOps & Cloud Secrets detection.
39
39
  */
40
40
  export const DEVOPS_SECRETS_DETECTOR = [
41
- { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g, label: 'AWS_KEY' },
41
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
42
+ { name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
43
+ { 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' },
44
+ { 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' },
45
+ { name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
46
+ { name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
47
+ { 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' },
48
+ { 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' },
49
+ { name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
50
+ { name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
51
+ { 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' },
52
+ { name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
53
+ { 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' },
42
54
  { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
43
- { 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' },
44
- { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g, label: 'STRIPE_KEY' },
55
+ { 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' },
56
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
45
57
  { name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
58
+ { 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' },
59
+ { 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' },
46
60
  { name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
47
- { name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
61
+ { 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' },
48
62
  { 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' },
63
+ { 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' },
64
+ { 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' },
65
+ { name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
49
66
  { name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
50
- { 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' },
67
+ { 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' },
51
68
  { 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' },
52
69
  { name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
53
70
  ];
@@ -143,8 +160,8 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
143
160
  frameworksSet.add('CCPA/CPRA');
144
161
  }
145
162
  if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
146
- frameworksSet.add('SOC 2 Type II');
147
- frameworksSet.add('ISO 27001 (A.8.11)');
163
+ frameworksSet.add('SOC 2 controls');
164
+ frameworksSet.add('ISO 27001 A.8.11 controls');
148
165
  }
149
166
  if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
150
167
  frameworksSet.add('PCI DSS v4.0');
@@ -233,7 +250,7 @@ export function formatAuditReceipt(telemetry) {
233
250
 
234
251
  return `> ๐Ÿ›ก๏ธ **PrivacyScrubber Audit Receipt**\n` +
235
252
  `> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
236
- `> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n` +
253
+ `> * **Framework References (informational):** ${telemetry.frameworksList.join(', ')}\n` +
237
254
  `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
238
255
  `> * โญ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
239
256
  }
@@ -1468,13 +1485,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
1468
1485
 
1469
1486
  for (const k of sortedKeys) {
1470
1487
  const val = tokenMap[k];
1471
- const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
1488
+ const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
1472
1489
  const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
1473
1490
 
1474
1491
  if (match) {
1475
- const label = escapeRegex(match[1]);
1492
+ const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
1476
1493
  const idx = match[2];
1477
- 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)';
1494
+ 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)';
1478
1495
  patterns.push(patternStr);
1479
1496
  const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
1480
1497
  lookup.set(normKey, val);
@@ -1503,10 +1520,14 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
1503
1520
  } else if (lookup.has(coreMatch.toUpperCase())) {
1504
1521
  restored = lookup.get(coreMatch.toUpperCase());
1505
1522
  } else {
1506
- const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
1507
- const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
1523
+ const inner = coreMatch
1524
+ .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, '')
1525
+ .replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
1526
+ .trim();
1527
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
1508
1528
  if (m) {
1509
- const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
1529
+ const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
1530
+ const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
1510
1531
  if (lookup.has(normKey)) {
1511
1532
  restored = lookup.get(normKey);
1512
1533
  }
@@ -1612,18 +1633,17 @@ export class StreamingDetokenizer {
1612
1633
  this.pendingBuffer += text;
1613
1634
 
1614
1635
  // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
1615
- // Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
1636
+ // Matches incomplete tokens starting with [, ใ€, ๏ผป, <, {, __, %5B, \u005b, or HTML entity &#91; / &lbrack;
1616
1637
  const tailLen = 160;
1617
1638
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
1618
1639
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
1619
- const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-ะ-ะฏะฐ-ัะั‘ ]{0,128}$/);
1640
+ 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)?)?)?)?)?)?)$/);
1620
1641
 
1621
1642
  if (m) {
1622
1643
  const candidate = m[0];
1623
- // Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
1624
- // Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
1625
- const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
1626
- if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
1644
+ // Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
1645
+ const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
1646
+ if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
1627
1647
  const toEmit = this.pendingBuffer;
1628
1648
  this.pendingBuffer = '';
1629
1649
  return this.replacer(toEmit);
@@ -1845,13 +1865,8 @@ export function withPrivacyScrubber(handler, options = {}) {
1845
1865
  tokenMap = res.tokenMap;
1846
1866
  telemetry = res.telemetry;
1847
1867
  } else if (rawBody && typeof rawBody === 'object') {
1848
- const serialized = JSON.stringify(rawBody);
1849
- const res = sanitize(serialized, options);
1850
- try {
1851
- scrubbedBody = JSON.parse(res.scrubbedText);
1852
- } catch (_) {
1853
- scrubbedBody = rawBody;
1854
- }
1868
+ const res = sanitizeObject(rawBody, options);
1869
+ scrubbedBody = res.sanitized;
1855
1870
  tokenMap = res.tokenMap;
1856
1871
  telemetry = res.telemetry;
1857
1872
  }
@@ -1862,7 +1877,8 @@ export function withPrivacyScrubber(handler, options = {}) {
1862
1877
  scrubbedBody,
1863
1878
  tokenMap,
1864
1879
  telemetry,
1865
- restore: (text) => restore(text, tokenMap),
1880
+ restore: (text) => (text && typeof text === 'object') ? restoreObject(text, tokenMap) : restore(text, tokenMap),
1881
+ restoreObject: (obj) => restoreObject(obj, tokenMap),
1866
1882
  createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
1867
1883
  };
1868
1884
 
@@ -1880,15 +1896,13 @@ export function withPrivacyScrubber(handler, options = {}) {
1880
1896
  export function createExpressMiddleware(options = {}) {
1881
1897
  return function privacyScrubberExpress(req, res, next) {
1882
1898
  if (req && req.body && typeof req.body === 'object') {
1883
- const serialized = JSON.stringify(req.body);
1884
- const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
1899
+ const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
1885
1900
  req.rawBody = req.body;
1886
- try {
1887
- req.body = JSON.parse(scrubbedText);
1888
- } catch (_) {}
1901
+ req.body = sanitized;
1889
1902
  req.psTokenMap = tokenMap;
1890
1903
  req.psTelemetry = telemetry;
1891
- req.restore = (aiResponse) => restore(aiResponse, tokenMap);
1904
+ req.restore = (aiResponse) => (aiResponse && typeof aiResponse === 'object') ? restoreObject(aiResponse, tokenMap) : restore(aiResponse, tokenMap);
1905
+ req.restoreObject = (obj) => restoreObject(obj, tokenMap);
1892
1906
  req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
1893
1907
  }
1894
1908
  if (typeof next === 'function') next();
@@ -1905,7 +1919,7 @@ export function formatCefEvent(eventData = {}) {
1905
1919
  const {
1906
1920
  vendor = 'PrivacyScrubber',
1907
1921
  product = 'ZTDS',
1908
- version = '2.2.5',
1922
+ version = '2.4.0',
1909
1923
  eventClassId = 'PII_REDACTION',
1910
1924
  eventName = 'Zero-Trust PII Sanitization Executed',
1911
1925
  riskLevel = 'LOW EXPOSURE',
@@ -1974,7 +1988,7 @@ export function formatSyslogEvent(eventData = {}) {
1974
1988
  export function formatJsonlEvent(eventData = {}) {
1975
1989
  const {
1976
1990
  timestamp = new Date().toISOString(),
1977
- version = '2.2.5',
1991
+ version = '2.4.0',
1978
1992
  protocol = 'ZTDS',
1979
1993
  event = 'sanitization',
1980
1994
  riskLevel = 'LOW EXPOSURE',
@@ -1999,7 +2013,7 @@ export function formatJsonlEvent(eventData = {}) {
1999
2013
  frameworks,
2000
2014
  session_hash: sessionHash,
2001
2015
  zero_egress: true,
2002
- verification_mode: "100% Offline (In-Memory RAM)",
2016
+ processing_mode: "local in-memory (by design, not measured)",
2003
2017
  ...(organization && { organization }),
2004
2018
  ...(department && { department }),
2005
2019
  ...(executionMs && { execution_ms: executionMs }),
@@ -2091,13 +2105,7 @@ export function createAuditLogger(options = {}) {
2091
2105
  telemetry = buildAuditTelemetry(tokenMap);
2092
2106
  }
2093
2107
 
2094
- let sessionHash = '';
2095
- try {
2096
- const cryptoMod = require('crypto');
2097
- sessionHash = cryptoMod.createHash('sha256').update(JSON.stringify(tokenMap) + Date.now()).digest('hex');
2098
- } catch {
2099
- sessionHash = 'ztds_' + Math.random().toString(16).substring(2, 10);
2100
- }
2108
+ const sessionHash = buildPsr2Receipt(tokenMap).receiptSha256;
2101
2109
 
2102
2110
  const eventData = {
2103
2111
  event: 'pii_sanitization',
@@ -2143,108 +2151,138 @@ export function createAuditLogger(options = {}) {
2143
2151
  };
2144
2152
  }
2145
2153
 
2154
+ // โ”€โ”€ PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) โ”€โ”€
2155
+ const PSR2_VERSION = 'PSR-2';
2156
+ const PSR2_STATEMENTS = {
2157
+ measured: [
2158
+ 'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
2159
+ 'Entity counts per type.',
2160
+ 'Receipt SHA-256: digest of the canonical field string, so edits to this receipt are detectable.'
2161
+ ],
2162
+ byDesign: [
2163
+ '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.',
2164
+ 'Token-to-original mappings are held in volatile memory for the session. This receipt does not record their contents.'
2165
+ ],
2166
+ limits: [
2167
+ 'This receipt is not a digital signature and does not prove who generated it or when.',
2168
+ 'No final output digest is recorded by this export (Output SHA-256: not recorded).',
2169
+ 'Detection is pattern-based. Free-text narrative can contain indirect identifiers that remain after sanitization; review output before sharing.',
2170
+ 'Regulatory references are informational technical-safeguard mappings, not a legal determination of compliance.'
2171
+ ]
2172
+ };
2173
+ function psr2Sha256(input) {
2174
+ return require('crypto').createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
2175
+ }
2176
+ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2177
+ const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
2178
+ const counts = {};
2179
+ for (const t of labels) {
2180
+ const m = t.match(/\[([A-Z_]+)_\d+\]/);
2181
+ const type = m ? m[1] : 'CUSTOM';
2182
+ counts[type] = (counts[type] || 0) + 1;
2183
+ }
2184
+ const entityCounts = Object.keys(counts).sort().map(k => `${k}=${counts[k]}`).join(',') || 'none';
2185
+ const manifestSha256 = psr2Sha256(labels.join('\n'));
2186
+ const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
2187
+ return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
2188
+ }
2189
+ function psr2Lines(r) {
2190
+ return [
2191
+ `Receipt Version: ${r.version}`,
2192
+ `Timestamp (UTC): ${r.timestamp}`,
2193
+ `Output SHA-256: not recorded`,
2194
+ `Token Manifest SHA-256: ${r.manifestSha256}`,
2195
+ `Entity Counts: ${r.entityCounts}`,
2196
+ `Tokens In Final Output: not recorded`,
2197
+ `Canonical: ${r.canonical}`,
2198
+ `Receipt SHA-256: ${r.receiptSha256}`
2199
+ ];
2200
+ }
2201
+ function formatPsr2Report({ format, receipt, telemetry, company_name, department, utmMedium }) {
2202
+ const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
2203
+ const teamsUrl = `https://privacyscrubber.com/teams?tier=teams&utm_source=sdk_audit_report&utm_medium=${utmMedium}&utm_campaign=teams_upgrade`;
2204
+ const sdkUrl = `https://privacyscrubber.com/sdk/?tier=sdk&utm_source=sdk_audit_report&utm_medium=${utmMedium}&utm_campaign=sdk_pipeline`;
2205
+ if (format === 'json') {
2206
+ return JSON.stringify({
2207
+ protocol: 'Zero-Trust Data Sanitization (ZTDS)',
2208
+ receipt_version: receipt.version,
2209
+ company: company_name,
2210
+ department,
2211
+ timestamp: receipt.timestamp,
2212
+ output_sha256: null,
2213
+ token_manifest_sha256: receipt.manifestSha256,
2214
+ entity_counts: receipt.entityCounts,
2215
+ canonical: receipt.canonical,
2216
+ receipt_sha256: receipt.receiptSha256,
2217
+ risk_level: telemetry.riskLevel,
2218
+ frameworks_referenced: telemetry.frameworksList,
2219
+ total_masked_tokens: telemetry.totalCount,
2220
+ entities_breakdown: telemetry.entities,
2221
+ scope: { measured: PSR2_STATEMENTS.measured, by_design_not_measured: PSR2_STATEMENTS.byDesign, limits: PSR2_STATEMENTS.limits },
2222
+ verification_url: 'https://privacyscrubber.com/features/audit-receipt/#verify',
2223
+ licensing_and_governance: { teams_governance_url: teamsUrl, sdk_pipeline_url: sdkUrl }
2224
+ }, null, 2);
2225
+ }
2226
+ if (format === 'summary') {
2227
+ 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(', ')}`;
2228
+ }
2229
+ return `# Zero-Trust Data Sanitization Receipt
2230
+ **Organization:** ${company_name} (${department})
2231
+ **Timestamp:** ${receipt.timestamp}
2232
+ **Receipt Format:** ${receipt.version} (SHA-256 digests; not a digital signature)
2233
+
2234
+ ---
2235
+
2236
+ ### Sanitization Metrics & Risk Assessment
2237
+ * **Overall Risk Rating:** **${telemetry.riskLevel}**
2238
+ * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
2239
+ * **Entity Breakdown:** ${entitySummary}
2240
+
2241
+ ### Informational Framework References
2242
+ ${telemetry.frameworksList.map(f => `- ${f}`).join('\n')}
2243
+
2244
+ These are technical-safeguard mappings, not a legal determination of compliance.
2245
+
2246
+ ### Receipt Integrity (${receipt.version})
2247
+ \`\`\`text
2248
+ ${psr2Lines(receipt).join('\n')}
2249
+ \`\`\`
2250
+
2251
+ ### Scope of This Receipt
2252
+ **Measured:** ${PSR2_STATEMENTS.measured.join(' ')}
2253
+
2254
+ **Stated by design, not measured:** ${PSR2_STATEMENTS.byDesign.join(' ')}
2255
+
2256
+ **Limits:** ${PSR2_STATEMENTS.limits.join(' ')}
2257
+
2258
+ ### Enterprise & Team Governance
2259
+ * **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules and encrypted session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](${teamsUrl})
2260
+ * **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](${sdkUrl})
2261
+
2262
+ *Generated locally by PrivacyScrubber Core SDK*
2263
+ *Verify locally (paste this receipt): https://privacyscrubber.com/features/audit-receipt/#verify*`;
2264
+ }
2265
+
2146
2266
  export function generateAuditReport(sessionMap = {}, options = {}) {
2147
2267
  const { companyName = 'PrivacyScrubber Client', department = 'SecOps / Compliance', format = 'markdown' } = options;
2148
2268
  const telemetry = buildAuditTelemetry(sessionMap);
2149
-
2150
- let sessionHash = '';
2151
- try {
2152
- const cryptoMod = require('crypto');
2153
- sessionHash = cryptoMod.createHash('sha256').update(JSON.stringify(sessionMap) + Date.now().toString()).digest('hex');
2154
- } catch {
2155
- sessionHash = Array.from(crypto.getRandomValues(new Uint8Array(16))).map(b => b.toString(16).padStart(2, '0')).join('');
2156
- }
2157
-
2158
2269
  const timestamp = new Date().toISOString();
2159
- const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
2270
+ const receipt = buildPsr2Receipt(sessionMap, timestamp);
2271
+ const sessionHash = receipt.receiptSha256;
2160
2272
 
2161
2273
  if (format === 'cef') {
2162
- return formatCefEvent({
2163
- organization: companyName,
2164
- department: department,
2165
- timestamp,
2166
- sessionHash,
2167
- riskLevel: telemetry.riskLevel,
2168
- tokensMasked: telemetry.totalCount,
2169
- entities: telemetry.entities,
2170
- frameworks: telemetry.frameworksList
2171
- });
2274
+ return formatCefEvent({ organization: companyName, department, timestamp, sessionHash, riskLevel: telemetry.riskLevel, tokensMasked: telemetry.totalCount, entities: telemetry.entities, frameworks: telemetry.frameworksList });
2172
2275
  }
2173
-
2174
2276
  if (format === 'syslog') {
2175
- return formatSyslogEvent({
2176
- organization: companyName,
2177
- department: department,
2178
- timestamp,
2179
- sessionHash,
2180
- riskLevel: telemetry.riskLevel,
2181
- tokensMasked: telemetry.totalCount,
2182
- entities: telemetry.entities,
2183
- frameworks: telemetry.frameworksList
2184
- });
2277
+ return formatSyslogEvent({ organization: companyName, department, timestamp, sessionHash, riskLevel: telemetry.riskLevel, tokensMasked: telemetry.totalCount, entities: telemetry.entities, frameworks: telemetry.frameworksList });
2185
2278
  }
2186
-
2187
2279
  if (format === 'jsonl') {
2188
- return formatJsonlEvent({
2189
- organization: companyName,
2190
- department: department,
2191
- timestamp,
2192
- sessionHash,
2193
- riskLevel: telemetry.riskLevel,
2194
- tokensMasked: telemetry.totalCount,
2195
- entities: telemetry.entities,
2196
- frameworks: telemetry.frameworksList
2197
- });
2280
+ return formatJsonlEvent({ organization: companyName, department, timestamp, sessionHash, riskLevel: telemetry.riskLevel, tokensMasked: telemetry.totalCount, entities: telemetry.entities, frameworks: telemetry.frameworksList });
2198
2281
  }
2199
-
2200
2282
  if (format === 'json') {
2201
- return {
2202
- protocol: "Zero-Trust Data Sanitization (ZTDS)",
2203
- certificateId: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
2204
- organization: companyName,
2205
- department: department,
2206
- timestamp: timestamp,
2207
- sessionHash: sessionHash,
2208
- verificationMode: "100% Offline (In-Memory RAM)",
2209
- complianceStatus: "VERIFIED PASS",
2210
- riskLevel: telemetry.riskLevel,
2211
- frameworksEnforced: telemetry.frameworksList,
2212
- totalMaskedTokens: telemetry.totalCount,
2213
- entitiesBreakdown: telemetry.entities,
2214
- zeroEgressVerified: true,
2215
- verificationUrl: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`
2216
- };
2283
+ return JSON.parse(formatPsr2Report({ format: 'json', receipt, telemetry, company_name: companyName, department, utmMedium: 'json' }));
2217
2284
  }
2218
-
2219
- if (format === 'summary') {
2220
- return `[PrivacyScrubber Compliance Certificate] ID: ZTDS-${sessionHash.substring(0, 8).toUpperCase()} | Organization: ${companyName} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks: ${telemetry.frameworksList.join(', ')} | Status: PASS (100% Air-Gapped)`;
2221
- }
2222
-
2223
- return `# ๐Ÿ›ก๏ธ Zero-Trust Data Sanitization Compliance Certificate
2224
- **Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
2225
- **Organization:** ${companyName} (${department})
2226
- **Timestamp:** ${timestamp}
2227
- **Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
2228
- **Status:** **VERIFIED PASS** (Zero Network Egress)
2229
-
2230
- ---
2231
-
2232
- ### ๐Ÿ“Š Sanitization Metrics & Risk Assessment
2233
- * **Overall Risk Rating:** **${telemetry.riskLevel}**
2234
- * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
2235
- * **Entity Breakdown:** ${entitySummary}
2236
- * **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
2237
-
2238
- ### ๐Ÿ“œ Regulatory Frameworks Enforced
2239
- ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2240
-
2241
- ### ๐Ÿ”’ CISO Compliance Declaration
2242
- 1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
2243
- 2. **HIPAA Safe Harbor (ยง164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
2244
- 3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
2245
-
2246
- *Certified Offline by PrivacyScrubber Core SDK*
2247
- *Verify at: https://privacyscrubber.com/features/audit-receipt/*`;
2285
+ return formatPsr2Report({ format: format === 'summary' ? 'summary' : 'markdown', receipt, telemetry, company_name: companyName, department, utmMedium: 'markdown' });
2248
2286
  }
2249
2287
 
2250
2288
  /**
@@ -2486,7 +2524,46 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2486
2524
  throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
2487
2525
  }
2488
2526
 
2489
- const raw = fs.readFileSync(resolved, 'utf8');
2527
+ const rawBuf = fs.readFileSync(resolved);
2528
+ const checkLen = Math.min(rawBuf.length, 8000);
2529
+ for (let i = 0; i < checkLen; i++) {
2530
+ if (rawBuf[i] === 0) {
2531
+ throw new Error(`[guard_read_file] Binary file detected (${path.basename(resolved)}). Only text, code, and MIME files are supported.`);
2532
+ }
2533
+ }
2534
+ let raw = rawBuf.toString('utf8');
2535
+ const ext = path.extname(resolved).toLowerCase();
2536
+ if (ext === '.eml') {
2537
+ const decodeQpUtf8 = (str) =>
2538
+ str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
2539
+ try {
2540
+ const bytes = Buffer.alloc(seq.length / 3);
2541
+ for (let i = 0; i < bytes.length; i++) {
2542
+ bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
2543
+ }
2544
+ return bytes.toString('utf8');
2545
+ } catch (_) { return seq; }
2546
+ });
2547
+ raw = raw.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, _cs, enc, payload) => {
2548
+ try {
2549
+ return enc.toUpperCase() === 'B'
2550
+ ? Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8')
2551
+ : decodeQpUtf8(payload.replace(/_/g, ' '));
2552
+ } catch (_) { return full; }
2553
+ });
2554
+ raw = decodeQpUtf8(raw);
2555
+ } else if (ext === '.rtf') {
2556
+ raw = raw
2557
+ .replace(/\\par\b|\\line\b/gi, '\n')
2558
+ .replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
2559
+ .replace(/\\u(-?\d+)\??/g, (_, code) => {
2560
+ const n = parseInt(code, 10);
2561
+ return String.fromCharCode(n < 0 ? n + 65536 : n);
2562
+ })
2563
+ .replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
2564
+ .replace(/[{}]/g, ' ')
2565
+ .replace(/[ \t]+/g, ' ');
2566
+ }
2490
2567
  const lines = raw.split(/\r?\n/);
2491
2568
  const capped = lines.slice(0, maxLines).join('\n');
2492
2569
  const wasTruncated = lines.length > maxLines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.3.7",
3
+ "version": "2.4.0",
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",
@@ -25,6 +25,12 @@
25
25
  "import": "./fastmcp.js",
26
26
  "require": "./fastmcp.cjs",
27
27
  "default": "./fastmcp.js"
28
+ },
29
+ "./vercel-ai": {
30
+ "types": "./vercel-ai.d.ts",
31
+ "import": "./vercel-ai.js",
32
+ "require": "./vercel-ai.cjs",
33
+ "default": "./vercel-ai.js"
28
34
  }
29
35
  },
30
36
  "publishConfig": {
@@ -40,8 +46,12 @@
40
46
  "fastmcp.js",
41
47
  "fastmcp.cjs",
42
48
  "fastmcp.d.ts",
49
+ "vercel-ai.js",
50
+ "vercel-ai.cjs",
51
+ "vercel-ai.d.ts",
43
52
  "index.d.ts",
44
53
  "cli.js",
54
+ "benchmark.js",
45
55
  "polyfill.js",
46
56
  "ps-license-manager.js",
47
57
  "ps-pii-engine.cjs",
@@ -58,8 +68,10 @@
58
68
  "ps-git-guard": "cli.js"
59
69
  },
60
70
  "scripts": {
61
- "build": "node ../scripts/build-sdk.js",
62
- "test": "node test-sdk.js",
71
+ "build": "node ../scripts/build-sdk.js --keep-obfuscated",
72
+ "test": "node test-sdk.js && node test-vercel-ai.js",
73
+ "test:python": "python3 examples/python-bridge/test_python_bridge.py",
74
+ "benchmark": "node benchmark.js",
63
75
  "prepublishOnly": "npm run build && npm test"
64
76
  },
65
77
  "keywords": [