@privacyscrubber/mcp-server 2.3.8 → 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/index.js CHANGED
@@ -26,6 +26,7 @@ import { fileURLToPath } from 'url';
26
26
  import crypto from 'crypto';
27
27
  import os from 'os';
28
28
  import { execSync } from 'child_process';
29
+ import { Transform } from 'stream';
29
30
 
30
31
  const require = createRequire(import.meta.url);
31
32
  const __filename = fileURLToPath(import.meta.url);
@@ -63,6 +64,54 @@ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
63
64
  // Initialize core engine
64
65
  PrivacyScrubberCore.init();
65
66
 
67
+ let piiEngine;
68
+ try {
69
+ piiEngine = require('./ps-pii-engine.cjs');
70
+ } catch (e) {
71
+ try {
72
+ piiEngine = require('./ps-pii-engine.js');
73
+ } catch (err) {}
74
+ }
75
+
76
+ export const CANONICAL_INDUSTRY_PROFILES = [
77
+ 'General', 'Dev', 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA',
78
+ 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting',
79
+ 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support',
80
+ 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'
81
+ ];
82
+
83
+ export function resolveRequestedProfile(args, defaultProfile = 'General') {
84
+ if (!args || typeof args !== 'object') return defaultProfile;
85
+ const sec = typeof args.sector === 'string' ? args.sector.trim() : '';
86
+ const prof = typeof args.profile === 'string' ? args.profile.trim() : '';
87
+
88
+ let candidate = defaultProfile;
89
+ if (sec && (!prof || prof.toLowerCase() === 'general')) {
90
+ candidate = sec;
91
+ } else if (prof) {
92
+ candidate = prof;
93
+ } else if (sec) {
94
+ candidate = sec;
95
+ }
96
+
97
+ // Canonical normalization via PROFILE_ALIAS_MAP if available
98
+ if (candidate && candidate.toLowerCase() !== 'general' && piiEngine && piiEngine.PROFILE_ALIAS_MAP) {
99
+ const mapped = piiEngine.PROFILE_ALIAS_MAP[candidate.toLowerCase()];
100
+ if (mapped) {
101
+ if (candidate.toLowerCase() === mapped.toLowerCase()) {
102
+ return candidate;
103
+ }
104
+ return mapped.charAt(0).toUpperCase() + mapped.slice(1);
105
+ }
106
+ }
107
+
108
+ return candidate;
109
+ }
110
+
111
+ const PROFILE_PARAM_DESCRIPTION = "The detection profile to use. Available: 'General' (Free, 15,000 chars), or 30 specialized industry profiles (5,000 chars Free trial quota; unlimited on PRO/TEAMS/SDK): 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting', 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'. Defaults to 'General'.";
112
+
113
+ const SECTOR_PARAM_DESCRIPTION = "Optional alias for 'profile'. The industry sector or compliance vertical to apply (e.g. 'medical', 'finance', 'legal', 'dev', 'hr', 'accounting', 'security', 'compliance', 'underwriting', 'telecom').";
114
+
66
115
  // Volatile in-memory token map per session (multi-task agent isolation)
67
116
  const agentSessions = new Map();
68
117
  const MAX_CONCURRENT_SESSIONS = 200;
@@ -75,9 +124,13 @@ function getSession(sessionId = 'default') {
75
124
  if (!session) {
76
125
  if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
77
126
  const oldestKey = agentSessions.keys().next().value;
127
+ const oldestSession = agentSessions.get(oldestKey);
128
+ if (oldestSession && oldestSession.streamDetokenizer) {
129
+ oldestSession.streamDetokenizer.reset();
130
+ }
78
131
  agentSessions.delete(oldestKey);
79
132
  }
80
- session = { map: {}, ignoreList: new Set(), createdAt: now, lastActive: now };
133
+ session = { map: {}, ignoreList: new Set(), streamDetokenizer: null, createdAt: now, lastActive: now };
81
134
  agentSessions.set(sid, session);
82
135
  }
83
136
  session.lastActive = now;
