@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/vercel-ai.cjs
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @privacyscrubber/sdk/vercel-ai
|
|
3
|
+
* Zero-Trust Data Sanitization (ZTDS) In-Memory Middleware for Vercel AI SDK
|
|
4
|
+
* Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-01
|
|
5
|
+
*
|
|
6
|
+
* Core Protocol Invariants:
|
|
7
|
+
* 1. Zero External Egress Prior to Sanitization
|
|
8
|
+
* 2. Deterministic Reversible Tokenization
|
|
9
|
+
* 3. Sub-millisecond In-Memory Sliding-Window Streaming Detokenization
|
|
10
|
+
* 4. Zero Network Dependencies (100% Local RAM Execution)
|
|
11
|
+
*
|
|
12
|
+
* Compatible with Vercel AI SDK v3/v4: streamText, generateText, wrapLanguageModel.
|
|
13
|
+
*
|
|
14
|
+
* https://privacyscrubber.com/sdk/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
PrivacyScrubberEngine,
|
|
19
|
+
StreamingDetokenizer,
|
|
20
|
+
buildStreamReplacer,
|
|
21
|
+
buildCisoAuditTelemetry
|
|
22
|
+
} = require('./index.cjs');
|
|
23
|
+
|
|
24
|
+
const TOKEN_MAP_SYMBOL = Symbol('ztds_token_map');
|
|
25
|
+
const tokenMapsWeakMap = new WeakMap();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sanitizes Vercel AI SDK prompt structures (array of messages, string, or multipart parts)
|
|
29
|
+
* in local RAM before model invocation.
|
|
30
|
+
*
|
|
31
|
+
* @param {any} prompt - Array of LanguageModelV1Prompt messages or string.
|
|
32
|
+
* @param {PrivacyScrubberEngine} engine - Initialized PrivacyScrubberEngine instance.
|
|
33
|
+
* @param {Record<string, string>} sessionTokenMap - Volatile map populated with detected tokens.
|
|
34
|
+
* @returns {any} Sanitized prompt with tokens replacing authentic PII.
|
|
35
|
+
*/
|
|
36
|
+
function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
37
|
+
if (!prompt) return prompt;
|
|
38
|
+
|
|
39
|
+
if (typeof prompt === 'string') {
|
|
40
|
+
const res = engine.sanitize(prompt, { existingSessionMap: sessionTokenMap });
|
|
41
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
42
|
+
return res.scrubbedText;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Array.isArray(prompt)) return prompt;
|
|
46
|
+
|
|
47
|
+
return prompt.map(msg => {
|
|
48
|
+
if (!msg || typeof msg !== 'object') return msg;
|
|
49
|
+
|
|
50
|
+
// Standard string content: { role: 'user', content: 'Contact john@example.com' }
|
|
51
|
+
if (typeof msg.content === 'string') {
|
|
52
|
+
const res = engine.sanitize(msg.content, { existingSessionMap: sessionTokenMap });
|
|
53
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
54
|
+
return { ...msg, content: res.scrubbedText };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Multipart content: { role: 'user', content: [{ type: 'text', text: '...' }] }
|
|
58
|
+
if (Array.isArray(msg.content)) {
|
|
59
|
+
const newContent = msg.content.map(part => {
|
|
60
|
+
if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
61
|
+
const res = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap });
|
|
62
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
63
|
+
return { ...part, text: res.scrubbedText };
|
|
64
|
+
}
|
|
65
|
+
if (part && part.type === 'tool-result' && typeof part.result === 'string') {
|
|
66
|
+
const res = engine.sanitize(part.result, { existingSessionMap: sessionTokenMap });
|
|
67
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
68
|
+
return { ...part, result: res.scrubbedText };
|
|
69
|
+
}
|
|
70
|
+
return part;
|
|
71
|
+
});
|
|
72
|
+
return { ...msg, content: newContent };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return msg;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a Vercel AI SDK LanguageModelV1Middleware instance.
|
|
81
|
+
* Intercepts outbound prompt parameters to sanitize PII in RAM, and transparently
|
|
82
|
+
* restores tokens in generated responses and streaming deltas.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} [options={}] - PrivacyScrubber configuration options.
|
|
85
|
+
* @param {string} [options.profile='General'] - Detection profile ('General', 'Medical', 'Finance', 'Dev', etc.).
|
|
86
|
+
* @param {boolean} [options.detectSecrets=true] - Whether to detect API keys, tokens, and credentials.
|
|
87
|
+
* @param {string} [options.licenseKey] - PrivacyScrubber SDK license key.
|
|
88
|
+
* @param {boolean} [options.sanitizeInputs=true] - Whether to sanitize inbound prompt messages.
|
|
89
|
+
* @param {boolean} [options.sanitizeOutputs=true] - Whether to restore tokens in outbound responses/deltas.
|
|
90
|
+
* @param {Function} [options.onSanitized] - Optional telemetry callback on sanitization.
|
|
91
|
+
* @param {object} [options.streamingOptions] - Sliding window buffer tuning options for StreamingDetokenizer.
|
|
92
|
+
* @returns {object} LanguageModelV1Middleware compliant object.
|
|
93
|
+
*/
|
|
94
|
+
function createPrivacyMiddleware(options = {}) {
|
|
95
|
+
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
96
|
+
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
async transformParams({ type, params }) {
|
|
100
|
+
if (!sanitizeInputs || !params) {
|
|
101
|
+
return params;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
105
|
+
const sessionTokenMap = {};
|
|
106
|
+
const sanitizedPrompt = sanitizePromptParts(params.prompt, engine, sessionTokenMap);
|
|
107
|
+
const sanitizedParams = {
|
|
108
|
+
...params,
|
|
109
|
+
prompt: sanitizedPrompt
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
tokenMapsWeakMap.set(sanitizedParams, sessionTokenMap);
|
|
113
|
+
tokenMapsWeakMap.set(params, sessionTokenMap);
|
|
114
|
+
try {
|
|
115
|
+
Object.defineProperty(sanitizedParams, TOKEN_MAP_SYMBOL, {
|
|
116
|
+
value: sessionTokenMap,
|
|
117
|
+
writable: true,
|
|
118
|
+
enumerable: false,
|
|
119
|
+
configurable: true
|
|
120
|
+
});
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
|
|
123
|
+
if (typeof options.onSanitized === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
options.onSanitized({
|
|
126
|
+
type,
|
|
127
|
+
prompt: sanitizedPrompt,
|
|
128
|
+
tokenMap: sessionTokenMap,
|
|
129
|
+
tokenCount: Object.keys(sessionTokenMap).length
|
|
130
|
+
});
|
|
131
|
+
} catch (_) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return sanitizedParams;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async wrapGenerate({ doGenerate, params, model }) {
|
|
138
|
+
const tokenMap = (params && (tokenMapsWeakMap.get(params) || params[TOKEN_MAP_SYMBOL])) || {};
|
|
139
|
+
const result = await doGenerate();
|
|
140
|
+
|
|
141
|
+
if (sanitizeOutputs && result && Object.keys(tokenMap).length > 0) {
|
|
142
|
+
if (typeof result.text === 'string') {
|
|
143
|
+
result.text = detokenizeText(result.text, tokenMap);
|
|
144
|
+
}
|
|
145
|
+
if (typeof result.reasoning === 'string') {
|
|
146
|
+
result.reasoning = detokenizeText(result.reasoning, tokenMap);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async wrapStream({ doStream, params, model }) {
|
|
154
|
+
const tokenMap = (params && (tokenMapsWeakMap.get(params) || params[TOKEN_MAP_SYMBOL])) || {};
|
|
155
|
+
const result = await doStream();
|
|
156
|
+
|
|
157
|
+
if (sanitizeOutputs && result && result.stream && Object.keys(tokenMap).length > 0) {
|
|
158
|
+
const detok = new StreamingDetokenizer(tokenMap, options.streamingOptions);
|
|
159
|
+
const originalStream = result.stream;
|
|
160
|
+
|
|
161
|
+
if (typeof originalStream.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
162
|
+
result.stream = originalStream.pipeThrough(new TransformStream({
|
|
163
|
+
transform(chunk, controller) {
|
|
164
|
+
if (chunk && chunk.type === 'text-delta' && typeof chunk.textDelta === 'string') {
|
|
165
|
+
const restored = detok.feed(chunk.textDelta);
|
|
166
|
+
if (restored) controller.enqueue({ ...chunk, textDelta: restored });
|
|
167
|
+
} else if (chunk && chunk.type === 'reasoning' && typeof chunk.textDelta === 'string') {
|
|
168
|
+
const restored = detok.feed(chunk.textDelta);
|
|
169
|
+
if (restored) controller.enqueue({ ...chunk, textDelta: restored });
|
|
170
|
+
} else {
|
|
171
|
+
controller.enqueue(chunk);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
flush(controller) {
|
|
175
|
+
const rem = detok.flush();
|
|
176
|
+
if (rem) controller.enqueue({ type: 'text-delta', textDelta: rem });
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
} else if (typeof originalStream[Symbol.asyncIterator] === 'function') {
|
|
180
|
+
async function* wrappedAsync() {
|
|
181
|
+
for await (const chunk of originalStream) {
|
|
182
|
+
if (chunk && chunk.type === 'text-delta' && typeof chunk.textDelta === 'string') {
|
|
183
|
+
const restored = detok.feed(chunk.textDelta);
|
|
184
|
+
if (restored) yield { ...chunk, textDelta: restored };
|
|
185
|
+
} else if (chunk && chunk.type === 'reasoning' && typeof chunk.textDelta === 'string') {
|
|
186
|
+
const restored = detok.feed(chunk.textDelta);
|
|
187
|
+
if (restored) yield { ...chunk, textDelta: restored };
|
|
188
|
+
} else {
|
|
189
|
+
yield chunk;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const rem = detok.flush();
|
|
193
|
+
if (rem) yield { type: 'text-delta', textDelta: rem };
|
|
194
|
+
}
|
|
195
|
+
result.stream = wrappedAsync();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Wraps a Vercel AI SDK LanguageModel instance with the specified middleware.
|
|
206
|
+
* Fully compatible with Vercel AI SDK's experimental_wrapLanguageModel / wrapLanguageModel.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} params - Parameters object.
|
|
209
|
+
* @param {object} params.model - The LanguageModel instance to wrap.
|
|
210
|
+
* @param {object} params.middleware - LanguageModelV1Middleware instance.
|
|
211
|
+
* @returns {object} Wrapped LanguageModel instance.
|
|
212
|
+
*/
|
|
213
|
+
function wrapLanguageModel({ model, middleware }) {
|
|
214
|
+
if (!model || typeof model !== 'object') {
|
|
215
|
+
throw new Error('[PrivacyScrubber] wrapLanguageModel requires a valid LanguageModel instance.');
|
|
216
|
+
}
|
|
217
|
+
if (!middleware || typeof middleware !== 'object') {
|
|
218
|
+
return model;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const wrapped = Object.create(model);
|
|
222
|
+
|
|
223
|
+
if (typeof model.doGenerate === 'function') {
|
|
224
|
+
wrapped.doGenerate = async function(params) {
|
|
225
|
+
let callParams = params;
|
|
226
|
+
if (typeof middleware.transformParams === 'function') {
|
|
227
|
+
callParams = await middleware.transformParams({ type: 'generate', params: callParams });
|
|
228
|
+
}
|
|
229
|
+
if (typeof middleware.wrapGenerate === 'function') {
|
|
230
|
+
return middleware.wrapGenerate({
|
|
231
|
+
doGenerate: () => model.doGenerate(callParams),
|
|
232
|
+
params: callParams,
|
|
233
|
+
model
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return model.doGenerate(callParams);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof model.doStream === 'function') {
|
|
241
|
+
wrapped.doStream = async function(params) {
|
|
242
|
+
let callParams = params;
|
|
243
|
+
if (typeof middleware.transformParams === 'function') {
|
|
244
|
+
callParams = await middleware.transformParams({ type: 'stream', params: callParams });
|
|
245
|
+
}
|
|
246
|
+
if (typeof middleware.wrapStream === 'function') {
|
|
247
|
+
return middleware.wrapStream({
|
|
248
|
+
doStream: () => model.doStream(callParams),
|
|
249
|
+
params: callParams,
|
|
250
|
+
model
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return model.doStream(callParams);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return wrapped;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Universal wrapper for Vercel AI SDK.
|
|
262
|
+
*
|
|
263
|
+
* When passed a LanguageModel instance:
|
|
264
|
+
* const safeModel = withPrivacyScrubber(openai('gpt-4o'), { profile: 'General' });
|
|
265
|
+
* Returns the privacy-shielded model directly.
|
|
266
|
+
*
|
|
267
|
+
* When passed options (or 0 arguments):
|
|
268
|
+
* const middleware = withPrivacyScrubber({ profile: 'General' });
|
|
269
|
+
* Returns the LanguageModelV1Middleware object for wrapLanguageModel.
|
|
270
|
+
*
|
|
271
|
+
* @param {any} [modelOrOptions] - LanguageModel instance OR options object.
|
|
272
|
+
* @param {object} [maybeOptions] - Options if model was passed as first argument.
|
|
273
|
+
* @returns {any} Shielded model OR middleware object.
|
|
274
|
+
*/
|
|
275
|
+
function withPrivacyScrubber(modelOrOptions, maybeOptions) {
|
|
276
|
+
if (modelOrOptions && (typeof modelOrOptions.doGenerate === 'function' || typeof modelOrOptions.doStream === 'function' || modelOrOptions.specificationVersion)) {
|
|
277
|
+
const middleware = createPrivacyMiddleware(maybeOptions || {});
|
|
278
|
+
const wrapped = wrapLanguageModel({ model: modelOrOptions, middleware });
|
|
279
|
+
wrapped.middleware = middleware;
|
|
280
|
+
return wrapped;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return createPrivacyMiddleware(modelOrOptions || {});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Creates a standard Web API TransformStream for piping text or deltas through the StreamingDetokenizer.
|
|
288
|
+
* Compatible with Next.js Route Handlers and Web Streams:
|
|
289
|
+
* response.pipeThrough(createDetokenizerTransformStream(tokenMap));
|
|
290
|
+
*
|
|
291
|
+
* @param {Record<string, string>} [tokenMap={}] - Volatile session token map.
|
|
292
|
+
* @param {object} [options={}] - Detokenizer tuning options.
|
|
293
|
+
* @returns {TransformStream}
|
|
294
|
+
*/
|
|
295
|
+
function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
296
|
+
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
297
|
+
if (typeof TransformStream === 'undefined') {
|
|
298
|
+
throw new Error('[PrivacyScrubber] TransformStream is not supported in this runtime.');
|
|
299
|
+
}
|
|
300
|
+
let isObjectStream = false;
|
|
301
|
+
return new TransformStream({
|
|
302
|
+
transform(chunk, controller) {
|
|
303
|
+
if (typeof chunk === 'string') {
|
|
304
|
+
const out = detok.feed(chunk);
|
|
305
|
+
if (out) controller.enqueue(out);
|
|
306
|
+
} else if (chunk && typeof chunk === 'object' && typeof chunk.textDelta === 'string') {
|
|
307
|
+
isObjectStream = true;
|
|
308
|
+
const out = detok.feed(chunk.textDelta);
|
|
309
|
+
if (out) controller.enqueue({ ...chunk, textDelta: out });
|
|
310
|
+
} else {
|
|
311
|
+
controller.enqueue(chunk);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
flush(controller) {
|
|
315
|
+
const rem = detok.flush();
|
|
316
|
+
if (rem) {
|
|
317
|
+
if (isObjectStream) {
|
|
318
|
+
controller.enqueue({ type: 'text-delta', textDelta: rem });
|
|
319
|
+
} else {
|
|
320
|
+
controller.enqueue(rem);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Wraps an async iterable stream (e.g. result.textStream from streamText) and transparently
|
|
329
|
+
* de-tokenizes split tokens on the fly.
|
|
330
|
+
*
|
|
331
|
+
* @param {AsyncIterable<string|object>} stream - The incoming stream.
|
|
332
|
+
* @param {Record<string, string>} [tokenMap={}] - Volatile session token map.
|
|
333
|
+
* @param {object} [options={}] - Detokenizer tuning options.
|
|
334
|
+
* @returns {AsyncGenerator<any>}
|
|
335
|
+
*/
|
|
336
|
+
async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
337
|
+
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
338
|
+
for await (const chunk of stream) {
|
|
339
|
+
if (typeof chunk === 'string') {
|
|
340
|
+
const out = detok.feed(chunk);
|
|
341
|
+
if (out) yield out;
|
|
342
|
+
} else if (chunk && typeof chunk === 'object' && typeof chunk.textDelta === 'string') {
|
|
343
|
+
const out = detok.feed(chunk.textDelta);
|
|
344
|
+
if (out) yield { ...chunk, textDelta: out };
|
|
345
|
+
} else {
|
|
346
|
+
yield chunk;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const rem = detok.flush();
|
|
350
|
+
if (rem) {
|
|
351
|
+
yield rem;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Replaces tokens in text with authentic values in local RAM.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} text - Text containing tokens.
|
|
359
|
+
* @param {Record<string, string>} [tokenMap={}] - Volatile session token map.
|
|
360
|
+
* @returns {string} Restored text.
|
|
361
|
+
*/
|
|
362
|
+
function detokenizeText(text, tokenMap = {}) {
|
|
363
|
+
if (typeof text !== 'string') return text;
|
|
364
|
+
const replacer = buildStreamReplacer(tokenMap);
|
|
365
|
+
return replacer(text);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Direct prompt sanitization helper for Vercel AI SDK prompts.
|
|
370
|
+
*
|
|
371
|
+
* @param {any} prompt - Array of messages or raw string prompt.
|
|
372
|
+
* @param {object} [options={}] - Sanitization options.
|
|
373
|
+
* @returns {{ sanitizedPrompt: any, tokenMap: Record<string, string>, tokenCount: number, telemetry: object }}
|
|
374
|
+
*/
|
|
375
|
+
function sanitizePrompt(prompt, options = {}) {
|
|
376
|
+
const engine = new PrivacyScrubberEngine(options);
|
|
377
|
+
const tokenMap = {};
|
|
378
|
+
const sanitizedPrompt = sanitizePromptParts(prompt, engine, tokenMap);
|
|
379
|
+
const telemetry = buildCisoAuditTelemetry(tokenMap, {
|
|
380
|
+
frameworks: ['GDPR', 'HIPAA', 'SOC2', 'PCI-DSS', 'NIST-AI-RMF']
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
sanitizedPrompt,
|
|
384
|
+
tokenMap,
|
|
385
|
+
tokenCount: Object.keys(tokenMap).length,
|
|
386
|
+
telemetry
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
createPrivacyMiddleware,
|
|
392
|
+
wrapLanguageModel,
|
|
393
|
+
withPrivacyScrubber,
|
|
394
|
+
createDetokenizerTransformStream,
|
|
395
|
+
detokenizeStream,
|
|
396
|
+
detokenizeText,
|
|
397
|
+
sanitizePrompt,
|
|
398
|
+
StreamingDetokenizer
|
|
399
|
+
};
|
|
400
|
+
module.exports.default = module.exports;
|
package/vercel-ai.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @privacyscrubber/sdk/vercel-ai
|
|
3
|
+
* TypeScript Definitions for Vercel AI SDK Zero-Trust Middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PrivacyMiddlewareOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Detection profile to apply.
|
|
9
|
+
* Universal profiles: 'General' (universal consumer PII).
|
|
10
|
+
* 30 Specialized Industry Profiles: 'Medical' (HIPAA), 'Finance' (GLBA/PCI-DSS), 'Dev' (API Keys/Secrets), 'Legal', etc.
|
|
11
|
+
* Default: 'General'
|
|
12
|
+
*/
|
|
13
|
+
profile?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether to scan for DevOps secrets (OpenAI, AWS, GitHub tokens, private keys).
|
|
17
|
+
* Default: true
|
|
18
|
+
*/
|
|
19
|
+
detectSecrets?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Commercial Developer SDK or Enterprise license key.
|
|
23
|
+
*/
|
|
24
|
+
licenseKey?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to sanitize inbound prompt messages before LLM model invocation.
|
|
28
|
+
* Default: true
|
|
29
|
+
*/
|
|
30
|
+
sanitizeInputs?: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether to de-tokenize outbound generated text and real-time streaming deltas.
|
|
34
|
+
* Default: true
|
|
35
|
+
*/
|
|
36
|
+
sanitizeOutputs?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional telemetry callback invoked upon inbound prompt sanitization.
|
|
40
|
+
*/
|
|
41
|
+
onSanitized?: (telemetry: {
|
|
42
|
+
type: 'generate' | 'stream';
|
|
43
|
+
prompt: any;
|
|
44
|
+
tokenMap: Record<string, string>;
|
|
45
|
+
tokenCount: number;
|
|
46
|
+
}) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Optional sliding window buffer options for StreamingDetokenizer.
|
|
50
|
+
*/
|
|
51
|
+
streamingOptions?: {
|
|
52
|
+
tailWindow?: number;
|
|
53
|
+
maxSessionTokens?: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Custom user-defined regex rules to run in addition to the selected profile.
|
|
58
|
+
*/
|
|
59
|
+
customRules?: Array<{ name: string; pattern: RegExp; tokenType: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PrivacyMiddleware {
|
|
63
|
+
transformParams: (options: {
|
|
64
|
+
type: 'generate' | 'stream';
|
|
65
|
+
params: any;
|
|
66
|
+
}) => Promise<any>;
|
|
67
|
+
|
|
68
|
+
wrapGenerate: (options: {
|
|
69
|
+
doGenerate: () => Promise<any>;
|
|
70
|
+
params: any;
|
|
71
|
+
model: any;
|
|
72
|
+
}) => Promise<any>;
|
|
73
|
+
|
|
74
|
+
wrapStream: (options: {
|
|
75
|
+
doStream: () => Promise<any>;
|
|
76
|
+
params: any;
|
|
77
|
+
model: any;
|
|
78
|
+
}) => Promise<any>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SanitizePromptResult {
|
|
82
|
+
sanitizedPrompt: any;
|
|
83
|
+
tokenMap: Record<string, string>;
|
|
84
|
+
tokenCount: number;
|
|
85
|
+
telemetry: any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates a Vercel AI SDK LanguageModelV1Middleware instance.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createPrivacyMiddleware(options?: PrivacyMiddlewareOptions): PrivacyMiddleware;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Universal wrapper for Vercel AI SDK models or middleware creator.
|
|
95
|
+
*
|
|
96
|
+
* When passed a model:
|
|
97
|
+
* const safeModel = withPrivacyScrubber(openai('gpt-4o'), { profile: 'General' });
|
|
98
|
+
* Returns the shielded model directly.
|
|
99
|
+
*
|
|
100
|
+
* When passed options:
|
|
101
|
+
* const middleware = withPrivacyScrubber({ profile: 'General' });
|
|
102
|
+
* Returns the middleware for wrapLanguageModel.
|
|
103
|
+
*/
|
|
104
|
+
export declare function withPrivacyScrubber<TModel = any>(model: TModel, options?: PrivacyMiddlewareOptions): TModel;
|
|
105
|
+
export declare function withPrivacyScrubber(options?: PrivacyMiddlewareOptions): PrivacyMiddleware;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wraps a Vercel AI SDK LanguageModel with the specified middleware.
|
|
109
|
+
*/
|
|
110
|
+
export declare function wrapLanguageModel<TModel = any>(params: { model: TModel; middleware: any }): TModel;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a standard Web API TransformStream for piping text or deltas through the StreamingDetokenizer.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createDetokenizerTransformStream(
|
|
116
|
+
tokenMap?: Record<string, string>,
|
|
117
|
+
options?: any
|
|
118
|
+
): TransformStream;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Wraps an async iterable stream (e.g. result.textStream from streamText) and transparently
|
|
122
|
+
* de-tokenizes split tokens on the fly.
|
|
123
|
+
*/
|
|
124
|
+
export declare function detokenizeStream<T = any>(
|
|
125
|
+
stream: AsyncIterable<T>,
|
|
126
|
+
tokenMap?: Record<string, string>,
|
|
127
|
+
options?: any
|
|
128
|
+
): AsyncGenerator<T>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Replaces tokens in text with authentic values in local RAM.
|
|
132
|
+
*/
|
|
133
|
+
export declare function detokenizeText(text: string, tokenMap?: Record<string, string>): string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Direct prompt sanitization helper for Vercel AI SDK prompts.
|
|
137
|
+
*/
|
|
138
|
+
export declare function sanitizePrompt(prompt: any, options?: PrivacyMiddlewareOptions): SanitizePromptResult;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
|
|
142
|
+
*/
|
|
143
|
+
export { StreamingDetokenizer } from './index';
|
|
144
|
+
|
|
145
|
+
declare const _default: {
|
|
146
|
+
createPrivacyMiddleware: typeof createPrivacyMiddleware;
|
|
147
|
+
withPrivacyScrubber: typeof withPrivacyScrubber;
|
|
148
|
+
wrapLanguageModel: typeof wrapLanguageModel;
|
|
149
|
+
createDetokenizerTransformStream: typeof createDetokenizerTransformStream;
|
|
150
|
+
detokenizeStream: typeof detokenizeStream;
|
|
151
|
+
detokenizeText: typeof detokenizeText;
|
|
152
|
+
sanitizePrompt: typeof sanitizePrompt;
|
|
153
|
+
StreamingDetokenizer: any;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default _default;
|
package/vercel-ai.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @privacyscrubber/sdk/vercel-ai
|
|
3
|
+
* Zero-Trust Data Sanitization (ZTDS) In-Memory Middleware for Vercel AI SDK
|
|
4
|
+
* Conforms to IETF Standards Track: draft-sibiryakov-ztds-protocol-01
|
|
5
|
+
*
|
|
6
|
+
* Sub-millisecond in-memory prompt sanitization and real-time streaming de-tokenization.
|
|
7
|
+
* Compatible with Vercel AI SDK v3/v4: streamText, generateText, wrapLanguageModel.
|
|
8
|
+
* Zero external dependencies. Zero network calls. 100% local RAM execution.
|
|
9
|
+
*
|
|
10
|
+
* https://privacyscrubber.com/sdk/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
const vercelAi = require('./vercel-ai.cjs');
|
|
17
|
+
|
|
18
|
+
export const createPrivacyMiddleware = vercelAi.createPrivacyMiddleware;
|
|
19
|
+
export const withPrivacyScrubber = vercelAi.withPrivacyScrubber;
|
|
20
|
+
export const wrapLanguageModel = vercelAi.wrapLanguageModel;
|
|
21
|
+
export const createDetokenizerTransformStream = vercelAi.createDetokenizerTransformStream;
|
|
22
|
+
export const detokenizeStream = vercelAi.detokenizeStream;
|
|
23
|
+
export const detokenizeText = vercelAi.detokenizeText;
|
|
24
|
+
export const sanitizePrompt = vercelAi.sanitizePrompt;
|
|
25
|
+
export const StreamingDetokenizer = vercelAi.StreamingDetokenizer;
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
createPrivacyMiddleware,
|
|
29
|
+
withPrivacyScrubber,
|
|
30
|
+
wrapLanguageModel,
|
|
31
|
+
createDetokenizerTransformStream,
|
|
32
|
+
detokenizeStream,
|
|
33
|
+
detokenizeText,
|
|
34
|
+
sanitizePrompt,
|
|
35
|
+
StreamingDetokenizer
|
|
36
|
+
};
|
package/vision.cjs
CHANGED
|
@@ -71,15 +71,23 @@ function stripExifMetadata(inputBuffer) {
|
|
|
71
71
|
|
|
72
72
|
// Markers to strip:
|
|
73
73
|
// 0xE1 = APP1 (EXIF, XMP)
|
|
74
|
+
// 0xE2 = APP2 (ICC Profile, FlashPix, MPF)
|
|
75
|
+
// 0xEC = APP12 (Picture Info, Ducky)
|
|
74
76
|
// 0xED = APP13 (Photoshop IRB, IPTC)
|
|
75
77
|
// 0xFE = COM (Comment)
|
|
76
78
|
const isExif = marker === 0xE1;
|
|
79
|
+
const isApp2 = marker === 0xE2;
|
|
80
|
+
const isApp12 = marker === 0xEC;
|
|
77
81
|
const isIptc = marker === 0xED;
|
|
78
82
|
const isComment = marker === 0xFE;
|
|
79
83
|
|
|
80
|
-
if (isExif || isIptc || isComment) {
|
|
84
|
+
if (isExif || isApp2 || isApp12 || isIptc || isComment) {
|
|
81
85
|
stripped = true;
|
|
82
|
-
const segmentName = isExif ? 'APP1 (Exif/XMP)'
|
|
86
|
+
const segmentName = isExif ? 'APP1 (Exif/XMP)'
|
|
87
|
+
: isApp2 ? 'APP2 (ICC/MPF)'
|
|
88
|
+
: isApp12 ? 'APP12 (PictureInfo/Ducky)'
|
|
89
|
+
: isIptc ? 'APP13 (IPTC)'
|
|
90
|
+
: 'COM (Comment)';
|
|
83
91
|
removedSegments.push(segmentName);
|
|
84
92
|
offset = segmentEnd;
|
|
85
93
|
} else if (marker === 0xDA) { // SOS (Start of Scan) - compressed bitstream follows until EOI
|
|
@@ -128,8 +136,8 @@ function stripExifMetadata(inputBuffer) {
|
|
|
128
136
|
let offset = 8;
|
|
129
137
|
let stripped = false;
|
|
130
138
|
|
|
131
|
-
// Ancillary chunk types to strip: eXIf, tEXt, zTXt, iTXt, tIME, pHYs
|
|
132
|
-
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs']);
|
|
139
|
+
// Ancillary chunk types to strip: eXIf, tEXt, zTXt, iTXt, tIME, pHYs, iCCP
|
|
140
|
+
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs', 'iCCP']);
|
|
133
141
|
|
|
134
142
|
while (offset + 8 <= u8.length) {
|
|
135
143
|
const length = (u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3];
|
|
@@ -199,7 +207,7 @@ function stripExifMetadata(inputBuffer) {
|
|
|
199
207
|
break;
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
if (fourCC === 'EXIF' || fourCC === 'XMP ') {
|
|
210
|
+
if (fourCC === 'EXIF' || fourCC === 'XMP ' || fourCC === 'ICCP') {
|
|
203
211
|
stripped = true;
|
|
204
212
|
removedSegments.push(`WEBP_${fourCC.trim()}`);
|
|
205
213
|
offset += totalChunkSize;
|
package/vision.js
CHANGED
|
@@ -71,15 +71,23 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
71
71
|
|
|
72
72
|
// Markers to strip:
|
|
73
73
|
// 0xE1 = APP1 (EXIF, XMP)
|
|
74
|
+
// 0xE2 = APP2 (ICC Profile, FlashPix, MPF)
|
|
75
|
+
// 0xEC = APP12 (Picture Info, Ducky)
|
|
74
76
|
// 0xED = APP13 (Photoshop IRB, IPTC)
|
|
75
77
|
// 0xFE = COM (Comment)
|
|
76
78
|
const isExif = marker === 0xE1;
|
|
79
|
+
const isApp2 = marker === 0xE2;
|
|
80
|
+
const isApp12 = marker === 0xEC;
|
|
77
81
|
const isIptc = marker === 0xED;
|
|
78
82
|
const isComment = marker === 0xFE;
|
|
79
83
|
|
|
80
|
-
if (isExif || isIptc || isComment) {
|
|
84
|
+
if (isExif || isApp2 || isApp12 || isIptc || isComment) {
|
|
81
85
|
stripped = true;
|
|
82
|
-
const segmentName = isExif ? 'APP1 (Exif/XMP)'
|
|
86
|
+
const segmentName = isExif ? 'APP1 (Exif/XMP)'
|
|
87
|
+
: isApp2 ? 'APP2 (ICC/MPF)'
|
|
88
|
+
: isApp12 ? 'APP12 (PictureInfo/Ducky)'
|
|
89
|
+
: isIptc ? 'APP13 (IPTC)'
|
|
90
|
+
: 'COM (Comment)';
|
|
83
91
|
removedSegments.push(segmentName);
|
|
84
92
|
offset = segmentEnd;
|
|
85
93
|
} else if (marker === 0xDA) { // SOS (Start of Scan) - compressed bitstream follows until EOI
|
|
@@ -128,8 +136,8 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
128
136
|
let offset = 8;
|
|
129
137
|
let stripped = false;
|
|
130
138
|
|
|
131
|
-
// Ancillary chunk types to strip: eXIf, tEXt, zTXt, iTXt, tIME, pHYs
|
|
132
|
-
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs']);
|
|
139
|
+
// Ancillary chunk types to strip: eXIf, tEXt, zTXt, iTXt, tIME, pHYs, iCCP
|
|
140
|
+
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs', 'iCCP']);
|
|
133
141
|
|
|
134
142
|
while (offset + 8 <= u8.length) {
|
|
135
143
|
const length = (u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3];
|
|
@@ -199,7 +207,7 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
199
207
|
break;
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
if (fourCC === 'EXIF' || fourCC === 'XMP ') {
|
|
210
|
+
if (fourCC === 'EXIF' || fourCC === 'XMP ' || fourCC === 'ICCP') {
|
|
203
211
|
stripped = true;
|
|
204
212
|
removedSegments.push(`WEBP_${fourCC.trim()}`);
|
|
205
213
|
offset += totalChunkSize;
|