@privacyscrubber/sdk 2.3.1 โ†’ 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![Air-Gapped](https://img.shields.io/badge/network_calls-0-blueviolet.svg)](https://privacyscrubber.com)
7
7
  [![TypeScript Typings](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://www.npmjs.com/package/@privacyscrubber/sdk)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
- [![Patent Pending](https://img.shields.io/badge/Patent-Pending%20(ILPO%20331905)-06b6d4.svg)](https://privacyscrubber.com/patents/)
9
+ [![Patent Pending](https://img.shields.io/badge/Patent-Pending%20(US%20%26%20Intl)-06b6d4.svg)](https://privacyscrubber.com/patents/)
10
10
  [![IETF Specification](https://img.shields.io/badge/IETF-ZTDS--Protocol-0284c7.svg)](https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/)
11
11
  [![ZTDS Verified](https://ztds.ai/badge/privacyscrubber-sdk.svg)](https://ztds.ai/registry/#privacyscrubber-sdk)
12
12
  [![GitHub Stars](https://img.shields.io/github/stars/moxno/privacyscrubber-mcp?style=social)](https://github.com/moxno/privacyscrubber-mcp)
@@ -71,7 +71,7 @@ Backend engineering and InfoSec review boards consistently ask four core archite
71
71
  > - **CERN / Zenodo Foundation**: [DOI 10.5281/zenodo.22058770](https://doi.org/10.5281/zenodo.22058770)
72
72
  > - **Center for Open Science (OSF) Benchmark**: [DOI 10.17605/OSF.IO/5BYJF](https://doi.org/10.17605/OSF.IO/5BYJF)
73
73
  > - **Law Archive Treatise**: [osf.io/preprints/lawarchive/4wc86/](https://osf.io/preprints/lawarchive/4wc86/)
74
- > - **Patent Pending**: Israel Patent Office Application `IL 331905` (WIPO DAS Code: `B17B`)
74
+ > - **Patent Pending**: U.S. & International Patents Pending (Paris Convention / PCT Priority)
75
75
 
76
76
  ---
77
77
 
@@ -700,16 +700,15 @@ MIT License ยฉ 2026 PrivacyScrubber. Commercial production usage without quota l
700
700
 
701
701
  ---
702
702
 
703
- ## ๐Ÿ›๏ธ Intellectual Property & Virtual Patent Marking
703
+ ## ๐Ÿ›๏ธ Intellectual Property & Patent Disclosures
704
704
 
705
705
  The Zero-Trust Data Sanitization (ZTDS) architecture, in-memory deterministic tokenization, cryptographic session handoff, and headless execution methods implemented in this SDK are proprietary technology of Ilya Sibiryakov (BrandMeWeb) and are protected under **Patent Pending** status:
706
706
 
707
- - **Patent Office:** State of Israel Ministry of Justice, Patent Office (ILPO)
708
- - **Application Number:** `331905` (Tracking ID: `94221`)
709
- - **WIPO DAS Access Control Code:** `B17B` (World Intellectual Property Organization Digital Access Service electronic priority document exchange for USPTO, EPO, CNIPA, and PCT receiving offices)
710
- - **Filing / Priority Date:** September 14, 2026 (Paris Convention Art. 4 & 35 U.S.C. ยง 119 Priority locked through September 14, 2027)
707
+ - **Legal Status:** U.S. & International Patents Pending
708
+ - **Priority Framework:** Paris Convention Art. 4 & 35 U.S.C. ยง 119 Priority (locked through September 14, 2027)
709
+ - **Filing / Priority Date:** September 14, 2026
711
710
  - **Official Title:** *SYSTEM AND METHOD FOR CLIENT-SIDE ZERO-TRUST DATA SANITIZATION AND CRYPTOGRAPHIC SESSION HANDOFF IN ARTIFICIAL INTELLIGENCE WORKFLOWS*
712
- - **Virtual Patent Marking:** [privacyscrubber.com/patents/](https://privacyscrubber.com/patents/) in accordance with 35 U.S.C. ยง 287(a).
711
+ - **Patent Disclosures Hub:** [privacyscrubber.com/patents/](https://privacyscrubber.com/patents/) (formal verification via counsel NDA).
713
712
 
714
713
  ### ๐ŸŒ Internet Engineering Task Force (IETF) Specification
715
714
  - **Specification Title:** *The Zero-Trust Data Sanitization (ZTDS) Protocol for Frontier Artificial Intelligence Ingestion*
package/browser.js CHANGED
@@ -549,84 +549,230 @@ export class PrivacyScrubberEngine {
549
549
  }
550
550
 
551
551
  /**
552
- * Browser & Web Worker Streaming Detokenizer.
552
+ * Helper to escape special regex characters.
553
553
  */
554
- export class StreamingDetokenizer {
555
- constructor(tokenMap = {}, options = {}) {
556
- this.tokenMap = { ...tokenMap };
557
- this.buffer = '';
558
- this.onTokenRestored = typeof options.onTokenRestored === 'function' ? options.onTokenRestored : null;
559
- this.maxTokenLength = Math.max(
560
- 32,
561
- ...Object.keys(this.tokenMap).map(k => k.length + 8)
562
- );
563
- }
554
+ function escapeRegex(s) {
555
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
556
+ }
564
557
 
565
- updateTokenMap(newTokenMap) {
566
- Object.assign(this.tokenMap, newTokenMap);
567
- this.maxTokenLength = Math.max(
568
- this.maxTokenLength,
569
- ...Object.keys(this.tokenMap).map(k => k.length + 8)
570
- );
558
+ /**
559
+ * Builds an ultra-fast streaming token replacement function.
560
+ * Preserves all whitespace, indentation, newlines, and code blocks (does NOT trim).
561
+ * Handles descending key ordering (e.g. [NAME_10] before [NAME_1]), case insensitivity,
562
+ * alternative delimiters (<...>, {{...}}, __...__), and possessive suffixes.
563
+ *
564
+ * @param {Record<string, string>} tokenMap
565
+ * @param {Object} [options]
566
+ * @returns {(text: string) => string}
567
+ */
568
+ export function buildStreamReplacer(tokenMap = {}, options = {}) {
569
+ if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
570
+ return (text) => text || '';
571
+ }
572
+
573
+ const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
574
+ const lookup = new Map();
575
+ const patterns = [];
576
+ const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
577
+
578
+ for (const k of sortedKeys) {
579
+ const val = tokenMap[k];
580
+ const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
581
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
582
+
583
+ if (match) {
584
+ const label = escapeRegex(match[1]);
585
+ const idx = match[2];
586
+ const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
587
+ patterns.push(patternStr);
588
+ const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
589
+ lookup.set(normKey, val);
590
+ lookup.set(k, val);
591
+ lookup.set(k.toUpperCase(), val);
592
+ } else {
593
+ const safe = escapeRegex(k);
594
+ patterns.push(safe);
595
+ lookup.set(k, val);
596
+ lookup.set(k.toUpperCase(), val);
597
+ }
571
598
  }
572
599
 
573
- process(chunk) {
574
- if (!chunk) return '';
575
- this.buffer += chunk;
576
- let output = '';
577
-
578
- while (true) {
579
- const openIdx = this.buffer.search(/\[|\<|\{\{|\_\_/);
580
- if (openIdx === -1) {
581
- output += this.buffer;
582
- this.buffer = '';
583
- break;
584
- }
600
+ const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|โ€™s|s)?', 'gi');
585
601
 
586
- output += this.buffer.slice(0, openIdx);
587
- this.buffer = this.buffer.slice(openIdx);
602
+ return function replaceTokens(text) {
603
+ if (!text) return '';
604
+ unifiedRegex.lastIndex = 0;
605
+ return text.replace(unifiedRegex, (match, suffix) => {
606
+ const isPossessive = suffix && /('s|โ€™s)$/i.test(suffix);
607
+ const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
608
+ let restored = null;
588
609
 
589
- const closeIdx = this.buffer.search(/\]|\>|\}\}|\_\_/);
590
- if (closeIdx === -1) {
591
- if (this.buffer.length > this.maxTokenLength) {
592
- output += this.buffer.slice(0, this.maxTokenLength);
593
- this.buffer = this.buffer.slice(this.maxTokenLength);
594
- continue;
610
+ if (lookup.has(coreMatch)) {
611
+ restored = lookup.get(coreMatch);
612
+ } else if (lookup.has(coreMatch.toUpperCase())) {
613
+ restored = lookup.get(coreMatch.toUpperCase());
614
+ } else {
615
+ const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
616
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
617
+ if (m) {
618
+ const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
619
+ if (lookup.has(normKey)) {
620
+ restored = lookup.get(normKey);
621
+ }
595
622
  }
596
- break;
597
623
  }
598
624
 
599
- const candidateToken = this.buffer.slice(0, closeIdx + 1);
600
- const originalValue = this.tokenMap[candidateToken];
625
+ if (restored !== null) {
626
+ if (onTokenRestored) {
627
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
628
+ }
629
+ return restored + (isPossessive ? "'s" : '');
630
+ }
601
631
 
602
- if (originalValue !== undefined) {
603
- output += originalValue;
604
- if (this.onTokenRestored) {
605
- this.onTokenRestored({ token: candidateToken, originalValue });
632
+ if (lookup.has(match)) {
633
+ restored = lookup.get(match);
634
+ if (onTokenRestored) {
635
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
606
636
  }
607
- this.buffer = this.buffer.slice(closeIdx + 1);
608
- } else {
609
- output += candidateToken[0];
610
- this.buffer = this.buffer.slice(1);
637
+ return restored;
611
638
  }
639
+
640
+ return match;
641
+ });
642
+ };
643
+ }
644
+
645
+ /**
646
+ * Browser & Web Worker Streaming Detokenizer with sliding window buffer for LLM chunk boundary splits.
647
+ * Seamlessly restores split tokens (e.g. [EM + AIL_1]), preserves multi-byte UTF-8,
648
+ * and maintains zero-latency streaming throughput.
649
+ */
650
+ export class StreamingDetokenizer {
651
+ constructor(tokenMap = {}, options = {}) {
652
+ this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
653
+ this.options = options || {};
654
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
655
+ this.pendingBuffer = '';
656
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
657
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
658
+ this.decoder = new TextDecoder('utf-8', { fatal: false });
659
+ }
660
+
661
+ /**
662
+ * Backward-compatible buffer property getter/setter.
663
+ */
664
+ get buffer() {
665
+ return this.pendingBuffer;
666
+ }
667
+
668
+ set buffer(val) {
669
+ this.pendingBuffer = typeof val === 'string' ? val : '';
670
+ }
671
+
672
+ /**
673
+ * Dynamically updates the active token map for this streaming session.
674
+ * @param {Record<string, string>} newMap
675
+ */
676
+ updateTokenMap(newMap) {
677
+ if (newMap && typeof newMap === 'object') {
678
+ this.tokenMap = newMap;
679
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
680
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
681
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Resets the internal sliding window buffer.
687
+ */
688
+ reset() {
689
+ this.pendingBuffer = '';
690
+ }
691
+
692
+ /**
693
+ * Feeds an incoming chunk (string, Uint8Array, or Buffer) and returns de-tokenized text.
694
+ * Buffers any unclosed token candidates at chunk boundaries until closed or flushed.
695
+ * @param {string | Uint8Array | Buffer} chunk
696
+ * @returns {string}
697
+ */
698
+ feed(chunk) {
699
+ if (chunk === undefined || chunk === null) return '';
700
+ let text = '';
701
+ if (typeof chunk === 'string') {
702
+ text = chunk;
703
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
704
+ text = this.decoder.decode(chunk, { stream: true });
705
+ } else {
706
+ text = String(chunk);
707
+ }
708
+
709
+ if (!text) return '';
710
+
711
+ // Dynamically recompile if tokens were added to the referenced map
712
+ const currentTokenCount = Object.keys(this.tokenMap).length;
713
+ if (currentTokenCount !== this._lastTokenCount) {
714
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
715
+ this.hasTokens = currentTokenCount > 0;
716
+ this._lastTokenCount = currentTokenCount;
717
+ }
718
+
719
+ if (!this.hasTokens) return text;
720
+
721
+ this.pendingBuffer += text;
722
+
723
+ // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
724
+ // Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
725
+ const tailLen = 160;
726
+ const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
727
+ const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
728
+ const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-ะ-ะฏะฐ-ัะั‘ ]{0,128}$/);
729
+
730
+ if (m) {
731
+ const candidate = m[0];
732
+ // Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
733
+ // Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
734
+ const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
735
+ if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
736
+ const toEmit = this.pendingBuffer;
737
+ this.pendingBuffer = '';
738
+ return this.replacer(toEmit);
739
+ }
740
+ const splitIdx = offset + m.index;
741
+ const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
742
+ this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
743
+ return safeToEmit ? this.replacer(safeToEmit) : '';
612
744
  }
613
745
 
614
- return output;
746
+ const toEmit = this.pendingBuffer;
747
+ this.pendingBuffer = '';
748
+ return this.replacer(toEmit);
615
749
  }
616
750
 
751
+ /**
752
+ * Alias for feed() to maintain backwards compatibility with process() callers.
753
+ */
754
+ process(chunk) {
755
+ return this.feed(chunk);
756
+ }
757
+
758
+ /**
759
+ * Flushes remaining buffered tokens and finalizes UTF-8 stream decoder.
760
+ * @returns {string}
761
+ */
617
762
  flush() {
618
- let output = '';
619
- if (this.buffer) {
620
- let flushed = this.buffer;
621
- for (const [token, original] of Object.entries(this.tokenMap)) {
622
- if (flushed.includes(token)) {
623
- flushed = flushed.split(token).join(original);
624
- }
625
- }
626
- output = flushed;
627
- this.buffer = '';
763
+ let text = '';
764
+ try {
765
+ text = this.decoder.decode(new Uint8Array(), { stream: false });
766
+ } catch (_) {}
767
+
768
+ if (text) {
769
+ this.pendingBuffer += text;
628
770
  }
629
- return output;
771
+
772
+ if (!this.pendingBuffer) return '';
773
+ const remaining = this.pendingBuffer;
774
+ this.pendingBuffer = '';
775
+ return this.replacer(remaining);
630
776
  }
631
777
  }
632
778
 
@@ -682,11 +828,6 @@ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
682
828
  };
683
829
  }
684
830
 
685
- export function buildStreamReplacer(tokenMap = {}) {
686
- const detok = new StreamingDetokenizer(tokenMap);
687
- return (chunk) => detok.process(chunk);
688
- }
689
-
690
831
  export const scrubText = scrubberPkg ? scrubberPkg.scrubText : null;
691
832
  export const unscrubText = scrubberPkg ? scrubberPkg.unscrubText : null;
692
833
 
package/cli.js CHANGED
@@ -14,6 +14,8 @@ import path from 'node:path';
14
14
  import crypto from 'node:crypto';
15
15
  import http from 'node:http';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { createRequire } from 'node:module';
18
+ const require = createRequire(import.meta.url);
17
19
  import { sanitize, restore, validateLicense, generateProcurementMemo, StreamingDetokenizer } from './index.js';
18
20
 
19
21
  const __filename = fileURLToPath(import.meta.url);
@@ -54,6 +56,28 @@ if (args.includes('memo') || args.includes('--memo') || args.includes('--procure
54
56
  process.exit(0);
55
57
  }
56
58
 
59
+ // โ”€โ”€ MCP Mode: Launch Standard Model Context Protocol Server over stdio โ”€โ”€
60
+ if (args.includes('mcp') || args.includes('--stdio-mcp') || args.includes('--mcp')) {
61
+ const candidateMcpPaths = [
62
+ path.resolve(__dirname, '../mcp-server/index.js'),
63
+ path.resolve(__dirname, './mcp-server/index.js'),
64
+ path.resolve(process.cwd(), 'mcp-server/index.js')
65
+ ];
66
+ const mcpServerPath = candidateMcpPaths.find(p => fs.existsSync(p));
67
+ if (mcpServerPath) {
68
+ const { spawn } = await import('node:child_process');
69
+ const forwardedArgs = args.filter(a => !['mcp', '--stdio-mcp', '--mcp'].includes(a));
70
+ const child = spawn(process.execPath, [mcpServerPath, ...forwardedArgs], {
71
+ stdio: 'inherit'
72
+ });
73
+ child.on('exit', code => process.exit(code || 0));
74
+ await new Promise(() => {});
75
+ } else {
76
+ console.error('[PrivacyScrubber CLI Error] MCP Server runtime not found in local workspace.');
77
+ process.exit(1);
78
+ }
79
+ }
80
+
57
81
  // โ”€โ”€ Sidecar / Server Mode: Lightweight Local HTTP & Unix Socket Daemon โ”€โ”€
58
82
  if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('serve') || args.includes('--serve')) {
59
83
  let port = 3100;
@@ -279,9 +303,12 @@ Commands:
279
303
  memo Generate executive CISO & DPO procurement memo
280
304
  demo Run live in-memory benchmark (<0.1ms latency check)
281
305
  sidecar, serve Run zero-dependency local HTTP/Unix daemon (<1ms in-memory)
306
+ mcp, --stdio-mcp Launch standard Model Context Protocol (MCP) server over stdio
282
307
 
283
308
  Options:
284
309
  --verify-ci Generate CI/CD PR verification stamp (audit receipt)
310
+ --format <fmt> Output format: text, json, sarif (OASIS SARIF 2.1.0 for GitHub/GitLab)
311
+ --config, -c <file> Load configuration file (.privacyscrubber.json / ztds.config.json)
285
312
  --json Output structured JSON (scrubbedText, tokenMap, telemetry)
286
313
  --restore, --unscrub Restore masked tokens in text back to original values
287
314
  --stream Stream de-tokenization chunks in real-time from stdin
@@ -308,20 +335,53 @@ Commercial Licensing:
308
335
  process.exit(0);
309
336
  }
310
337
 
338
+ // Config file discovery: --config, -c, or auto-discovery (.privacyscrubber.json / ztds.config.json)
339
+ let configFile = null;
340
+ const configIdx = args.indexOf('--config') !== -1 ? args.indexOf('--config') : args.indexOf('-c');
341
+ if (configIdx !== -1 && args[configIdx + 1]) {
342
+ configFile = args[configIdx + 1];
343
+ } else if (fs.existsSync(path.resolve(process.cwd(), '.privacyscrubber.json'))) {
344
+ configFile = path.resolve(process.cwd(), '.privacyscrubber.json');
345
+ } else if (fs.existsSync(path.resolve(process.cwd(), 'ztds.config.json'))) {
346
+ configFile = path.resolve(process.cwd(), 'ztds.config.json');
347
+ }
348
+
349
+ let loadedConfig = {};
350
+ if (configFile) {
351
+ try {
352
+ if (fs.existsSync(configFile)) {
353
+ loadedConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
354
+ } else {
355
+ console.error(`[PrivacyScrubber CLI Error] Config file not found: ${configFile}`);
356
+ process.exit(1);
357
+ }
358
+ } catch (err) {
359
+ console.error(`[PrivacyScrubber CLI Error] Failed to parse config file ${configFile}: ${err.message}`);
360
+ process.exit(1);
361
+ }
362
+ }
363
+
311
364
  // Parse command-line options
312
- let profile = 'Dev';
313
365
  const profIdx = args.indexOf('--profile');
314
- if (profIdx !== -1 && args[profIdx + 1]) {
315
- profile = args[profIdx + 1];
316
- }
366
+ let profile = profIdx !== -1 ? args[profIdx + 1] : (loadedConfig.profile || 'Dev');
317
367
 
318
368
  const keyIdx = args.indexOf('--key');
319
369
  if (keyIdx !== -1 && args[keyIdx + 1]) {
320
370
  process.env.PRIVACYSCRUBBER_KEY = args[keyIdx + 1];
371
+ } else if (loadedConfig.licenseKey || loadedConfig.key) {
372
+ process.env.PRIVACYSCRUBBER_KEY = loadedConfig.licenseKey || loadedConfig.key;
321
373
  }
322
374
 
375
+ const formatIdx = args.indexOf('--format');
376
+ let outputFormat = 'text';
377
+ if (formatIdx !== -1 && args[formatIdx + 1]) {
378
+ outputFormat = args[formatIdx + 1].toLowerCase();
379
+ } else if (loadedConfig.format) {
380
+ outputFormat = String(loadedConfig.format).toLowerCase();
381
+ }
382
+ const isSarif = outputFormat === 'sarif' || args.includes('--sarif');
323
383
  const isVerifyCi = args.includes('--verify-ci');
324
- const isJson = args.includes('--json');
384
+ const isJson = args.includes('--json') || outputFormat === 'json';
325
385
  const isRestore = args.includes('--restore') || args.includes('--unscrub');
326
386
  const isStream = args.includes('--stream');
327
387
 
@@ -343,14 +403,16 @@ if (mapIdx !== -1 && args[mapIdx + 1]) {
343
403
 
344
404
  // Locate input argument (first positional parameter that is not a flag or flag value)
345
405
  const skipIndices = new Set();
406
+ if (configIdx !== -1) { skipIndices.add(configIdx); skipIndices.add(configIdx + 1); }
346
407
  if (profIdx !== -1) { skipIndices.add(profIdx); skipIndices.add(profIdx + 1); }
347
408
  if (keyIdx !== -1) { skipIndices.add(keyIdx); skipIndices.add(keyIdx + 1); }
348
409
  if (mapIdx !== -1) { skipIndices.add(mapIdx); skipIndices.add(mapIdx + 1); }
410
+ if (formatIdx !== -1) { skipIndices.add(formatIdx); skipIndices.add(formatIdx + 1); }
349
411
 
350
412
  let inputArg = null;
351
413
  for (let i = 0; i < args.length; i++) {
352
414
  if (skipIndices.has(i)) continue;
353
- if (args[i] === '--verify-ci' || args[i] === '--json' || args[i] === '--restore' || args[i] === '--unscrub') continue;
415
+ if (args[i] === '--verify-ci' || args[i] === '--json' || args[i] === '--sarif' || args[i] === '--restore' || args[i] === '--unscrub') continue;
354
416
  if (args[i].startsWith('-')) continue;
355
417
  inputArg = args[i];
356
418
  break;
@@ -373,7 +435,11 @@ function handleExecution(text) {
373
435
  process.exit(0);
374
436
  }
375
437
 
376
- const { scrubbedText, tokenMap: resMap, telemetry } = sanitize(text, { profile, detectSecrets: true });
438
+ const { scrubbedText, tokenMap: resMap, telemetry } = sanitize(text, {
439
+ profile,
440
+ detectSecrets: loadedConfig.detectSecrets !== false,
441
+ customRules: loadedConfig.customRules || []
442
+ });
377
443
  const maskedCount = Object.keys(resMap || {}).length;
378
444
  const hash = crypto.createHash('sha256').update(scrubbedText).digest('hex').slice(0, 16);
379
445
 
@@ -389,14 +455,26 @@ function handleExecution(text) {
389
455
  }
390
456
 
391
457
  if (!text) {
392
- if (isJson) {
458
+ if (isSarif) {
459
+ const { emitSarif } = require('./sarif.js');
460
+ console.log(emitSarif([], { pretty: true }));
461
+ } else if (isJson) {
393
462
  console.log(JSON.stringify({ scrubbedText: '', tokenMap: {}, count: 0 }, null, 2));
394
463
  }
395
464
  process.exit(0);
396
465
  }
397
466
 
398
- const res = sanitize(text, { profile, detectSecrets: true });
399
- if (isJson) {
467
+ const res = sanitize(text, {
468
+ profile,
469
+ detectSecrets: loadedConfig.detectSecrets !== false,
470
+ customRules: loadedConfig.customRules || []
471
+ });
472
+ if (isSarif) {
473
+ const { emitSarif, sanitizeResultToFindings } = require('./sarif.js');
474
+ const findings = sanitizeResultToFindings(res, text, inputArg || 'stdin');
475
+ const sarifOutput = emitSarif(findings, { pretty: true });
476
+ process.stdout.write(sarifOutput + '\n');
477
+ } else if (isJson) {
400
478
  console.log(JSON.stringify({
401
479
  scrubbedText: res.scrubbedText,
402
480
  tokenMap: res.tokenMap,
package/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export type PiiProfile =
26
26
  | 'Compliance'
27
27
  | 'CCPA'
28
28
  | 'Engineering'
29
+ | 'Manufacturing'
30
+ | 'Industrial'
29
31
  | 'Agents'
30
32
  | 'Academic'
31
33
  | 'Creative'