@@ -96,6 +149,282 @@ function getSessionIgnoreList(sessionId = 'default') {
96
149
  const sessionMap = getSessionMap('default');
97
150
  const sessionIgnoreList = getSessionIgnoreList('default');
98
151
 
152
+ function escapeRegex(s) {
153
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
154
+ }
155
+
156
+ /**
157
+ * Builds a fast streaming token replacement function for MCP.
158
+ * Preserves all whitespace, indentation, newlines, and code blocks.
159
+ * Handles descending key ordering, case-insensitivity, alternative delimiters (<...>, {{...}}, __...__),
160
+ * and possessive suffixes ('s).
161
+ */
162
+ export function buildStreamReplacer(tokenMap = {}, options = {}) {
163
+ if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
164
+ return (text) => text || '';
165
+ }
166
+
167
+ const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
168
+ const lookup = new Map();
169
+ const patterns = [];
170
+ const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
171
+
172
+ for (const k of sortedKeys) {
173
+ const val = tokenMap[k];
174
+ const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
175
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
176
+
177
+ if (match) {
178
+ const label = escapeRegex(match[1]);
179
+ const idx = match[2];
180
+ const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\s]*0*' + idx + '[*`_~.,;:!?\\s]*';
181
+ const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
182
+ patterns.push(patternStr);
183
+ const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
184
+ lookup.set(normKey, val);
185
+ lookup.set(k, val);
186
+ lookup.set(k.toUpperCase(), val);
187
+ } else {
188
+ const safe = escapeRegex(k);
189
+ patterns.push(safe);
190
+ lookup.set(k, val);
191
+ lookup.set(k.toUpperCase(), val);
192
+ }
193
+ }
194
+
195
+ const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
196
+
197
+ return function replaceTokens(text) {
198
+ if (!text) return '';
199
+ unifiedRegex.lastIndex = 0;
200
+ return text.replace(unifiedRegex, (match, suffix) => {
201
+ const isPossessive = suffix && /('s|’s)$/i.test(suffix);
202
+ const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
203
+ let restored = null;
204
+
205
+ if (lookup.has(coreMatch)) {
206
+ restored = lookup.get(coreMatch);
207
+ } else if (lookup.has(coreMatch.toUpperCase())) {
208
+ restored = lookup.get(coreMatch.toUpperCase());
209
+ } else {
210
+ const inner = coreMatch
211
+ .replace(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
212
+ .replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
213
+ .replace(/[*`_~.,;:!?\s]+/g, ' ')
214
+ .trim();
215
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
216
+ if (m) {
217
+ const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
218
+ if (lookup.has(normKey)) {
219
+ restored = lookup.get(normKey);
220
+ }
221
+ }
222
+ }
223
+
224
+ if (restored !== null) {
225
+ if (onTokenRestored) {
226
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
227
+ }
228
+ return restored + (isPossessive ? "'s" : '');
229
+ }
230
+
231
+ if (lookup.has(match)) {
232
+ restored = lookup.get(match);
233
+ if (onTokenRestored) {
234
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
235
+ }
236
+ return restored;
237
+ }
238
+
239
+ return match;
240
+ });
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
246
+ * Buffers partial token candidates at chunk boundaries across stdio and Server-Sent Events,
247
+ * preventing character loss and token corruption.
248
+ */
249
+ export class StreamingDetokenizer {
250
+ constructor(tokenMap = {}, options = {}) {
251
+ this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
252
+ this.options = options || {};
253
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
254
+ this.pendingBuffer = '';
255
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
256
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
257
+ this.decoder = new TextDecoder('utf-8', { fatal: false });
258
+ }
259
+
260
+ get buffer() {
261
+ return this.pendingBuffer;
262
+ }
263
+
264
+ set buffer(val) {
265
+ this.pendingBuffer = typeof val === 'string' ? val : '';
266
+ }
267
+
268
+ updateTokenMap(newMap) {
269
+ if (newMap && typeof newMap === 'object') {
270
+ this.tokenMap = newMap;
271
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
272
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
273
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
274
+ }
275
+ }
276
+
277
+ reset() {
278
+ this.pendingBuffer = '';
279
+ }
280
+
281
+ feed(chunk) {
282
+ if (chunk === undefined || chunk === null) return '';
283
+ let text = '';
284
+ if (typeof chunk === 'string') {
285
+ text = chunk;
286
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
287
+ text = this.decoder.decode(chunk, { stream: true });
288
+ } else {
289
+ text = String(chunk);
290
+ }
291
+
292
+ if (!text) return '';
293
+
294
+ const currentTokenCount = Object.keys(this.tokenMap).length;
295
+ if (currentTokenCount !== this._lastTokenCount) {
296
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
297
+ this.hasTokens = currentTokenCount > 0;
298
+ this._lastTokenCount = currentTokenCount;
299
+ }
300
+
301
+ if (!this.hasTokens) return text;
302
+
303
+ this.pendingBuffer += text;
304
+
305
+ const tailLen = 160;
306
+ const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
307
+ const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
308
+ 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)?)?)?)?)?)?)$/);
309
+
310
+ if (m) {
311
+ const candidate = m[0];
312
+ const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
313
+ if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
314
+ const toEmit = this.pendingBuffer;
315
+ this.pendingBuffer = '';
316
+ return this.replacer(toEmit);
317
+ }
318
+ const splitIdx = offset + m.index;
319
+ const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
320
+ this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
321
+ return safeToEmit ? this.replacer(safeToEmit) : '';
322
+ }
323
+
324
+ const toEmit = this.pendingBuffer;
325
+ this.pendingBuffer = '';
326
+ return this.replacer(toEmit);
327
+ }
328
+
329
+ process(chunk) {
330
+ return this.feed(chunk);
331
+ }
332
+
333
+ flush() {
334
+ let text = '';
335
+ try {
336
+ text = this.decoder.decode(new Uint8Array(), { stream: false });
337
+ } catch (_) {}
338
+
339
+ if (text) {
340
+ this.pendingBuffer += text;
341
+ }
342
+
343
+ if (!this.pendingBuffer) return '';
344
+ const remaining = this.pendingBuffer;
345
+ this.pendingBuffer = '';
346
+ return this.replacer(remaining);
347
+ }
348
+
349
+ toNodeTransform() {
350
+ const self = this;
351
+ const transformStream = new Transform({
352
+ transform(chunk, encoding, callback) {
353
+ try {
354
+ const out = self.feed(chunk);
355
+ if (out) this.push(out);
356
+ callback();
357
+ } catch (err) {
358
+ callback(err);
359
+ }
360
+ },
361
+ flush(callback) {
362
+ try {
363
+ const out = self.flush();
364
+ if (out) this.push(out);
365
+ callback();
366
+ } catch (err) {
367
+ callback(err);
368
+ }
369
+ },
370
+ destroy(err, callback) {
371
+ self.reset();
372
+ callback(err);
373
+ }
374
+ });
375
+ transformStream.detokenizer = self;
376
+ return transformStream;
377
+ }
378
+
379
+ async* wrapStream(asyncIterable) {
380
+ for await (const chunk of asyncIterable) {
381
+ if (chunk === null || chunk === undefined) continue;
382
+ if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
383
+ const delta = chunk.choices[0].delta.content;
384
+ if (typeof delta === 'string') {
385
+ const restored = this.feed(delta);
386
+ if (restored) {
387
+ chunk.choices[0].delta.content = restored;
388
+ yield chunk;
389
+ }
390
+ } else {
391
+ yield chunk;
392
+ }
393
+ continue;
394
+ }
395
+
396
+ let text = '';
397
+ if (typeof chunk === 'string') {
398
+ text = chunk;
399
+ } else if (typeof chunk?.text === 'string') {
400
+ text = chunk.text;
401
+ } else if (typeof chunk?.content === 'string') {
402
+ text = chunk.content;
403
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
404
+ text = chunk;
405
+ } else {
406
+ text = String(chunk);
407
+ }
408
+
409
+ const emitted = this.feed(text);
410
+ if (emitted) yield emitted;
411
+ }
412
+
413
+ const finalChunk = this.flush();
414
+ if (finalChunk) yield finalChunk;
415
+ }
416
+ }
417
+
418
+ export function getStreamDetokenizer(sessionId = 'default') {
419
+ const session = getSession(sessionId);
420
+ if (!session.streamDetokenizer) {
421
+ session.streamDetokenizer = new StreamingDetokenizer(session.map);
422
+ } else {
423
+ session.streamDetokenizer.updateTokenMap(session.map);
424
+ }
425
+ return session.streamDetokenizer;
426
+ }
427
+
99
428
  function reverseTokens(text, map) {
100
429
  if (!text || typeof text !== 'string' || !map || Object.keys(map).length === 0) return text;
101
430
  if (PrivacyScrubberCore && typeof PrivacyScrubberCore.unscrubText === 'function') {
@@ -130,16 +459,33 @@ function mcpLog(msg) {
130
459
 
131
460
  // Secrets detection patterns — defined once here, shared by detectSecrets() and performSanitization()
132
461
  const DEVOPS_SECRETS_DETECTOR = [
133
- { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
462
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g },
463
+ { name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
464
+ { name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g },
465
+ { name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g },
466
+ { name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g },
467
+ { name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g },
468
+ { name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g },
469
+ { 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 },
470
+ { name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g },
471
+ { name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g },
472
+ { 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 },
473
+ { name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g },
474
+ { 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 },
134
475
  { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
135
- { 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 },
136
- { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g },
476
+ { 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 },
477
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g },
137
478
  { name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi },
479
+ { 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 },
480
+ { 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 },
138
481
  { name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi },
139
- { name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi },
482
+ { 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 },
140
483
  { name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi },
484
+ { 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 },
485
+ { 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 },
486
+ { name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi },
141
487
  { name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi },
142
- { 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 },
488
+ { 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 },
143
489
  { 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 },
144
490
  { name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g }
145
491
  ];
@@ -151,7 +497,9 @@ function checkLicenseStatus() {
151
497
  const result = LicenseManager.validate(key);
152
498
 
153
499
  if (!result.valid) {
154
- mcpLog(`DEBUG: License invalid because: ${result.reason || "Unknown"}`);
500
+ if (process.env.DEBUG_PRIVACYSCRUBBER) {
501
+ mcpLog(`[License] Key validation notice: ${result.reason || "Unknown"}`);
502
+ }
155
503
  return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
156
504
  }
157
505
 
@@ -264,8 +612,8 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
264
612
  frameworksSet.add('CCPA/CPRA');
265
613
  }
266
614
  if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
267
- frameworksSet.add('SOC 2 Type II');
268
- frameworksSet.add('ISO 27001 (A.8.11)');
615
+ frameworksSet.add('SOC 2 controls');
616
+ frameworksSet.add('ISO 27001 A.8.11 controls');
269
617
  }
270
618
  if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
271
619
  frameworksSet.add('PCI DSS v4.0');
@@ -331,6 +679,118 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
331
679
  };
332
680
  }
333
681
 
682
+ // ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
683
+ const PSR2_VERSION = 'PSR-2';
684
+ const PSR2_STATEMENTS = {
685
+ measured: [
686
+ 'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
687
+ 'Entity counts per type.',
688
+ 'Receipt SHA-256: digest of the canonical field string, so edits to this receipt are detectable.'
689
+ ],
690
+ byDesign: [
691
+ '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.',
692
+ 'Token-to-original mappings are held in volatile memory for the session. This receipt does not record their contents.'
693
+ ],
694
+ limits: [
695
+ 'This receipt is not a digital signature and does not prove who generated it or when.',
696
+ 'No final output digest is recorded by this export (Output SHA-256: not recorded).',
697
+ 'Detection is pattern-based. Free-text narrative can contain indirect identifiers that remain after sanitization; review output before sharing.',
698
+ 'Regulatory references are informational technical-safeguard mappings, not a legal determination of compliance.'
699
+ ]
700
+ };
701
+ function psr2Sha256(input) {
702
+ return crypto.createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
703
+ }
704
+ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
705
+ const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
706
+ const counts = {};
707
+ for (const t of labels) {
708
+ const m = t.match(/\[([A-Z_]+)_\d+\]/);
709
+ const type = m ? m[1] : 'CUSTOM';
710
+ counts[type] = (counts[type] || 0) + 1;
711
+ }
712
+ const entityCounts = Object.keys(counts).sort().map(k => `${k}=${counts[k]}`).join(',') || 'none';
713
+ const manifestSha256 = psr2Sha256(labels.join('\n'));
714
+ const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
715
+ return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
716
+ }
717
+ function psr2Lines(r) {
718
+ return [
719
+ `Receipt Version: ${r.version}`,
720
+ `Timestamp (UTC): ${r.timestamp}`,
721
+ `Output SHA-256: not recorded`,
722
+ `Token Manifest SHA-256: ${r.manifestSha256}`,
723
+ `Entity Counts: ${r.entityCounts}`,
724
+ `Tokens In Final Output: not recorded`,
725
+ `Canonical: ${r.canonical}`,
726
+ `Receipt SHA-256: ${r.receiptSha256}`
727
+ ];
728
+ }
729
+ function formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource, utmMedium }) {
730
+ const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
731
+ const teamsUrl = `https://privacyscrubber.com/teams?tier=teams&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_campaign=teams_upgrade`;
732
+ const sdkUrl = `https://privacyscrubber.com/sdk/?tier=sdk&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_campaign=sdk_pipeline`;
733
+ if (format === 'json') {
734
+ return JSON.stringify({
735
+ protocol: 'Zero-Trust Data Sanitization (ZTDS)',
736
+ receipt_version: receipt.version,
737
+ company: company_name,
738
+ department,
739
+ timestamp: receipt.timestamp,
740
+ output_sha256: null,
741
+ token_manifest_sha256: receipt.manifestSha256,
742
+ entity_counts: receipt.entityCounts,
743
+ canonical: receipt.canonical,
744
+ receipt_sha256: receipt.receiptSha256,
745
+ risk_level: telemetry.riskLevel,
746
+ frameworks_referenced: telemetry.frameworksList,
747
+ total_masked_tokens: telemetry.totalCount,
748
+ entities_breakdown: telemetry.entities,
749
+ scope: { measured: PSR2_STATEMENTS.measured, by_design_not_measured: PSR2_STATEMENTS.byDesign, limits: PSR2_STATEMENTS.limits },
750
+ verification_url: 'https://privacyscrubber.com/features/audit-receipt/#verify',
751
+ licensing_and_governance: { teams_governance_url: teamsUrl, sdk_pipeline_url: sdkUrl }
752
+ }, null, 2);
753
+ }
754
+ if (format === 'summary') {
755
+ 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(', ')}`;
756
+ }
757
+ return `# Zero-Trust Data Sanitization Receipt
758
+ **Organization:** ${company_name} (${department})
759
+ **Timestamp:** ${receipt.timestamp}
760
+ **Receipt Format:** ${receipt.version} (SHA-256 digests; not a digital signature)
761
+
762
+ ---
763
+
764
+ ### Sanitization Metrics & Risk Assessment
765
+ * **Overall Risk Rating:** **${telemetry.riskLevel}**
766
+ * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
767
+ * **Entity Breakdown:** ${entitySummary}
768
+
769
+ ### Informational Framework References
770
+ ${telemetry.frameworksList.map(f => `- ${f}`).join('\n')}
771
+
772
+ These are technical-safeguard mappings, not a legal determination of compliance.
773
+
774
+ ### Receipt Integrity (${receipt.version})
775
+ \`\`\`text
776
+ ${psr2Lines(receipt).join('\n')}
777
+ \`\`\`
778
+
779
+ ### Scope of This Receipt
780
+ **Measured:** ${PSR2_STATEMENTS.measured.join(' ')}
781
+
782
+ **Stated by design, not measured:** ${PSR2_STATEMENTS.byDesign.join(' ')}
783
+
784
+ **Limits:** ${PSR2_STATEMENTS.limits.join(' ')}
785
+
786
+ ### Enterprise & Team Governance
787
+ * **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules and encrypted session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](${teamsUrl})
788
+ * **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](${sdkUrl})
789
+
790
+ *Generated locally by PrivacyScrubber Engine v${MCP_VERSION}*
791
+ *Verify locally (paste this receipt): https://privacyscrubber.com/features/audit-receipt/#verify*`;
792
+ }
793
+
334
794
  function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, charLimit = 15000) {
335
795
  const isCompact = compact === true || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === '1' || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === 'true';
336
796
  const truncationLine = wasTruncated
@@ -360,7 +820,7 @@ function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, ch
360
820
 
361
821
  const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
362
822
 
363
- return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n${truncationLine}> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) (\`gh repo star moxno/privacyscrubber-mcp\`) | **SDK & Enterprise:** [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp&utm_medium=audit_receipt&utm_campaign=sdk_pipeline)\n`;
823
+ return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Framework References (informational):** ${telemetry.frameworksList.join(', ')}\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n${truncationLine}> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) (\`gh repo star moxno/privacyscrubber-mcp\`) | **SDK & Enterprise:** [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp&utm_medium=audit_receipt&utm_campaign=sdk_pipeline)\n`;
364
824
  }
