@privacyscrubber/sdk 2.3.9 → 2.4.1

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 (37) hide show
  1. package/README.md +85 -27
  2. package/benchmark.js +355 -0
  3. package/browser.js +276 -39
  4. package/cli.js +1 -1
  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/fastapi_example.py +206 -0
  9. package/examples/python-bridge/langchain_adapter.py +20 -0
  10. package/examples/python-bridge/langchain_example.py +126 -0
  11. package/examples/python-bridge/llamaindex_example.py +103 -0
  12. package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
  13. package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
  14. package/examples/python-bridge/privacyscrubber/cli.py +138 -0
  15. package/examples/python-bridge/privacyscrubber/client.py +548 -0
  16. package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
  17. package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
  18. package/examples/python-bridge/privacyscrubber/types.py +52 -0
  19. package/examples/python-bridge/privacyscrubber.py +36 -0
  20. package/examples/python-bridge/pyproject.toml +62 -0
  21. package/examples/python-bridge/setup.py +66 -0
  22. package/examples/python-bridge/test_python_bridge.py +320 -0
  23. package/examples/vercel-ai-stream.js +195 -0
  24. package/fastmcp.cjs +289 -8
  25. package/fastmcp.d.ts +18 -0
  26. package/fastmcp.js +4 -0
  27. package/index.cjs +99 -42
  28. package/index.js +103 -46
  29. package/package.json +15 -3
  30. package/ps-license-manager.js +1 -1
  31. package/ps-pii-engine.cjs +1 -1
  32. package/scrubber-core.cjs +1 -1
  33. package/vercel-ai.cjs +400 -0
  34. package/vercel-ai.d.ts +156 -0
  35. package/vercel-ai.js +36 -0
  36. package/vision.cjs +13 -5
  37. 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');
@@ -162,7 +179,7 @@ function buildAuditTelemetry(currentTokenMap = {}) {
162
179
  }
163
180
 
164
181
  const ipHoldings = {
165
- standard: 'IETF draft-sibiryakov-ztds-protocol-00',
182
+ standard: 'IETF draft-sibiryakov-ztds-protocol-01',
166
183
  standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
167
184
  patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
168
185
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
@@ -172,27 +189,27 @@ function buildAuditTelemetry(currentTokenMap = {}) {
172
189
  {
173
190
  statute: 'EU AI Act (Art. 50)',
174
191
  scope: 'AI Transparency & Data Minimization',
175
- 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.'
176
193
  },
177
194
  {
178
195
  statute: 'GDPR (Art. 4, 25, 28, 32)',
179
196
  scope: 'Pseudonymization & Processor Liability',
180
- 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.'
181
198
  },
182
199
  {
183
200
  statute: 'HIPAA 45 CFR § 164.514(b)',
184
201
  scope: 'Safe Harbor De-Identification',
185
- 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.'
186
203
  },
187
204
  {
188
205
  statute: 'IRC § 7216 & IRS Pub. 1075',
189
206
  scope: 'Tax Return Information Confidentiality',
190
- 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.'
191
208
  },
192
209
  {
193
210
  statute: 'FRE 502',
194
211
  scope: 'Attorney-Client Privilege Preservation',
195
- 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.'
196
213
  }
197
214
  ];
198
215
 
@@ -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.1',
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.1',
1976
1990
  protocol = 'ZTDS',
1977
1991
  event = 'sanitization',
1978
1992
  riskLevel = 'LOW EXPOSURE',
@@ -2508,7 +2522,46 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2508
2522
  throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
2509
2523
  }
2510
2524
 
2511
- 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
+ }
2512
2565
  const lines = raw.split(/\r?\n/);
2513
2566
  const capped = lines.slice(0, maxLines).join('\n');
2514
2567
  const wasTruncated = lines.length > maxLines;
