@privacyscrubber/sdk 2.2.10 → 2.3.3
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 +260 -70
- package/cli.js +64 -12
- package/index.d.ts +19 -0
- package/index.js +181 -25
- 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
|
@@ -125,29 +125,77 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
125
125
|
frameworksSet.add('GDPR (Art. 4)');
|
|
126
126
|
frameworksSet.add('CCPA/CPRA');
|
|
127
127
|
}
|
|
128
|
-
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT')) {
|
|
128
|
+
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
129
129
|
frameworksSet.add('SOC 2 Type II');
|
|
130
130
|
frameworksSet.add('ISO 27001 (A.8.11)');
|
|
131
131
|
}
|
|
132
|
-
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY'].includes(t))) {
|
|
132
|
+
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
133
133
|
frameworksSet.add('PCI DSS v4.0');
|
|
134
|
+
frameworksSet.add('IRC § 7216 & GLBA');
|
|
134
135
|
}
|
|
135
|
-
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT'].includes(t))) {
|
|
136
|
+
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
136
137
|
frameworksSet.add('HIPAA §164.514');
|
|
137
138
|
}
|
|
138
|
-
if (types.some(t => ['
|
|
139
|
+
if (types.some(t => ['CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
140
|
+
frameworksSet.add('FRE 502 (Privilege)');
|
|
141
|
+
}
|
|
142
|
+
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN', 'CREDENTIAL'].includes(t))) {
|
|
139
143
|
frameworksSet.add('NIST SP 800-53');
|
|
140
144
|
}
|
|
145
|
+
if (totalCount > 0) {
|
|
146
|
+
frameworksSet.add('EU AI Act (Art. 50)');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ipHoldings = {
|
|
150
|
+
standard: 'IETF draft-sibiryakov-ztds-protocol-00',
|
|
151
|
+
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
152
|
+
patent: 'IL 331905 (Tracking: 94221)',
|
|
153
|
+
wipoDas: 'B17B',
|
|
154
|
+
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const statutoryCitations = [
|
|
158
|
+
{
|
|
159
|
+
statute: 'EU AI Act (Art. 50)',
|
|
160
|
+
scope: 'AI Transparency & Data Minimization',
|
|
161
|
+
rule: '0-byte external network transmission; prevents unauthorized ingestion into frontier models.'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
165
|
+
scope: 'Pseudonymization & Processor Liability',
|
|
166
|
+
rule: 'Volatile client-side tokenization eliminates GDPR Article 28 data processor liability.'
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
statute: 'HIPAA 45 CFR § 164.514(b)',
|
|
170
|
+
scope: 'Safe Harbor De-Identification',
|
|
171
|
+
rule: 'Deterministic removal of all 18 PHI identifiers prior to AI analysis.'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
statute: 'IRC § 7216 & IRS Pub. 1075',
|
|
175
|
+
scope: 'Tax Return Information Confidentiality',
|
|
176
|
+
rule: 'Strict criminal penalty safeguard against unauthorized disclosure of tax records to cloud LLMs.'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
statute: 'FRE 502',
|
|
180
|
+
scope: 'Attorney-Client Privilege Preservation',
|
|
181
|
+
rule: 'Automated in-memory redaction prevents inadvertent waiver of legal privilege in AI workflows.'
|
|
182
|
+
}
|
|
183
|
+
];
|
|
141
184
|
|
|
142
185
|
return {
|
|
143
186
|
totalCount,
|
|
144
187
|
entities,
|
|
145
188
|
types,
|
|
146
189
|
riskLevel,
|
|
147
|
-
frameworksList: Array.from(frameworksSet)
|
|
190
|
+
frameworksList: Array.from(frameworksSet),
|
|
191
|
+
statutoryCitations,
|
|
192
|
+
ipHoldings,
|
|
193
|
+
multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
|
|
148
194
|
};
|
|
149
195
|
}
|
|
150
196
|
|
|
197
|
+
export const buildCisoAuditTelemetry = buildAuditTelemetry;
|
|
198
|
+
|
|
151
199
|
export function formatAuditReceipt(telemetry) {
|
|
152
200
|
if (!telemetry || telemetry.totalCount === 0) {
|
|
153
201
|
return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
|
|
@@ -501,84 +549,230 @@ export class PrivacyScrubberEngine {
|
|
|
501
549
|
}
|
|
502
550
|
|
|
503
551
|
/**
|
|
504
|
-
*
|
|
552
|
+
* Helper to escape special regex characters.
|
|
505
553
|
*/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.buffer = '';
|
|
510
|
-
this.onTokenRestored = typeof options.onTokenRestored === 'function' ? options.onTokenRestored : null;
|
|
511
|
-
this.maxTokenLength = Math.max(
|
|
512
|
-
32,
|
|
513
|
-
...Object.keys(this.tokenMap).map(k => k.length + 8)
|
|
514
|
-
);
|
|
515
|
-
}
|
|
554
|
+
function escapeRegex(s) {
|
|
555
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
556
|
+
}
|
|
516
557
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
}
|
|
523
598
|
}
|
|
524
599
|
|
|
525
|
-
|
|
526
|
-
if (!chunk) return '';
|
|
527
|
-
this.buffer += chunk;
|
|
528
|
-
let output = '';
|
|
529
|
-
|
|
530
|
-
while (true) {
|
|
531
|
-
const openIdx = this.buffer.search(/\[|\<|\{\{|\_\_/);
|
|
532
|
-
if (openIdx === -1) {
|
|
533
|
-
output += this.buffer;
|
|
534
|
-
this.buffer = '';
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
600
|
+
const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
|
|
537
601
|
|
|
538
|
-
|
|
539
|
-
|
|
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;
|
|
540
609
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
622
|
}
|
|
548
|
-
break;
|
|
549
623
|
}
|
|
550
624
|
|
|
551
|
-
|
|
552
|
-
|
|
625
|
+
if (restored !== null) {
|
|
626
|
+
if (onTokenRestored) {
|
|
627
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
628
|
+
}
|
|
629
|
+
return restored + (isPossessive ? "'s" : '');
|
|
630
|
+
}
|
|
553
631
|
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
|
|
632
|
+
if (lookup.has(match)) {
|
|
633
|
+
restored = lookup.get(match);
|
|
634
|
+
if (onTokenRestored) {
|
|
635
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
558
636
|
}
|
|
559
|
-
|
|
560
|
-
} else {
|
|
561
|
-
output += candidateToken[0];
|
|
562
|
-
this.buffer = this.buffer.slice(1);
|
|
637
|
+
return restored;
|
|
563
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;
|
|
564
717
|
}
|
|
565
718
|
|
|
566
|
-
return
|
|
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) : '';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const toEmit = this.pendingBuffer;
|
|
747
|
+
this.pendingBuffer = '';
|
|
748
|
+
return this.replacer(toEmit);
|
|
567
749
|
}
|
|
568
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
|
+
*/
|
|
569
762
|
flush() {
|
|
570
|
-
let
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
output = flushed;
|
|
579
|
-
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;
|
|
580
770
|
}
|
|
581
|
-
|
|
771
|
+
|
|
772
|
+
if (!this.pendingBuffer) return '';
|
|
773
|
+
const remaining = this.pendingBuffer;
|
|
774
|
+
this.pendingBuffer = '';
|
|
775
|
+
return this.replacer(remaining);
|
|
582
776
|
}
|
|
583
777
|
}
|
|
584
778
|
|
|
@@ -634,11 +828,6 @@ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
|
|
|
634
828
|
};
|
|
635
829
|
}
|
|
636
830
|
|
|
637
|
-
export function buildStreamReplacer(tokenMap = {}) {
|
|
638
|
-
const detok = new StreamingDetokenizer(tokenMap);
|
|
639
|
-
return (chunk) => detok.process(chunk);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
831
|
export const scrubText = scrubberPkg ? scrubberPkg.scrubText : null;
|
|
643
832
|
export const unscrubText = scrubberPkg ? scrubberPkg.unscrubText : null;
|
|
644
833
|
|
|
@@ -665,6 +854,7 @@ export default {
|
|
|
665
854
|
validateLicense,
|
|
666
855
|
validateLicenseKey,
|
|
667
856
|
buildAuditTelemetry,
|
|
857
|
+
buildCisoAuditTelemetry,
|
|
668
858
|
formatAuditReceipt,
|
|
669
859
|
PrivacyScrubberEngine,
|
|
670
860
|
PrivacyScrubberLicenseError,
|
package/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ 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 { sanitize, restore, generateProcurementMemo, StreamingDetokenizer } from './index.js';
|
|
17
|
+
import { sanitize, restore, validateLicense, generateProcurementMemo, StreamingDetokenizer } from './index.js';
|
|
18
18
|
|
|
19
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
20
|
const __dirname = path.dirname(__filename);
|
|
@@ -61,17 +61,24 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
61
61
|
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
62
62
|
port = parseInt(args[portIdx + 1], 10);
|
|
63
63
|
}
|
|
64
|
+
let host = process.env.HOST || '127.0.0.1';
|
|
65
|
+
const hostIdx = args.indexOf('--host');
|
|
66
|
+
if (hostIdx !== -1 && args[hostIdx + 1]) {
|
|
67
|
+
host = args[hostIdx + 1];
|
|
68
|
+
}
|
|
64
69
|
let socketPath = null;
|
|
65
70
|
const sockIdx = args.indexOf('--socket');
|
|
66
71
|
if (sockIdx !== -1 && args[sockIdx + 1]) {
|
|
67
72
|
socketPath = args[sockIdx + 1];
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
const defaultEnvKey = process.env.PRIVACYSCRUBBER_KEY || process.env.PRIVACYSCRUBBER_LICENSE_KEY || process.env.ZTDS_LICENSE_KEY || '';
|
|
76
|
+
|
|
70
77
|
const server = http.createServer((req, res) => {
|
|
71
|
-
// CORS headers for local loopback
|
|
78
|
+
// CORS headers for local loopback & container IPC
|
|
72
79
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
73
80
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
74
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
81
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-License-Key');
|
|
75
82
|
|
|
76
83
|
if (req.method === 'OPTIONS') {
|
|
77
84
|
res.writeHead(204);
|
|
@@ -79,9 +86,46 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
79
86
|
return;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
const reqUrl = req.url ? req.url.split('?')[0] : '/';
|
|
90
|
+
|
|
91
|
+
// Extract authorization key from header if present
|
|
92
|
+
let headerKey = '';
|
|
93
|
+
const authHeader = req.headers['authorization'] || '';
|
|
94
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
95
|
+
headerKey = authHeader.slice(7).trim();
|
|
96
|
+
} else if (authHeader) {
|
|
97
|
+
headerKey = authHeader.trim();
|
|
98
|
+
}
|
|
99
|
+
const xKey = req.headers['x-license-key'] ? String(req.headers['x-license-key']).trim() : '';
|
|
100
|
+
const effectiveServerKey = headerKey || xKey || defaultEnvKey;
|
|
101
|
+
|
|
102
|
+
if (req.method === 'GET' && (reqUrl === '/' || reqUrl === '/health' || reqUrl === '/v1/health')) {
|
|
103
|
+
const lic = validateLicense(effectiveServerKey);
|
|
83
104
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
84
|
-
res.end(JSON.stringify({
|
|
105
|
+
res.end(JSON.stringify({
|
|
106
|
+
status: 'ok',
|
|
107
|
+
engine: 'ZTDS',
|
|
108
|
+
tier: lic.valid ? (lic.tier || 'SDK') : 'Community (Free Tier)',
|
|
109
|
+
licensed: !!lic.valid,
|
|
110
|
+
version: '2.2.6',
|
|
111
|
+
networkEgress: '0 bytes (100% in-memory RAM)',
|
|
112
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
113
|
+
memoryMb: (process.memoryUsage().heapUsed / (1024 * 1024)).toFixed(2)
|
|
114
|
+
}));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (req.method === 'GET' && (reqUrl === '/metrics' || reqUrl === '/v1/metrics')) {
|
|
119
|
+
const mem = process.memoryUsage();
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4' });
|
|
121
|
+
res.end(
|
|
122
|
+
`# HELP ztds_uptime_seconds Process uptime\n` +
|
|
123
|
+
`# TYPE ztds_uptime_seconds counter\n` +
|
|
124
|
+
`ztds_uptime_seconds ${Math.floor(process.uptime())}\n` +
|
|
125
|
+
`# HELP ztds_memory_heap_bytes Process heap usage\n` +
|
|
126
|
+
`# TYPE ztds_memory_heap_bytes gauge\n` +
|
|
127
|
+
`ztds_memory_heap_bytes ${mem.heapUsed}\n`
|
|
128
|
+
);
|
|
85
129
|
return;
|
|
86
130
|
}
|
|
87
131
|
|
|
@@ -99,9 +143,16 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
99
143
|
req.on('end', () => {
|
|
100
144
|
try {
|
|
101
145
|
const payload = bodyStr ? JSON.parse(bodyStr) : {};
|
|
102
|
-
|
|
146
|
+
const isScrub = reqUrl === '/scrub' || reqUrl === '/sanitize' || reqUrl === '/v1/scrub' || reqUrl === '/v1/sanitize';
|
|
147
|
+
const isUnscrub = reqUrl === '/unscrub' || reqUrl === '/restore' || reqUrl === '/v1/unscrub' || reqUrl === '/v1/restore';
|
|
148
|
+
|
|
149
|
+
if (isScrub) {
|
|
103
150
|
const text = payload.text || '';
|
|
104
151
|
const opts = payload.options || { profile: payload.profile || 'General', detectSecrets: true };
|
|
152
|
+
if (!opts.licenseKey) {
|
|
153
|
+
opts.licenseKey = payload.licenseKey || effectiveServerKey;
|
|
154
|
+
}
|
|
155
|
+
|
|
105
156
|
const start = process.hrtime.bigint();
|
|
106
157
|
const result = sanitize(text, opts);
|
|
107
158
|
const end = process.hrtime.bigint();
|
|
@@ -111,6 +162,7 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
111
162
|
res.end(JSON.stringify({
|
|
112
163
|
sanitizedText: result.scrubbedText,
|
|
113
164
|
tokenMap: result.tokenMap,
|
|
165
|
+
license: result.license,
|
|
114
166
|
telemetry: {
|
|
115
167
|
...result.telemetry,
|
|
116
168
|
latencyUs: durationUs,
|
|
@@ -120,7 +172,7 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
120
172
|
return;
|
|
121
173
|
}
|
|
122
174
|
|
|
123
|
-
if (
|
|
175
|
+
if (isUnscrub) {
|
|
124
176
|
const text = payload.text || '';
|
|
125
177
|
const tokenMap = payload.tokenMap || {};
|
|
126
178
|
const { restoredText, restoredCount } = restore(text, tokenMap);
|
|
@@ -130,10 +182,10 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
130
182
|
}
|
|
131
183
|
|
|
132
184
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
133
|
-
res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /scrub, /unscrub, /health' }));
|
|
185
|
+
res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /v1/scrub, /v1/unscrub, /v1/health, /v1/metrics' }));
|
|
134
186
|
} catch (err) {
|
|
135
187
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
136
|
-
res.end(JSON.stringify({ error: 'Invalid JSON payload', message: err.message }));
|
|
188
|
+
res.end(JSON.stringify({ error: 'Invalid JSON payload or processing error', message: err.message }));
|
|
137
189
|
}
|
|
138
190
|
});
|
|
139
191
|
return;
|
|
@@ -152,9 +204,9 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
|
|
|
152
204
|
console.log(`[ZTDS Sidecar] Ready for Python/Go local IPC with <1ms in-memory execution.`);
|
|
153
205
|
});
|
|
154
206
|
} else {
|
|
155
|
-
server.listen(port,
|
|
156
|
-
console.log(`[ZTDS Sidecar] HTTP Daemon listening on http
|
|
157
|
-
console.log(`[ZTDS Sidecar] Endpoints: POST /scrub | POST /unscrub | GET /health`);
|
|
207
|
+
server.listen(port, host, () => {
|
|
208
|
+
console.log(`[ZTDS Sidecar] HTTP Daemon listening on http://${host}:${port}`);
|
|
209
|
+
console.log(`[ZTDS Sidecar] Endpoints: POST /v1/scrub | POST /v1/unscrub | GET /v1/health | GET /v1/metrics`);
|
|
158
210
|
console.log(`[ZTDS Sidecar] Latency: <1.2ms (Zero network hops, local loopback)`);
|
|
159
211
|
});
|
|
160
212
|
}
|
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'
|
|
@@ -103,12 +105,28 @@ export interface LicenseMetadata {
|
|
|
103
105
|
recommendation?: string;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
export interface StatutoryCitation {
|
|
109
|
+
statute: string;
|
|
110
|
+
scope: string;
|
|
111
|
+
rule: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface IpHoldings {
|
|
115
|
+
standard: string;
|
|
116
|
+
standardUrl: string;
|
|
117
|
+
patent: string;
|
|
118
|
+
wipoDas: string;
|
|
119
|
+
trademark: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
export interface AuditTelemetry {
|
|
107
123
|
totalCount: number;
|
|
108
124
|
entities: Record<string, number>;
|
|
109
125
|
types: string[];
|
|
110
126
|
riskLevel: 'CLEAN (ZERO PII)' | 'LOW EXPOSURE' | 'MODERATE EXPOSURE' | 'CRITICAL (HIGH EXPOSURE)';
|
|
111
127
|
frameworksList: string[];
|
|
128
|
+
statutoryCitations?: StatutoryCitation[];
|
|
129
|
+
ipHoldings?: IpHoldings;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
export interface SanitizeResult {
|
|
@@ -160,6 +178,7 @@ export function validateLicense(key: string): LicenseValidationResult;
|
|
|
160
178
|
export function validateLicenseKey(key: string): 'PRO' | 'TEAMS' | 'SDK' | 'ENTERPRISE' | 'OEM' | null;
|
|
161
179
|
|
|
162
180
|
export function buildAuditTelemetry(currentTokenMap?: Record<string, string> | string[]): AuditTelemetry;
|
|
181
|
+
export const buildCisoAuditTelemetry: typeof buildAuditTelemetry;
|
|
163
182
|
export function formatAuditReceipt(telemetry: AuditTelemetry): string;
|
|
164
183
|
|
|
165
184
|
export interface AuditEventData {
|