365
825
 
366
826
  function formatCefEvent(eventData = {}) {
@@ -461,7 +921,7 @@ function formatJsonlEvent(eventData = {}) {
461
921
  frameworks,
462
922
  session_hash: sessionHash,
463
923
  zero_egress: true,
464
- verification_mode: "100% Offline (In-Memory RAM)",
924
+ processing_mode: "local in-memory (by design, not measured)",
465
925
  ...(organization && { organization }),
466
926
  ...(department && { department }),
467
927
  ...(executionMs && { execution_ms: executionMs }),
@@ -822,7 +1282,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
822
1282
  },
823
1283
  profile: {
824
1284
  type: "string",
825
- description: "The detection profile to use (e.g. 'General', 'Dev', 'Medical', 'Finance', 'Legal'). Defaults to 'General'."
1285
+ description: PROFILE_PARAM_DESCRIPTION
1286
+ },
1287
+ sector: {
1288
+ type: "string",
1289
+ description: SECTOR_PARAM_DESCRIPTION
826
1290
  },
827
1291
  format: {
828
1292
  type: "string",
@@ -835,7 +1299,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
835
1299
  },
836
1300
  {
837
1301
  name: "sanitize_text",
838
- description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
1302
+ description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
839
1303
  inputSchema: {
840
1304
  type: "object",
841
1305
  properties: {
@@ -845,7 +1309,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
845
1309
  },
846
1310
  profile: {
847
1311
  type: "string",
848
- description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting', 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
1312
+ description: PROFILE_PARAM_DESCRIPTION
1313
+ },
1314
+ sector: {
1315
+ type: "string",
1316
+ description: SECTOR_PARAM_DESCRIPTION
849
1317
  },
850
1318
  compact: {
851
1319
  type: "boolean",
@@ -866,7 +1334,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
866
1334
  },
867
1335
  {
868
1336
  name: "scrub_text",
869
- description: "Alias for 'sanitize_text'. Locally scrubs PII and secrets before LLM ingestion.",
1337
+ description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
870
1338
  inputSchema: {
871
1339
  type: "object",
872
1340
  properties: {
@@ -876,7 +1344,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
876
1344
  },
877
1345
  profile: {
878
1346
  type: "string",
879
- description: "The detection profile to use. Defaults to 'General'."
1347
+ description: PROFILE_PARAM_DESCRIPTION
1348
+ },
1349
+ sector: {
1350
+ type: "string",
1351
+ description: SECTOR_PARAM_DESCRIPTION
880
1352
  },
881
1353
  compact: {
882
1354
  type: "boolean",
@@ -897,13 +1369,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
897
1369
  },
898
1370
  {
899
1371
  name: "reveal_text",
900
- description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user.",
1372
+ description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user. Supports both standard batch mode and real-time streaming mode (stream: true).",
901
1373
  inputSchema: {
902
1374
  type: "object",
903
1375
  properties: {
904
1376
  text: {
905
1377
  type: "string",
906
- description: "The AI generated response containing placeholders to restore."
1378
+ description: "The AI generated response (or streaming chunk) containing placeholders to restore."
1379
+ },
1380
+ stream: {
1381
+ type: "boolean",
1382
+ description: "Optional. When true, uses the sliding-window StreamingDetokenizer to prevent token splitting across chunk boundaries."
1383
+ },
1384
+ flush: {
1385
+ type: "boolean",
1386
+ description: "Optional. When true in streaming mode, flushes any remaining buffered text at EOF."
907
1387
  },
908
1388
  session_id: {
909
1389
  type: "string",
@@ -913,6 +1393,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
913
1393
  required: ["text"]
914
1394
  }
915
1395
  },
1396
+ {
1397
+ name: "stream_reveal_text",
1398
+ description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
1399
+ inputSchema: {
1400
+ type: "object",
1401
+ properties: {
1402
+ chunk: {
1403
+ type: "string",
1404
+ description: "The incoming text chunk from the LLM stream or stdout."
1405
+ },
1406
+ flush: {
1407
+ type: "boolean",
1408
+ description: "Optional. Set to true on the final chunk or at EOF to flush remaining buffer. Defaults to false."
1409
+ },
1410
+ reset: {
1411
+ type: "boolean",
1412
+ description: "Optional. Set to true to reset internal streaming buffer for this session. Defaults to false."
1413
+ },
1414
+ session_id: {
1415
+ type: "string",
1416
+ description: "Optional session ID matching the sanitize_text call for multi-agent isolation. Defaults to 'default'."
1417
+ }
1418
+ },
1419
+ required: ["chunk"]
1420
+ }
1421
+ },
916
1422
  {
917
1423
  name: "sanitize_file",
918
1424
  description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
@@ -925,7 +1431,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
925
1431
  },
926
1432
  profile: {
927
1433
  type: "string",
928
- description: "The detection profile to use. Defaults to 'General'."
1434
+ description: PROFILE_PARAM_DESCRIPTION
1435
+ },
1436
+ sector: {
1437
+ type: "string",
1438
+ description: SECTOR_PARAM_DESCRIPTION
929
1439
  },
930
1440
  compact: {
931
1441
  type: "boolean",
@@ -951,7 +1461,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
951
1461
  },
952
1462
  profile: {
953
1463
  type: "string",
954
- description: "The detection profile to use. Defaults to 'General'."
1464
+ description: PROFILE_PARAM_DESCRIPTION
1465
+ },
1466
+ sector: {
1467
+ type: "string",
1468
+ description: SECTOR_PARAM_DESCRIPTION
955
1469
  },
956
1470
  compact: {
957
1471
  type: "boolean",
@@ -977,7 +1491,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
977
1491
  },
978
1492
  profile: {
979
1493
  type: "string",
980
- description: "The detection profile to use. Defaults to 'General'."
1494
+ description: PROFILE_PARAM_DESCRIPTION
1495
+ },
1496
+ sector: {
1497
+ type: "string",
1498
+ description: SECTOR_PARAM_DESCRIPTION
981
1499
  },
982
1500
  extensions: {
983
1501
  type: "array",
@@ -1004,7 +1522,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1004
1522
  },
1005
1523
  profile: {
1006
1524
  type: "string",
1007
- description: "The detection profile to use. Defaults to 'General'."
1525
+ description: PROFILE_PARAM_DESCRIPTION
1526
+ },
1527
+ sector: {
1528
+ type: "string",
1529
+ description: SECTOR_PARAM_DESCRIPTION
1008
1530
  },
1009
1531
  dry_run: {
1010
1532
  type: "boolean",
@@ -1045,6 +1567,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1045
1567
  department: {
1046
1568
  type: "string",
1047
1569
  description: "Optional department or auditor ID (e.g. 'SecOps / Engineering')."
1570
+ },
1571
+ session_id: {
1572
+ type: "string",
1573
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1048
1574
  }
1049
1575
  },
1050
1576
  required: []
@@ -1093,7 +1619,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1093
1619
  },
1094
1620
  profile: {
1095
1621
  type: "string",
1096
- description: "Detection profile. Available: 'General', 'Dev' (recommended for code/secrets), 'Security', 'Finance', etc. Defaults to 'Dev'."
1622
+ description: "Detection profile. Available: 'Dev' (default for code/secrets), 'General', or any of the 30 specialized industry profiles (e.g. 'Security', 'Finance', 'Medical'). Defaults to 'Dev'."
1623
+ },
1624
+ sector: {
1625
+ type: "string",
1626
+ description: SECTOR_PARAM_DESCRIPTION
1097
1627
  },
1098
1628
  timeout_ms: {
1099
1629
  type: "number",
@@ -1115,7 +1645,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1115
1645
  },
1116
1646
  profile: {
1117
1647
  type: "string",
1118
- description: "Detection profile. Defaults to 'Dev'."
1648
+ description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
1649
+ },
1650
+ sector: {
1651
+ type: "string",
1652
+ description: SECTOR_PARAM_DESCRIPTION
1119
1653
  },
1120
1654
  max_lines: {
1121
1655
  type: "number",
@@ -1141,7 +1675,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1141
1675
  },
1142
1676
  profile: {
1143
1677
  type: "string",
1144
- description: "Detection profile. Defaults to 'Dev'."
1678
+ description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
1679
+ },
1680
+ sector: {
1681
+ type: "string",
1682
+ description: SECTOR_PARAM_DESCRIPTION
1145
1683
  },
1146
1684
  session_id: {
1147
1685
  type: "string",
@@ -1270,7 +1808,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1270
1808
  },
1271
1809
  profile: {
1272
1810
  type: "string",
1273
- description: "The detection profile to use. Defaults to 'General'."
1811
+ description: PROFILE_PARAM_DESCRIPTION
1812
+ },
1813
+ sector: {
1814
+ type: "string",
1815
+ description: SECTOR_PARAM_DESCRIPTION
1274
1816
  },
1275
1817
  session_id: {
1276
1818
  type: "string",
@@ -1507,14 +2049,14 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1507
2049
  "biotech": "Biotech & Genomics profile. Scrubs accessions, patient phenotypes, HGVS variants, DNA/RNA samples, and flowcell IDs.",
1508
2050
  "telecom": "Call Centers & Telephony profile. Scrubs CTI interaction IDs, Amazon Connect/Genesys sessions, agent extensions, ANI/DNIS, IVR DTMF, and audio transcript diarization headers."
1509
2051
  };
1510
- const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a PRO-tier detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
2052
+ const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a specialized detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
1511
2053
 
1512
2054
  return {
1513
2055
  contents: [
1514
2056
  {
1515
2057
  uri,
1516
2058
  mimeType: "text/markdown",
1517
- text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` to the \`sanitize_text\` or \`sanitize_file\` tools. Advanced profiles require a PRO license.*`
2059
+ text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` or \`"sector": "${profileName}"\` to tools like \`scrub_text\`, \`sanitize_text\`, or \`sanitize_file\`. Advanced profiles include a 5,000 character Free trial quota; PRO/TEAMS tiers grant unlimited characters.*`
1518
2060
  }
1519
2061
  ]
1520
2062
  };
@@ -1529,7 +2071,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1529
2071
 
1530
2072
  try {
1531
2073
  if (name === "audit_context") {
1532
- const { text, profile = "General", format = "detailed" } = args || {};
2074
+ const { text, format = "detailed" } = args || {};
1533
2075
 
1534
2076
  if (text === undefined || text === null) {
1535
2077
  return {
@@ -1544,7 +2086,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1544
2086
  };
1545
2087
  }
1546
2088
 
1547
- const targetProfile = (profile || "General").trim();
2089
+ const targetProfile = resolveRequestedProfile(args, "General");
1548
2090
  const customRules = loadCustomRules();
1549
2091
  const normalizedProfile = targetProfile.toLowerCase();
1550
2092
  const license = checkLicenseStatus();
@@ -1595,7 +2137,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1595
2137
  }
1596
2138
 
1597
2139
  if (name === "sanitize_text" || name === "scrub_text") {
1598
- const { text, profile = "General", ignore_list, compact = false, session_id = "default" } = args || {};
2140
+ const { text, ignore_list, compact = false, session_id = "default" } = args || {};
1599
2141
  const currentSession = getSessionMap(session_id);
1600
2142
  const currentIgnoreList = getSessionIgnoreList(session_id);
1601
2143
 
@@ -1615,7 +2157,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1615
2157
  content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'. Stringify objects before passing.` }]
1616
2158
  };
1617
2159
  }
1618
- const targetProfile = (profile || "General").trim();
2160
+ const targetProfile = resolveRequestedProfile(args, "General");
1619
2161
 
1620
2162
  // Check tier gating for advanced profiles
1621
2163
  const isAdvanced = targetProfile.toLowerCase() !== "general";
@@ -1660,7 +2202,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1660
2202
  }
1661
2203
 
1662
2204
  if (name === "reveal_text") {
1663
- const { text, session_id = "default" } = args || {};
2205
+ const { text, stream = false, flush = false, session_id = "default" } = args || {};
2206
+
2207
+ if (stream) {
2208
+ if (text === undefined || text === null) {
2209
+ if (flush) {
2210
+ const detok = getStreamDetokenizer(session_id);
2211
+ const flushedText = detok.flush();
2212
+ return {
2213
+ content: [{ type: "text", text: flushedText }]
2214
+ };
2215
+ }
2216
+ return {
2217
+ isError: true,
2218
+ content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the stream chunk to restore." }]
2219
+ };
2220
+ }
2221
+ if (typeof text !== "string") {
2222
+ return {
2223
+ isError: true,
2224
+ content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
2225
+ };
2226
+ }
2227
+ const detok = getStreamDetokenizer(session_id);
2228
+ let emitted = detok.feed(text);
2229
+ if (flush) {
2230
+ emitted += detok.flush();
2231
+ }
2232
+ return {
2233
+ content: [{ type: "text", text: emitted }]
2234
+ };
2235
+ }
2236
+
1664
2237
  if (text === undefined || text === null) {
1665
2238
  return {
1666
2239
  isError: true,
@@ -1700,6 +2273,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1700
2273
  };
1701
2274
  }
1702
2275
 
2276
+ if (name === "stream_reveal_text") {
2277
+ const { chunk, flush = false, reset = false, session_id = "default" } = args || {};
2278
+ if (chunk === undefined || chunk === null) {
2279
+ if (flush) {
2280
+ const detok = getStreamDetokenizer(session_id);
2281
+ const flushedText = detok.flush();
2282
+ return {
2283
+ content: [{ type: "text", text: flushedText }]
2284
+ };
2285
+ }
2286
+ return {
2287
+ isError: true,
2288
+ content: [{ type: "text", text: "Error: Missing required parameter 'chunk'. Provide the text chunk to process." }]
2289
+ };
2290
+ }
2291
+ if (typeof chunk !== "string") {
2292
+ return {
2293
+ isError: true,
2294
+ content: [{ type: "text", text: `Error: Parameter 'chunk' must be a string, got '${typeof chunk}'.` }]
2295
+ };
2296
+ }
2297
+
2298
+ const detok = getStreamDetokenizer(session_id);
2299
+ if (reset) {
2300
+ detok.reset();
2301
+ }
2302
+
2303
+ let emitted = detok.feed(chunk);
2304
+ if (flush) {
2305
+ emitted += detok.flush();
2306
+ }
2307
+
2308
+ return {
2309
+ content: [{ type: "text", text: emitted }]
2310
+ };
2311
+ }
2312
+
1703
2313
  if (name === "mark_false_positive") {
1704
2314
  const { token, session_id = "default" } = args || {};
1705
2315
  if (!token || typeof token !== 'string') {
@@ -1748,7 +2358,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1748
2358
  ]
1749
2359
  };
1750
2360
  }
1751
- const { profile = "General", compact = false, session_id = "default" } = args || {};
2361
+ const targetProfile = resolveRequestedProfile(args, "General");
2362
+ const { compact = false, session_id = "default" } = args || {};
1752
2363
  const targetSession = getSessionMap(session_id);
1753
2364
  const targetIgnoreList = getSessionIgnoreList(session_id);
1754
2365
  const filePath = rawPath;
@@ -1796,9 +2407,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1796
2407
  if (resolvedPath.toLowerCase().endsWith(".docx")) {
1797
2408
  try {
1798
2409
  const mammoth = require('mammoth');
1799
- const result = await mammoth.extractRawText({ path: resolvedPath });
1800
- const content = result.value;
1801
- const targetProfile = profile.trim();
2410
+ const zlib = require('zlib');
2411
+ const docxBuf = fs.readFileSync(resolvedPath);
2412
+ const result = await mammoth.extractRawText({ buffer: docxBuf });
2413
+ let extraXmlText = "";
2414
+ try {
2415
+ const zipEntries = {};
2416
+ let eocdOffset = -1;
2417
+ for (let i = docxBuf.length - 22; i >= Math.max(0, docxBuf.length - 65557); i--) {
2418
+ if (docxBuf.readUInt32LE(i) === 0x06054b50) {
2419
+ eocdOffset = i;
2420
+ break;
2421
+ }
2422
+ }
2423
+ if (eocdOffset !== -1) {
2424
+ const cdEntries = docxBuf.readUInt16LE(eocdOffset + 10);
2425
+ let pos = docxBuf.readUInt32LE(eocdOffset + 16);
2426
+ for (let e = 0; e < cdEntries && pos + 46 <= docxBuf.length; e++) {
2427
+ if (docxBuf.readUInt32LE(pos) !== 0x02014b50) break;
2428
+ const method = docxBuf.readUInt16LE(pos + 10);
2429
+ const compSize = docxBuf.readUInt32LE(pos + 20);
2430
+ const nameLen = docxBuf.readUInt16LE(pos + 28);
2431
+ const extraLen = docxBuf.readUInt16LE(pos + 30);
2432
+ const commentLen = docxBuf.readUInt16LE(pos + 32);
2433
+ const localOffset = docxBuf.readUInt32LE(pos + 42);
2434
+ const entryName = docxBuf.toString('utf8', pos + 46, pos + 46 + nameLen);
2435
+ if (localOffset + 30 <= docxBuf.length && docxBuf.readUInt32LE(localOffset) === 0x04034b50) {
2436
+ const lNameLen = docxBuf.readUInt16LE(localOffset + 26);
2437
+ const lExtraLen = docxBuf.readUInt16LE(localOffset + 28);
2438
+ const dataStart = localOffset + 30 + lNameLen + lExtraLen;
2439
+ const compData = docxBuf.subarray(dataStart, dataStart + compSize);
2440
+ if (method === 0) {
2441
+ zipEntries[entryName] = compData.toString('utf8');
2442
+ } else if (method === 8) {
2443
+ zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
2444
+ }
2445
+ }
2446
+ pos += 46 + nameLen + extraLen + commentLen;
2447
+ }
2448
+ }
2449
+ const xmlTargets = Object.keys(zipEntries).filter((n) =>
2450
+ /^(?:word\/(?:header\d*|footer\d*|footnotes|endnotes|comments|commentsExtended|people)\.xml|docProps\/(?:core|app|custom)\.xml)$/i.test(n)
2451
+ );
2452
+ const docAuthors = new Set();
2453
+ for (const xmlPath of xmlTargets) {
2454
+ const rawXml = zipEntries[xmlPath];
2455
+ const authorRx = /\b(?:w:author|w15:author|w15:userId)="([^"]+)"/gi;
2456
+ let am;
2457
+ while ((am = authorRx.exec(rawXml)) !== null) {
2458
+ if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
2459
+ }
2460
+ const stripped = rawXml
2461
+ .replace(/<\/w:p>/gi, "\n")
2462
+ .replace(/<w:tab\b[^>]*\/?>/gi, "\t")
2463
+ .replace(/<[^>]+>/g, " ")
2464
+ .replace(/&amp;/g, "&")
2465
+ .replace(/&lt;/g, "<")
2466
+ .replace(/&gt;/g, ">")
2467
+ .replace(/&quot;/g, '"')
2468
+ .replace(/&apos;/g, "'")
2469
+ .replace(/[ \t]+/g, " ")
2470
+ .trim();
2471
+ if (stripped) extraXmlText += `\n${stripped}\n`;
2472
+ }
2473
+ if (zipEntries["word/document.xml"]) {
2474
+ const docXml = zipEntries["word/document.xml"];
2475
+ const authorRx = /\bw:author="([^"]+)"/gi;
2476
+ let am;
2477
+ while ((am = authorRx.exec(docXml)) !== null) {
2478
+ if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
2479
+ }
2480
+ const delAndInstrRx = /<w:(?:delText|instrText)\b[^>]*>([\s\S]*?)<\/w:(?:delText|instrText)>/gi;
2481
+ let dm;
2482
+ const hiddenChunks = [];
2483
+ while ((dm = delAndInstrRx.exec(docXml)) !== null) {
2484
+ const val = dm[1]
2485
+ .replace(/<[^>]+>/g, " ")
2486
+ .replace(/&amp;/g, "&")
2487
+ .replace(/&lt;/g, "<")
2488
+ .replace(/&gt;/g, ">")
2489
+ .replace(/&quot;/g, '"')
2490
+ .replace(/&apos;/g, "'")
2491
+ .trim();
2492
+ if (val) hiddenChunks.push(val);
2493
+ }
2494
+ if (hiddenChunks.length > 0) {
2495
+ extraXmlText += `\n[Tracked Changes / Field Text]: ${hiddenChunks.join(" | ")}\n`;
2496
+ }
2497
+ }
2498
+ if (docAuthors.size > 0) {
2499
+ extraXmlText += `\n[Document Reviewers]: ${Array.from(docAuthors).join(", ")}\n`;
2500
+ }
2501
+ const relsTargets = Object.keys(zipEntries).filter((n) => /^word\/_rels\/.*\.rels$/i.test(n));
2502
+ for (const relPath of relsTargets) {
2503
+ const relXml = zipEntries[relPath];
2504
+ const targetRegex = /Target="([^"]+)"/gi;
2505
+ let tm;
2506
+ while ((tm = targetRegex.exec(relXml)) !== null) {
2507
+ const val = tm[1];
2508
+ if (/^(?:mailto:|tel:|https?:\/\/)/i.test(val) && !/schemas\.openxmlformats\.org|schemas\.microsoft\.com|w3\.org/i.test(val)) {
2509
+ extraXmlText += `\n[Document Link]: ${val}\n`;
2510
+ }
2511
+ }
2512
+ }
2513
+ } catch (_) {}
2514
+ const content = (result.value || '') + (extraXmlText ? '\n' + extraXmlText : '');
1802
2515
  const isAdvanced = targetProfile.toLowerCase() !== "general";
1803
2516
  const license = checkLicenseStatus();
1804
2517
  let finalProfile = targetProfile;
@@ -1861,7 +2574,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1861
2574
  const pdf = require('pdf-parse');
1862
2575
  const data = await pdf(buffer);
1863
2576
  const content = data.text;
1864
- const targetProfile = profile.trim();
1865
2577
  let finalProfile = targetProfile;
1866
2578
 
1867
2579
  const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
@@ -1888,13 +2600,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1888
2600
  const XLSX = require('xlsx');
1889
2601
  const workbook = XLSX.read(buffer, { type: 'buffer' });
1890
2602
  let content = '';
2603
+ if (workbook.Props && typeof workbook.Props === 'object') {
2604
+ const propFields = ['Author', 'LastAuthor', 'Company', 'Manager', 'Title', 'Subject', 'Comments'];
2605
+ for (const pf of propFields) {
2606
+ if (workbook.Props[pf] && String(workbook.Props[pf]).trim()) {
2607
+ content += `[Workbook ${pf}]: ${String(workbook.Props[pf]).trim()}\n`;
2608
+ }
2609
+ }
2610
+ }
2611
+ if (workbook.CustProps && typeof workbook.CustProps === 'object') {
2612
+ for (const [ck, cv] of Object.entries(workbook.CustProps)) {
2613
+ if (cv !== undefined && cv !== null && String(cv).trim()) {
2614
+ content += `[Custom Property ${ck}]: ${String(cv).trim()}\n`;
2615
+ }
2616
+ }
2617
+ }
1891
2618
  workbook.SheetNames.forEach(sheetName => {
1892
2619
  const worksheet = workbook.Sheets[sheetName];
1893
2620
  content += `--- Sheet: ${sheetName} ---\n`;
1894
2621
  content += XLSX.utils.sheet_to_csv(worksheet) + '\n';
2622
+ if (worksheet && typeof worksheet === 'object') {
2623
+ for (const cellKey of Object.keys(worksheet)) {
2624
+ if (cellKey.startsWith('!')) continue;
2625
+ const cell = worksheet[cellKey];
2626
+ if (!cell || typeof cell !== 'object') continue;
2627
+ if (cell.l && cell.l.Target) {
2628
+ content += `[Hyperlink ${cellKey}]: ${cell.l.Target}\n`;
2629
+ }
2630
+ if (Array.isArray(cell.c)) {
2631
+ for (const comm of cell.c) {
2632
+ const author = comm.a ? `${comm.a}: ` : '';
2633
+ const cText = (comm.t || '').trim();
2634
+ if (cText) {
2635
+ content += `[Comment ${cellKey}] ${author}${cText}\n`;
2636
+ }
2637
+ }
2638
+ }
2639
+ }
2640
+ }
1895
2641
  });
1896
2642
 
1897
- const targetProfile = profile.trim();
1898
2643
  let finalProfile = targetProfile;
1899
2644
 
1900
2645
  const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
@@ -1939,14 +2684,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1939
2684
  };
