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