@@ -2664,6 +2717,10 @@ const _defaultExport = {
2664
2717
  buildCisoAuditTelemetry,
2665
2718
  formatAuditReceipt,
2666
2719
  generateAuditReport,
2720
+ PSR2_VERSION,
2721
+ PSR2_STATEMENTS,
2722
+ buildPsr2Receipt,
2723
+ psr2Lines,
2667
2724
  formatCefEvent,
2668
2725
  formatSyslogEvent,
2669
2726
  formatJsonlEvent,
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');
@@ -164,7 +181,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
164
181
  }
165
182
 
166
183
  const ipHoldings = {
167
- standard: 'IETF draft-sibiryakov-ztds-protocol-00',
184
+ standard: 'IETF draft-sibiryakov-ztds-protocol-01',
168
185
  standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
169
186
  patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
170
187
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
@@ -174,27 +191,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
174
191
  {
175
192
  statute: 'EU AI Act (Art. 50)',
176
193
  scope: 'AI Transparency & Data Minimization',
177
- 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.'
178
195
  },
179
196
  {
180
197
  statute: 'GDPR (Art. 4, 25, 28, 32)',
181
198
  scope: 'Pseudonymization & Processor Liability',
182
- 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.'
183
200
  },
184
201
  {
185
202
  statute: 'HIPAA 45 CFR § 164.514(b)',
186
203
  scope: 'Safe Harbor De-Identification',
187
- 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.'
188
205
  },
189
206
  {
190
207
  statute: 'IRC § 7216 & IRS Pub. 1075',
191
208
  scope: 'Tax Return Information Confidentiality',
192
- 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.'
193
210
  },
194
211
  {
195
212
  statute: 'FRE 502',
196
213
  scope: 'Attorney-Client Privilege Preservation',
197
- 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.'
198
215
  }
199
216
  ];
200
217
 
@@ -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.1',
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.1',
1978
1992
  protocol = 'ZTDS',
1979
1993
  event = 'sanitization',
1980
1994
  riskLevel = 'LOW EXPOSURE',
@@ -2138,8 +2152,8 @@ export function createAuditLogger(options = {}) {
2138
2152
  }
2139
2153
 
2140
2154
  // ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
2141
- const PSR2_VERSION = 'PSR-2';
2142
- const PSR2_STATEMENTS = {
2155
+ export const PSR2_VERSION = 'PSR-2';
2156
+ export const PSR2_STATEMENTS = {
2143
2157
  measured: [
2144
2158
  'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
2145
2159
  'Entity counts per type.',
@@ -2159,7 +2173,7 @@ const PSR2_STATEMENTS = {
2159
2173
  function psr2Sha256(input) {
2160
2174
  return require('crypto').createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
2161
2175
  }
2162
- function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2176
+ export function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2163
2177
  const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
2164
2178
  const counts = {};
2165
2179
  for (const t of labels) {
@@ -2172,7 +2186,7 @@ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2172
2186
  const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
2173
2187
  return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
2174
2188
  }
2175
- function psr2Lines(r) {
2189
+ export function psr2Lines(r) {
2176
2190
  return [
2177
2191
  `Receipt Version: ${r.version}`,
2178
2192
  `Timestamp (UTC): ${r.timestamp}`,
@@ -2510,7 +2524,46 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2510
2524
  throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
2511
2525
  }
2512
2526
 
2513
- 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
+ }
2514
2567
  const lines = raw.split(/\r?\n/);
2515
2568
  const capped = lines.slice(0, maxLines).join('\n');
2516
2569
  const wasTruncated = lines.length > maxLines;
@@ -2671,6 +2724,10 @@ export default {
2671
2724
  buildCisoAuditTelemetry,
2672
2725
  formatAuditReceipt,
2673
2726
  generateAuditReport,
2727
+ PSR2_VERSION,
2728
+ PSR2_STATEMENTS,
2729
+ buildPsr2Receipt,
2730
+ psr2Lines,
2674
2731
  formatCefEvent,
2675
2732
  formatSyslogEvent,
2676
2733
  formatJsonlEvent,