1940
2685
  }
1941
2686
 
1942
- const content = buffer.toString("utf8");
1943
- const targetProfile = profile.trim();
2687
+ let content = buffer.toString("utf8");
2688
+ const ext = path.extname(resolvedPath).toLowerCase();
2689
+ if (ext === ".eml") {
2690
+ const decodeQpUtf8 = (str) =>
2691
+ str
2692
+ .replace(/=\r?\n/g, "")
2693
+ .replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
2694
+ try {
2695
+ const bytes = Buffer.alloc(seq.length / 3);
2696
+ for (let i = 0; i < bytes.length; i++) {
2697
+ bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
2698
+ }
2699
+ return bytes.toString("utf8");
2700
+ } catch (_) {
2701
+ return seq;
2702
+ }
2703
+ });
2704
+ content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
2705
+ try {
2706
+ if (enc.toUpperCase() === "B") {
2707
+ return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
2708
+ }
2709
+ return decodeQpUtf8(payload.replace(/_/g, " "));
2710
+ } catch (_) {
2711
+ return full;
2712
+ }
2713
+ });
2714
+ content = content.replace(
2715
+ /(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi,
2716
+ (full, hdr, b64Block) => {
2717
+ try {
2718
+ const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
2719
+ if (cleanB64.length < 8) return full;
2720
+ return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
2721
+ } catch (_) {
2722
+ return full;
2723
+ }
2724
+ }
2725
+ );
2726
+ content = decodeQpUtf8(content);
2727
+ } else if (ext === ".rtf") {
2728
+ content = content
2729
+ .replace(/\\par\b|\\line\b/gi, "\n")
2730
+ .replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
2731
+ .replace(/\\u(-?\d+)\??/g, (_, code) => {
2732
+ const n = parseInt(code, 10);
2733
+ return String.fromCharCode(n < 0 ? n + 65536 : n);
2734
+ })
2735
+ .replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
2736
+ .replace(/[{}]/g, " ")
2737
+ .replace(/[ \t]+/g, " ");
2738
+ }
1944
2739
  const isAdvanced = targetProfile.toLowerCase() !== "general";
