@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.
- package/README.md +86 -28
- package/benchmark.js +347 -0
- package/browser.js +2 -2
- package/cli.js +54 -2
- package/examples/python-bridge/MANIFEST.in +5 -0
- package/examples/python-bridge/README.md +265 -0
- package/examples/python-bridge/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
- package/examples/python-bridge/fastapi_example.py +206 -0
- package/examples/python-bridge/langchain_adapter.py +20 -0
- package/examples/python-bridge/langchain_example.py +126 -0
- package/examples/python-bridge/llamaindex_example.py +103 -0
- package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/privacyscrubber/cli.py +138 -0
- package/examples/python-bridge/privacyscrubber/client.py +542 -0
- package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
- package/examples/python-bridge/privacyscrubber/types.py +52 -0
- package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
- package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
- package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
- package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
- package/examples/python-bridge/privacyscrubber.py +36 -0
- package/examples/python-bridge/pyproject.toml +62 -0
- package/examples/python-bridge/setup.py +66 -0
- package/examples/python-bridge/test_python_bridge.py +320 -0
- package/examples/vercel-ai-stream.js +195 -0
- package/fastmcp.cjs +291 -2
- package/fastmcp.d.ts +18 -0
- package/fastmcp.js +4 -0
- package/index.cjs +211 -134
- package/index.js +211 -134
- package/package.json +15 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +400 -0
- package/vercel-ai.d.ts +156 -0
- package/vercel-ai.js +36 -0
- package/vision.cjs +13 -5
- 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]{
|
|
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]{
|
|
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,}
|
|
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
|
|
145
|
-
frameworksSet.add('ISO 27001
|
|
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
|
-
`> * **
|
|
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(/^\[|<|\{\{
|
|
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 + '[-_
|
|
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
|
|
1505
|
-
|
|
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
|
|
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 [, <, {
|
|
1634
|
+
// Matches incomplete tokens starting with [, ใ, ๏ผป, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
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(/(?:\[
|
|
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
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
|
1847
|
-
|
|
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
|
|
1882
|
-
const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
|
|
1897
|
+
const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
|
|
1883
1898
|
req.rawBody = req.body;
|
|
1884
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|