@privacyscrubber/sdk 2.2.0 → 2.2.2

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/index.js CHANGED
@@ -43,7 +43,12 @@ export const DEVOPS_SECRETS_DETECTOR = [
43
43
  { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g, label: 'STRIPE_KEY' },
44
44
  { name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
45
45
  { name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
46
- { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi, label: 'SECRET' }
46
+ { name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
47
+ { name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
48
+ { name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
49
+ { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi, label: 'SECRET' },
50
+ { name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
51
+ { name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
47
52
  ];
48
53
 
49
54
  /**
@@ -164,7 +169,8 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
164
169
  */
165
170
  export function formatAuditReceipt(telemetry) {
166
171
  if (!telemetry || telemetry.totalCount === 0) {
167
- return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n";
172
+ return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
173
+ "> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
168
174
  }
169
175
  const entitiesList = Object.entries(telemetry.entities)
170
176
  .map(([type, count]) => `${count} ${type}`)
@@ -175,28 +181,203 @@ export function formatAuditReceipt(telemetry) {
175
181
  return `> 🛡️ **PrivacyScrubber Audit Receipt**\n` +
176
182
  `> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
177
183
  `> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n` +
178
- `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n`;
184
+ `> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
185
+ `> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
186
+ }
187
+
188
+ let communityNoticeLogged = false;
189
+
190
+ function emitCommunityNoticeIfNeeded(isPro) {
191
+ if (communityNoticeLogged || isPro) return;
192
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
193
+ communityNoticeLogged = true;
194
+ console.info('[@privacyscrubber/sdk] Running Community Edition (Core PII).\nTo unlock 25 Enterprise Profiles & DevOps Secrets in production: https://privacyscrubber.com/pricing/?tier=sdk');
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Resets the community notice state for testing purposes.
200
+ * @private
201
+ */
202
+ export function _resetCommunityNoticeForTesting() {
203
+ communityNoticeLogged = false;
204
+ }
205
+
206
+ let devSecretNoticeLogged = false;
207
+
208
+ function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
209
+ if (devSecretNoticeLogged || isPro) return;
210
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;
211
+
212
+ const tokens = Object.keys(tokenMap || {});
213
+ const secretTokens = tokens.filter(t => {
214
+ const upper = t.toUpperCase();
215
+ return ['API_KEY', 'SECRET', 'PASSWORD', 'KEY', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN', 'TOKEN'].some(k => upper.includes(k));
216
+ });
217
+
218
+ if (secretTokens.length > 0) {
219
+ devSecretNoticeLogged = true;
220
+ const count = secretTokens.length;
221
+ const secretSample = secretTokens.slice(0, 2).join(', ');
222
+ if (typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
223
+ process.stderr.write(
224
+ `\n[PrivacyScrubber] Intercepted ${count} API secret(s) before LLM egress: ${secretSample}\n` +
225
+ `[PrivacyScrubber] Protected in local RAM (0 byte network egress). Free Tier active (${limit.toLocaleString()} char cap).\n` +
226
+ `[PrivacyScrubber] Developer SDK ($199/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Resets the dev secret notice state for testing purposes.
234
+ * @private
235
+ */
236
+ export function _resetDevSecretNoticeForTesting() {
237
+ devSecretNoticeLogged = false;
238
+ }
239
+
240
+ /**
241
+ * Generates an executive CISO & Legal Procurement Memo for enterprise approval of @privacyscrubber/sdk.
242
+ *
243
+ * @param {object} [options={}] - Custom memo configuration options.
244
+ * @param {string} [options.appName='Internal AI Applications & LLM Pipelines'] - Target system name.
245
+ * @param {string} [options.developer='Engineering / AI Platform Team'] - Authoring team name.
246
+ * @param {'markdown'|'text'} [options.format='markdown'] - Output format ('markdown' or 'text').
247
+ * @param {number} [options.teamSize=10] - Size of engineering team.
248
+ * @param {string} [options.estimatedVolume='50M - 500M characters/month'] - Projected token volume.
249
+ * @returns {string}
250
+ */
251
+ export function generateProcurementMemo(options = {}) {
252
+ const {
253
+ appName = 'Internal AI Applications & LLM Pipelines',
254
+ developer = 'Engineering / AI Platform Team',
255
+ format = 'markdown',
256
+ teamSize = 10,
257
+ estimatedVolume = '50M - 500M characters/month'
258
+ } = options;
259
+
260
+ if (format === 'text') {
261
+ return `MEMORANDUM: ADOPTION OF PRIVACYSCRUBBER DEVELOPER SDK (ZTDS STANDARD)
262
+
263
+ TO: Chief Information Security Officer (CISO), Data Protection Officer (DPO), VP Engineering
264
+ FROM: ${developer}
265
+ SUBJECT: Approval Request: Zero-Trust Data Sanitization (@privacyscrubber/sdk)
266
+ TARGET SYSTEM: ${appName}
267
+ ESTIMATED VOLUME: ${estimatedVolume}
268
+
269
+ 1. EXECUTIVE SUMMARY
270
+ We propose standardizing on the PrivacyScrubber Developer SDK (@privacyscrubber/sdk) for in-memory Zero-Trust Data Sanitization across our AI ingestion pipelines. The SDK intercepts and redacts PII, regulatory identifiers, and cloud secrets locally in RAM before any prompt leaves our infrastructure.
271
+
272
+ 2. ZERO-TRUST ARCHITECTURAL INVARIANTS
273
+ * Zero Network Egress: 100% local in-memory processing. Zero external API calls, zero telemetry egress, zero third-party sub-processors.
274
+ * In-Memory Volatility: All masking maps remain in volatile process RAM and are discarded after request completion.
275
+ * Sub-Millisecond Latency: Executes in <0.1ms per prompt in local RAM, eliminating latency overhead.
276
+
277
+ 3. COMPLIANCE & REGULATORY COVERAGE
278
+ Pre-configured for 25 specialized industry profiles and compliance frameworks:
279
+ * GDPR (Art. 4, 9, 32) & CCPA/CPRA: Ephemeral tokenization.
280
+ * HIPAA (§164.514 Safe Harbor): MRNs, medical record codes, patient identifiers.
281
+ * SOC 2 Type II & ISO 27001 (A.8.11): Zero persistent unencrypted PII.
282
+ * PCI DSS v4.0: PAN, CVV, IBAN, bank ledger sanitization.
283
+ * NIST SP 800-53: Cloud API keys, JWT tokens, AWS/Stripe secret leak protection.
284
+
285
+ 4. TOTAL COST OF OWNERSHIP (TCO) COMPARISON
286
+ * Cloud DLP Proxies (AWS Macie, GCP DLP, Nightfall):
287
+ - Cost: $1,500 - $4,000 / month (usage-based billing).
288
+ - Latency: +200ms - 400ms added network roundtrip per AI prompt.
289
+ - Sub-processor Risk: Requires vendor DPA, external network egress.
290
+ * PrivacyScrubber Developer SDK:
291
+ - Cost: $199 / month flat (or $1,990 / year) for unlimited nodes, microservices, and prompts.
292
+ - Latency: <0.1ms (in-process RAM).
293
+ - Sub-processor Risk: Zero (pure in-memory software library).
294
+
295
+ 5. ACTION REQUESTED
296
+ Approve commercial license upgrade for ${developer} (${teamSize} engineers, unlimited backend nodes):
297
+ * License Tier: Developer SDK ($199/month flat or $1,990/year)
298
+ * Direct Procurement & License Activation: https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo
299
+ * Enterprise / Invoicing Contact: enterprise@privacyscrubber.com
300
+ `;
301
+ }
302
+
303
+ return `# Internal Memo: Adoption of PrivacyScrubber Developer SDK (ZTDS Standard)
304
+
305
+ | **To** | Chief Information Security Officer (CISO), Data Protection Officer (DPO), VP Engineering |
306
+ | :--- | :--- |
307
+ | **From** | ${developer} |
308
+ | **Date** | ${new Date().toISOString().split('T')[0]} |
309
+ | **Subject** | Architectural Approval & License Procurement for Zero-Trust AI Sanitization |
310
+ | **Target System** | ${appName} |
311
+ | **Estimated Scope** | ~${teamSize} engineers / ${estimatedVolume} |
312
+
313
+ ---
314
+
315
+ ### 1. Executive Summary
316
+ We recommend adopting the **PrivacyScrubber Developer SDK** (\`@privacyscrubber/sdk\`) to enforce **Zero-Trust Data Sanitization (ZTDS)** across all internal AI pipelines, LangChain agents, microservices, and developer toolchains.
317
+
318
+ The SDK redacts PII, regulatory identifiers, and developer secrets **in-process before external API egress to OpenAI, Anthropic, or external LLM providers**, and deterministically restores original values upon response return.
319
+
320
+ ---
321
+
322
+ ### 2. Architectural Invariants (Zero-Trust Data Sanitization)
323
+ * **Zero Remote Network Egress:** 100% of sanitization and token restoration executes in local Node.js process RAM. No prompt data or telemetry ever leaves our VPC/infrastructure.
324
+ * **Ephemeral In-Memory Tokens:** Masking mappings (\`[EMAIL_1]\`, \`[NAME_1]\`, \`[SECRET_1]\`) are volatile and strictly scoped to request execution context.
325
+ * **Ultra-Low Latency:** Micro-benchmarks verify <0.1ms execution overhead per prompt, compared to 200–400ms for network-based cloud DLP proxies.
326
+ * **Full Reversibility:** Bidirectional round-trip reconstruction ensures downstream application logic receives intact responses without exposing sensitive data to model providers.
327
+
328
+ ---
329
+
330
+ ### 3. Compliance Framework Mapping
331
+ The SDK enforces out-of-the-box sanitization matching mandatory compliance controls:
332
+ * **GDPR (Regulation (EU) 2016/679) & CCPA/CPRA:** Eliminates cross-border PII transfer risk to third-party AI models.
333
+ * **HIPAA (§164.514 Safe Harbor):** Automated scrubbing of Medical Record Numbers (MRNs), patient identifiers, and health insurance IDs.
334
+ * **SOC 2 Type II & ISO 27001 (Control A.8.11):** Enforces data masking and prevents unauthorized logging of confidential client data.
335
+ * **PCI DSS v4.0:** Sanitization of payment cards, IBANs, and financial ledger data.
336
+ * **NIST SP 800-53:** DevOps secret interception for AWS keys, JWT tokens, database URIs, and private API keys.
337
+
338
+ ---
339
+
340
+ ### 4. Financial & Architectural Comparison (TCO)
341
+
342
+ | Evaluation Factor | Traditional Cloud DLP (AWS/GCP/Nightfall) | PrivacyScrubber Developer SDK |
343
+ | :--- | :--- | :--- |
344
+ | **Pricing Model** | \$1,500 – \$4,000 / month (metered per GB/token) | **\$199 / month flat** (unlimited nodes & tokens) |
345
+ | **Network Egress** | Requires outbound network calls to DLP vendor | **0 bytes** (100% local Node.js process memory) |
346
+ | **Added Latency** | +200ms – +450ms network round-trip | **<0.1ms** (in-memory regular expressions) |
347
+ | **Vendor Sub-processors** | Third-party cloud vendor added to vendor list | **None** (zero-network software library) |
348
+ | **Deployment** | Complex API gateways and proxy configurations | \`npm install @privacyscrubber/sdk\` in 3 minutes |
349
+
350
+ ---
351
+
352
+ ### 5. Procurement Recommendation
353
+ We request approval to acquire a **Developer SDK License** (\$199/month flat or \$1,990/year) to unlock unlimited throughput across all microservices and CI/CD pipelines.
354
+
355
+ * **Direct Procurement Portal:** [https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo](https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo)
356
+ * **Procurement / Invoicing Contact:** \`enterprise@privacyscrubber.com\`
357
+ `;
179
358
  }
180
359
 
181
360
  /**
182
- * Primary High-Level Function: Sanitize / Scrub text of PII and secrets.
361
+ * Sanitizes input text, masking detected PII & secrets with reversible tokens.
183
362
  *
184
- * @param {string} text - Raw input text containing potential PII/secrets.
185
- * @param {object} [options={}] - Configuration options.
186
- * @param {string} [options.profile='General'] - PII Profile (General, Dev, Medical, Financial, Legal, HR, etc.).
363
+ * @param {string} text - Raw input text.
364
+ * @param {object} [options={}] - Sanitization options.
365
+ * @param {string} [options.profile='General'] - Compliance detection profile.
187
366
  * @param {string} [options.licenseKey] - Optional license key. Falls back to process.env.PRIVACYSCRUBBER_KEY.
188
- * @param {Array} [options.customRules=[]] - Custom regex rules array.
189
- * @param {Record<string, string>} [options.tokenLabelMap={}] - Custom label overrides.
190
- * @param {Record<string, string>} [options.existingSessionMap={}] - Session map to maintain consistency across turns.
191
- * @param {boolean} [options.generateReceipt=true] - Whether to generate Markdown audit receipt.
367
+ * @param {Array<{name: string, pattern: string, regex: RegExp, label?: string}>} [options.customRules=[]] - Custom regex rules.
368
+ * @param {Record<string, string>} [options.tokenLabelMap={}] - Custom label mapping.
369
+ * @param {Record<string, string>} [options.existingSessionMap={}] - In-memory session map to preserve token IDs across turns.
370
+ * @param {boolean} [options.generateReceipt=true] - Generate a CISO Markdown audit receipt.
192
371
  * @param {boolean} [options.throwOnLimit=true] - Throw PrivacyScrubberLicenseError when free tier limit is hit.
193
- * @param {boolean} [options.detectSecrets=false] - Auto-inject DevOps & Cloud secrets detection rules.
372
+ * @param {'throw'|'truncate'} [options.onLimit='throw'] - Action when character limit exceeded ('throw' or 'truncate').
373
+ * @param {boolean} [options.detectSecrets=false] - Auto-detect cloud credentials and API keys.
374
+ * @param {Set<string>|Array<string>} [options.ignoreList=null] - Values to preserve in cleartext.
194
375
  * @returns {{
195
376
  * scrubbedText: string,
196
377
  * tokenMap: Record<string, string>,
197
378
  * count: number,
198
379
  * executionMs: number,
199
- * license: { isPro: boolean, tier: 'PRO'|'TEAMS'|'FREE', limit: number, upgradeUrl?: string },
380
+ * license: { isPro: boolean, tier: 'PRO'|'TEAMS'|'FREE', limit: number, wasTruncated: boolean, upgradeUrl?: string, recommendation?: string },
200
381
  * telemetry: object,
201
382
  * auditReceipt: string
202
383
  * }}
@@ -208,7 +389,7 @@ export function sanitize(text, options = {}) {
208
389
  tokenMap: {},
209
390
  count: 0,
210
391
  executionMs: 0,
211
- license: { isPro: false, tier: 'FREE', limit: 15000 },
392
+ license: { isPro: false, tier: 'FREE', limit: 15000, wasTruncated: false },
212
393
  telemetry: buildAuditTelemetry({}),
213
394
  auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
214
395
  };
@@ -222,6 +403,7 @@ export function sanitize(text, options = {}) {
222
403
  existingSessionMap = {},
223
404
  generateReceipt = true,
224
405
  throwOnLimit = true,
406
+ onLimit = 'throw',
225
407
  detectSecrets = false,
226
408
  ignoreList = null
227
409
  } = options;
@@ -230,12 +412,17 @@ export function sanitize(text, options = {}) {
230
412
  const isPro = licenseCheck.valid;
231
413
  const tier = isPro ? licenseCheck.tier : 'FREE';
232
414
 
415
+ emitCommunityNoticeIfNeeded(isPro);
416
+
233
417
  const isSpecialized = profile && profile.toLowerCase() !== 'general';
234
418
  const limit = isPro ? Infinity : (isSpecialized ? 5000 : 15000);
235
419
 
420
+ const shouldThrow = throwOnLimit && onLimit !== 'truncate';
421
+ let wasTruncated = false;
422
+
236
423
  if (!isPro && text.length > limit) {
237
- const errorMsg = `[PrivacyScrubber] Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to PRO, TEAMS, or Developer SDK for unlimited throughput: https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
238
- if (throwOnLimit) {
424
+ if (shouldThrow) {
425
+ const errorMsg = `[PrivacyScrubber] Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to PRO, TEAMS, or Developer SDK for unlimited throughput: https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
239
426
  throw new PrivacyScrubberLicenseError(errorMsg, {
240
427
  tier: 'FREE',
241
428
  currentLength: text.length,
@@ -243,6 +430,7 @@ export function sanitize(text, options = {}) {
243
430
  profile: profile
244
431
  });
245
432
  }
433
+ wasTruncated = true;
246
434
  }
247
435
 
248
436
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
@@ -282,8 +470,15 @@ export function sanitize(text, options = {}) {
282
470
  const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
283
471
  const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
284
472
 
473
+ emitDevSecretNoticeIfNeeded(isPro, rawResult.tokenMap, limit);
474
+
475
+ let finalScrubbedText = rawResult.scrubbedText;
476
+ if (wasTruncated && (onLimit === 'truncate' || throwOnLimit === false)) {
477
+ finalScrubbedText += `\n\n<!-- [PrivacyScrubber] Input truncated at free tier limit (${limit.toLocaleString()} chars). For unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&src=truncation -->`;
478
+ }
479
+
285
480
  return {
286
- scrubbedText: rawResult.scrubbedText,
481
+ scrubbedText: finalScrubbedText,
287
482
  tokenMap: rawResult.tokenMap || {},
288
483
  count: rawResult.count || 0,
289
484
  executionMs: executionMs,
@@ -291,6 +486,7 @@ export function sanitize(text, options = {}) {
291
486
  isPro: isPro,
292
487
  tier: tier,
293
488
  limit: limit,
489
+ wasTruncated: wasTruncated,
294
490
  ...(!isPro && {
295
491
  upgradeUrl: 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_telemetry&utm_campaign=dev_upsell',
296
492
  recommendation: 'Upgrade to PRO, TEAMS, or Developer SDK for unlimited text processing, custom regex rules, and specialized compliance profiles.'
@@ -306,6 +502,18 @@ export function sanitize(text, options = {}) {
306
502
  */
307
503
  export const scrub = sanitize;
308
504
 
505
+ /**
506
+ * Ergonomic 1-liner: Sanitizes text and returns the scrubbed string directly.
507
+ * @param {string} text - Input text.
508
+ * @param {string} [profile='General'] - Target compliance profile.
509
+ * @returns {string} - Scrubbed string.
510
+ */
511
+ export function clean(text, profile = 'General') {
512
+ if (!text || typeof text !== 'string') return '';
513
+ const { scrubbedText } = sanitize(text, { profile });
514
+ return scrubbedText;
515
+ }
516
+
309
517
  /**
310
518
  * Primary High-Level Function: Restore / Unscrub AI response back to original entities in-memory.
311
519
  *
@@ -428,12 +636,34 @@ export class PrivacyScrubberEngine {
428
636
  clearIgnoreList() {
429
637
  this.ignoreList.clear();
430
638
  }
639
+
640
+ /**
641
+ * Creates a streaming token restorer bound to this engine's session map.
642
+ * @param {Record<string, string>} [tokenMap=null]
643
+ * @param {object} [options={}]
644
+ */
645
+ createStreamRestorer(tokenMap = null, options = {}) {
646
+ const activeMap = tokenMap || this.sessionMap;
647
+ return createStreamTokenRestorer(activeMap, options);
648
+ }
649
+
650
+ /**
651
+ * Wraps an async iterable LLM stream with real-time de-tokenization.
652
+ * @param {AsyncIterable<any>} stream
653
+ * @param {Record<string, string>} [tokenMap=null]
654
+ * @param {object} [options={}]
655
+ */
656
+ async* wrapStream(stream, tokenMap = null, options = {}) {
657
+ const activeMap = tokenMap || this.sessionMap;
658
+ yield* wrapAiStream(stream, activeMap, options);
659
+ }
431
660
  }
432
661
 
433
662
  /**
434
663
  * Transparent Zero-Trust Wrapper for OpenAI Client.
435
664
  * Automatically sanitizes outgoing messages before transmission to OpenAI,
436
665
  * and restores completion tokens in the assistant response before returning!
666
+ * Seamlessly de-tokenizes streaming responses in real-time when stream: true.
437
667
  *
438
668
  * @param {object} openaiClient - Instance of OpenAI SDK (e.g. new OpenAI()).
439
669
  * @param {object} [options={}] - PrivacyScrubber configuration.
@@ -465,6 +695,12 @@ export function wrapOpenAI(openaiClient, options = {}) {
465
695
  const modifiedParams = { ...params, messages: sanitizedMessages };
466
696
  const response = await originalCreate(modifiedParams, requestOptions);
467
697
 
698
+ // Streaming support: transparently de-tokenize async iterable chunks
699
+ if (params.stream) {
700
+ const detok = new StreamingDetokenizer(sessionTokenMap);
701
+ return detok.wrapStream(response);
702
+ }
703
+
468
704
  if (response && Array.isArray(response.choices)) {
469
705
  response.choices.forEach(choice => {
470
706
  if (choice.message && typeof choice.message.content === 'string') {
@@ -490,6 +726,370 @@ export function createLangChainTransform(options = {}) {
490
726
  };
491
727
  }
492
728
 
729
+ /**
730
+ * Helper to escape special regex characters.
731
+ */
732
+ function escapeRegex(s) {
733
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
734
+ }
735
+
736
+ /**
737
+ * Builds an ultra-fast streaming token replacement function.
738
+ * Preserves all whitespace, indentation, newlines, and code blocks (does NOT trim).
739
+ * Handles descending key ordering (e.g. [NAME_10] before [NAME_1]), case insensitivity,
740
+ * alternative delimiters (<...>, {{...}}, __...__), and possessive suffixes.
741
+ *
742
+ * @param {Record<string, string>} tokenMap
743
+ * @returns {(text: string) => string}
744
+ */
745
+ export function buildStreamReplacer(tokenMap = {}) {
746
+ if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
747
+ return (text) => text || '';
748
+ }
749
+
750
+ const rules = [];
751
+ const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
752
+
753
+ for (const k of sortedKeys) {
754
+ const val = tokenMap[k];
755
+ const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
756
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
757
+
758
+ if (match) {
759
+ const label = escapeRegex(match[1]);
760
+ const idx = match[2];
761
+ const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)(?:\'s|’s|s)?';
762
+ rules.push({
763
+ regex: new RegExp(patternStr, 'gi'),
764
+ val,
765
+ k
766
+ });
767
+ } else {
768
+ const safe = escapeRegex(k);
769
+ rules.push({
770
+ regex: new RegExp(safe + '(?:\'s|’s|s)?', 'gi'),
771
+ val,
772
+ k
773
+ });
774
+ }
775
+ }
776
+
777
+ return function replaceTokens(text) {
778
+ if (!text) return '';
779
+ let result = text;
780
+ for (let i = 0; i < rules.length; i++) {
781
+ const rule = rules[i];
782
+ rule.regex.lastIndex = 0;
783
+ result = result.replace(rule.regex, (m) => {
784
+ const isPossessive = /('s|’s)$/i.test(m);
785
+ return rule.val + (isPossessive ? "'s" : '');
786
+ });
787
+ }
788
+ return result;
789
+ };
790
+ }
791
+
792
+ /**
793
+ * Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
794
+ * Seamlessly restores split tokens (e.g. [EM + AIL_1]), preserves multi-byte UTF-8,
795
+ * and maintains zero-latency streaming throughput.
796
+ */
797
+ export class StreamingDetokenizer {
798
+ constructor(tokenMap = {}, options = {}) {
799
+ this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
800
+ this.options = options || {};
801
+ this.replacer = buildStreamReplacer(this.tokenMap);
802
+ this.pendingBuffer = '';
803
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
804
+ this.decoder = new TextDecoder('utf-8', { fatal: false });
805
+ }
806
+
807
+ /**
808
+ * Feeds an incoming chunk (string, Uint8Array, or Buffer) and returns de-tokenized text.
809
+ * Buffers any unclosed token candidates at chunk boundaries until closed or flushed.
810
+ * @param {string | Uint8Array | Buffer} chunk
811
+ * @returns {string}
812
+ */
813
+ feed(chunk) {
814
+ if (chunk === undefined || chunk === null) return '';
815
+ let text = '';
816
+ if (typeof chunk === 'string') {
817
+ text = chunk;
818
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
819
+ text = this.decoder.decode(chunk, { stream: true });
820
+ } else {
821
+ text = String(chunk);
822
+ }
823
+
824
+ if (!text) return '';
825
+ if (!this.hasTokens) return text;
826
+
827
+ this.pendingBuffer += text;
828
+
829
+ // Candidate unclosed token at tail of buffer (up to 64 chars)
830
+ // Matches incomplete tokens starting with [, <, {{, or __
831
+ const m = this.pendingBuffer.match(/(?:\[|<|\{\{|__)[A-Za-z0-9_\-А-Яа-яЁё ]{0,64}$/);
832
+ if (m) {
833
+ const safeToEmit = this.pendingBuffer.slice(0, m.index);
834
+ this.pendingBuffer = this.pendingBuffer.slice(m.index);
835
+ return safeToEmit ? this.replacer(safeToEmit) : '';
836
+ }
837
+
838
+ const toEmit = this.pendingBuffer;
839
+ this.pendingBuffer = '';
840
+ return this.replacer(toEmit);
841
+ }
842
+
843
+ /**
844
+ * Alias for feed() to maintain backwards compatibility with process() callers.
845
+ */
846
+ process(chunk) {
847
+ return this.feed(chunk);
848
+ }
849
+
850
+ /**
851
+ * Flushes remaining buffered tokens and finalizes UTF-8 stream decoder.
852
+ * @returns {string}
853
+ */
854
+ flush() {
855
+ let text = '';
856
+ try {
857
+ text = this.decoder.decode(new Uint8Array(), { stream: false });
858
+ } catch (_) {}
859
+
860
+ if (text) {
861
+ this.pendingBuffer += text;
862
+ }
863
+
864
+ if (!this.pendingBuffer) return '';
865
+ const remaining = this.pendingBuffer;
866
+ this.pendingBuffer = '';
867
+ return this.replacer(remaining);
868
+ }
869
+
870
+ /**
871
+ * Returns a native Node.js Transform stream instance for .pipe() / pipeline().
872
+ * @returns {Transform}
873
+ */
874
+ toNodeTransform() {
875
+ const self = this;
876
+ return new Transform({
877
+ transform(chunk, encoding, callback) {
878
+ try {
879
+ const out = self.feed(chunk);
880
+ if (out) this.push(out);
881
+ callback();
882
+ } catch (err) {
883
+ callback(err);
884
+ }
885
+ },
886
+ flush(callback) {
887
+ try {
888
+ const out = self.flush();
889
+ if (out) this.push(out);
890
+ callback();
891
+ } catch (err) {
892
+ callback(err);
893
+ }
894
+ }
895
+ });
896
+ }
897
+
898
+ /**
899
+ * Wraps an async iterable (e.g. LLM streaming response) yielding restored chunks.
900
+ * @param {AsyncIterable<any>} asyncIterable
901
+ * @returns {AsyncGenerator<any>}
902
+ */
903
+ async* wrapStream(asyncIterable) {
904
+ for await (const chunk of asyncIterable) {
905
+ if (chunk === null || chunk === undefined) continue;
906
+ // Handle OpenAI chunk object
907
+ if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
908
+ const delta = chunk.choices[0].delta.content;
909
+ if (typeof delta === 'string') {
910
+ const restored = this.feed(delta);
911
+ if (restored) {
912
+ chunk.choices[0].delta.content = restored;
913
+ yield chunk;
914
+ }
915
+ } else {
916
+ yield chunk;
917
+ }
918
+ continue;
919
+ }
920
+
921
+ let text = '';
922
+ if (typeof chunk === 'string') {
923
+ text = chunk;
924
+ } else if (typeof chunk?.text === 'string') {
925
+ text = chunk.text;
926
+ } else if (typeof chunk?.content === 'string') {
927
+ text = chunk.content;
928
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
929
+ text = chunk;
930
+ } else {
931
+ text = String(chunk);
932
+ }
933
+
934
+ const emitted = this.feed(text);
935
+ if (emitted) yield emitted;
936
+ }
937
+
938
+ const finalChunk = this.flush();
939
+ if (finalChunk) yield finalChunk;
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Creates a zero-latency streaming token restorer for LLM Server-Sent Events (SSE) and Web Streams.
945
+ * Solves the broken chunk boundary problem where tokens like [EMAIL_1] are split across multiple chunks.
946
+ *
947
+ * Works with:
948
+ * - Vercel AI SDK (streamText / createDataStreamResponse)
949
+ * - OpenAI streaming (chat.completions.create({ stream: true }))
950
+ * - LangChain streaming
951
+ * - Native Web Streams (ReadableStream.pipeThrough)
952
+ * - Async generators (for await ... of)
953
+ *
954
+ * @param {Record<string, string>} tokenMap - Volatile token mapping object.
955
+ * @param {object} [options] - Configuration options.
956
+ * @returns {TransformStream & { process: (chunk: string) => string, feed: (chunk: string) => string, flush: () => string, wrapStream: (asyncIterable: AsyncIterable<any>) => AsyncGenerator<any>, toNodeTransform: () => Transform, detokenizer: StreamingDetokenizer }}
957
+ */
958
+ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
959
+ const detok = new StreamingDetokenizer(tokenMap, options);
960
+
961
+ let stream;
962
+ if (typeof TransformStream !== 'undefined') {
963
+ stream = new TransformStream({
964
+ transform(chunk, controller) {
965
+ const out = detok.feed(chunk);
966
+ if (out) controller.enqueue(out);
967
+ },
968
+ flush(controller) {
969
+ const out = detok.flush();
970
+ if (out) controller.enqueue(out);
971
+ }
972
+ });
973
+ } else {
974
+ stream = {};
975
+ }
976
+
977
+ stream.process = (chunk) => detok.feed(chunk);
978
+ stream.feed = (chunk) => detok.feed(chunk);
979
+ stream.flush = () => detok.flush();
980
+ stream.wrapStream = (asyncIterable) => detok.wrapStream(asyncIterable);
981
+ stream.toNodeTransform = () => detok.toNodeTransform();
982
+ stream.detokenizer = detok;
983
+
984
+ return stream;
985
+ }
986
+
987
+ /**
988
+ * Creates a Node.js native Transform stream for backend streaming pipelines (stream.pipeline / .pipe()).
989
+ * @param {Record<string, string>} tokenMap - Volatile token mapping object.
990
+ * @param {object} [options] - Configuration options.
991
+ * @returns {Transform}
992
+ */
993
+ export function createNodeStreamTokenRestorer(tokenMap = {}, options = {}) {
994
+ const detok = new StreamingDetokenizer(tokenMap, options);
995
+ return detok.toNodeTransform();
996
+ }
997
+
998
+ /**
999
+ * Wraps an async iterable LLM stream (e.g. from Vercel AI SDK textStream or OpenAI stream)
1000
+ * and yields de-tokenized text chunks in real-time, seamlessly stitching split tokens across chunks.
1001
+ *
1002
+ * @param {AsyncIterable<string|object>} stream - The incoming LLM stream.
1003
+ * @param {Record<string, string>} tokenMap - Volatile token map.
1004
+ * @returns {AsyncGenerator<any>}
1005
+ */
1006
+ export async function* wrapAiStream(stream, tokenMap = {}) {
1007
+ const detok = new StreamingDetokenizer(tokenMap);
1008
+ yield* detok.wrapStream(stream);
1009
+ }
1010
+
1011
+ /**
1012
+ * Middleware wrapper for Next.js App Router Route Handlers (POST/PUT).
1013
+ * Automatically sanitizes incoming prompt payloads in RAM (<1ms) before the handler runs,
1014
+ * and attaches sanitized text, tokenMap, and helper restoration functions.
1015
+ *
1016
+ * @param {Function} handler - The async Next.js route handler (req, context) => Response
1017
+ * @param {object} [options] - Sanitization options (profile, detectSecrets, etc.)
1018
+ * @returns {Function} Wrapped Next.js route handler
1019
+ */
1020
+ export function withPrivacyScrubber(handler, options = {}) {
1021
+ return async function privacyScrubberHandler(req, context = {}) {
1022
+ if (!req || typeof req.json !== 'function') {
1023
+ return handler(req, context);
1024
+ }
1025
+
1026
+ let rawBody;
1027
+ try {
1028
+ rawBody = await req.json();
1029
+ } catch (_) {
1030
+ return handler(req, context);
1031
+ }
1032
+
1033
+ let scrubbedBody = rawBody;
1034
+ let tokenMap = {};
1035
+ let telemetry = null;
1036
+
1037
+ if (typeof rawBody === 'string') {
1038
+ const res = sanitize(rawBody, options);
1039
+ scrubbedBody = res.scrubbedText;
1040
+ tokenMap = res.tokenMap;
1041
+ telemetry = res.telemetry;
1042
+ } else if (rawBody && typeof rawBody === 'object') {
1043
+ const serialized = JSON.stringify(rawBody);
1044
+ const res = sanitize(serialized, options);
1045
+ try {
1046
+ scrubbedBody = JSON.parse(res.scrubbedText);
1047
+ } catch (_) {
1048
+ scrubbedBody = rawBody;
1049
+ }
1050
+ tokenMap = res.tokenMap;
1051
+ telemetry = res.telemetry;
1052
+ }
1053
+
1054
+ const psContext = {
1055
+ ...context,
1056
+ rawBody,
1057
+ scrubbedBody,
1058
+ tokenMap,
1059
+ telemetry,
1060
+ restore: (text) => restore(text, tokenMap),
1061
+ createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
1062
+ };
1063
+
1064
+ return handler(req, psContext);
1065
+ };
1066
+ }
1067
+
1068
+ /**
1069
+ * Express.js / Connect middleware for automatic zero-trust PII sanitization.
1070
+ * Sanitizes req.body in local RAM and attaches req.psTokenMap and req.restore(aiResponse).
1071
+ *
1072
+ * @param {object} [options] - Sanitization options.
1073
+ * @returns {Function} Express middleware (req, res, next)
1074
+ */
1075
+ export function createExpressMiddleware(options = {}) {
1076
+ return function privacyScrubberExpress(req, res, next) {
1077
+ if (req && req.body && typeof req.body === 'object') {
1078
+ const serialized = JSON.stringify(req.body);
1079
+ const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
1080
+ req.rawBody = req.body;
1081
+ try {
1082
+ req.body = JSON.parse(scrubbedText);
1083
+ } catch (_) {}
1084
+ req.psTokenMap = tokenMap;
1085
+ req.psTelemetry = telemetry;
1086
+ req.restore = (aiResponse) => restore(aiResponse, tokenMap);
1087
+ req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
1088
+ }
1089
+ if (typeof next === 'function') next();
1090
+ };
1091
+ }
1092
+
493
1093
  /**
494
1094
  * Generates an official Zero-Trust Compliance Audit Report.
495
1095
  * @param {Record<string, string>} sessionMap - Map of token bindings.
@@ -921,6 +1521,7 @@ export const unscrubText = scrubberPkg.unscrubText;
921
1521
  export default {
922
1522
  sanitize,
923
1523
  scrub,
1524
+ clean,
924
1525
  restore,
925
1526
  unscrub,
926
1527
  validateLicense,
@@ -928,10 +1529,18 @@ export default {
928
1529
  buildAuditTelemetry,
929
1530
  formatAuditReceipt,
930
1531
  generateAuditReport,
1532
+ generateProcurementMemo,
931
1533
  PrivacyScrubberEngine,
932
1534
  PrivacyScrubberLicenseError,
933
1535
  wrapOpenAI,
934
1536
  createLangChainTransform,
1537
+ StreamingDetokenizer,
1538
+ createStreamTokenRestorer,
1539
+ createNodeStreamTokenRestorer,
1540
+ buildStreamReplacer,
1541
+ wrapAiStream,
1542
+ withPrivacyScrubber,
1543
+ createExpressMiddleware,
935
1544
  createSanitizeStream,
936
1545
  applyPatch,
937
1546
  restoreToFile,