1945
2740
  const license = checkLicenseStatus();
1946
2741
  let finalProfile = targetProfile;
1947
2742
  let isAutoElevated = false;
1948
2743
 
1949
- const ext = path.extname(resolvedPath).toLowerCase();
1950
2744
  const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.sh', '.bash', '.zsh', '.env', '.yaml', '.yml', '.toml', '.sql']);
1951
2745
  const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
1952
2746
 
@@ -1983,7 +2777,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1983
2777
  }
1984
2778
 
1985
2779
  if (name === "redact_file") {
1986
- const { file_path, profile, dry_run, no_backup } = args;
2780
+ const { file_path, dry_run, no_backup } = args;
1987
2781
  if (!file_path || !fs.existsSync(file_path)) {
1988
2782
  return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
1989
2783
  }
@@ -1998,10 +2792,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1998
2792
  if (buffer.indexOf(0) !== -1) {
1999
2793
  return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
2000
2794
  }
2001
- const content = buffer.toString('utf8');
2795
+ let content = buffer.toString('utf8');
2796
+ const ext = path.extname(file_path).toLowerCase();
2797
+ if (ext === ".eml") {
2798
+ const decodeQpUtf8 = (str) =>
2799
+ str.replace(/=\r?\n/g, "").replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
2800
+ try {
2801
+ const bytes = Buffer.alloc(seq.length / 3);
2802
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
2803
+ return bytes.toString("utf8");
2804
+ } catch (_) { return seq; }
2805
+ });
2806
+ content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
2807
+ try {
2808
+ if (enc.toUpperCase() === "B") return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
2809
+ return decodeQpUtf8(payload.replace(/_/g, " "));
2810
+ } catch (_) { return full; }
2811
+ });
2812
+ content = content.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
2813
+ try {
2814
+ const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
2815
+ if (cleanB64.length < 8) return full;
2816
+ return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
2817
+ } catch (_) { return full; }
2818
+ });
2819
+ content = decodeQpUtf8(content);
2820
+ } else if (ext === ".rtf") {
2821
+ content = content
2822
+ .replace(/\\par\b|\\line\b/gi, "\n")
2823
+ .replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
2824
+ .replace(/\\u(-?\d+)\??/g, (_, code) => {
2825
+ const n = parseInt(code, 10);
2826
+ return String.fromCharCode(n < 0 ? n + 65536 : n);
2827
+ })
2828
+ .replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
2829
+ .replace(/[{}]/g, " ")
2830
+ .replace(/[ \t]+/g, " ");
2831
+ }
2002
2832
 
