@privacyscrubber/sdk 2.2.1 → 2.2.3
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/LICENSE.md +21 -0
- package/README.md +76 -7
- package/browser.js +654 -0
- package/cli.js +166 -19
- package/index.d.ts +104 -6
- package/index.js +596 -68
- package/package.json +21 -4
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -11
- package/vision.d.ts +56 -0
- package/vision.js +362 -0
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: '
|
|
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
|
/**
|
|
@@ -198,6 +203,160 @@ export function _resetCommunityNoticeForTesting() {
|
|
|
198
203
|
communityNoticeLogged = false;
|
|
199
204
|
}
|
|
200
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 30 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
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
|
|
201
360
|
/**
|
|
202
361
|
* Sanitizes input text, masking detected PII & secrets with reversible tokens.
|
|
203
362
|
*
|
|
@@ -210,6 +369,7 @@ export function _resetCommunityNoticeForTesting() {
|
|
|
210
369
|
* @param {Record<string, string>} [options.existingSessionMap={}] - In-memory session map to preserve token IDs across turns.
|
|
211
370
|
* @param {boolean} [options.generateReceipt=true] - Generate a CISO Markdown audit receipt.
|
|
212
371
|
* @param {boolean} [options.throwOnLimit=true] - Throw PrivacyScrubberLicenseError when free tier limit is hit.
|
|
372
|
+
* @param {'throw'|'truncate'} [options.onLimit='throw'] - Action when character limit exceeded ('throw' or 'truncate').
|
|
213
373
|
* @param {boolean} [options.detectSecrets=false] - Auto-detect cloud credentials and API keys.
|
|
214
374
|
* @param {Set<string>|Array<string>} [options.ignoreList=null] - Values to preserve in cleartext.
|
|
215
375
|
* @returns {{
|
|
@@ -217,7 +377,7 @@ export function _resetCommunityNoticeForTesting() {
|
|
|
217
377
|
* tokenMap: Record<string, string>,
|
|
218
378
|
* count: number,
|
|
219
379
|
* executionMs: number,
|
|
220
|
-
* 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 },
|
|
221
381
|
* telemetry: object,
|
|
222
382
|
* auditReceipt: string
|
|
223
383
|
* }}
|
|
@@ -229,7 +389,7 @@ export function sanitize(text, options = {}) {
|
|
|
229
389
|
tokenMap: {},
|
|
230
390
|
count: 0,
|
|
231
391
|
executionMs: 0,
|
|
232
|
-
license: { isPro: false, tier: 'FREE', limit: 15000 },
|
|
392
|
+
license: { isPro: false, tier: 'FREE', limit: 15000, wasTruncated: false },
|
|
233
393
|
telemetry: buildAuditTelemetry({}),
|
|
234
394
|
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
235
395
|
};
|
|
@@ -243,6 +403,7 @@ export function sanitize(text, options = {}) {
|
|
|
243
403
|
existingSessionMap = {},
|
|
244
404
|
generateReceipt = true,
|
|
245
405
|
throwOnLimit = true,
|
|
406
|
+
onLimit = 'throw',
|
|
246
407
|
detectSecrets = false,
|
|
247
408
|
ignoreList = null
|
|
248
409
|
} = options;
|
|
@@ -256,9 +417,12 @@ export function sanitize(text, options = {}) {
|
|
|
256
417
|
const isSpecialized = profile && profile.toLowerCase() !== 'general';
|
|
257
418
|
const limit = isPro ? Infinity : (isSpecialized ? 5000 : 15000);
|
|
258
419
|
|
|
420
|
+
const shouldThrow = throwOnLimit && onLimit !== 'truncate';
|
|
421
|
+
let wasTruncated = false;
|
|
422
|
+
|
|
259
423
|
if (!isPro && text.length > limit) {
|
|
260
|
-
|
|
261
|
-
|
|
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`;
|
|
262
426
|
throw new PrivacyScrubberLicenseError(errorMsg, {
|
|
263
427
|
tier: 'FREE',
|
|
264
428
|
currentLength: text.length,
|
|
@@ -266,6 +430,7 @@ export function sanitize(text, options = {}) {
|
|
|
266
430
|
profile: profile
|
|
267
431
|
});
|
|
268
432
|
}
|
|
433
|
+
wasTruncated = true;
|
|
269
434
|
}
|
|
270
435
|
|
|
271
436
|
// Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
|
|
@@ -305,8 +470,15 @@ export function sanitize(text, options = {}) {
|
|
|
305
470
|
const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
|
|
306
471
|
const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
|
|
307
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
|
+
|
|
308
480
|
return {
|
|
309
|
-
scrubbedText:
|
|
481
|
+
scrubbedText: finalScrubbedText,
|
|
310
482
|
tokenMap: rawResult.tokenMap || {},
|
|
311
483
|
count: rawResult.count || 0,
|
|
312
484
|
executionMs: executionMs,
|
|
@@ -314,6 +486,7 @@ export function sanitize(text, options = {}) {
|
|
|
314
486
|
isPro: isPro,
|
|
315
487
|
tier: tier,
|
|
316
488
|
limit: limit,
|
|
489
|
+
wasTruncated: wasTruncated,
|
|
317
490
|
...(!isPro && {
|
|
318
491
|
upgradeUrl: 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_telemetry&utm_campaign=dev_upsell',
|
|
319
492
|
recommendation: 'Upgrade to PRO, TEAMS, or Developer SDK for unlimited text processing, custom regex rules, and specialized compliance profiles.'
|
|
@@ -364,6 +537,139 @@ export function restore(aiResponse, tokenMap = {}) {
|
|
|
364
537
|
*/
|
|
365
538
|
export const unscrub = restore;
|
|
366
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Recursively sanitizes all string values within an object or array.
|
|
542
|
+
* Preserves the exact structure, object keys, numbers, booleans, dates, and nulls.
|
|
543
|
+
* Maintains unified session token IDs and returns combined audit telemetry.
|
|
544
|
+
*
|
|
545
|
+
* @param {any} obj - Object, array, or primitive to sanitize.
|
|
546
|
+
* @param {object} [options={}] - Standard sanitization options (profile, licenseKey, etc.).
|
|
547
|
+
* @returns {{
|
|
548
|
+
* sanitized: any,
|
|
549
|
+
* tokenMap: Record<string, string>,
|
|
550
|
+
* count: number,
|
|
551
|
+
* executionMs: number,
|
|
552
|
+
* telemetry: object,
|
|
553
|
+
* auditReceipt: string
|
|
554
|
+
* }}
|
|
555
|
+
*/
|
|
556
|
+
export function sanitizeObject(obj, options = {}) {
|
|
557
|
+
if (obj === null || obj === undefined) {
|
|
558
|
+
return {
|
|
559
|
+
sanitized: obj,
|
|
560
|
+
tokenMap: {},
|
|
561
|
+
count: 0,
|
|
562
|
+
executionMs: 0,
|
|
563
|
+
telemetry: buildAuditTelemetry({}),
|
|
564
|
+
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (typeof obj === 'string') {
|
|
569
|
+
const res = sanitize(obj, options);
|
|
570
|
+
return {
|
|
571
|
+
sanitized: res.scrubbedText,
|
|
572
|
+
tokenMap: res.tokenMap,
|
|
573
|
+
count: res.count,
|
|
574
|
+
executionMs: res.executionMs,
|
|
575
|
+
telemetry: res.telemetry,
|
|
576
|
+
auditReceipt: res.auditReceipt
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (typeof obj !== 'object') {
|
|
581
|
+
return {
|
|
582
|
+
sanitized: obj,
|
|
583
|
+
tokenMap: {},
|
|
584
|
+
count: 0,
|
|
585
|
+
executionMs: 0,
|
|
586
|
+
telemetry: buildAuditTelemetry({}),
|
|
587
|
+
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const startTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
592
|
+
? performance.now()
|
|
593
|
+
: Date.now();
|
|
594
|
+
|
|
595
|
+
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
596
|
+
let totalCount = 0;
|
|
597
|
+
|
|
598
|
+
function traverseAndSanitize(val) {
|
|
599
|
+
if (val === null || val === undefined) return val;
|
|
600
|
+
if (typeof val === 'string') {
|
|
601
|
+
if (val.trim().length === 0) return val;
|
|
602
|
+
const res = sanitize(val, {
|
|
603
|
+
...options,
|
|
604
|
+
existingSessionMap: mergedSessionMap,
|
|
605
|
+
generateReceipt: false
|
|
606
|
+
});
|
|
607
|
+
Object.assign(mergedSessionMap, res.tokenMap);
|
|
608
|
+
totalCount += res.count;
|
|
609
|
+
return res.scrubbedText;
|
|
610
|
+
}
|
|
611
|
+
if (Array.isArray(val)) {
|
|
612
|
+
return val.map(item => traverseAndSanitize(item));
|
|
613
|
+
}
|
|
614
|
+
if (val instanceof Date || val instanceof RegExp) {
|
|
615
|
+
return val;
|
|
616
|
+
}
|
|
617
|
+
if (typeof val === 'object') {
|
|
618
|
+
const out = {};
|
|
619
|
+
for (const [k, v] of Object.entries(val)) {
|
|
620
|
+
out[k] = traverseAndSanitize(v);
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
return val;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const sanitizedResult = traverseAndSanitize(obj);
|
|
628
|
+
|
|
629
|
+
const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
630
|
+
? performance.now()
|
|
631
|
+
: Date.now();
|
|
632
|
+
|
|
633
|
+
const executionMs = Math.round((endTime - startTime) * 100) / 100;
|
|
634
|
+
const telemetry = buildAuditTelemetry(mergedSessionMap);
|
|
635
|
+
const auditReceipt = options.generateReceipt !== false ? formatAuditReceipt(telemetry) : '';
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
sanitized: sanitizedResult,
|
|
639
|
+
tokenMap: mergedSessionMap,
|
|
640
|
+
count: totalCount,
|
|
641
|
+
executionMs: executionMs,
|
|
642
|
+
telemetry: telemetry,
|
|
643
|
+
auditReceipt: auditReceipt
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Recursively restores all masked tokens within a sanitized object or array using the tokenMap.
|
|
649
|
+
*
|
|
650
|
+
* @param {any} obj - Sanitized object, array, or string.
|
|
651
|
+
* @param {Record<string, string>} tokenMap - Token map returned from sanitize() or sanitizeObject().
|
|
652
|
+
* @returns {any} - Restored object with original values.
|
|
653
|
+
*/
|
|
654
|
+
export function restoreObject(obj, tokenMap = {}) {
|
|
655
|
+
if (obj === null || obj === undefined) return obj;
|
|
656
|
+
if (typeof obj === 'string') {
|
|
657
|
+
const res = restore(obj, tokenMap);
|
|
658
|
+
return res.restoredText;
|
|
659
|
+
}
|
|
660
|
+
if (Array.isArray(obj)) {
|
|
661
|
+
return obj.map(item => restoreObject(item, tokenMap));
|
|
662
|
+
}
|
|
663
|
+
if (typeof obj === 'object' && !(obj instanceof Date) && !(obj instanceof RegExp)) {
|
|
664
|
+
const out = {};
|
|
665
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
666
|
+
out[k] = restoreObject(v, tokenMap);
|
|
667
|
+
}
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
return obj;
|
|
671
|
+
}
|
|
672
|
+
|
|
367
673
|
/**
|
|
368
674
|
* Stateful PrivacyScrubber Engine class for backend servers & conversational pipelines.
|
|
369
675
|
*/
|
|
@@ -463,12 +769,34 @@ export class PrivacyScrubberEngine {
|
|
|
463
769
|
clearIgnoreList() {
|
|
464
770
|
this.ignoreList.clear();
|
|
465
771
|
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Creates a streaming token restorer bound to this engine's session map.
|
|
775
|
+
* @param {Record<string, string>} [tokenMap=null]
|
|
776
|
+
* @param {object} [options={}]
|
|
777
|
+
*/
|
|
778
|
+
createStreamRestorer(tokenMap = null, options = {}) {
|
|
779
|
+
const activeMap = tokenMap || this.sessionMap;
|
|
780
|
+
return createStreamTokenRestorer(activeMap, options);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Wraps an async iterable LLM stream with real-time de-tokenization.
|
|
785
|
+
* @param {AsyncIterable<any>} stream
|
|
786
|
+
* @param {Record<string, string>} [tokenMap=null]
|
|
787
|
+
* @param {object} [options={}]
|
|
788
|
+
*/
|
|
789
|
+
async* wrapStream(stream, tokenMap = null, options = {}) {
|
|
790
|
+
const activeMap = tokenMap || this.sessionMap;
|
|
791
|
+
yield* wrapAiStream(stream, activeMap, options);
|
|
792
|
+
}
|
|
466
793
|
}
|
|
467
794
|
|
|
468
795
|
/**
|
|
469
796
|
* Transparent Zero-Trust Wrapper for OpenAI Client.
|
|
470
797
|
* Automatically sanitizes outgoing messages before transmission to OpenAI,
|
|
471
798
|
* and restores completion tokens in the assistant response before returning!
|
|
799
|
+
* Seamlessly de-tokenizes streaming responses in real-time when stream: true.
|
|
472
800
|
*
|
|
473
801
|
* @param {object} openaiClient - Instance of OpenAI SDK (e.g. new OpenAI()).
|
|
474
802
|
* @param {object} [options={}] - PrivacyScrubber configuration.
|
|
@@ -500,6 +828,12 @@ export function wrapOpenAI(openaiClient, options = {}) {
|
|
|
500
828
|
const modifiedParams = { ...params, messages: sanitizedMessages };
|
|
501
829
|
const response = await originalCreate(modifiedParams, requestOptions);
|
|
502
830
|
|
|
831
|
+
// Streaming support: transparently de-tokenize async iterable chunks
|
|
832
|
+
if (params.stream) {
|
|
833
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
834
|
+
return detok.wrapStream(response);
|
|
835
|
+
}
|
|
836
|
+
|
|
503
837
|
if (response && Array.isArray(response.choices)) {
|
|
504
838
|
response.choices.forEach(choice => {
|
|
505
839
|
if (choice.message && typeof choice.message.content === 'string') {
|
|
@@ -526,88 +860,246 @@ export function createLangChainTransform(options = {}) {
|
|
|
526
860
|
}
|
|
527
861
|
|
|
528
862
|
/**
|
|
529
|
-
*
|
|
530
|
-
* Solves the broken chunk boundary problem where tokens like [EMAIL_1] are split across multiple chunks.
|
|
531
|
-
*
|
|
532
|
-
* Works with:
|
|
533
|
-
* - Vercel AI SDK (streamText / createDataStreamResponse)
|
|
534
|
-
* - OpenAI streaming (chat.completions.create({ stream: true }))
|
|
535
|
-
* - LangChain streaming
|
|
536
|
-
* - Native Web Streams (ReadableStream.pipeThrough)
|
|
537
|
-
* - Async generators (for await ... of)
|
|
538
|
-
*
|
|
539
|
-
* @param {Record<string, string>} tokenMap - Volatile token mapping object.
|
|
540
|
-
* @param {object} [options] - Configuration options.
|
|
541
|
-
* @returns {TransformStream & { process: (chunk: string) => string, flush: () => string, wrapStream: (asyncIterable: AsyncIterable<string>) => AsyncGenerator<string> }}
|
|
863
|
+
* Helper to escape special regex characters.
|
|
542
864
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
865
|
+
function escapeRegex(s) {
|
|
866
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
867
|
+
}
|
|
546
868
|
|
|
547
|
-
|
|
869
|
+
/**
|
|
870
|
+
* Builds an ultra-fast streaming token replacement function.
|
|
871
|
+
* Preserves all whitespace, indentation, newlines, and code blocks (does NOT trim).
|
|
872
|
+
* Handles descending key ordering (e.g. [NAME_10] before [NAME_1]), case insensitivity,
|
|
873
|
+
* alternative delimiters (<...>, {{...}}, __...__), and possessive suffixes.
|
|
874
|
+
*
|
|
875
|
+
* @param {Record<string, string>} tokenMap
|
|
876
|
+
* @returns {(text: string) => string}
|
|
877
|
+
*/
|
|
878
|
+
export function buildStreamReplacer(tokenMap = {}) {
|
|
879
|
+
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
880
|
+
return (text) => text || '';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const rules = [];
|
|
884
|
+
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
885
|
+
|
|
886
|
+
for (const k of sortedKeys) {
|
|
887
|
+
const val = tokenMap[k];
|
|
888
|
+
const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
|
|
889
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
890
|
+
|
|
891
|
+
if (match) {
|
|
892
|
+
const label = escapeRegex(match[1]);
|
|
893
|
+
const idx = match[2];
|
|
894
|
+
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)?';
|
|
895
|
+
rules.push({
|
|
896
|
+
regex: new RegExp(patternStr, 'gi'),
|
|
897
|
+
val,
|
|
898
|
+
k
|
|
899
|
+
});
|
|
900
|
+
} else {
|
|
901
|
+
const safe = escapeRegex(k);
|
|
902
|
+
rules.push({
|
|
903
|
+
regex: new RegExp(safe + '(?:\'s|’s|s)?', 'gi'),
|
|
904
|
+
val,
|
|
905
|
+
k
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return function replaceTokens(text) {
|
|
548
911
|
if (!text) return '';
|
|
549
912
|
let result = text;
|
|
550
|
-
for (
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
913
|
+
for (let i = 0; i < rules.length; i++) {
|
|
914
|
+
const rule = rules[i];
|
|
915
|
+
rule.regex.lastIndex = 0;
|
|
916
|
+
result = result.replace(rule.regex, (m) => {
|
|
917
|
+
const isPossessive = /('s|’s)$/i.test(m);
|
|
918
|
+
return rule.val + (isPossessive ? "'s" : '');
|
|
919
|
+
});
|
|
554
920
|
}
|
|
555
921
|
return result;
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
|
|
927
|
+
* Seamlessly restores split tokens (e.g. [EM + AIL_1]), preserves multi-byte UTF-8,
|
|
928
|
+
* and maintains zero-latency streaming throughput.
|
|
929
|
+
*/
|
|
930
|
+
export class StreamingDetokenizer {
|
|
931
|
+
constructor(tokenMap = {}, options = {}) {
|
|
932
|
+
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
933
|
+
this.options = options || {};
|
|
934
|
+
this.replacer = buildStreamReplacer(this.tokenMap);
|
|
935
|
+
this.pendingBuffer = '';
|
|
936
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
937
|
+
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
556
938
|
}
|
|
557
939
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
940
|
+
/**
|
|
941
|
+
* Feeds an incoming chunk (string, Uint8Array, or Buffer) and returns de-tokenized text.
|
|
942
|
+
* Buffers any unclosed token candidates at chunk boundaries until closed or flushed.
|
|
943
|
+
* @param {string | Uint8Array | Buffer} chunk
|
|
944
|
+
* @returns {string}
|
|
945
|
+
*/
|
|
946
|
+
feed(chunk) {
|
|
947
|
+
if (chunk === undefined || chunk === null) return '';
|
|
948
|
+
let text = '';
|
|
949
|
+
if (typeof chunk === 'string') {
|
|
950
|
+
text = chunk;
|
|
951
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
952
|
+
text = this.decoder.decode(chunk, { stream: true });
|
|
953
|
+
} else {
|
|
954
|
+
text = String(chunk);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!text) return '';
|
|
958
|
+
if (!this.hasTokens) return text;
|
|
959
|
+
|
|
960
|
+
this.pendingBuffer += text;
|
|
961
|
+
|
|
962
|
+
// Candidate unclosed token at tail of buffer (up to 64 chars)
|
|
963
|
+
// Matches incomplete tokens starting with [, <, {{, or __
|
|
964
|
+
const m = this.pendingBuffer.match(/(?:\[|<|\{\{|__)[A-Za-z0-9_\-А-Яа-яЁё ]{0,64}$/);
|
|
965
|
+
if (m) {
|
|
966
|
+
const safeToEmit = this.pendingBuffer.slice(0, m.index);
|
|
967
|
+
this.pendingBuffer = this.pendingBuffer.slice(m.index);
|
|
968
|
+
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const toEmit = this.pendingBuffer;
|
|
972
|
+
this.pendingBuffer = '';
|
|
973
|
+
return this.replacer(toEmit);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Alias for feed() to maintain backwards compatibility with process() callers.
|
|
978
|
+
*/
|
|
979
|
+
process(chunk) {
|
|
980
|
+
return this.feed(chunk);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Flushes remaining buffered tokens and finalizes UTF-8 stream decoder.
|
|
985
|
+
* @returns {string}
|
|
986
|
+
*/
|
|
987
|
+
flush() {
|
|
988
|
+
let text = '';
|
|
989
|
+
try {
|
|
990
|
+
text = this.decoder.decode(new Uint8Array(), { stream: false });
|
|
991
|
+
} catch (_) {}
|
|
992
|
+
|
|
993
|
+
if (text) {
|
|
994
|
+
this.pendingBuffer += text;
|
|
577
995
|
}
|
|
578
996
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
997
|
+
if (!this.pendingBuffer) return '';
|
|
998
|
+
const remaining = this.pendingBuffer;
|
|
999
|
+
this.pendingBuffer = '';
|
|
1000
|
+
return this.replacer(remaining);
|
|
582
1001
|
}
|
|
583
1002
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Returns a native Node.js Transform stream instance for .pipe() / pipeline().
|
|
1005
|
+
* @returns {Transform}
|
|
1006
|
+
*/
|
|
1007
|
+
toNodeTransform() {
|
|
1008
|
+
const self = this;
|
|
1009
|
+
return new Transform({
|
|
1010
|
+
transform(chunk, encoding, callback) {
|
|
1011
|
+
try {
|
|
1012
|
+
const out = self.feed(chunk);
|
|
1013
|
+
if (out) this.push(out);
|
|
1014
|
+
callback();
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
callback(err);
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
flush(callback) {
|
|
1020
|
+
try {
|
|
1021
|
+
const out = self.flush();
|
|
1022
|
+
if (out) this.push(out);
|
|
1023
|
+
callback();
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
callback(err);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
589
1029
|
}
|
|
590
1030
|
|
|
591
|
-
|
|
1031
|
+
/**
|
|
1032
|
+
* Wraps an async iterable (e.g. LLM streaming response) yielding restored chunks.
|
|
1033
|
+
* @param {AsyncIterable<any>} asyncIterable
|
|
1034
|
+
* @returns {AsyncGenerator<any>}
|
|
1035
|
+
*/
|
|
1036
|
+
async* wrapStream(asyncIterable) {
|
|
592
1037
|
for await (const chunk of asyncIterable) {
|
|
593
|
-
|
|
594
|
-
|
|
1038
|
+
if (chunk === null || chunk === undefined) continue;
|
|
1039
|
+
// Handle OpenAI chunk object
|
|
1040
|
+
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
1041
|
+
const delta = chunk.choices[0].delta.content;
|
|
1042
|
+
if (typeof delta === 'string') {
|
|
1043
|
+
const restored = this.feed(delta);
|
|
1044
|
+
if (restored) {
|
|
1045
|
+
chunk.choices[0].delta.content = restored;
|
|
1046
|
+
yield chunk;
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
yield chunk;
|
|
1050
|
+
}
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let text = '';
|
|
1055
|
+
if (typeof chunk === 'string') {
|
|
1056
|
+
text = chunk;
|
|
1057
|
+
} else if (typeof chunk?.text === 'string') {
|
|
1058
|
+
text = chunk.text;
|
|
1059
|
+
} else if (typeof chunk?.content === 'string') {
|
|
1060
|
+
text = chunk.content;
|
|
1061
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
1062
|
+
text = chunk;
|
|
1063
|
+
} else {
|
|
1064
|
+
text = String(chunk);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const emitted = this.feed(text);
|
|
595
1068
|
if (emitted) yield emitted;
|
|
596
1069
|
}
|
|
597
|
-
|
|
1070
|
+
|
|
1071
|
+
const finalChunk = this.flush();
|
|
598
1072
|
if (finalChunk) yield finalChunk;
|
|
599
1073
|
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Creates a zero-latency streaming token restorer for LLM Server-Sent Events (SSE) and Web Streams.
|
|
1078
|
+
* Solves the broken chunk boundary problem where tokens like [EMAIL_1] are split across multiple chunks.
|
|
1079
|
+
*
|
|
1080
|
+
* Works with:
|
|
1081
|
+
* - Vercel AI SDK (streamText / createDataStreamResponse)
|
|
1082
|
+
* - OpenAI streaming (chat.completions.create({ stream: true }))
|
|
1083
|
+
* - LangChain streaming
|
|
1084
|
+
* - Native Web Streams (ReadableStream.pipeThrough)
|
|
1085
|
+
* - Async generators (for await ... of)
|
|
1086
|
+
*
|
|
1087
|
+
* @param {Record<string, string>} tokenMap - Volatile token mapping object.
|
|
1088
|
+
* @param {object} [options] - Configuration options.
|
|
1089
|
+
* @returns {TransformStream & { process: (chunk: string) => string, feed: (chunk: string) => string, flush: () => string, wrapStream: (asyncIterable: AsyncIterable<any>) => AsyncGenerator<any>, toNodeTransform: () => Transform, detokenizer: StreamingDetokenizer }}
|
|
1090
|
+
*/
|
|
1091
|
+
export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
|
|
1092
|
+
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
600
1093
|
|
|
601
1094
|
let stream;
|
|
602
1095
|
if (typeof TransformStream !== 'undefined') {
|
|
603
1096
|
stream = new TransformStream({
|
|
604
1097
|
transform(chunk, controller) {
|
|
605
|
-
const
|
|
606
|
-
const out = processChunk(text);
|
|
1098
|
+
const out = detok.feed(chunk);
|
|
607
1099
|
if (out) controller.enqueue(out);
|
|
608
1100
|
},
|
|
609
1101
|
flush(controller) {
|
|
610
|
-
const out =
|
|
1102
|
+
const out = detok.flush();
|
|
611
1103
|
if (out) controller.enqueue(out);
|
|
612
1104
|
}
|
|
613
1105
|
});
|
|
@@ -615,24 +1107,38 @@ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
|
|
|
615
1107
|
stream = {};
|
|
616
1108
|
}
|
|
617
1109
|
|
|
618
|
-
stream.process =
|
|
619
|
-
stream.
|
|
620
|
-
stream.
|
|
1110
|
+
stream.process = (chunk) => detok.feed(chunk);
|
|
1111
|
+
stream.feed = (chunk) => detok.feed(chunk);
|
|
1112
|
+
stream.flush = () => detok.flush();
|
|
1113
|
+
stream.wrapStream = (asyncIterable) => detok.wrapStream(asyncIterable);
|
|
1114
|
+
stream.toNodeTransform = () => detok.toNodeTransform();
|
|
1115
|
+
stream.detokenizer = detok;
|
|
621
1116
|
|
|
622
1117
|
return stream;
|
|
623
1118
|
}
|
|
624
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* Creates a Node.js native Transform stream for backend streaming pipelines (stream.pipeline / .pipe()).
|
|
1122
|
+
* @param {Record<string, string>} tokenMap - Volatile token mapping object.
|
|
1123
|
+
* @param {object} [options] - Configuration options.
|
|
1124
|
+
* @returns {Transform}
|
|
1125
|
+
*/
|
|
1126
|
+
export function createNodeStreamTokenRestorer(tokenMap = {}, options = {}) {
|
|
1127
|
+
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
1128
|
+
return detok.toNodeTransform();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
625
1131
|
/**
|
|
626
1132
|
* Wraps an async iterable LLM stream (e.g. from Vercel AI SDK textStream or OpenAI stream)
|
|
627
1133
|
* and yields de-tokenized text chunks in real-time, seamlessly stitching split tokens across chunks.
|
|
628
1134
|
*
|
|
629
1135
|
* @param {AsyncIterable<string|object>} stream - The incoming LLM stream.
|
|
630
1136
|
* @param {Record<string, string>} tokenMap - Volatile token map.
|
|
631
|
-
* @returns {AsyncGenerator<
|
|
1137
|
+
* @returns {AsyncGenerator<any>}
|
|
632
1138
|
*/
|
|
633
1139
|
export async function* wrapAiStream(stream, tokenMap = {}) {
|
|
634
|
-
const
|
|
635
|
-
yield*
|
|
1140
|
+
const detok = new StreamingDetokenizer(tokenMap);
|
|
1141
|
+
yield* detok.wrapStream(stream);
|
|
636
1142
|
}
|
|
637
1143
|
|
|
638
1144
|
/**
|
|
@@ -1145,22 +1651,40 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
1145
1651
|
export const scrubText = scrubberPkg.scrubText;
|
|
1146
1652
|
export const unscrubText = scrubberPkg.unscrubText;
|
|
1147
1653
|
|
|
1654
|
+
// Vision & Multimodal exports
|
|
1655
|
+
export {
|
|
1656
|
+
stripExifMetadata,
|
|
1657
|
+
redactImageBoundingBoxes,
|
|
1658
|
+
generateSvgRedactionOverlay
|
|
1659
|
+
} from './vision.js';
|
|
1660
|
+
import {
|
|
1661
|
+
stripExifMetadata,
|
|
1662
|
+
redactImageBoundingBoxes,
|
|
1663
|
+
generateSvgRedactionOverlay
|
|
1664
|
+
} from './vision.js';
|
|
1665
|
+
|
|
1148
1666
|
export default {
|
|
1149
1667
|
sanitize,
|
|
1668
|
+
sanitizeObject,
|
|
1150
1669
|
scrub,
|
|
1151
1670
|
clean,
|
|
1152
1671
|
restore,
|
|
1672
|
+
restoreObject,
|
|
1153
1673
|
unscrub,
|
|
1154
1674
|
validateLicense,
|
|
1155
1675
|
validateLicenseKey,
|
|
1156
1676
|
buildAuditTelemetry,
|
|
1157
1677
|
formatAuditReceipt,
|
|
1158
1678
|
generateAuditReport,
|
|
1679
|
+
generateProcurementMemo,
|
|
1159
1680
|
PrivacyScrubberEngine,
|
|
1160
1681
|
PrivacyScrubberLicenseError,
|
|
1161
1682
|
wrapOpenAI,
|
|
1162
1683
|
createLangChainTransform,
|
|
1684
|
+
StreamingDetokenizer,
|
|
1163
1685
|
createStreamTokenRestorer,
|
|
1686
|
+
createNodeStreamTokenRestorer,
|
|
1687
|
+
buildStreamReplacer,
|
|
1164
1688
|
wrapAiStream,
|
|
1165
1689
|
withPrivacyScrubber,
|
|
1166
1690
|
createExpressMiddleware,
|
|
@@ -1170,5 +1694,9 @@ export default {
|
|
|
1170
1694
|
createGuardedTools,
|
|
1171
1695
|
scrubText,
|
|
1172
1696
|
unscrubText,
|
|
1173
|
-
DEVOPS_SECRETS_DETECTOR
|
|
1697
|
+
DEVOPS_SECRETS_DETECTOR,
|
|
1698
|
+
stripExifMetadata,
|
|
1699
|
+
redactImageBoundingBoxes,
|
|
1700
|
+
generateSvgRedactionOverlay
|
|
1174
1701
|
};
|
|
1702
|
+
|