@privacyscrubber/sdk 2.3.9 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -27
- package/benchmark.js +355 -0
- package/browser.js +276 -39
- package/cli.js +1 -1
- package/examples/python-bridge/MANIFEST.in +5 -0
- package/examples/python-bridge/README.md +265 -0
- package/examples/python-bridge/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/fastapi_example.py +206 -0
- package/examples/python-bridge/langchain_adapter.py +20 -0
- package/examples/python-bridge/langchain_example.py +126 -0
- package/examples/python-bridge/llamaindex_example.py +103 -0
- package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/privacyscrubber/cli.py +138 -0
- package/examples/python-bridge/privacyscrubber/client.py +548 -0
- package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
- package/examples/python-bridge/privacyscrubber/types.py +52 -0
- package/examples/python-bridge/privacyscrubber.py +36 -0
- package/examples/python-bridge/pyproject.toml +62 -0
- package/examples/python-bridge/setup.py +66 -0
- package/examples/python-bridge/test_python_bridge.py +320 -0
- package/examples/vercel-ai-stream.js +195 -0
- package/fastmcp.cjs +289 -8
- package/fastmcp.d.ts +18 -0
- package/fastmcp.js +4 -0
- package/index.cjs +99 -42
- package/index.js +103 -46
- package/package.json +15 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +400 -0
- package/vercel-ai.d.ts +156 -0
- package/vercel-ai.js +36 -0
- package/vision.cjs +13 -5
- package/vision.js +13 -5
package/fastmcp.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @privacyscrubber/fastmcp-guard
|
|
3
3
|
* Zero-Trust Data Sanitization (ZTDS) In-Memory Middleware for FastMCP
|
|
4
|
-
* Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-
|
|
4
|
+
* Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-01
|
|
5
5
|
*
|
|
6
6
|
* Core Protocol Invariants:
|
|
7
7
|
* 1. Zero External Egress Prior to Sanitization
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const crypto = require("node:crypto");
|
|
15
15
|
|
|
16
16
|
const ZTDS_PATTERNS = {
|
|
17
|
-
API_SECRET: /\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{20,}|(?:ghp|gho|ghu|ghs|ghr|github_pat_)[A-Za-z0-9_]{
|
|
17
|
+
API_SECRET: /\b(?:sk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}|(?:ghp|gho|ghu|ghs|ghr|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{16,}|(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}|eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,})\b/g,
|
|
18
18
|
AWS_KEY: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g,
|
|
19
19
|
CREDIT_CARD: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
|
|
20
20
|
EMAIL: /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}/g,
|
|
@@ -24,7 +24,213 @@ const ZTDS_PATTERNS = {
|
|
|
24
24
|
SSN: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
const TOKEN_PATTERN =
|
|
27
|
+
const TOKEN_PATTERN = /(?:%5[Bb]|\\u005[Bb]|&(?:amp;)?#0*91;|\[|<|\{\{?|__|【|[)[*`_~\s]*([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)[*`_~.,;:!?\s]*(?:%5[Dd]|\\u005[Dd]|&(?:amp;)?#0*93;|\]|>|\}\}?|__|】|])/gi;
|
|
28
|
+
|
|
29
|
+
function escapeRegex(s) {
|
|
30
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
34
|
+
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
35
|
+
return (text) => text || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lookup = new Map();
|
|
39
|
+
const patterns = [];
|
|
40
|
+
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
41
|
+
|
|
42
|
+
for (const k of sortedKeys) {
|
|
43
|
+
const val = tokenMap[k];
|
|
44
|
+
const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
|
|
45
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]([a-zA-Z0-9]+)$/);
|
|
46
|
+
|
|
47
|
+
if (match) {
|
|
48
|
+
const label = escapeRegex(match[1]);
|
|
49
|
+
const idx = escapeRegex(match[2]);
|
|
50
|
+
const idxPat = /^\d+$/.test(match[2]) ? '0*' + idx : idx;
|
|
51
|
+
const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\s]*' + idxPat + '[*`_~.,;:!?\\s]*';
|
|
52
|
+
const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+' + idxPat + '\\b)';
|
|
53
|
+
patterns.push(patternStr);
|
|
54
|
+
const normSuffix = /^\d+$/.test(match[2]) ? String(parseInt(match[2], 10)) : match[2].toUpperCase();
|
|
55
|
+
const normKey = (match[1] + '_' + normSuffix).toUpperCase();
|
|
56
|
+
lookup.set(normKey, val);
|
|
57
|
+
lookup.set(k, val);
|
|
58
|
+
lookup.set(k.toUpperCase(), val);
|
|
59
|
+
} else {
|
|
60
|
+
const safe = escapeRegex(k);
|
|
61
|
+
patterns.push(safe);
|
|
62
|
+
lookup.set(k, val);
|
|
63
|
+
lookup.set(k.toUpperCase(), val);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
|
|
68
|
+
|
|
69
|
+
return function replaceTokens(text) {
|
|
70
|
+
if (!text) return '';
|
|
71
|
+
unifiedRegex.lastIndex = 0;
|
|
72
|
+
return text.replace(unifiedRegex, (match, suffix) => {
|
|
73
|
+
const isPossessive = suffix && /('s|’s)$/i.test(suffix);
|
|
74
|
+
const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
|
|
75
|
+
let restored = null;
|
|
76
|
+
|
|
77
|
+
if (lookup.has(coreMatch)) {
|
|
78
|
+
restored = lookup.get(coreMatch);
|
|
79
|
+
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
80
|
+
restored = lookup.get(coreMatch.toUpperCase());
|
|
81
|
+
} else {
|
|
82
|
+
const inner = coreMatch
|
|
83
|
+
.replace(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
|
|
84
|
+
.replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
|
|
85
|
+
.replace(/[*`_~.,;:!?\s]+/g, ' ')
|
|
86
|
+
.trim();
|
|
87
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]([a-zA-Z0-9]+)$/);
|
|
88
|
+
if (m) {
|
|
89
|
+
const normSuffix = /^\d+$/.test(m[2]) ? String(parseInt(m[2], 10)) : m[2].toUpperCase();
|
|
90
|
+
const normKey = (m[1] + '_' + normSuffix).toUpperCase();
|
|
91
|
+
if (lookup.has(normKey)) {
|
|
92
|
+
restored = lookup.get(normKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (restored !== null) {
|
|
98
|
+
return restored + (isPossessive ? "'s" : '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (lookup.has(match)) {
|
|
102
|
+
return lookup.get(match);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return match;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class StreamingDetokenizer {
|
|
111
|
+
constructor(tokenMap = {}, options = {}) {
|
|
112
|
+
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
113
|
+
this.options = options || {};
|
|
114
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
115
|
+
this.pendingBuffer = '';
|
|
116
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
117
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
118
|
+
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get buffer() {
|
|
122
|
+
return this.pendingBuffer;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set buffer(val) {
|
|
126
|
+
this.pendingBuffer = typeof val === 'string' ? val : '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
updateTokenMap(newMap) {
|
|
130
|
+
if (newMap && typeof newMap === 'object') {
|
|
131
|
+
this.tokenMap = newMap;
|
|
132
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
133
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
134
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
reset() {
|
|
139
|
+
this.pendingBuffer = '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
feed(chunk) {
|
|
143
|
+
if (chunk === undefined || chunk === null) return '';
|
|
144
|
+
let text = '';
|
|
145
|
+
if (typeof chunk === 'string') {
|
|
146
|
+
text = chunk;
|
|
147
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
148
|
+
text = this.decoder.decode(chunk, { stream: true });
|
|
149
|
+
} else {
|
|
150
|
+
text = String(chunk);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!text) return '';
|
|
154
|
+
|
|
155
|
+
const currentTokenCount = Object.keys(this.tokenMap).length;
|
|
156
|
+
if (currentTokenCount !== this._lastTokenCount) {
|
|
157
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
158
|
+
this.hasTokens = currentTokenCount > 0;
|
|
159
|
+
this._lastTokenCount = currentTokenCount;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!this.hasTokens) return text;
|
|
163
|
+
|
|
164
|
+
this.pendingBuffer += text;
|
|
165
|
+
|
|
166
|
+
const tailLen = 160;
|
|
167
|
+
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
168
|
+
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
169
|
+
const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
|
|
170
|
+
|
|
171
|
+
if (m) {
|
|
172
|
+
const candidate = m[0];
|
|
173
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
174
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
175
|
+
const toEmit = this.pendingBuffer;
|
|
176
|
+
this.pendingBuffer = '';
|
|
177
|
+
return this.replacer(toEmit);
|
|
178
|
+
}
|
|
179
|
+
const splitIdx = offset + m.index;
|
|
180
|
+
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
181
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
|
|
182
|
+
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const toEmit = this.pendingBuffer;
|
|
186
|
+
this.pendingBuffer = '';
|
|
187
|
+
return this.replacer(toEmit);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process(chunk) {
|
|
191
|
+
return this.feed(chunk);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
flush() {
|
|
195
|
+
let text = '';
|
|
196
|
+
try {
|
|
197
|
+
text = this.decoder.decode(new Uint8Array(), { stream: false });
|
|
198
|
+
} catch (_) {}
|
|
199
|
+
|
|
200
|
+
if (text) {
|
|
201
|
+
this.pendingBuffer += text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!this.pendingBuffer) return '';
|
|
205
|
+
const remaining = this.pendingBuffer;
|
|
206
|
+
this.pendingBuffer = '';
|
|
207
|
+
return this.replacer(remaining);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async* wrapStream(asyncIterable) {
|
|
211
|
+
for await (const chunk of asyncIterable) {
|
|
212
|
+
if (chunk === null || chunk === undefined) continue;
|
|
213
|
+
let text = '';
|
|
214
|
+
if (typeof chunk === 'string') {
|
|
215
|
+
text = chunk;
|
|
216
|
+
} else if (typeof chunk?.text === 'string') {
|
|
217
|
+
text = chunk.text;
|
|
218
|
+
} else if (typeof chunk?.content === 'string') {
|
|
219
|
+
text = chunk.content;
|
|
220
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
221
|
+
text = chunk;
|
|
222
|
+
} else {
|
|
223
|
+
text = String(chunk);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const emitted = this.feed(text);
|
|
227
|
+
if (emitted) yield emitted;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const finalChunk = this.flush();
|
|
231
|
+
if (finalChunk) yield finalChunk;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
28
234
|
|
|
29
235
|
class ZTDSFastMCPGuard {
|
|
30
236
|
constructor(options = {}) {
|
|
@@ -36,6 +242,7 @@ class ZTDSFastMCPGuard {
|
|
|
36
242
|
this.maxSessionTokens = options.maxSessionTokens || 5000;
|
|
37
243
|
this._sessionMaps = new Map();
|
|
38
244
|
this._entityMaps = new Map();
|
|
245
|
+
this._streamDetokenizers = new Map();
|
|
39
246
|
}
|
|
40
247
|
|
|
41
248
|
_generateToken(entityType, tokenMap) {
|
|
@@ -69,7 +276,9 @@ class ZTDSFastMCPGuard {
|
|
|
69
276
|
|
|
70
277
|
const tokenMap = this._sessionMaps.get(sessionId);
|
|
71
278
|
const entityMap = this._entityMaps.get(sessionId);
|
|
72
|
-
let sanitized = text
|
|
279
|
+
let sanitized = text
|
|
280
|
+
.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u00AD]/g, "")
|
|
281
|
+
.replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
|
|
73
282
|
|
|
74
283
|
for (const entityType of this.enabledEntities) {
|
|
75
284
|
const pattern = ZTDS_PATTERNS[entityType];
|
|
@@ -118,8 +327,9 @@ class ZTDSFastMCPGuard {
|
|
|
118
327
|
return text;
|
|
119
328
|
}
|
|
120
329
|
|
|
121
|
-
return text.replace(TOKEN_PATTERN, (match) => {
|
|
122
|
-
|
|
330
|
+
return text.replace(TOKEN_PATTERN, (match, entityType, tokenId) => {
|
|
331
|
+
const canonical = `[${entityType.toUpperCase()}_TOKEN_${tokenId}]`;
|
|
332
|
+
return tokenMap.get(canonical) || tokenMap.get(match) || match;
|
|
123
333
|
});
|
|
124
334
|
}
|
|
125
335
|
|
|
@@ -157,6 +367,65 @@ class ZTDSFastMCPGuard {
|
|
|
157
367
|
return val;
|
|
158
368
|
}
|
|
159
369
|
|
|
370
|
+
getStreamDetokenizer(sessionId) {
|
|
371
|
+
if (!this._streamDetokenizers.has(sessionId)) {
|
|
372
|
+
const mapProxy = this._sessionMaps.get(sessionId) || new Map();
|
|
373
|
+
const objMap = {};
|
|
374
|
+
for (const [k, v] of mapProxy.entries()) {
|
|
375
|
+
objMap[k] = v;
|
|
376
|
+
}
|
|
377
|
+
this._streamDetokenizers.set(sessionId, new StreamingDetokenizer(objMap));
|
|
378
|
+
} else {
|
|
379
|
+
const mapProxy = this._sessionMaps.get(sessionId);
|
|
380
|
+
if (mapProxy) {
|
|
381
|
+
const objMap = {};
|
|
382
|
+
for (const [k, v] of mapProxy.entries()) {
|
|
383
|
+
objMap[k] = v;
|
|
384
|
+
}
|
|
385
|
+
this._streamDetokenizers.get(sessionId).updateTokenMap(objMap);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return this._streamDetokenizers.get(sessionId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
feedStream(chunk, sessionId) {
|
|
392
|
+
return this.getStreamDetokenizer(sessionId).feed(chunk);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
flushStream(sessionId) {
|
|
396
|
+
const detok = this._streamDetokenizers.get(sessionId);
|
|
397
|
+
return detok ? detok.flush() : "";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async *restoreStream(asyncIterable, sessionId) {
|
|
401
|
+
const detok = this.getStreamDetokenizer(sessionId);
|
|
402
|
+
try {
|
|
403
|
+
for await (const chunk of asyncIterable) {
|
|
404
|
+
if (chunk === null || chunk === undefined) continue;
|
|
405
|
+
let text = "";
|
|
406
|
+
if (typeof chunk === "string") {
|
|
407
|
+
text = chunk;
|
|
408
|
+
} else if (typeof chunk?.text === "string") {
|
|
409
|
+
text = chunk.text;
|
|
410
|
+
} else if (typeof chunk?.content === "string") {
|
|
411
|
+
text = chunk.content;
|
|
412
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(chunk))) {
|
|
413
|
+
text = chunk;
|
|
414
|
+
} else {
|
|
415
|
+
text = String(chunk);
|
|
416
|
+
}
|
|
417
|
+
const emitted = detok.feed(text);
|
|
418
|
+
if (emitted) yield emitted;
|
|
419
|
+
}
|
|
420
|
+
const finalChunk = detok.flush();
|
|
421
|
+
if (finalChunk) yield finalChunk;
|
|
422
|
+
} finally {
|
|
423
|
+
if (this.autoZeroize) {
|
|
424
|
+
this.zeroizeSession(sessionId);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
160
429
|
wrapTool(toolName, handler) {
|
|
161
430
|
return async (args, context = {}) => {
|
|
162
431
|
const sessionId =
|
|
@@ -169,6 +438,7 @@ class ZTDSFastMCPGuard {
|
|
|
169
438
|
unmask: (val) => this.restoreObject(val, sessionId),
|
|
170
439
|
};
|
|
171
440
|
|
|
441
|
+
let isStreaming = false;
|
|
172
442
|
try {
|
|
173
443
|
let processedArgs = args;
|
|
174
444
|
if (this.sanitizeInputs && args !== undefined && args !== null) {
|
|
@@ -177,6 +447,11 @@ class ZTDSFastMCPGuard {
|
|
|
177
447
|
|
|
178
448
|
const rawResult = await handler(processedArgs, enrichedContext);
|
|
179
449
|
|
|
450
|
+
if (rawResult && typeof rawResult[Symbol.asyncIterator] === "function") {
|
|
451
|
+
isStreaming = true;
|
|
452
|
+
return this.restoreStream(rawResult, sessionId);
|
|
453
|
+
}
|
|
454
|
+
|
|
180
455
|
let finalResult = rawResult;
|
|
181
456
|
if (this.sanitizeOutputs && rawResult !== undefined && rawResult !== null) {
|
|
182
457
|
if (typeof rawResult === "string") {
|
|
@@ -211,7 +486,7 @@ class ZTDSFastMCPGuard {
|
|
|
211
486
|
|
|
212
487
|
const ztdsMeta = {
|
|
213
488
|
sessionId,
|
|
214
|
-
standard: "IETF draft-sibiryakov-ztds-protocol-
|
|
489
|
+
standard: "IETF draft-sibiryakov-ztds-protocol-01",
|
|
215
490
|
zeroEgress: true,
|
|
216
491
|
};
|
|
217
492
|
|
|
@@ -234,7 +509,7 @@ class ZTDSFastMCPGuard {
|
|
|
234
509
|
_ztds: ztdsMeta,
|
|
235
510
|
};
|
|
236
511
|
} finally {
|
|
237
|
-
if (this.autoZeroize) {
|
|
512
|
+
if (!isStreaming && this.autoZeroize) {
|
|
238
513
|
this.zeroizeSession(sessionId);
|
|
239
514
|
}
|
|
240
515
|
}
|
|
@@ -250,6 +525,10 @@ class ZTDSFastMCPGuard {
|
|
|
250
525
|
this._entityMaps.get(sessionId).clear();
|
|
251
526
|
this._entityMaps.delete(sessionId);
|
|
252
527
|
}
|
|
528
|
+
if (this._streamDetokenizers.has(sessionId)) {
|
|
529
|
+
this._streamDetokenizers.get(sessionId).reset();
|
|
530
|
+
this._streamDetokenizers.delete(sessionId);
|
|
531
|
+
}
|
|
253
532
|
}
|
|
254
533
|
}
|
|
255
534
|
|
|
@@ -285,6 +564,8 @@ module.exports = {
|
|
|
285
564
|
ZTDSFastMCPGuard,
|
|
286
565
|
ZTDS_PATTERNS,
|
|
287
566
|
TOKEN_PATTERN,
|
|
567
|
+
StreamingDetokenizer,
|
|
568
|
+
createStreamTokenRestorer: (tokenMap, options) => new StreamingDetokenizer(tokenMap, options),
|
|
288
569
|
createZTDSGuard,
|
|
289
570
|
ztdsGuardTool,
|
|
290
571
|
wrapExecute,
|
package/fastmcp.d.ts
CHANGED
|
@@ -26,6 +26,19 @@ export interface ZTDSContext {
|
|
|
26
26
|
export declare const ZTDS_PATTERNS: Record<string, RegExp>;
|
|
27
27
|
export declare const TOKEN_PATTERN: RegExp;
|
|
28
28
|
|
|
29
|
+
export declare class StreamingDetokenizer {
|
|
30
|
+
constructor(tokenMap?: Record<string, string>, options?: object);
|
|
31
|
+
buffer: string;
|
|
32
|
+
feed(chunk: string | Uint8Array | Buffer): string;
|
|
33
|
+
process(chunk: string | Uint8Array | Buffer): string;
|
|
34
|
+
flush(): string;
|
|
35
|
+
reset(): void;
|
|
36
|
+
updateTokenMap(newMap: Record<string, string>): void;
|
|
37
|
+
wrapStream<T = any>(asyncIterable: AsyncIterable<T>): AsyncGenerator<T>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare function createStreamTokenRestorer(tokenMap: Record<string, string>, options?: object): StreamingDetokenizer;
|
|
41
|
+
|
|
29
42
|
export declare class ZTDSFastMCPGuard {
|
|
30
43
|
constructor(options?: ZTDSGuardOptions);
|
|
31
44
|
enabledEntities: string[];
|
|
@@ -39,6 +52,11 @@ export declare class ZTDSFastMCPGuard {
|
|
|
39
52
|
sanitizeObject(val: unknown, sessionId: string): unknown;
|
|
40
53
|
restoreObject(val: unknown, sessionId: string): unknown;
|
|
41
54
|
|
|
55
|
+
getStreamDetokenizer(sessionId: string): StreamingDetokenizer;
|
|
56
|
+
feedStream(chunk: string, sessionId: string): string;
|
|
57
|
+
flushStream(sessionId: string): string;
|
|
58
|
+
restoreStream<T = any>(asyncIterable: AsyncIterable<T>, sessionId: string): AsyncGenerator<T>;
|
|
59
|
+
|
|
42
60
|
wrapTool<TArgs = unknown, TResult = unknown>(
|
|
43
61
|
toolName: string,
|
|
44
62
|
handler: (args: TArgs, context: ZTDSContext) => Promise<TResult>
|
package/fastmcp.js
CHANGED
|
@@ -11,6 +11,8 @@ const fastmcpGuard = require('./fastmcp.cjs');
|
|
|
11
11
|
export const ZTDSFastMCPGuard = fastmcpGuard.ZTDSFastMCPGuard;
|
|
12
12
|
export const ZTDS_PATTERNS = fastmcpGuard.ZTDS_PATTERNS;
|
|
13
13
|
export const TOKEN_PATTERN = fastmcpGuard.TOKEN_PATTERN;
|
|
14
|
+
export const StreamingDetokenizer = fastmcpGuard.StreamingDetokenizer;
|
|
15
|
+
export const createStreamTokenRestorer = fastmcpGuard.createStreamTokenRestorer;
|
|
14
16
|
export const createZTDSGuard = fastmcpGuard.createZTDSGuard;
|
|
15
17
|
export const ztdsGuardTool = fastmcpGuard.ztdsGuardTool;
|
|
16
18
|
export const wrapExecute = fastmcpGuard.wrapExecute;
|
|
@@ -20,6 +22,8 @@ export default {
|
|
|
20
22
|
ZTDSFastMCPGuard,
|
|
21
23
|
ZTDS_PATTERNS,
|
|
22
24
|
TOKEN_PATTERN,
|
|
25
|
+
StreamingDetokenizer,
|
|
26
|
+
createStreamTokenRestorer,
|
|
23
27
|
createZTDSGuard,
|
|
24
28
|
ztdsGuardTool,
|
|
25
29
|
wrapExecute,
|