2003
2833
  const customRules = loadCustomRules();
2004
- const normalizedProfile = (profile || "general").trim().toLowerCase();
2834
+ const activeRules = [...customRules];
2835
+ DEVOPS_SECRETS_DETECTOR.forEach(rule => {
2836
+ activeRules.push({
2837
+ name: rule.name,
2838
+ pattern: rule.regex.source,
2839
+ regex: new RegExp(rule.regex.source, rule.regex.flags),
2840
+ label: rule.label || 'SECRET'
2841
+ });
2842
+ });
2843
+ const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
2005
2844
  const license = checkLicenseStatus();
2006
2845
 
2007
2846
  const limitStatus = checkFreeTierLimit(license.isPro);
@@ -2013,7 +2852,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2013
2852
  }
2014
2853
  const localSessionMap = {};
2015
2854
 
2016
- const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
2855
+ const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
2017
2856
 
2018
2857
  let isSecretLeak = false;
2019
2858
  if (!license.isPro && result.tokenMap) {
@@ -2128,8 +2967,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2128
2967
  const tierIcon = license.isPro ? '✅' : '🔓';
2129
2968
  const profileList = license.isPro
2130
2969
  ? 'All 30 specialized profiles active (General, Dev, Medical, Legal, Finance, HR…)'
2131
- : 'General only — PRO unlocks all 30 specialized industry profiles';
2132
- const sizeLimit = license.isPro ? 'Unlimited' : '15,000 characters per request';
2970
+ : 'All 30 profiles active (5k quota) · PRO unlocks unlimited';
2971
+ const sizeLimit = license.isPro ? 'Unlimited' : '15,000 char (General) / 5,000 char (Industry)';
2133
2972
 
2134
2973
  const configPath = resolveConfigPath();
2135
2974
  let rulesStatus = '';
@@ -2211,7 +3050,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2211
3050
  }
2212
3051
 
2213
3052
  if (name === "audit_directory_for_pii") {
2214
- const { directory_path, profile, extensions, ignore_node_modules } = args;
3053
+ const { directory_path, extensions, ignore_node_modules } = args;
2215
3054
  if (!directory_path || !fs.existsSync(directory_path)) {
2216
3055
  return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
2217
3056
  }
@@ -2240,7 +3079,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2240
3079
  let reportLines = [`## Security Audit Report: ${directory_path}`];
2241
3080
  let filesWithIssues = 0;
2242
3081
  const customRules = loadCustomRules();
2243
- const normalizedProfile = (profile || "general").trim().toLowerCase();
3082
+ const activeRules = [...customRules];
3083
+ DEVOPS_SECRETS_DETECTOR.forEach(rule => {
3084
+ activeRules.push({
3085
+ name: rule.name,
3086
+ pattern: rule.regex.source,
3087
+ regex: new RegExp(rule.regex.source, rule.regex.flags),
3088
+ label: rule.label || 'SECRET'
3089
+ });
3090
+ });
3091
+ const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
2244
3092
  const license = checkLicenseStatus();
2245
3093
 
2246
3094
  const limitStatus = checkFreeTierLimit(license.isPro);
@@ -2262,7 +3110,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2262
3110
  const content = buffer.toString('utf8');
2263
3111
 
2264
3112
  const localSessionMap = {};
2265
- const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
3113
+ const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
2266
3114
 
2267
3115
  Object.assign(totalTokensForDirectory, localSessionMap);
2268
3116
 
@@ -2310,90 +3158,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2310
3158
  }
2311
3159
 
