@privacyscrubber/sdk 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  import { createRequire } from 'module';
12
12
  const require = createRequire(import.meta.url);
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { execSync } from 'child_process';
16
+ import { Transform } from 'stream';
13
17
 
14
18
  import './polyfill.js';
15
19
  import './ps-license-manager.js';
@@ -160,7 +164,8 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
160
164
  */
161
165
  export function formatAuditReceipt(telemetry) {
162
166
  if (!telemetry || telemetry.totalCount === 0) {
163
- return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n";
167
+ return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
168
+ "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
164
169
  }
165
170
  const entitiesList = Object.entries(telemetry.entities)
166
171
  .map(([type, count]) => `${count} ${type}`)
@@ -171,22 +176,42 @@ export function formatAuditReceipt(telemetry) {
171
176
  return `> 🛡️ **PrivacyScrubber Audit Receipt**\n` +
172
177
  `> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
173
178
  `> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n` +
174
- `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n`;
179
+ `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
180
+ `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
181
+ }
182
+
183
+ let communityNoticeLogged = false;
184
+
185
+ function emitCommunityNoticeIfNeeded(isPro) {
186
+ if (communityNoticeLogged || isPro) return;
187
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
188
+ communityNoticeLogged = true;
189
+ console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 25 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk');
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Resets the community notice state for testing purposes.
195
+ * @private
196
+ */
197
+ export function _resetCommunityNoticeForTesting() {
198
+ communityNoticeLogged = false;
175
199
  }
176
200
 
177
201
  /**
178
- * Primary High-Level Function: Sanitize / Scrub text of PII and secrets.
202
+ * Sanitizes input text, masking detected PII & secrets with reversible tokens.
179
203
  *
180
- * @param {string} text - Raw input text containing potential PII/secrets.
181
- * @param {object} [options={}] - Configuration options.
182
- * @param {string} [options.profile='General'] - PII Profile (General, Dev, Medical, Financial, Legal, HR, etc.).
204
+ * @param {string} text - Raw input text.
205
+ * @param {object} [options={}] - Sanitization options.
206
+ * @param {string} [options.profile='General'] - Compliance detection profile.
183
207
  * @param {string} [options.licenseKey] - Optional license key. Falls back to process.env.PRIVACYSCRUBBER_KEY.
184
- * @param {Array} [options.customRules=[]] - Custom regex rules array.
185
- * @param {Record<string, string>} [options.tokenLabelMap={}] - Custom label overrides.
186
- * @param {Record<string, string>} [options.existingSessionMap={}] - Session map to maintain consistency across turns.
187
- * @param {boolean} [options.generateReceipt=true] - Whether to generate Markdown audit receipt.
208
+ * @param {Array<{name: string, pattern: string, regex: RegExp, label?: string}>} [options.customRules=[]] - Custom regex rules.
209
+ * @param {Record<string, string>} [options.tokenLabelMap={}] - Custom label mapping.
210
+ * @param {Record<string, string>} [options.existingSessionMap={}] - In-memory session map to preserve token IDs across turns.
211
+ * @param {boolean} [options.generateReceipt=true] - Generate a CISO Markdown audit receipt.
188
212
  * @param {boolean} [options.throwOnLimit=true] - Throw PrivacyScrubberLicenseError when free tier limit is hit.
189
- * @param {boolean} [options.detectSecrets=false] - Auto-inject DevOps & Cloud secrets detection rules.
213
+ * @param {boolean} [options.detectSecrets=false] - Auto-detect cloud credentials and API keys.
214
+ * @param {Set<string>|Array<string>} [options.ignoreList=null] - Values to preserve in cleartext.
190
215
  * @returns {{
191
216
  * scrubbedText: string,
192
217
  * tokenMap: Record<string, string>,
@@ -226,6 +251,8 @@ export function sanitize(text, options = {}) {
226
251
  const isPro = licenseCheck.valid;
227
252
  const tier = isPro ? licenseCheck.tier : 'FREE';
228
253
 
254
+ emitCommunityNoticeIfNeeded(isPro);
255
+
229
256
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
230
257
  const limit = isPro ? Infinity : (isSpecialized ? 5000 : 15000);
231
258
 
@@ -302,6 +329,18 @@ export function sanitize(text, options = {}) {
302
329
  */
303
330
  export const scrub = sanitize;
304
331
 
332
+ /**
333
+ * Ergonomic 1-liner: Sanitizes text and returns the scrubbed string directly.
334
+ * @param {string} text - Input text.
335
+ * @param {string} [profile='General'] - Target compliance profile.
336
+ * @returns {string} - Scrubbed string.
337
+ */
338
+ export function clean(text, profile = 'General') {
339
+ if (!text || typeof text !== 'string') return '';
340
+ const { scrubbedText } = sanitize(text, { profile });
341
+ return scrubbedText;
342
+ }
343
+
305
344
  /**
306
345
  * Primary High-Level Function: Restore / Unscrub AI response back to original entities in-memory.
307
346
  *
@@ -486,6 +525,198 @@ export function createLangChainTransform(options = {}) {
486
525
  };
487
526
  }
488
527
 
528
+ /**
529
+ * Creates a zero-latency streaming token restorer for LLM Server-Sent Events (SSE) and Web Streams.
530
+ * Solves the broken chunk boundary problem where tokens like [EMAIL_1] are split across multiple chunks.
531
+ *
532
+ * Works with:
533
+ * - Vercel AI SDK (streamText / createDataStreamResponse)
534
+ * - OpenAI streaming (chat.completions.create({ stream: true }))
535
+ * - LangChain streaming
536
+ * - Native Web Streams (ReadableStream.pipeThrough)
537
+ * - Async generators (for await ... of)
538
+ *
539
+ * @param {Record<string, string>} tokenMap - Volatile token mapping object.
540
+ * @param {object} [options] - Configuration options.
541
+ * @returns {TransformStream & { process: (chunk: string) => string, flush: () => string, wrapStream: (asyncIterable: AsyncIterable<string>) => AsyncGenerator<string> }}
542
+ */
543
+ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
544
+ let pendingBuffer = '';
545
+ const map = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
546
+
547
+ function directRestore(text) {
548
+ if (!text) return '';
549
+ let result = text;
550
+ for (const token in map) {
551
+ if (result.includes(token)) {
552
+ result = result.split(token).join(map[token]);
553
+ }
554
+ }
555
+ return result;
556
+ }
557
+
558
+ function processChunk(chunk) {
559
+ if (!chunk || typeof chunk !== 'string') return '';
560
+ pendingBuffer += chunk;
561
+
562
+ // Check if there is an unclosed potential token bracket at the tail of the buffer.
563
+ // Token format: [TYPE_INDEX] or [CUSTOM_NAME] e.g. [EMAIL_1], [NAME_2], [SECRET_1]
564
+ const lastOpenIdx = pendingBuffer.lastIndexOf('[');
565
+ const lastCloseIdx = pendingBuffer.lastIndexOf(']');
566
+
567
+ if (lastOpenIdx !== -1 && lastOpenIdx > lastCloseIdx) {
568
+ const partial = pendingBuffer.slice(lastOpenIdx);
569
+ if (/^\[[A-Za-z0-9_]*$/.test(partial) && partial.length <= 64) {
570
+ const safeToEmit = pendingBuffer.slice(0, lastOpenIdx);
571
+ pendingBuffer = partial;
572
+ if (safeToEmit) {
573
+ return directRestore(safeToEmit);
574
+ }
575
+ return '';
576
+ }
577
+ }
578
+
579
+ const textToRestore = pendingBuffer;
580
+ pendingBuffer = '';
581
+ return directRestore(textToRestore);
582
+ }
583
+
584
+ function flushBuffer() {
585
+ if (!pendingBuffer) return '';
586
+ const remaining = pendingBuffer;
587
+ pendingBuffer = '';
588
+ return directRestore(remaining);
589
+ }
590
+
591
+ async function* wrapStream(asyncIterable) {
592
+ for await (const chunk of asyncIterable) {
593
+ const text = typeof chunk === 'string' ? chunk : (chunk?.text || chunk?.content || '');
594
+ const emitted = processChunk(text);
595
+ if (emitted) yield emitted;
596
+ }
597
+ const finalChunk = flushBuffer();
598
+ if (finalChunk) yield finalChunk;
599
+ }
600
+
601
+ let stream;
602
+ if (typeof TransformStream !== 'undefined') {
603
+ stream = new TransformStream({
604
+ transform(chunk, controller) {
605
+ const text = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
606
+ const out = processChunk(text);
607
+ if (out) controller.enqueue(out);
608
+ },
609
+ flush(controller) {
610
+ const out = flushBuffer();
611
+ if (out) controller.enqueue(out);
612
+ }
613
+ });
614
+ } else {
615
+ stream = {};
616
+ }
617
+
618
+ stream.process = processChunk;
619
+ stream.flush = flushBuffer;
620
+ stream.wrapStream = wrapStream;
621
+
622
+ return stream;
623
+ }
624
+
625
+ /**
626
+ * Wraps an async iterable LLM stream (e.g. from Vercel AI SDK textStream or OpenAI stream)
627
+ * and yields de-tokenized text chunks in real-time, seamlessly stitching split tokens across chunks.
628
+ *
629
+ * @param {AsyncIterable<string|object>} stream - The incoming LLM stream.
630
+ * @param {Record<string, string>} tokenMap - Volatile token map.
631
+ * @returns {AsyncGenerator<string>}
632
+ */
633
+ export async function* wrapAiStream(stream, tokenMap = {}) {
634
+ const restorer = createStreamTokenRestorer(tokenMap);
635
+ yield* restorer.wrapStream(stream);
636
+ }
637
+
638
+ /**
639
+ * Middleware wrapper for Next.js App Router Route Handlers (POST/PUT).
640
+ * Automatically sanitizes incoming prompt payloads in RAM (<1ms) before the handler runs,
641
+ * and attaches sanitized text, tokenMap, and helper restoration functions.
642
+ *
643
+ * @param {Function} handler - The async Next.js route handler (req, context) => Response
644
+ * @param {object} [options] - Sanitization options (profile, detectSecrets, etc.)
645
+ * @returns {Function} Wrapped Next.js route handler
646
+ */
647
+ export function withPrivacyScrubber(handler, options = {}) {
648
+ return async function privacyScrubberHandler(req, context = {}) {
649
+ if (!req || typeof req.json !== 'function') {
650
+ return handler(req, context);
651
+ }
652
+
653
+ let rawBody;
654
+ try {
655
+ rawBody = await req.json();
656
+ } catch (_) {
657
+ return handler(req, context);
658
+ }
659
+
660
+ let scrubbedBody = rawBody;
661
+ let tokenMap = {};
662
+ let telemetry = null;
663
+
664
+ if (typeof rawBody === 'string') {
665
+ const res = sanitize(rawBody, options);
666
+ scrubbedBody = res.scrubbedText;
667
+ tokenMap = res.tokenMap;
668
+ telemetry = res.telemetry;
669
+ } else if (rawBody && typeof rawBody === 'object') {
670
+ const serialized = JSON.stringify(rawBody);
671
+ const res = sanitize(serialized, options);
672
+ try {
673
+ scrubbedBody = JSON.parse(res.scrubbedText);
674
+ } catch (_) {
675
+ scrubbedBody = rawBody;
676
+ }
677
+ tokenMap = res.tokenMap;
678
+ telemetry = res.telemetry;
679
+ }
680
+
681
+ const psContext = {
682
+ ...context,
683
+ rawBody,
684
+ scrubbedBody,
685
+ tokenMap,
686
+ telemetry,
687
+ restore: (text) => restore(text, tokenMap),
688
+ createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
689
+ };
690
+
691
+ return handler(req, psContext);
692
+ };
693
+ }
694
+
695
+ /**
696
+ * Express.js / Connect middleware for automatic zero-trust PII sanitization.
697
+ * Sanitizes req.body in local RAM and attaches req.psTokenMap and req.restore(aiResponse).
698
+ *
699
+ * @param {object} [options] - Sanitization options.
700
+ * @returns {Function} Express middleware (req, res, next)
701
+ */
702
+ export function createExpressMiddleware(options = {}) {
703
+ return function privacyScrubberExpress(req, res, next) {
704
+ if (req && req.body && typeof req.body === 'object') {
705
+ const serialized = JSON.stringify(req.body);
706
+ const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
707
+ req.rawBody = req.body;
708
+ try {
709
+ req.body = JSON.parse(scrubbedText);
710
+ } catch (_) {}
711
+ req.psTokenMap = tokenMap;
712
+ req.psTelemetry = telemetry;
713
+ req.restore = (aiResponse) => restore(aiResponse, tokenMap);
714
+ req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
715
+ }
716
+ if (typeof next === 'function') next();
717
+ };
718
+ }
719
+
489
720
  /**
490
721
  * Generates an official Zero-Trust Compliance Audit Report.
491
722
  * @param {Record<string, string>} sessionMap - Map of token bindings.
@@ -557,6 +788,359 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
557
788
  *Verify at: https://privacyscrubber.com/features/audit-receipt/*`;
558
789
  }
559
790
 
791
+ /**
792
+ * Creates a Node.js Transform stream that sanitizes incoming text chunks line-by-line in volatile RAM.
793
+ * Zero external network egress. Zero memory bloat. Ideal for streaming server logs, CSVs, or HTTP responses.
794
+ *
795
+ * @param {object} [options={}] - Stream configuration or PrivacyScrubberEngine options.
796
+ * @param {PrivacyScrubberEngine} [options.engine] - Optional pre-configured PrivacyScrubberEngine instance.
797
+ * @param {string} [options.profile='General'] - Compliance detection profile.
798
+ * @param {string} [options.licenseKey] - Optional license key.
799
+ * @param {boolean} [options.detectSecrets=false] - Auto-detect cloud credentials and API keys.
800
+ * @param {Array} [options.customRules=[]] - Custom regex rules.
801
+ * @returns {Transform & { getSessionMap: () => Record<string, string>, getTelemetry: () => object, engine: PrivacyScrubberEngine }}
802
+ */
803
+ export function createSanitizeStream(options = {}) {
804
+ const engine = options.engine || new PrivacyScrubberEngine(options);
805
+ let tail = '';
806
+
807
+ const stream = new Transform({
808
+ transform(chunk, encoding, callback) {
809
+ try {
810
+ const str = tail + (Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
811
+ const lines = str.split('\n');
812
+ tail = lines.pop() || '';
813
+
814
+ for (let i = 0; i < lines.length; i++) {
815
+ const sanitized = engine.sanitize(lines[i], { persistSession: true, generateReceipt: false });
816
+ this.push(sanitized.scrubbedText + '\n');
817
+ }
818
+ callback();
819
+ } catch (err) {
820
+ callback(err);
821
+ }
822
+ },
823
+ flush(callback) {
824
+ try {
825
+ if (tail.length > 0) {
826
+ const sanitized = engine.sanitize(tail, { persistSession: true, generateReceipt: false });
827
+ this.push(sanitized.scrubbedText);
828
+ tail = '';
829
+ }
830
+ callback();
831
+ } catch (err) {
832
+ callback(err);
833
+ }
834
+ }
835
+ });
836
+
837
+ stream.engine = engine;
838
+ stream.getSessionMap = () => ({ ...engine.sessionMap });
839
+ stream.getTelemetry = () => buildAuditTelemetry(engine.sessionMap);
840
+
841
+ return stream;
842
+ }
843
+
844
+ /**
845
+ * Restores masked tokens in AI-generated code or text locally in volatile RAM,
846
+ * and writes the authentic content directly to the destination file on disk.
847
+ *
848
+ * @param {string} filePath - Target file destination.
849
+ * @param {string} content - Code/text containing placeholder tokens (e.g. [API_KEY_1]).
850
+ * @param {object} [options={}] - Options: tokenMap, engine, createBackup (default: true).
851
+ * @returns {{ success: boolean, filePath: string, restoredCount: number, backupCreated: boolean, backupPath?: string }}
852
+ */
853
+ export function applyPatch(filePath, content, options = {}) {
854
+ if (!filePath || typeof filePath !== 'string') {
855
+ throw new Error('[PrivacyScrubber] Missing required parameter: filePath must be a string.');
856
+ }
857
+ if (content === undefined || content === null || typeof content !== 'string') {
858
+ throw new Error('[PrivacyScrubber] Missing required parameter: content must be a string.');
859
+ }
860
+
861
+ const {
862
+ tokenMap = null,
863
+ engine = null,
864
+ createBackup = true
865
+ } = options;
866
+
867
+ const activeTokenMap = tokenMap || (engine && engine.sessionMap) || {};
868
+ const resolvedPath = path.resolve(process.cwd(), filePath);
869
+ const restored = unscrub(content, activeTokenMap);
870
+
871
+ let backupCreated = false;
872
+ let backupPath;
873
+
874
+ if (createBackup && fs.existsSync(resolvedPath)) {
875
+ backupPath = `${resolvedPath}.bak`;
876
+ fs.copyFileSync(resolvedPath, backupPath);
877
+ backupCreated = true;
878
+ }
879
+
880
+ const parentDir = path.dirname(resolvedPath);
881
+ if (!fs.existsSync(parentDir)) {
882
+ fs.mkdirSync(parentDir, { recursive: true });
883
+ }
884
+
885
+ fs.writeFileSync(resolvedPath, restored.restoredText, 'utf8');
886
+
887
+ return {
888
+ success: true,
889
+ filePath: resolvedPath,
890
+ restoredCount: restored.restoredCount || 0,
891
+ backupCreated,
892
+ backupPath
893
+ };
894
+ }
895
+
896
+ /**
897
+ * Alias for applyPatch.
898
+ */
899
+ export function restoreToFile(filePath, content, tokenMapOrOptions = {}, options = {}) {
900
+ if (tokenMapOrOptions && typeof tokenMapOrOptions === 'object' && !tokenMapOrOptions.sessionMap && !tokenMapOrOptions.tokenMap) {
901
+ return applyPatch(filePath, content, { ...options, tokenMap: tokenMapOrOptions });
902
+ }
903
+ return applyPatch(filePath, content, typeof tokenMapOrOptions === 'object' ? tokenMapOrOptions : options);
904
+ }
905
+
906
+ /**
907
+ * Creates CISO-grade Zero-Trust Agentic Guard tools for AI agents & LLM frameworks.
908
+ * Compatible with Vercel AI SDK, OpenAI function calling, LangChain, and AutoGen.
909
+ * Intercepts terminal commands, file reads, and code writes, masking secrets into volatile RAM.
910
+ *
911
+ * @param {PrivacyScrubberEngine|object} [engineOrOptions={}] - PrivacyScrubberEngine or config options.
912
+ * @param {object} [toolConfig={}] - Default configuration: cwd, profile.
913
+ * @returns {object} Object containing guardExec, guardReadFile, guardApplyPatch, guardGitDiff, getDefinitions, and engine.
914
+ */
915
+ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
916
+ const engine = (engineOrOptions instanceof PrivacyScrubberEngine)
917
+ ? engineOrOptions
918
+ : new PrivacyScrubberEngine(engineOrOptions);
919
+
920
+ const baseCwd = toolConfig.cwd || process.cwd();
921
+ const defaultProfile = toolConfig.profile || engine.defaultProfile || 'Dev';
922
+
923
+ const tools = {
924
+ guardExec: {
925
+ name: 'guard_exec',
926
+ description: 'Executes a shell command locally, intercepts stdout/stderr line-by-line, sanitizes credentials and PII into RAM tokens, and returns clean text to model context.',
927
+ parameters: {
928
+ type: 'object',
929
+ properties: {
930
+ command: { type: 'string', description: 'Shell command to execute.' },
931
+ cwd: { type: 'string', description: 'Working directory.' },
932
+ profile: { type: 'string', description: 'Detection profile (default: Dev).' },
933
+ timeoutMs: { type: 'number', description: 'Execution timeout in milliseconds (default: 15000).' }
934
+ },
935
+ required: ['command']
936
+ },
937
+ execute: async ({ command, cwd, profile = defaultProfile, timeoutMs = 15000 } = {}) => {
938
+ if (!command || typeof command !== 'string') {
939
+ throw new Error('[guard_exec] Missing or invalid parameter: command must be a string.');
940
+ }
941
+ const execCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
942
+ let stdout = '';
943
+ let stderr = '';
944
+ let exitCode = 0;
945
+
946
+ try {
947
+ stdout = execSync(command, {
948
+ cwd: execCwd,
949
+ timeout: Math.min(Math.max(timeoutMs, 1000), 60000),
950
+ maxBuffer: 10 * 1024 * 1024,
951
+ encoding: 'utf8',
952
+ stdio: ['pipe', 'pipe', 'pipe']
953
+ });
954
+ } catch (err) {
955
+ stdout = err.stdout ? err.stdout.toString() : '';
956
+ stderr = err.stderr ? err.stderr.toString() : (err.message || '');
957
+ exitCode = typeof err.status === 'number' ? err.status : 1;
958
+ }
959
+
960
+ const resCommand = engine.sanitize(command, { profile, detectSecrets: true, persistSession: true, generateReceipt: false });
961
+ const resStdout = engine.sanitize(stdout, { profile, detectSecrets: true, persistSession: true, generateReceipt: false });
962
+ const resStderr = engine.sanitize(stderr, { profile, detectSecrets: true, persistSession: true, generateReceipt: false });
963
+
964
+ const newTokens = { ...resCommand.tokenMap, ...resStdout.tokenMap, ...resStderr.tokenMap };
965
+ const telemetry = buildAuditTelemetry(newTokens);
966
+ const receipt = formatAuditReceipt(telemetry);
967
+
968
+ const responseText = `[Zero-Trust Agentic Guard: Exec]\n` +
969
+ `Command: ${resCommand.scrubbedText}\n` +
970
+ `Exit Code: ${exitCode}\n` +
971
+ `Tokens Redacted: ${Object.keys(newTokens).length}\n\n` +
972
+ (resStdout.scrubbedText ? `--- STDOUT ---\n${resStdout.scrubbedText}\n\n` : '') +
973
+ (resStderr.scrubbedText ? `--- STDERR ---\n${resStderr.scrubbedText}\n\n` : '') +
974
+ receipt;
975
+
976
+ return {
977
+ command: resCommand.scrubbedText,
978
+ stdout: resStdout.scrubbedText,
979
+ stderr: resStderr.scrubbedText,
980
+ exitCode,
981
+ tokenCount: Object.keys(newTokens).length,
982
+ telemetry,
983
+ receipt,
984
+ responseText
985
+ };
986
+ }
987
+ },
988
+
989
+ guardReadFile: {
990
+ name: 'guard_read_file',
991
+ description: 'Reads local file contents, tokenizing sensitive credentials and PII in volatile RAM before returning text to model context.',
992
+ parameters: {
993
+ type: 'object',
994
+ properties: {
995
+ filePath: { type: 'string', description: 'Path to target file.' },
996
+ maxLines: { type: 'number', description: 'Maximum lines to read (default: 500).' },
997
+ profile: { type: 'string', description: 'Detection profile (default: Dev).' }
998
+ },
999
+ required: ['filePath']
1000
+ },
1001
+ execute: async ({ filePath, maxLines = 500, profile = defaultProfile } = {}) => {
1002
+ if (!filePath || typeof filePath !== 'string') {
1003
+ throw new Error('[guard_read_file] Missing or invalid parameter: filePath must be a string.');
1004
+ }
1005
+ const resolved = path.resolve(baseCwd, filePath);
1006
+ if (!fs.existsSync(resolved)) {
1007
+ throw new Error(`[guard_read_file] File not found: ${resolved}`);
1008
+ }
1009
+ const stat = fs.statSync(resolved);
1010
+ if (stat.isDirectory()) {
1011
+ throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
1012
+ }
1013
+
1014
+ const raw = fs.readFileSync(resolved, 'utf8');
1015
+ const lines = raw.split(/\r?\n/);
1016
+ const capped = lines.slice(0, maxLines).join('\n');
1017
+ const wasTruncated = lines.length > maxLines;
1018
+
1019
+ const res = engine.sanitize(capped, { profile, detectSecrets: true, persistSession: true, generateReceipt: false });
1020
+ const telemetry = buildAuditTelemetry(res.tokenMap);
1021
+ const receipt = formatAuditReceipt(telemetry);
1022
+
1023
+ const responseText = `[Zero-Trust Agentic Guard: File Read: ${path.basename(resolved)}]\n` +
1024
+ `Path: ${resolved}\n` +
1025
+ `Total Lines: ${lines.length}${wasTruncated ? ` (Capped to ${maxLines} lines)` : ''}\n` +
1026
+ `Tokens Redacted: ${res.count}\n\n` +
1027
+ `--- SANITIZED CONTENT ---\n` +
1028
+ res.scrubbedText + '\n\n' +
1029
+ receipt;
1030
+
1031
+ return {
1032
+ filePath: resolved,
1033
+ content: res.scrubbedText,
1034
+ totalLines: lines.length,
1035
+ wasTruncated,
1036
+ tokenCount: res.count,
1037
+ telemetry,
1038
+ receipt,
1039
+ responseText
1040
+ };
1041
+ }
1042
+ },
1043
+
1044
+ guardApplyPatch: {
1045
+ name: 'guard_apply_patch',
1046
+ description: 'Restores authentic secrets locally from volatile RAM session map and writes real content directly to target file with automatic .bak backup.',
1047
+ parameters: {
1048
+ type: 'object',
1049
+ properties: {
1050
+ filePath: { type: 'string', description: 'Destination file path.' },
1051
+ content: { type: 'string', description: 'Content containing token placeholders (e.g. [API_KEY_1]).' },
1052
+ createBackup: { type: 'boolean', description: 'Whether to create .bak backup if file exists (default: true).' }
1053
+ },
1054
+ required: ['filePath', 'content']
1055
+ },
1056
+ execute: async ({ filePath, content, createBackup = true } = {}) => {
1057
+ const patchResult = applyPatch(filePath, content, {
1058
+ tokenMap: engine.sessionMap,
1059
+ createBackup
1060
+ });
1061
+
1062
+ const responseText = `[Zero-Trust Agentic Guard: Patch Applied]\n` +
1063
+ `Destination: ${patchResult.filePath}\n` +
1064
+ `Tokens Restored Locally: ${patchResult.restoredCount}\n` +
1065
+ `Backup Created: ${patchResult.backupCreated ? `${patchResult.filePath}.bak` : 'No'}\n` +
1066
+ `Status: File written with 100% authentic local values. Remote LLM received zero secrets.`;
1067
+
1068
+ return {
1069
+ ...patchResult,
1070
+ responseText
1071
+ };
1072
+ }
1073
+ },
1074
+
1075
+ guardGitDiff: {
1076
+ name: 'guard_git_diff',
1077
+ description: 'Inspects staged or unstaged git diff changes with credentials and PII masked into volatile RAM tokens.',
1078
+ parameters: {
1079
+ type: 'object',
1080
+ properties: {
1081
+ staged: { type: 'boolean', description: 'Whether to review staged changes (--cached).' },
1082
+ cwd: { type: 'string', description: 'Repository directory.' },
1083
+ profile: { type: 'string', description: 'Detection profile (default: Dev).' }
1084
+ },
1085
+ required: []
1086
+ },
1087
+ execute: async ({ staged = false, cwd, profile = defaultProfile } = {}) => {
1088
+ const repoCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
1089
+ const gitCmd = staged ? 'git diff --cached' : 'git diff';
1090
+ let diffOutput = '';
1091
+
1092
+ try {
1093
+ diffOutput = execSync(gitCmd, { cwd: repoCwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
1094
+ } catch (err) {
1095
+ if (err.code === 'ENOBUFS' && err.stdout) {
1096
+ diffOutput = err.stdout.toString('utf8');
1097
+ } else {
1098
+ throw new Error(`[guard_git_diff] Git command failed: ${err.message}`);
1099
+ }
1100
+ }
1101
+
1102
+ if (!diffOutput.trim()) {
1103
+ return {
1104
+ diff: '',
1105
+ isStaged: !!staged,
1106
+ tokenCount: 0,
1107
+ responseText: `[Zero-Trust Agentic Guard: Git Diff]\nNo changes detected (${staged ? 'staged' : 'unstaged'}).`
1108
+ };
1109
+ }
1110
+
1111
+ const res = engine.sanitize(diffOutput, { profile, detectSecrets: true, persistSession: true, generateReceipt: false });
1112
+ const telemetry = buildAuditTelemetry(res.tokenMap);
1113
+ const receipt = formatAuditReceipt(telemetry);
1114
+
1115
+ const responseText = `[Zero-Trust Agentic Guard: Git Diff (${staged ? 'Staged' : 'Unstaged'})]\n` +
1116
+ `Tokens Redacted: ${res.count}\n\n` +
1117
+ `--- SANITIZED DIFF ---\n` +
1118
+ res.scrubbedText + '\n\n' +
1119
+ receipt;
1120
+
1121
+ return {
1122
+ diff: res.scrubbedText,
1123
+ isStaged: !!staged,
1124
+ tokenCount: res.count,
1125
+ telemetry,
1126
+ receipt,
1127
+ responseText
1128
+ };
1129
+ }
1130
+ }
1131
+ };
1132
+
1133
+ tools.getDefinitions = () => [
1134
+ { type: 'function', function: { name: tools.guardExec.name, description: tools.guardExec.description, parameters: tools.guardExec.parameters } },
1135
+ { type: 'function', function: { name: tools.guardReadFile.name, description: tools.guardReadFile.description, parameters: tools.guardReadFile.parameters } },
1136
+ { type: 'function', function: { name: tools.guardApplyPatch.name, description: tools.guardApplyPatch.description, parameters: tools.guardApplyPatch.parameters } },
1137
+ { type: 'function', function: { name: tools.guardGitDiff.name, description: tools.guardGitDiff.description, parameters: tools.guardGitDiff.parameters } }
1138
+ ];
1139
+
1140
+ tools.engine = engine;
1141
+ return tools;
1142
+ }
1143
+
560
1144
  // Low-level backwards-compatible exports
561
1145
  export const scrubText = scrubberPkg.scrubText;
562
1146
  export const unscrubText = scrubberPkg.unscrubText;
@@ -564,6 +1148,7 @@ export const unscrubText = scrubberPkg.unscrubText;
564
1148
  export default {
565
1149
  sanitize,
566
1150
  scrub,
1151
+ clean,
567
1152
  restore,
568
1153
  unscrub,
569
1154
  validateLicense,
@@ -575,6 +1160,14 @@ export default {
575
1160
  PrivacyScrubberLicenseError,
576
1161
  wrapOpenAI,
577
1162
  createLangChainTransform,
1163
+ createStreamTokenRestorer,
1164
+ wrapAiStream,
1165
+ withPrivacyScrubber,
1166
+ createExpressMiddleware,
1167
+ createSanitizeStream,
1168
+ applyPatch,
1169
+ restoreToFile,
1170
+ createGuardedTools,
578
1171
  scrubText,
579
1172
  unscrubText,
580
1173
  DEVOPS_SECRETS_DETECTOR