@privacyscrubber/sdk 2.4.4 → 2.4.6
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 +50 -1
- package/browser.js +399 -19
- package/cli.js +119 -11
- package/examples/python-bridge/README.md +3 -3
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/langchain_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +2 -2
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/index.cjs +1194 -86
- package/index.d.ts +147 -2
- package/index.js +1194 -86
- package/package.json +13 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +14 -2
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +123 -34
- package/vercel-ai.d.ts +7 -0
- package/benchmark.js +0 -355
- package/examples/python-bridge/bridge_daemon.cjs +0 -260
- package/examples/python-bridge/langchain_adapter.py +0 -20
- package/examples/python-bridge/privacyscrubber.py +0 -36
- package/examples/python-bridge/test_python_bridge.py +0 -320
package/vercel-ai.cjs
CHANGED
|
@@ -33,24 +33,34 @@ const tokenMapsWeakMap = new WeakMap();
|
|
|
33
33
|
* @param {Record<string, string>} sessionTokenMap - Volatile map populated with detected tokens.
|
|
34
34
|
* @returns {any} Sanitized prompt with tokens replacing authentic PII.
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
37
|
+
|
|
38
|
+
function sanitizeDeepValue(val, engine, sessionTokenMap, seen = new WeakMap()) {
|
|
37
39
|
if (val === null || val === undefined) return val;
|
|
38
40
|
if (typeof val === 'string') {
|
|
39
41
|
if (!val.trim()) return val;
|
|
40
|
-
const res = engine.sanitize(val, { existingSessionMap: sessionTokenMap });
|
|
42
|
+
const res = engine.sanitize(val, { existingSessionMap: sessionTokenMap, persistSession: false });
|
|
41
43
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
42
44
|
return res.scrubbedText;
|
|
43
45
|
}
|
|
44
|
-
if (Array.isArray(val)) {
|
|
45
|
-
return val.map(item => sanitizeDeepValue(item, engine, sessionTokenMap));
|
|
46
|
-
}
|
|
47
46
|
if (val instanceof Date || val instanceof RegExp || val instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(val))) {
|
|
48
47
|
return val;
|
|
49
48
|
}
|
|
50
49
|
if (typeof val === 'object') {
|
|
50
|
+
if (seen.has(val)) return seen.get(val);
|
|
51
|
+
if (Array.isArray(val)) {
|
|
52
|
+
const arr = [];
|
|
53
|
+
seen.set(val, arr);
|
|
54
|
+
for (const item of val) {
|
|
55
|
+
arr.push(sanitizeDeepValue(item, engine, sessionTokenMap, seen));
|
|
56
|
+
}
|
|
57
|
+
return arr;
|
|
58
|
+
}
|
|
51
59
|
const out = {};
|
|
60
|
+
seen.set(val, out);
|
|
52
61
|
for (const [k, v] of Object.entries(val)) {
|
|
53
|
-
|
|
62
|
+
if (DANGEROUS_KEYS.has(k)) continue;
|
|
63
|
+
out[k] = sanitizeDeepValue(v, engine, sessionTokenMap, seen);
|
|
54
64
|
}
|
|
55
65
|
return out;
|
|
56
66
|
}
|
|
@@ -61,7 +71,7 @@ function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
|
61
71
|
if (!prompt) return prompt;
|
|
62
72
|
|
|
63
73
|
if (typeof prompt === 'string') {
|
|
64
|
-
const res = engine.sanitize(prompt, { existingSessionMap: sessionTokenMap });
|
|
74
|
+
const res = engine.sanitize(prompt, { existingSessionMap: sessionTokenMap, persistSession: false });
|
|
65
75
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
66
76
|
return res.scrubbedText;
|
|
67
77
|
}
|
|
@@ -73,7 +83,7 @@ function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
|
73
83
|
|
|
74
84
|
// Standard string content: { role: 'user', content: 'Contact john@example.com' }
|
|
75
85
|
if (typeof msg.content === 'string') {
|
|
76
|
-
const res = engine.sanitize(msg.content, { existingSessionMap: sessionTokenMap });
|
|
86
|
+
const res = engine.sanitize(msg.content, { existingSessionMap: sessionTokenMap, persistSession: false });
|
|
77
87
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
78
88
|
return { ...msg, content: res.scrubbedText };
|
|
79
89
|
}
|
|
@@ -82,13 +92,13 @@ function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
|
82
92
|
if (Array.isArray(msg.content)) {
|
|
83
93
|
const newContent = msg.content.map(part => {
|
|
84
94
|
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
85
|
-
const res = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap });
|
|
95
|
+
const res = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap, persistSession: false });
|
|
86
96
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
87
97
|
return { ...part, text: res.scrubbedText };
|
|
88
98
|
}
|
|
89
99
|
if (part && part.type === 'tool-result') {
|
|
90
100
|
if (typeof part.result === 'string') {
|
|
91
|
-
const res = engine.sanitize(part.result, { existingSessionMap: sessionTokenMap });
|
|
101
|
+
const res = engine.sanitize(part.result, { existingSessionMap: sessionTokenMap, persistSession: false });
|
|
92
102
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
93
103
|
return { ...part, result: res.scrubbedText };
|
|
94
104
|
}
|
|
@@ -126,16 +136,29 @@ function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
|
126
136
|
function createPrivacyMiddleware(options = {}) {
|
|
127
137
|
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
128
138
|
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
139
|
+
const sharedEngine = options.engine || new PrivacyScrubberEngine(options);
|
|
140
|
+
const sharedSessionMap = {};
|
|
129
141
|
|
|
130
142
|
return {
|
|
143
|
+
specificationVersion: options.specificationVersion || 'v1',
|
|
144
|
+
|
|
131
145
|
async transformParams({ type, params }) {
|
|
132
146
|
if (!sanitizeInputs || !params) {
|
|
133
147
|
return params;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
const engine =
|
|
137
|
-
const sessionTokenMap = {};
|
|
150
|
+
const engine = sharedEngine;
|
|
151
|
+
const sessionTokenMap = options.persistSession ? { ...sharedSessionMap } : {};
|
|
152
|
+
if (!options.persistSession && params.prompt && Object.keys(sharedSessionMap).length > 0) {
|
|
153
|
+
const promptSerialized = typeof params.prompt === 'string' ? params.prompt : JSON.stringify(params.prompt);
|
|
154
|
+
for (const [tok, orig] of Object.entries(sharedSessionMap)) {
|
|
155
|
+
if (promptSerialized.includes(tok)) {
|
|
156
|
+
sessionTokenMap[tok] = orig;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
138
160
|
const sanitizedPrompt = sanitizePromptParts(params.prompt, engine, sessionTokenMap);
|
|
161
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
139
162
|
const sanitizedParams = {
|
|
140
163
|
...params,
|
|
141
164
|
prompt: sanitizedPrompt
|
|
@@ -182,9 +205,26 @@ function createPrivacyMiddleware(options = {}) {
|
|
|
182
205
|
if (tc && typeof tc.args === 'string') {
|
|
183
206
|
return { ...tc, args: detokenizeText(tc.args, tokenMap) };
|
|
184
207
|
}
|
|
208
|
+
if (tc && typeof tc.input === 'string') {
|
|
209
|
+
return { ...tc, input: detokenizeText(tc.input, tokenMap) };
|
|
210
|
+
}
|
|
185
211
|
return tc;
|
|
186
212
|
});
|
|
187
213
|
}
|
|
214
|
+
if (Array.isArray(result.content)) {
|
|
215
|
+
result.content = result.content.map(part => {
|
|
216
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
217
|
+
return { ...part, text: detokenizeText(part.text, tokenMap) };
|
|
218
|
+
}
|
|
219
|
+
if (part && part.type === 'tool-call') {
|
|
220
|
+
const nextPart = { ...part };
|
|
221
|
+
if (typeof nextPart.args === 'string') nextPart.args = detokenizeText(nextPart.args, tokenMap);
|
|
222
|
+
if (typeof nextPart.input === 'string') nextPart.input = detokenizeText(nextPart.input, tokenMap);
|
|
223
|
+
return nextPart;
|
|
224
|
+
}
|
|
225
|
+
return part;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
188
228
|
}
|
|
189
229
|
|
|
190
230
|
return result;
|
|
@@ -197,40 +237,77 @@ function createPrivacyMiddleware(options = {}) {
|
|
|
197
237
|
if (sanitizeOutputs && result && result.stream && Object.keys(tokenMap).length > 0) {
|
|
198
238
|
const detok = new StreamingDetokenizer(tokenMap, options.streamingOptions);
|
|
199
239
|
const originalStream = result.stream;
|
|
240
|
+
let usesV2Delta = false;
|
|
200
241
|
|
|
201
242
|
if (typeof originalStream.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
202
243
|
result.stream = originalStream.pipeThrough(new TransformStream({
|
|
203
244
|
transform(chunk, controller) {
|
|
204
|
-
if (chunk && chunk.type === 'text-delta'
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
246
|
+
if (typeof chunk.textDelta === 'string') {
|
|
247
|
+
const restored = detok.feed(chunk.textDelta);
|
|
248
|
+
if (restored) {
|
|
249
|
+
controller.enqueue({
|
|
250
|
+
...chunk,
|
|
251
|
+
textDelta: restored,
|
|
252
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (typeof chunk.delta === 'string') {
|
|
258
|
+
usesV2Delta = true;
|
|
259
|
+
const restored = detok.feed(chunk.delta);
|
|
260
|
+
if (restored) {
|
|
261
|
+
controller.enqueue({ ...chunk, delta: restored, textDelta: restored });
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
212
265
|
}
|
|
266
|
+
controller.enqueue(chunk);
|
|
213
267
|
},
|
|
214
268
|
flush(controller) {
|
|
215
269
|
const rem = detok.flush();
|
|
216
|
-
if (rem)
|
|
270
|
+
if (rem) {
|
|
271
|
+
controller.enqueue(
|
|
272
|
+
usesV2Delta
|
|
273
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
274
|
+
: { type: 'text-delta', textDelta: rem, delta: rem }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
217
277
|
}
|
|
218
278
|
}));
|
|
219
279
|
} else if (typeof originalStream[Symbol.asyncIterator] === 'function') {
|
|
220
280
|
async function* wrappedAsync() {
|
|
221
281
|
for await (const chunk of originalStream) {
|
|
222
|
-
if (chunk && chunk.type === 'text-delta'
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
282
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
283
|
+
if (typeof chunk.textDelta === 'string') {
|
|
284
|
+
const restored = detok.feed(chunk.textDelta);
|
|
285
|
+
if (restored) {
|
|
286
|
+
yield {
|
|
287
|
+
...chunk,
|
|
288
|
+
textDelta: restored,
|
|
289
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (typeof chunk.delta === 'string') {
|
|
295
|
+
usesV2Delta = true;
|
|
296
|
+
const restored = detok.feed(chunk.delta);
|
|
297
|
+
if (restored) {
|
|
298
|
+
yield { ...chunk, delta: restored, textDelta: restored };
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
230
302
|
}
|
|
303
|
+
yield chunk;
|
|
231
304
|
}
|
|
232
305
|
const rem = detok.flush();
|
|
233
|
-
if (rem)
|
|
306
|
+
if (rem) {
|
|
307
|
+
yield usesV2Delta
|
|
308
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
309
|
+
: { type: 'text-delta', textDelta: rem, delta: rem };
|
|
310
|
+
}
|
|
234
311
|
}
|
|
235
312
|
result.stream = wrappedAsync();
|
|
236
313
|
}
|
|
@@ -338,6 +415,7 @@ function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
|
338
415
|
throw new Error('[PrivacyScrubber] TransformStream is not supported in this runtime.');
|
|
339
416
|
}
|
|
340
417
|
let isObjectStream = false;
|
|
418
|
+
let usesV2Delta = false;
|
|
341
419
|
return new TransformStream({
|
|
342
420
|
transform(chunk, controller) {
|
|
343
421
|
if (typeof chunk === 'string') {
|
|
@@ -346,7 +424,12 @@ function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
|
346
424
|
} else if (chunk && typeof chunk === 'object' && typeof chunk.textDelta === 'string') {
|
|
347
425
|
isObjectStream = true;
|
|
348
426
|
const out = detok.feed(chunk.textDelta);
|
|
349
|
-
if (out) controller.enqueue({ ...chunk, textDelta: out });
|
|
427
|
+
if (out) controller.enqueue({ ...chunk, textDelta: out, ...(typeof chunk.delta === 'string' ? { delta: out } : {}) });
|
|
428
|
+
} else if (chunk && typeof chunk === 'object' && typeof chunk.delta === 'string') {
|
|
429
|
+
isObjectStream = true;
|
|
430
|
+
usesV2Delta = true;
|
|
431
|
+
const out = detok.feed(chunk.delta);
|
|
432
|
+
if (out) controller.enqueue({ ...chunk, delta: out, textDelta: out });
|
|
350
433
|
} else {
|
|
351
434
|
controller.enqueue(chunk);
|
|
352
435
|
}
|
|
@@ -355,7 +438,7 @@ function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
|
355
438
|
const rem = detok.flush();
|
|
356
439
|
if (rem) {
|
|
357
440
|
if (isObjectStream) {
|
|
358
|
-
controller.enqueue({ type: 'text-delta', textDelta: rem });
|
|
441
|
+
controller.enqueue(usesV2Delta ? { type: 'text-delta', delta: rem, textDelta: rem } : { type: 'text-delta', textDelta: rem, delta: rem });
|
|
359
442
|
} else {
|
|
360
443
|
controller.enqueue(rem);
|
|
361
444
|
}
|
|
@@ -376,6 +459,7 @@ function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
|
376
459
|
async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
377
460
|
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
378
461
|
let isObjectStream = false;
|
|
462
|
+
let usesV2Delta = false;
|
|
379
463
|
for await (const chunk of stream) {
|
|
380
464
|
if (typeof chunk === 'string') {
|
|
381
465
|
const out = detok.feed(chunk);
|
|
@@ -383,7 +467,12 @@ async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
|
383
467
|
} else if (chunk && typeof chunk === 'object' && typeof chunk.textDelta === 'string') {
|
|
384
468
|
isObjectStream = true;
|
|
385
469
|
const out = detok.feed(chunk.textDelta);
|
|
386
|
-
if (out) yield { ...chunk, textDelta: out };
|
|
470
|
+
if (out) yield { ...chunk, textDelta: out, ...(typeof chunk.delta === 'string' ? { delta: out } : {}) };
|
|
471
|
+
} else if (chunk && typeof chunk === 'object' && typeof chunk.delta === 'string') {
|
|
472
|
+
isObjectStream = true;
|
|
473
|
+
usesV2Delta = true;
|
|
474
|
+
const out = detok.feed(chunk.delta);
|
|
475
|
+
if (out) yield { ...chunk, delta: out, textDelta: out };
|
|
387
476
|
} else {
|
|
388
477
|
yield chunk;
|
|
389
478
|
}
|
|
@@ -391,7 +480,7 @@ async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
|
391
480
|
const rem = detok.flush();
|
|
392
481
|
if (rem) {
|
|
393
482
|
if (isObjectStream) {
|
|
394
|
-
yield { type: 'text-delta', textDelta: rem };
|
|
483
|
+
yield usesV2Delta ? { type: 'text-delta', delta: rem, textDelta: rem } : { type: 'text-delta', textDelta: rem, delta: rem };
|
|
395
484
|
} else {
|
|
396
485
|
yield rem;
|
|
397
486
|
}
|
package/vercel-ai.d.ts
CHANGED
|
@@ -57,9 +57,16 @@ export interface PrivacyMiddlewareOptions {
|
|
|
57
57
|
* Custom user-defined regex rules to run in addition to the selected profile.
|
|
58
58
|
*/
|
|
59
59
|
customRules?: Array<{ name: string; pattern: RegExp; tokenType: string }>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Vercel AI SDK Middleware specification version ('v1' | 'v2' | 'v3').
|
|
63
|
+
* Default: 'v1'
|
|
64
|
+
*/
|
|
65
|
+
specificationVersion?: 'v1' | 'v2' | 'v3' | string;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export interface PrivacyMiddleware {
|
|
69
|
+
specificationVersion?: string;
|
|
63
70
|
transformParams: (options: {
|
|
64
71
|
type: 'generate' | 'stream';
|
|
65
72
|
params: any;
|
package/benchmark.js
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @privacyscrubber/sdk Micro-benchmarks & Performance Harness
|
|
3
|
-
*
|
|
4
|
-
* Measures in-process PII sanitization and detokenization latency,
|
|
5
|
-
* throughput, and memory consumption across varying payload sizes.
|
|
6
|
-
* Validates the sub-millisecond (<1ms) Zero-Trust Data Sanitization (ZTDS) SLA.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node benchmark.js # Standard terminal output
|
|
10
|
-
* node benchmark.js --json # Machine-readable JSON output
|
|
11
|
-
* node benchmark.js --markdown # Markdown table for documentation/memos
|
|
12
|
-
* node benchmark.js --assert-sla # Exits with code 1 if 10KB p50 >= 1.0ms
|
|
13
|
-
* node benchmark.js --profile=financial # Benchmark specific industry profile
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import os from 'node:os';
|
|
17
|
-
import process from 'node:process';
|
|
18
|
-
import { sanitize, restore } from './index.js';
|
|
19
|
-
|
|
20
|
-
// Configuration & CLI Arguments
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
const IS_JSON = args.includes('--json');
|
|
23
|
-
const IS_MARKDOWN = args.includes('--markdown');
|
|
24
|
-
const ASSERT_SLA = args.includes('--assert-sla');
|
|
25
|
-
|
|
26
|
-
function getArgValue(prefix, defaultValue) {
|
|
27
|
-
const match = args.find(a => a.startsWith(prefix));
|
|
28
|
-
return match ? match.split('=')[1] : defaultValue;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const REQUESTED_PROFILE = getArgValue('--profile=', 'General');
|
|
32
|
-
const ITERATIONS_OVERRIDE = parseInt(getArgValue('--iterations=', '0'), 10);
|
|
33
|
-
|
|
34
|
-
// Hardware & Environment Metadata
|
|
35
|
-
const ENV_INFO = {
|
|
36
|
-
runtime: `Node.js ${process.version}`,
|
|
37
|
-
platform: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
38
|
-
cpu: os.cpus()[0]?.model || 'Generic CPU',
|
|
39
|
-
cores: os.cpus().length,
|
|
40
|
-
v8Version: process.versions.v8,
|
|
41
|
-
memoryTotalMB: Math.round(os.totalmem() / (1024 * 1024))
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Synthetic Workload Generator with Non-trivial Mixed PII & Realistic Prose
|
|
45
|
-
function generateRealisticWorkload(targetBytes) {
|
|
46
|
-
const enterpriseProse = [
|
|
47
|
-
(i) => `Internal engineering escalation incident #INC-${28490 + i}: The infrastructure operations team investigated intermittent connection timeouts during automated microservice synchronization. Distributed tracing logs demonstrated that while ingress volume remained within standard operational thresholds, the upstream webhook dispatcher encountered resource saturation under peak traffic. Automated remediation scripts rebalanced cluster routing, and all pods were confirmed healthy. `,
|
|
48
|
-
(i) => `Enterprise architectural security assessment and data governance review. Pursuant to SOC 2 Type II controls and GDPR Article 32 technical safeguards, all customer communication channels, RAG knowledge retrieval databases, and LLM prompt ingress boundaries must execute strict in-memory sanitization before data transmission to external artificial intelligence models. `,
|
|
49
|
-
(i) => `Customer support ticket follow-up regarding transactional webhook retries and billing reconciliation. Our technical platform team reviewed the diagnostic transaction records and confirmed that webhook events were delivered successfully with HTTP 200 responses. Telemetry receipts and session audit proofs have been recorded for customer reference. `
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const piiTemplates = [
|
|
53
|
-
(i) => `Employee profile: Johnathan Doe_${i} (DOB 1985-04-12, SSN: 123-45-${String(1000 + (i % 9000))}). Contact email: john.doe.${i}@enterprise-corp.internal, personal phone +1-415-555-${String(1000 + (i % 9000))}. `,
|
|
54
|
-
(i) => `Patient clinical record: Jane E. Roe_${i}, MRN: MRN-${String(100000 + (i % 900000))}, emergency contact Dr. Robert Smith at +1-212-555-${String(2000 + (i % 8000))}. Insurance ID: INS-MED-${String(5000 + (i % 5000))}. `,
|
|
55
|
-
(i) => `Financial billing ledger: Cardholder Michael Chang_${i} billed on Visa PAN 4532-${String(1000 + (i % 9000))}-5678-9010, IBAN GB29XAAA01012345678901, routing account #09283741. Dispatched invoice to m.chang_${i}@fintech-capital.co.uk. `,
|
|
56
|
-
(i) => `DevOps security event log: Internal service pod deployed at IPv4 10.240.${i % 250}.${(i * 3) % 250} and IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:${String(1000 + (i % 9000))}. Authorization failed for Bearer token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c. `,
|
|
57
|
-
(i) => `Infrastructure secret rotation: AWS Key ID AKIAIOSFODNN7EXAMPLE_${i} accessed by administrator Sarah Connor_${i} via remote bastion host 192.168.1.${i % 250}. Github token ghp_39CharactersAlphanumericSecretKey${String(1000 + (i % 9000))} revoked. `
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
let content = "";
|
|
61
|
-
let i = 0;
|
|
62
|
-
while (Buffer.byteLength(content, 'utf8') < targetBytes) {
|
|
63
|
-
content += enterpriseProse[i % enterpriseProse.length](i);
|
|
64
|
-
content += piiTemplates[i % piiTemplates.length](i);
|
|
65
|
-
i++;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Trim to exact target bytes boundary
|
|
69
|
-
const buf = Buffer.from(content, 'utf8').subarray(0, targetBytes);
|
|
70
|
-
return buf.toString('utf8');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Statistical Metrics Calculator
|
|
74
|
-
function computeStats(timingsMs, payloadSizeBytes) {
|
|
75
|
-
timingsMs.sort((a, b) => a - b);
|
|
76
|
-
const n = timingsMs.length;
|
|
77
|
-
|
|
78
|
-
const min = timingsMs[0];
|
|
79
|
-
const max = timingsMs[n - 1];
|
|
80
|
-
const sum = timingsMs.reduce((acc, v) => acc + v, 0);
|
|
81
|
-
const mean = sum / n;
|
|
82
|
-
|
|
83
|
-
const p50 = timingsMs[Math.floor(n * 0.50)];
|
|
84
|
-
const p90 = timingsMs[Math.floor(n * 0.90)];
|
|
85
|
-
const p95 = timingsMs[Math.floor(n * 0.95)];
|
|
86
|
-
const p99 = timingsMs[Math.floor(n * 0.99)];
|
|
87
|
-
|
|
88
|
-
const variance = timingsMs.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / n;
|
|
89
|
-
const stdDev = Math.sqrt(variance);
|
|
90
|
-
|
|
91
|
-
// Throughput in MB/s (MegaBytes per second)
|
|
92
|
-
const payloadMB = payloadSizeBytes / (1024 * 1024);
|
|
93
|
-
const meanSeconds = mean / 1000;
|
|
94
|
-
const throughputMBs = meanSeconds > 0 ? (payloadMB / meanSeconds) : 0;
|
|
95
|
-
const opsPerSec = meanSeconds > 0 ? (1 / meanSeconds) : 0;
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
iterations: n,
|
|
99
|
-
min: Number(min.toFixed(4)),
|
|
100
|
-
mean: Number(mean.toFixed(4)),
|
|
101
|
-
p50: Number(p50.toFixed(4)),
|
|
102
|
-
p90: Number(p90.toFixed(4)),
|
|
103
|
-
p95: Number(p95.toFixed(4)),
|
|
104
|
-
p99: Number(p99.toFixed(4)),
|
|
105
|
-
max: Number(max.toFixed(4)),
|
|
106
|
-
stdDev: Number(stdDev.toFixed(4)),
|
|
107
|
-
throughputMBs: Number(throughputMBs.toFixed(2)),
|
|
108
|
-
opsPerSec: Math.round(opsPerSec)
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Single Benchmark Suite Runner
|
|
113
|
-
function benchmarkWorkload(label, targetBytes, warmupCount, measurementCount, profile, targetSlaMs = 1.0) {
|
|
114
|
-
const rawPayload = generateRealisticWorkload(targetBytes);
|
|
115
|
-
const actualBytes = Buffer.byteLength(rawPayload, 'utf8');
|
|
116
|
-
|
|
117
|
-
// Optional commercial evaluation license key from environment (defaults to Community tier)
|
|
118
|
-
const licenseKey = process.env.PRIVACYSCRUBBER_KEY || undefined;
|
|
119
|
-
|
|
120
|
-
// Warm-up phase (V8 TurboFan JIT optimization)
|
|
121
|
-
let warmupResult;
|
|
122
|
-
for (let w = 0; w < warmupCount; w++) {
|
|
123
|
-
warmupResult = sanitize(rawPayload, { profile, licenseKey });
|
|
124
|
-
restore(warmupResult.scrubbedText, warmupResult.tokenMap);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Verify Zero Data Loss Fidelity on the payload
|
|
128
|
-
const verification = sanitize(rawPayload, { profile, licenseKey });
|
|
129
|
-
const restored = restore(verification.scrubbedText, verification.tokenMap);
|
|
130
|
-
const isFidelityPreserved = (restored.restoredText === rawPayload);
|
|
131
|
-
const entityCount = verification.count || 0;
|
|
132
|
-
|
|
133
|
-
if (!isFidelityPreserved) {
|
|
134
|
-
throw new Error(`CRITICAL: Zero Data Loss check failed for ${label}! Plaintext mismatch after restore().`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Benchmark Phase 1: Inbound Sanitization (Tokenization)
|
|
138
|
-
const sanitizeTimings = [];
|
|
139
|
-
for (let m = 0; m < measurementCount; m++) {
|
|
140
|
-
const start = process.hrtime.bigint();
|
|
141
|
-
sanitize(rawPayload, { profile, licenseKey });
|
|
142
|
-
const end = process.hrtime.bigint();
|
|
143
|
-
const durationMs = Number(end - start) / 1e6;
|
|
144
|
-
sanitizeTimings.push(durationMs);
|
|
145
|
-
}
|
|
146
|
-
const sanitizeStats = computeStats(sanitizeTimings, actualBytes);
|
|
147
|
-
|
|
148
|
-
// Benchmark Phase 2: Outbound Restoration (Detokenization)
|
|
149
|
-
const restoreTimings = [];
|
|
150
|
-
const scrubbedSample = verification.scrubbedText;
|
|
151
|
-
const tokenMapSample = verification.tokenMap;
|
|
152
|
-
for (let m = 0; m < measurementCount; m++) {
|
|
153
|
-
const start = process.hrtime.bigint();
|
|
154
|
-
restore(scrubbedSample, tokenMapSample);
|
|
155
|
-
const end = process.hrtime.bigint();
|
|
156
|
-
const durationMs = Number(end - start) / 1e6;
|
|
157
|
-
restoreTimings.push(durationMs);
|
|
158
|
-
}
|
|
159
|
-
const restoreStats = computeStats(restoreTimings, actualBytes);
|
|
160
|
-
|
|
161
|
-
// Benchmark Phase 3: Combined End-to-End Roundtrip
|
|
162
|
-
const roundtripTimings = [];
|
|
163
|
-
for (let m = 0; m < measurementCount; m++) {
|
|
164
|
-
const start = process.hrtime.bigint();
|
|
165
|
-
const res = sanitize(rawPayload, { profile, licenseKey });
|
|
166
|
-
restore(res.scrubbedText, res.tokenMap);
|
|
167
|
-
const end = process.hrtime.bigint();
|
|
168
|
-
const durationMs = Number(end - start) / 1e6;
|
|
169
|
-
roundtripTimings.push(durationMs);
|
|
170
|
-
}
|
|
171
|
-
const roundtripStats = computeStats(roundtripTimings, actualBytes);
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
label,
|
|
175
|
-
sizeBytes: actualBytes,
|
|
176
|
-
sizeKB: Number((actualBytes / 1024).toFixed(1)),
|
|
177
|
-
entityCount,
|
|
178
|
-
fidelityPassed: isFidelityPreserved,
|
|
179
|
-
targetSlaMs,
|
|
180
|
-
slaPassed: sanitizeStats.p50 <= targetSlaMs,
|
|
181
|
-
sanitize: sanitizeStats,
|
|
182
|
-
restore: restoreStats,
|
|
183
|
-
roundtrip: roundtripStats
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Master Execution Function
|
|
188
|
-
export function runBenchmarks(options = {}) {
|
|
189
|
-
const profile = options.profile || REQUESTED_PROFILE;
|
|
190
|
-
const itersOverride = options.iterations || ITERATIONS_OVERRIDE;
|
|
191
|
-
|
|
192
|
-
const hasCommercialKey = Boolean(process.env.PRIVACYSCRUBBER_KEY);
|
|
193
|
-
const suites = [
|
|
194
|
-
{
|
|
195
|
-
label: '1 KB (Short Agentic Tool Call / Chat Turn)',
|
|
196
|
-
bytes: 1 * 1024,
|
|
197
|
-
warmup: 50,
|
|
198
|
-
iterations: itersOverride > 0 ? itersOverride : 500,
|
|
199
|
-
targetSlaMs: 0.8
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
label: '2 KB (Interactive LLM Chat Prompt)',
|
|
203
|
-
bytes: 2 * 1024,
|
|
204
|
-
warmup: 50,
|
|
205
|
-
iterations: itersOverride > 0 ? itersOverride : 500,
|
|
206
|
-
targetSlaMs: 1.0 // Sub-millisecond ZTDS SLA invariant
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
label: '4 KB (Multi-Entity Solid Pod / Clinical Note)',
|
|
210
|
-
bytes: 4 * 1024,
|
|
211
|
-
warmup: 30,
|
|
212
|
-
iterations: itersOverride > 0 ? itersOverride : 300,
|
|
213
|
-
targetSlaMs: 2.5
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
label: '10 KB (Enterprise Support Ticket / Incident)',
|
|
217
|
-
bytes: 10 * 1024,
|
|
218
|
-
warmup: 30,
|
|
219
|
-
iterations: itersOverride > 0 ? itersOverride : 250,
|
|
220
|
-
targetSlaMs: 5.0
|
|
221
|
-
},
|
|
222
|
-
...(hasCommercialKey ? [
|
|
223
|
-
{
|
|
224
|
-
label: '50 KB (RAG Context Chunk / Knowledge Retrieval)',
|
|
225
|
-
bytes: 50 * 1024,
|
|
226
|
-
warmup: 15,
|
|
227
|
-
iterations: itersOverride > 0 ? itersOverride : 100,
|
|
228
|
-
targetSlaMs: 25.0
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
label: '250 KB (Batch Processing / Multi-Page Document)',
|
|
232
|
-
bytes: 250 * 1024,
|
|
233
|
-
warmup: 5,
|
|
234
|
-
iterations: itersOverride > 0 ? itersOverride : 30,
|
|
235
|
-
targetSlaMs: 150.0
|
|
236
|
-
}
|
|
237
|
-
] : [])
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
const results = [];
|
|
241
|
-
for (const suite of suites) {
|
|
242
|
-
const result = benchmarkWorkload(
|
|
243
|
-
suite.label,
|
|
244
|
-
suite.bytes,
|
|
245
|
-
suite.warmup,
|
|
246
|
-
suite.iterations,
|
|
247
|
-
profile,
|
|
248
|
-
suite.targetSlaMs
|
|
249
|
-
);
|
|
250
|
-
results.push(result);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
timestamp: new Date().toISOString(),
|
|
255
|
-
environment: ENV_INFO,
|
|
256
|
-
profile,
|
|
257
|
-
results
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Output Formatting: Text Table (Zero Emojis)
|
|
262
|
-
function printTextReport(report) {
|
|
263
|
-
console.log("===============================================================================");
|
|
264
|
-
console.log("PrivacyScrubber Developer SDK (@privacyscrubber/sdk) Performance Benchmark");
|
|
265
|
-
console.log("Zero-Trust Data Sanitization (ZTDS) In-Memory Latency & SLA Verification");
|
|
266
|
-
console.log("===============================================================================");
|
|
267
|
-
console.log(`Runtime: ${report.environment.runtime} | V8: ${report.environment.v8Version}`);
|
|
268
|
-
console.log(`Platform: ${report.environment.platform}`);
|
|
269
|
-
console.log(`CPU: ${report.environment.cpu} (${report.environment.cores} cores)`);
|
|
270
|
-
console.log(`Profile: ${report.profile}`);
|
|
271
|
-
console.log(`Timestamp: ${report.timestamp}`);
|
|
272
|
-
console.log("-------------------------------------------------------------------------------\n");
|
|
273
|
-
|
|
274
|
-
for (const res of report.results) {
|
|
275
|
-
console.log(`[Payload: ${res.label}]`);
|
|
276
|
-
console.log(` Size: ${res.sizeKB} KB (${res.sizeBytes} bytes) | Entities Masked: ${res.entityCount}`);
|
|
277
|
-
console.log(` Fidelity (Zero Data Loss): ${res.fidelityPassed ? 'PASS (100% exact match)' : 'FAIL'}`);
|
|
278
|
-
console.log(` SLA Target: < ${res.targetSlaMs.toFixed(1)} ms | SLA Status: ${res.slaPassed ? 'PASSED' : 'FAILED'}`);
|
|
279
|
-
console.log("");
|
|
280
|
-
|
|
281
|
-
console.log(" Operation | p50 (Median) | p90 | p95 | p99 | Mean | Throughput");
|
|
282
|
-
console.log(" -------------+--------------+----------+----------+----------+----------+-----------");
|
|
283
|
-
|
|
284
|
-
const fmt = (v) => `${v.toFixed(3)} ms`.padEnd(8);
|
|
285
|
-
const tp = (t) => `${t.toFixed(1)} MB/s`.padEnd(10);
|
|
286
|
-
|
|
287
|
-
console.log(` Sanitize | ${fmt(res.sanitize.p50)} | ${fmt(res.sanitize.p90)} | ${fmt(res.sanitize.p95)} | ${fmt(res.sanitize.p99)} | ${fmt(res.sanitize.mean)} | ${tp(res.sanitize.throughputMBs)}`);
|
|
288
|
-
console.log(` Restore | ${fmt(res.restore.p50)} | ${fmt(res.restore.p90)} | ${fmt(res.restore.p95)} | ${fmt(res.restore.p99)} | ${fmt(res.restore.mean)} | ${tp(res.restore.throughputMBs)}`);
|
|
289
|
-
console.log(` Roundtrip | ${fmt(res.roundtrip.p50)} | ${fmt(res.roundtrip.p90)} | ${fmt(res.roundtrip.p95)} | ${fmt(res.roundtrip.p99)} | ${fmt(res.roundtrip.mean)} | ${tp(res.roundtrip.throughputMBs)}`);
|
|
290
|
-
console.log("");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
console.log("===============================================================================");
|
|
294
|
-
const interactivePayload = report.results.find(r => r.sizeBytes === 2 * 1024) || report.results[0];
|
|
295
|
-
let allSlasPassed = true;
|
|
296
|
-
if (interactivePayload) {
|
|
297
|
-
const p50 = interactivePayload.sanitize.p50;
|
|
298
|
-
const slaPassed = p50 < interactivePayload.targetSlaMs;
|
|
299
|
-
if (!slaPassed) allSlasPassed = false;
|
|
300
|
-
console.log(`ZTDS SUB-MILLISECOND SLA VERIFICATION (2KB Interactive Prompt < 1.00 ms):`);
|
|
301
|
-
console.log(` Measured p50: ${p50.toFixed(3)} ms -> ${slaPassed ? 'PASSED (Sub-millisecond SLA verified)' : 'FAILED'}`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const res of report.results) {
|
|
305
|
-
if (!res.slaPassed) allSlasPassed = false;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const allFidelityPassed = report.results.every(r => r.fidelityPassed);
|
|
309
|
-
console.log(`ZERO DATA LOSS FIDELITY (Bidirectional Plaintext Recovery):`);
|
|
310
|
-
console.log(` All Payloads: ${allFidelityPassed ? 'PASSED (100% exact round-trip match)' : 'FAILED'}`);
|
|
311
|
-
console.log("===============================================================================\n");
|
|
312
|
-
return allSlasPassed && allFidelityPassed;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Output Formatting: Markdown Table (for Documentation / Procurement Memos)
|
|
316
|
-
function printMarkdownReport(report) {
|
|
317
|
-
console.log(`## PrivacyScrubber SDK In-Memory Performance Benchmark Report\n`);
|
|
318
|
-
console.log(`- **Audit Timestamp:** \`${report.timestamp}\``);
|
|
319
|
-
console.log(`- **Host Runtime:** \`${report.environment.runtime}\` on \`${report.environment.platform}\``);
|
|
320
|
-
console.log(`- **CPU Architecture:** \`${report.environment.cpu}\` (${report.environment.cores} vCPUs)`);
|
|
321
|
-
console.log(`- **Sanitization Profile:** \`${report.profile}\`\n`);
|
|
322
|
-
|
|
323
|
-
console.log(`| Workload | Payload | Entities | Sanitize (p50) | Sanitize (p99) | Restore (p50) | Throughput | Target SLA | Status | Fidelity |`);
|
|
324
|
-
console.log(`| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |`);
|
|
325
|
-
|
|
326
|
-
for (const r of report.results) {
|
|
327
|
-
const sla = `< ${r.targetSlaMs} ms`;
|
|
328
|
-
const status = r.slaPassed ? 'PASS' : 'FAILED';
|
|
329
|
-
const fidelity = r.fidelityPassed ? 'PASS (100%)' : 'FAIL';
|
|
330
|
-
console.log(`| ${r.label} | ${r.sizeKB} KB | ${r.entityCount} | **${r.sanitize.p50} ms** | ${r.sanitize.p99} ms | **${r.restore.p50} ms** | **${r.sanitize.throughputMBs} MB/s** | ${sla} | **${status}** | ${fidelity} |`);
|
|
331
|
-
}
|
|
332
|
-
console.log("");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Direct Execution Entrypoint
|
|
336
|
-
if (process.argv[1] && process.argv[1].endsWith('benchmark.js')) {
|
|
337
|
-
try {
|
|
338
|
-
const report = runBenchmarks();
|
|
339
|
-
|
|
340
|
-
if (IS_JSON) {
|
|
341
|
-
console.log(JSON.stringify(report, null, 2));
|
|
342
|
-
} else if (IS_MARKDOWN) {
|
|
343
|
-
printMarkdownReport(report);
|
|
344
|
-
} else {
|
|
345
|
-
const passed = printTextReport(report);
|
|
346
|
-
if (ASSERT_SLA && !passed) {
|
|
347
|
-
console.error("ERROR: SLA assertion failed. Latency or fidelity bounds exceeded.");
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
} catch (err) {
|
|
352
|
-
console.error("Benchmark Execution Error:", err);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
}
|