2312
3160
  if (name === "generate_compliance_report") {
2313
- const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance" } = args || {};
2314
- const telemetry = buildCisoAuditTelemetry(sessionMap);
2315
-
2316
- const sessionHash = crypto.createHash('sha256')
2317
- .update(JSON.stringify(sessionMap) + Date.now().toString())
2318
- .digest('hex');
3161
+ const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance", session_id = "default" } = args || {};
3162
+ const targetSession = getSessionMap(session_id);
3163
+ const telemetry = buildCisoAuditTelemetry(targetSession);
2319
3164
 
2320
- const timestamp = new Date().toISOString();
2321
- const entitySummary = Object.entries(telemetry.entities)
2322
- .map(([t, count]) => `[${t}]: ${count}`)
2323
- .join(', ') || 'None (Clean)';
2324
-
2325
- if (format === "json") {
2326
- const jsonReport = {
2327
- protocol: "Zero-Trust Data Sanitization (ZTDS)",
2328
- certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
2329
- company: company_name,
2330
- department: department,
2331
- timestamp: timestamp,
2332
- session_hash: sessionHash,
2333
- verification_mode: "100% Offline (Local In-Memory RAM)",
2334
- compliance_status: "VERIFIED PASS",
2335
- risk_level: telemetry.riskLevel,
2336
- frameworks_enforced: telemetry.frameworksList,
2337
- total_masked_tokens: telemetry.totalCount,
2338
- entities_breakdown: telemetry.entities,
2339
- zero_egress_verified: true,
2340
- verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
2341
- licensing_and_governance: {
2342
- teams_governance_url: "https://privacyscrubber.com/teams?src=mcp_compliance_json",
2343
- sdk_pipeline_url: "https://privacyscrubber.com/sdk/?src=mcp_compliance_json"
2344
- }
2345
- };
2346
-
2347
- return {
2348
- content: [{ type: "text", text: JSON.stringify(jsonReport, null, 2) }]
2349
- };
2350
- }
2351
-
2352
- if (format === "summary") {
2353
- const summaryText = `[PrivacyScrubber Compliance Certificate] ID: ZTDS-${sessionHash.substring(0, 8).toUpperCase()} | Organization: ${company_name} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks: ${telemetry.frameworksList.join(', ')} | Status: PASS (100% Air-Gapped)`;
2354
- return {
2355
- content: [{ type: "text", text: summaryText }]
2356
- };
2357
- }
2358
-
2359
- // Default: Comprehensive Markdown Certificate
2360
- const mdReport = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
2361
- **Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
2362
- **Organization:** ${company_name} (${department})
2363
- **Timestamp:** ${timestamp}
2364
- **Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
2365
- **Status:** **VERIFIED PASS** (Zero Network Egress)
2366
-
2367
- ---
2368
-
2369
- ### 📊 Sanitization Metrics & Risk Assessment
2370
- * **Overall Risk Rating:** **${telemetry.riskLevel}**
2371
- * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
2372
- * **Entity Breakdown:** ${entitySummary}
2373
- * **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
2374
-
2375
- ### 📜 Regulatory Frameworks Enforced
2376
- ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2377
-
2378
- ### 🔒 CISO Compliance Declaration
2379
- 1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
2380
- 2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
2381
- 3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
2382
-
2383
- ### 🏢 Enterprise & Team Governance
2384
- * **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules, audit logs, and zero-knowledge session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](https://privacyscrubber.com/teams?src=mcp_compliance_report)
2385
- * **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?src=mcp_compliance_report)
2386
-
2387
- *Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
2388
- *Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
2389
-
3165
+ const receipt = buildPsr2Receipt(targetSession);
2390
3166
  return {
2391
- content: [{ type: "text", text: mdReport }]
3167
+ content: [{ type: "text", text: formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_compliance_report', utmMedium: format === 'json' ? 'json' : 'markdown' }) }]
2392
3168
  };
2393
3169
  }
2394
3170
 
2395
3171
  if (name === "guard_exec") {
2396
- const { command, cwd, profile = "Dev", timeout_ms = 15000, session_id = "default" } = args || {};
3172
+ const { command, cwd, timeout_ms = 15000, session_id = "default" } = args || {};
2397
3173
  if (!command || typeof command !== "string") {
2398
3174
  return {
2399
3175
  isError: true,
@@ -2433,7 +3209,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2433
3209
  exitCode = typeof err.status === 'number' ? err.status : 1;
2434
3210
  }
2435
3211
 
2436
- const targetProfile = (profile || "Dev").trim();
3212
+ const targetProfile = resolveRequestedProfile(args, "Dev");
2437
3213
  const isAdvanced = targetProfile.toLowerCase() !== "general";
2438
3214
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
2439
3215
 
@@ -2463,7 +3239,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2463
3239
  }
2464
3240
 
2465
3241
  if (name === "guard_read_file") {
2466
- const { file_path, profile = "Dev", max_lines = 500, session_id = "default" } = args || {};
3242
+ const { file_path, max_lines = 500, session_id = "default" } = args || {};
2467
3243
  if (!file_path || typeof file_path !== "string") {
2468
3244
  return {
2469
3245
  isError: true,
@@ -2484,6 +3260,22 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2484
3260
  content: [{ type: "text", text: `Error: Path is a directory. Use audit_directory_for_pii to scan directories.` }]
2485
3261
  };
2486
3262
  }
3263
+ if (stat.size > 10 * 1024 * 1024) {
3264
+ return {
3265
+ isError: true,
3266
+ content: [{ type: "text", text: `Error: File is too large (${(stat.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed size is 10MB.` }]
3267
+ };
3268
+ }
3269
+ const rawBuffer = fs.readFileSync(resolvedPath);
3270
+ const checkLen = Math.min(rawBuffer.length, 8000);
3271
+ for (let i = 0; i < checkLen; i++) {
3272
+ if (rawBuffer[i] === 0) {
3273
+ return {
3274
+ isError: true,
3275
+ content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
3276
+ };
3277
+ }
3278
+ }
2487
3279
  const license = checkLicenseStatus();
2488
3280
  const limitStatus = checkFreeTierLimit(license.isPro);
2489
3281
  if (limitStatus.blocked) {
@@ -2493,12 +3285,48 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2493
3285
  };
2494
3286
  }
2495
3287
 
2496
- let rawContent = fs.readFileSync(resolvedPath, 'utf8');
3288
+ let rawContent = rawBuffer.toString('utf8');
3289
+ const ext = path.extname(resolvedPath).toLowerCase();
3290
+ if (ext === '.eml') {
3291
+ const decodeQpUtf8 = (str) =>
3292
+ str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
3293
+ try {
3294
+ const bytes = Buffer.alloc(seq.length / 3);
3295
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
3296
+ return bytes.toString('utf8');
3297
+ } catch (_) { return seq; }
3298
+ });
3299
+ rawContent = rawContent.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
3300
+ try {
3301
+ if (enc.toUpperCase() === 'B') return Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8');
3302
+ return decodeQpUtf8(payload.replace(/_/g, ' '));
3303
+ } catch (_) { return full; }
3304
+ });
3305
+ rawContent = rawContent.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
3306
+ try {
3307
+ const cleanB64 = b64Block.replace(/[\r\n\s]+/g, '');
3308
+ if (cleanB64.length < 8) return full;
3309
+ return hdr + Buffer.from(cleanB64, 'base64').toString('utf8');
3310
+ } catch (_) { return full; }
3311
+ });
3312
+ rawContent = decodeQpUtf8(rawContent);
3313
+ } else if (ext === '.rtf') {
3314
+ rawContent = rawContent
3315
+ .replace(/\\par\b|\\line\b/gi, '\n')
3316
+ .replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
3317
+ .replace(/\\u(-?\d+)\??/g, (_, code) => {
3318
+ const n = parseInt(code, 10);
3319
+ return String.fromCharCode(n < 0 ? n + 65536 : n);
3320
+ })
3321
+ .replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
3322
+ .replace(/[{}]/g, ' ')
3323
+ .replace(/[ \t]+/g, ' ');
3324
+ }
2497
3325
  const lines = rawContent.split(/\r?\n/);
2498
3326
  const capped = lines.slice(0, max_lines).join('\n');
2499
3327
  const wasLineTruncated = lines.length > max_lines;
2500
3328
 
2501
- const targetProfile = (profile || "Dev").trim();
3329
+ const targetProfile = resolveRequestedProfile(args, "Dev");
2502
3330
  const isAdvanced = targetProfile.toLowerCase() !== "general";
2503
3331
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
2504
3332
 
@@ -2524,7 +3352,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2524
3352
  }
2525
3353
 
2526
3354
  if (name === "guard_git_diff") {
2527
- const { staged = false, cwd, profile = "Dev", session_id = "default" } = args || {};
3355
+ const { staged = false, cwd, session_id = "default" } = args || {};
2528
3356
  const targetSession = getSessionMap(session_id);
2529
3357
  const targetIgnoreList = getSessionIgnoreList(session_id);
2530
3358
  const repoCwd = cwd ? path.resolve(cwd) : process.cwd();
@@ -2558,7 +3386,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2558
3386
  };
2559
3387
  }
2560
3388
 
2561
- const targetProfile = (profile || "Dev").trim();
3389
+ const targetProfile = resolveRequestedProfile(args, "Dev");
2562
3390
  const isAdvanced = targetProfile.toLowerCase() !== "general";
2563
3391
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
2564
3392
 
@@ -2767,6 +3595,7 @@ Before reading sensitive files, running terminal commands that may print credent
2767
3595
  }
2768
3596
  const tokenEntries = Object.entries(session.map);
2769
3597
  const telemetry = buildCisoAuditTelemetry(session.map);
