@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/README.md +178 -6
- package/cli.js +166 -19
- package/index.d.ts +122 -0
- package/index.js +626 -17
- package/package.json +1 -1
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -21
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
|
/**
|
|
@@ -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
|
-
*
|
|
361
|
+
* Sanitizes input text, masking detected PII & secrets with reversible tokens.
|
|
183
362
|
*
|
|
184
|
-
* @param {string} text - Raw input text
|
|
185
|
-
* @param {object} [options={}] -
|
|
186
|
-
* @param {string} [options.profile='General'] -
|
|
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
|
|
189
|
-
* @param {Record<string, string>} [options.tokenLabelMap={}] - Custom label
|
|
190
|
-
* @param {Record<string, string>} [options.existingSessionMap={}] -
|
|
191
|
-
* @param {boolean} [options.generateReceipt=true] -
|
|
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 {
|
|
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
|
-
|
|
238
|
-
|
|
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:
|
|
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,
|