@privacyscrubber/sdk 2.4.5 → 2.4.7

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/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
- function sanitizeDeepValue(val, engine, sessionTokenMap) {
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
- out[k] = sanitizeDeepValue(v, engine, sessionTokenMap);
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 = options.engine || new PrivacyScrubberEngine(options);
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' && typeof chunk.textDelta === 'string') {
205
- const restored = detok.feed(chunk.textDelta);
206
- if (restored) controller.enqueue({ ...chunk, textDelta: restored });
207
- } else if (chunk && chunk.type === 'reasoning' && typeof chunk.textDelta === 'string') {
208
- const restored = detok.feed(chunk.textDelta);
209
- if (restored) controller.enqueue({ ...chunk, textDelta: restored });
210
- } else {
211
- controller.enqueue(chunk);
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) controller.enqueue({ type: 'text-delta', textDelta: 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' && typeof chunk.textDelta === 'string') {
223
- const restored = detok.feed(chunk.textDelta);
224
- if (restored) yield { ...chunk, textDelta: restored };
225
- } else if (chunk && chunk.type === 'reasoning' && typeof chunk.textDelta === 'string') {
226
- const restored = detok.feed(chunk.textDelta);
227
- if (restored) yield { ...chunk, textDelta: restored };
228
- } else {
229
- yield chunk;
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) yield { type: 'text-delta', textDelta: 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
- }