3598
+ const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
2770
3599
  return {
2771
3600
  content: [{
2772
3601
  type: "text",
@@ -2774,6 +3603,7 @@ Before reading sensitive files, running terminal commands that may print credent
2774
3603
  `Created: ${new Date(session.createdAt).toISOString()}\n` +
2775
3604
  `Last Active: ${new Date(session.lastActive).toISOString()}\n` +
2776
3605
  `Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
3606
+ `Stream Buffer: ${streamBufferLen} bytes pending\n` +
2777
3607
  `Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
2778
3608
  `Risk Level: ${telemetry.riskLevel}\n` +
2779
3609
  `Status: Volatile in-memory mapping active (0 bytes disk/network).`
@@ -2786,7 +3616,8 @@ Before reading sensitive files, running terminal commands that may print credent
2786
3616
  for (const [sid, sess] of agentSessions.entries()) {
2787
3617
  const count = Object.keys(sess.map).length;
2788
3618
  totalTokens += count;
2789
- sessionList.push({ id: sid, tokens: count, lastActive: new Date(sess.lastActive).toISOString() });
3619
+ const streamBufferLen = sess.streamDetokenizer ? sess.streamDetokenizer.buffer.length : 0;
3620
+ sessionList.push({ id: sid, tokens: count, streamBufferBytes: streamBufferLen, lastActive: new Date(sess.lastActive).toISOString() });
2790
3621
  }
2791
3622
 
2792
3623
  return {
@@ -2797,7 +3628,7 @@ Before reading sensitive files, running terminal commands that may print credent
2797
3628
  `Total Tokens in Memory: ${totalTokens}\n` +
2798
3629
  `Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
2799
3630
  `Uptime: ${Math.floor(process.uptime())} seconds\n` +
2800
- `Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
3631
+ `Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens, ${s.streamBufferBytes} buf)`).join(', ') || 'None'}`
2801
3632
  }]
2802
3633
  };
2803
3634
  }
@@ -2806,6 +3637,11 @@ Before reading sensitive files, running terminal commands that may print credent
2806
3637
  const { session_id, all = false, reset_all = false } = args || {};
2807
3638
  if (all || reset_all) {
2808
3639
  const count = agentSessions.size;
3640
+ for (const sess of agentSessions.values()) {
3641
+ if (sess.streamDetokenizer) {
3642
+ sess.streamDetokenizer.reset();
3643
+ }
3644
+ }
2809
3645
  agentSessions.clear();
2810
3646
  getSessionMap('default');
2811
3647
  sessionIgnoreList.clear();
@@ -2818,6 +3654,10 @@ Before reading sensitive files, running terminal commands that may print credent
2818
3654
  }
2819
3655
 
2820
3656
  const sid = (session_id || 'default').toString().trim();
3657
+ const sess = agentSessions.get(sid);
3658
+ if (sess && sess.streamDetokenizer) {
3659
+ sess.streamDetokenizer.reset();
3660
+ }
2821
3661
  const existed = agentSessions.delete(sid);
2822
3662
  if (sid === 'default') {
2823
3663
  getSessionMap('default');
@@ -2833,7 +3673,7 @@ Before reading sensitive files, running terminal commands that may print credent
2833
3673
  }
2834
3674
 
2835
3675
  if (name === "guard_rag_chunk") {
2836
- const { chunks, profile = "General", session_id = "default" } = args || {};
3676
+ const { chunks, session_id = "default" } = args || {};
2837
3677
  if (!Array.isArray(chunks)) {
2838
3678
  return {
2839
3679
  isError: true,
@@ -2841,6 +3681,7 @@ Before reading sensitive files, running terminal commands that may print credent
2841
3681
  };
2842
3682
  }
2843
3683
 
3684
+ const targetProfile = resolveRequestedProfile(args, "General");
2844
3685
  const targetSession = getSessionMap(session_id);
2845
3686
  const targetIgnoreList = getSessionIgnoreList(session_id);
2846
3687
  let totalTokensRedacted = 0;
@@ -2849,13 +3690,13 @@ Before reading sensitive files, running terminal commands that may print credent
2849
3690
  for (let i = 0; i < chunks.length; i++) {
2850
3691
  const item = chunks[i];
2851
3692
  if (typeof item === 'string') {
2852
- const res = performSanitization(item, profile, targetIgnoreList, targetSession);
3693
+ const res = performSanitization(item, targetProfile, targetIgnoreList, targetSession);
2853
3694
  totalTokensRedacted += Object.keys(res.newTokens || {}).length;
2854
3695
  sanitizedChunks.push(res.scrubbedText);
2855
3696
  } else if (item && typeof item === 'object') {
2856
3697
  const copy = { ...item };
2857
3698
  const rawText = copy.text || copy.pageContent || copy.document || '';
2858
- const res = performSanitization(rawText, profile, targetIgnoreList, targetSession);
3699
+ const res = performSanitization(rawText, targetProfile, targetIgnoreList, targetSession);
2859
3700
  totalTokensRedacted += Object.keys(res.newTokens || {}).length;
2860
3701
 
2861
3702
  if (copy.text !== undefined) copy.text = res.scrubbedText;
@@ -2971,11 +3812,9 @@ Before reading sensitive files, running terminal commands that may print credent
2971
3812
 
2972
3813
  const telemetry = buildCisoAuditTelemetry(targetSession);
2973
3814
 
2974
- const sessionHash = crypto.createHash('sha256')
2975
- .update(JSON.stringify(targetSession) + Date.now().toString())
2976
- .digest('hex');
2977
-
2978
3815
  const timestamp = new Date().toISOString();
3816
+ const receipt = buildPsr2Receipt(targetSession, timestamp);
3817
+ const sessionHash = receipt.receiptSha256;
2979
3818
 
2980
3819
  let outputText = "";
2981
3820
  const eventData = {
@@ -2996,60 +3835,8 @@ Before reading sensitive files, running terminal commands that may print credent
2996
3835
  outputText = formatCefEvent(eventData);
2997
3836
  } else if (format === "syslog") {
2998
3837
  outputText = formatSyslogEvent(eventData);
2999
- } else if (format === "json") {
3000
- outputText = JSON.stringify({
3001
- protocol: "Zero-Trust Data Sanitization (ZTDS)",
3002
- certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
3003
- company: company_name,
3004
- department,
3005
- timestamp,
3006
- session_hash: sessionHash,
3007
- verification_mode: "100% Offline (Local In-Memory RAM)",
3008
- compliance_status: "VERIFIED PASS",
3009
- risk_level: telemetry.riskLevel,
3010
- frameworks_enforced: telemetry.frameworksList,
3011
- total_masked_tokens: telemetry.totalCount,
3012
- entities_breakdown: telemetry.entities,
3013
- zero_egress_verified: true,
3014
- verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
3015
- licensing_and_governance: {
3016
- teams_governance_url: "https://privacyscrubber.com/teams?tier=teams&utm_source=mcp_audit_export&utm_medium=json&utm_campaign=teams_upgrade",
3017
- sdk_pipeline_url: "https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp_audit_export&utm_medium=json&utm_campaign=sdk_pipeline"
3018
- }
3019
- }, null, 2);
3020
- } else if (format === "markdown") {
3021
- const entitySummary = Object.entries(telemetry.entities)
3022
- .map(([t, count]) => `[${t}]: ${count}`)
3023
- .join(', ') || 'None (Clean)';
3024
- outputText = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
3025
- **Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
3026
- **Organization:** ${company_name} (${department})
3027
- **Timestamp:** ${timestamp}
3028
- **Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
3029
- **Status:** **VERIFIED PASS** (Zero Network Egress)
3030
-
3031
- ---
3032
-
3033
- ### 📊 Sanitization Metrics & Risk Assessment
3034
- * **Overall Risk Rating:** **${telemetry.riskLevel}**
3035
- * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
3036
- * **Entity Breakdown:** ${entitySummary}
3037
- * **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
3038
-
3039
- ### 📜 Regulatory Frameworks Enforced
3040
- ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
3041
-
3042
- ### 🔒 CISO Compliance Declaration
3043
- 1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
3044
- 2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
3045
- 3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
3046
-
3047
- ### 🏢 Enterprise & Team Governance
3048
- * **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules, audit logs, and zero-knowledge session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](https://privacyscrubber.com/teams?tier=teams&utm_source=mcp_audit_export&utm_medium=markdown&utm_campaign=teams_upgrade)
3049
- * **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp_audit_export&utm_medium=markdown&utm_campaign=sdk_pipeline)
3050
-
3051
- *Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
3052
- *Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
3838
+ } else if (format === "json" || format === "markdown") {
3839
+ outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
3053
3840
  } else {
3054
3841
  // Default: jsonl
3055
3842
  outputText = formatJsonlEvent(eventData);
@@ -3198,7 +3985,7 @@ function detectSecrets(text) {
3198
3985
  return detected;
3199
3986
  }
3200
3987
 
3201
- function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
3988
+ export function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
3202
3989
  const customRules = loadCustomRules();
3203
3990
  const normalizedProfile = (profile || "general").trim().toLowerCase();
3204
3991
  const license = checkLicenseStatus();
@@ -3226,7 +4013,7 @@ function performSanitization(text, profile, ignoreList = null, customSessionMap
3226
4013
  return { scrubbedText: result.scrubbedText, newTokens };
3227
4014
  }
3228
4015
 
3229
- function truncateIfFree(text, isPro, charLimit = 15000) {
4016
+ export function truncateIfFree(text, isPro, charLimit = 15000) {
3230
4017
  if (!isPro && text.length > charLimit) {
3231
4018
  mcpLog(`${colors.yellowBold}[PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}[Upgrade] Set PRIVACYSCRUBBER_KEY to your PRO/TEAMS/SDK license key for unlimited throughput: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade${colors.reset}\n`);
3232
4019
  const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade]`;