@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 +7 -8
- package/browser.js +206 -65
- package/cli.js +88 -10
- package/index.d.ts +2 -0
- package/index.js +62 -19
- package/package.json +1 -1
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://privacyscrubber.com)
|
|
7
7
|
[](https://www.npmjs.com/package/@privacyscrubber/sdk)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[-06b6d4.svg)](https://privacyscrubber.com/patents/)
|
|
10
10
|
[](https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/)
|
|
11
11
|
[](https://ztds.ai/registry/#privacyscrubber-sdk)
|
|
12
12
|
[](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**:
|
|
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 &
|
|
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
|
-
- **
|
|
708
|
-
- **
|
|
709
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
*
|
|
552
|
+
* Helper to escape special regex characters.
|
|
553
553
|
*/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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 (
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
632
|
+
if (lookup.has(match)) {
|
|
633
|
+
restored = lookup.get(match);
|
|
634
|
+
if (onTokenRestored) {
|
|
635
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
606
636
|
}
|
|
607
|
-
|
|
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
|
-
|
|
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
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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 (
|
|
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, {
|
|
399
|
-
|
|
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,
|