@privacyscrubber/sdk 2.3.1 → 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 +206 -65
- 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/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -621,6 +621,7 @@ export function restore(aiResponse, tokenMap = {}) {
|
|
|
621
621
|
* Alias for restore.
|
|
622
622
|
*/
|
|
623
623
|
export const unscrub = restore;
|
|
624
|
+
export const desanitize = restore;
|
|
624
625
|
|
|
625
626
|
/**
|
|
626
627
|
* Recursively sanitizes all string values within an object or array.
|
|
@@ -1346,11 +1347,12 @@ function escapeRegex(s) {
|
|
|
1346
1347
|
* @param {Record<string, string>} tokenMap
|
|
1347
1348
|
* @returns {(text: string) => string}
|
|
1348
1349
|
*/
|
|
1349
|
-
export function buildStreamReplacer(tokenMap = {}) {
|
|
1350
|
+
export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
1350
1351
|
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
1351
1352
|
return (text) => text || '';
|
|
1352
1353
|
}
|
|
1353
1354
|
|
|
1355
|
+
const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
|
|
1354
1356
|
const lookup = new Map();
|
|
1355
1357
|
const patterns = [];
|
|
1356
1358
|
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
@@ -1385,24 +1387,39 @@ export function buildStreamReplacer(tokenMap = {}) {
|
|
|
1385
1387
|
return text.replace(unifiedRegex, (match, suffix) => {
|
|
1386
1388
|
const isPossessive = suffix && /('s|’s)$/i.test(suffix);
|
|
1387
1389
|
const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
|
|
1390
|
+
let restored = null;
|
|
1388
1391
|
|
|
1389
1392
|
if (lookup.has(coreMatch)) {
|
|
1390
|
-
|
|
1393
|
+
restored = lookup.get(coreMatch);
|
|
1394
|
+
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
1395
|
+
restored = lookup.get(coreMatch.toUpperCase());
|
|
1396
|
+
} else {
|
|
1397
|
+
const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
|
|
1398
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
1399
|
+
if (m) {
|
|
1400
|
+
const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
1401
|
+
if (lookup.has(normKey)) {
|
|
1402
|
+
restored = lookup.get(normKey);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1391
1405
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1406
|
+
|
|
1407
|
+
if (restored !== null) {
|
|
1408
|
+
if (onTokenRestored) {
|
|
1409
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
1410
|
+
}
|
|
1411
|
+
return restored + (isPossessive ? "'s" : '');
|
|
1394
1412
|
}
|
|
1395
1413
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (lookup.has(normKey)) {
|
|
1401
|
-
return lookup.get(normKey) + (isPossessive ? "'s" : '');
|
|
1414
|
+
if (lookup.has(match)) {
|
|
1415
|
+
restored = lookup.get(match);
|
|
1416
|
+
if (onTokenRestored) {
|
|
1417
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
1402
1418
|
}
|
|
1419
|
+
return restored;
|
|
1403
1420
|
}
|
|
1404
1421
|
|
|
1405
|
-
return
|
|
1422
|
+
return match;
|
|
1406
1423
|
});
|
|
1407
1424
|
};
|
|
1408
1425
|
}
|
|
@@ -1416,13 +1433,24 @@ export class StreamingDetokenizer {
|
|
|
1416
1433
|
constructor(tokenMap = {}, options = {}) {
|
|
1417
1434
|
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
1418
1435
|
this.options = options || {};
|
|
1419
|
-
this.replacer = buildStreamReplacer(this.tokenMap);
|
|
1436
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
1420
1437
|
this.pendingBuffer = '';
|
|
1421
1438
|
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
1422
1439
|
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
1423
1440
|
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
1424
1441
|
}
|
|
1425
1442
|
|
|
1443
|
+
/**
|
|
1444
|
+
* Backward-compatible buffer property getter/setter.
|
|
1445
|
+
*/
|
|
1446
|
+
get buffer() {
|
|
1447
|
+
return this.pendingBuffer;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
set buffer(val) {
|
|
1451
|
+
this.pendingBuffer = typeof val === 'string' ? val : '';
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1426
1454
|
/**
|
|
1427
1455
|
* Dynamically updates the active token map for this streaming session.
|
|
1428
1456
|
* @param {Record<string, string>} newMap
|
|
@@ -1430,7 +1458,7 @@ export class StreamingDetokenizer {
|
|
|
1430
1458
|
updateTokenMap(newMap) {
|
|
1431
1459
|
if (newMap && typeof newMap === 'object') {
|
|
1432
1460
|
this.tokenMap = newMap;
|
|
1433
|
-
this.replacer = buildStreamReplacer(this.tokenMap);
|
|
1461
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
1434
1462
|
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
1435
1463
|
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
1436
1464
|
}
|
|
@@ -1465,7 +1493,7 @@ export class StreamingDetokenizer {
|
|
|
1465
1493
|
// Dynamically recompile if tokens were added to the referenced map
|
|
1466
1494
|
const currentTokenCount = Object.keys(this.tokenMap).length;
|
|
1467
1495
|
if (currentTokenCount !== this._lastTokenCount) {
|
|
1468
|
-
this.replacer = buildStreamReplacer(this.tokenMap);
|
|
1496
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
1469
1497
|
this.hasTokens = currentTokenCount > 0;
|
|
1470
1498
|
this._lastTokenCount = currentTokenCount;
|
|
1471
1499
|
}
|
|
@@ -1474,12 +1502,26 @@ export class StreamingDetokenizer {
|
|
|
1474
1502
|
|
|
1475
1503
|
this.pendingBuffer += text;
|
|
1476
1504
|
|
|
1477
|
-
// Candidate unclosed token at tail of buffer (up to
|
|
1478
|
-
// Matches incomplete tokens starting with [, <, {{, or __
|
|
1479
|
-
const
|
|
1505
|
+
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
1506
|
+
// Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
|
|
1507
|
+
const tailLen = 160;
|
|
1508
|
+
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
1509
|
+
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
1510
|
+
const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
|
|
1511
|
+
|
|
1480
1512
|
if (m) {
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1513
|
+
const candidate = m[0];
|
|
1514
|
+
// Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
|
|
1515
|
+
// Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
|
|
1516
|
+
const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
|
|
1517
|
+
if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
|
|
1518
|
+
const toEmit = this.pendingBuffer;
|
|
1519
|
+
this.pendingBuffer = '';
|
|
1520
|
+
return this.replacer(toEmit);
|
|
1521
|
+
}
|
|
1522
|
+
const splitIdx = offset + m.index;
|
|
1523
|
+
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
1524
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
|
|
1483
1525
|
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
1484
1526
|
}
|
|
1485
1527
|
|
|
@@ -2489,6 +2531,7 @@ export default {
|
|
|
2489
2531
|
restore,
|
|
2490
2532
|
restoreObject,
|
|
2491
2533
|
unscrub,
|
|
2534
|
+
desanitize,
|
|
2492
2535
|
validateLicense,
|
|
2493
2536
|
validateLicenseKey,
|
|
2494
2537
|
buildAuditTelemetry,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "In-process consumer privacy defense and pre-emptive PII interception SDK for Node.js, Next.js, and AI pipelines. Zero data loss, zero third parties, sub-millisecond in-memory execution.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "browser.js",
|