@privacyscrubber/sdk 2.2.2 → 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 +17 -6
- package/browser.js +654 -0
- package/index.d.ts +37 -0
- package/index.js +153 -2
- 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/browser.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrivacyScrubber Developer SDK — Browser & Edge Runtime Client Module
|
|
3
|
+
* (@privacyscrubber/sdk/browser)
|
|
4
|
+
*
|
|
5
|
+
* 100% Client-Side in-memory execution.
|
|
6
|
+
* Zero Node.js dependencies (no fs, child_process, or Node stream).
|
|
7
|
+
* Safe for Vite, Next.js client components ('use client'), Webpack, Web Workers, and Edge runtimes.
|
|
8
|
+
*
|
|
9
|
+
* https://privacyscrubber.com
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import './polyfill.js';
|
|
13
|
+
import './ps-license-manager.js';
|
|
14
|
+
import './scrubber-core.cjs';
|
|
15
|
+
|
|
16
|
+
// Access LicenseManager and PrivacyScrubberCore from global scope
|
|
17
|
+
const LicenseManager = (typeof globalThis !== 'undefined' && globalThis.LicenseManager)
|
|
18
|
+
|| (typeof window !== 'undefined' && window.LicenseManager);
|
|
19
|
+
|
|
20
|
+
const scrubberPkg = (typeof globalThis !== 'undefined' && globalThis.PrivacyScrubberCore)
|
|
21
|
+
|| (typeof window !== 'undefined' && window.PrivacyScrubberCore);
|
|
22
|
+
|
|
23
|
+
if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
24
|
+
scrubberPkg.init();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
29
|
+
*/
|
|
30
|
+
export const DEVOPS_SECRETS_DETECTOR = [
|
|
31
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g, label: 'AWS_KEY' },
|
|
32
|
+
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
|
|
33
|
+
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
|
|
34
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g, label: 'STRIPE_KEY' },
|
|
35
|
+
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
|
|
36
|
+
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
|
|
37
|
+
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
38
|
+
{ 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' },
|
|
39
|
+
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
|
|
40
|
+
{ 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' },
|
|
41
|
+
{ 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' },
|
|
42
|
+
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Custom Error thrown when Free Tier limits are exceeded.
|
|
47
|
+
*/
|
|
48
|
+
export class PrivacyScrubberLicenseError extends Error {
|
|
49
|
+
constructor(message, { tier = 'FREE', currentLength = 0, limit = 15000, profile = 'General' } = {}) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'PrivacyScrubberLicenseError';
|
|
52
|
+
this.tier = tier;
|
|
53
|
+
this.currentLength = currentLength;
|
|
54
|
+
this.limit = limit;
|
|
55
|
+
this.profile = profile;
|
|
56
|
+
this.upgradeUrl = 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validates a PrivacyScrubber license key.
|
|
62
|
+
* @param {string} key - License key string.
|
|
63
|
+
* @returns {{ valid: boolean, tier: 'PRO'|'TEAMS'|null, expires: number|null, error: string|null }}
|
|
64
|
+
*/
|
|
65
|
+
export function validateLicense(key) {
|
|
66
|
+
if (!key || typeof key !== 'string') {
|
|
67
|
+
return { valid: false, tier: null, expires: null, error: 'No license key provided.' };
|
|
68
|
+
}
|
|
69
|
+
const cleanKey = key.trim();
|
|
70
|
+
if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
71
|
+
return { valid: false, tier: null, expires: null, error: 'License manager engine unavailable.' };
|
|
72
|
+
}
|
|
73
|
+
const result = LicenseManager.validate(cleanKey);
|
|
74
|
+
return {
|
|
75
|
+
valid: !!result.valid,
|
|
76
|
+
tier: result.valid ? (result.tier || 'PRO') : null,
|
|
77
|
+
expires: result.expires || null,
|
|
78
|
+
error: result.valid ? null : (result.reason || 'Invalid license key.')
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function validateLicenseKey(key) {
|
|
83
|
+
const res = validateLicense(key);
|
|
84
|
+
return res.valid ? res.tier : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Builds CISO-grade compliance audit telemetry from masked tokens.
|
|
89
|
+
*/
|
|
90
|
+
export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
91
|
+
const entities = {};
|
|
92
|
+
let totalCount = 0;
|
|
93
|
+
|
|
94
|
+
if (currentTokenMap && typeof currentTokenMap === 'object') {
|
|
95
|
+
const keys = Array.isArray(currentTokenMap) ? currentTokenMap : Object.keys(currentTokenMap);
|
|
96
|
+
for (const t of keys) {
|
|
97
|
+
const tokenStr = typeof t === 'string' ? t : (t.token || t.mask || '');
|
|
98
|
+
const match = tokenStr.match(/\[([A-Z_]+)_\d+\]/);
|
|
99
|
+
const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
|
|
100
|
+
entities[baseType] = (entities[baseType] || 0) + 1;
|
|
101
|
+
totalCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const types = Object.keys(entities);
|
|
106
|
+
let riskLevel = 'LOW EXPOSURE';
|
|
107
|
+
|
|
108
|
+
if (totalCount > 0) {
|
|
109
|
+
const hasHighRisk = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
110
|
+
if (hasHighRisk || types.length >= 3 || totalCount >= 10) {
|
|
111
|
+
riskLevel = 'CRITICAL (HIGH EXPOSURE)';
|
|
112
|
+
} else if (types.length >= 2 || totalCount >= 3) {
|
|
113
|
+
riskLevel = 'MODERATE EXPOSURE';
|
|
114
|
+
} else {
|
|
115
|
+
riskLevel = 'LOW EXPOSURE';
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
riskLevel = 'CLEAN (ZERO PII)';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const frameworksSet = new Set(['ZTDS Standard']);
|
|
122
|
+
if (types.includes('NAME') || types.includes('EMAIL') || types.includes('PHONE')) {
|
|
123
|
+
frameworksSet.add('GDPR (Art. 4)');
|
|
124
|
+
frameworksSet.add('CCPA/CPRA');
|
|
125
|
+
}
|
|
126
|
+
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT')) {
|
|
127
|
+
frameworksSet.add('SOC 2 Type II');
|
|
128
|
+
frameworksSet.add('ISO 27001 (A.8.11)');
|
|
129
|
+
}
|
|
130
|
+
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY'].includes(t))) {
|
|
131
|
+
frameworksSet.add('PCI DSS v4.0');
|
|
132
|
+
}
|
|
133
|
+
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT'].includes(t))) {
|
|
134
|
+
frameworksSet.add('HIPAA §164.514');
|
|
135
|
+
}
|
|
136
|
+
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t))) {
|
|
137
|
+
frameworksSet.add('NIST SP 800-53');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
totalCount,
|
|
142
|
+
entities,
|
|
143
|
+
types,
|
|
144
|
+
riskLevel,
|
|
145
|
+
frameworksList: Array.from(frameworksSet)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatAuditReceipt(telemetry) {
|
|
150
|
+
if (!telemetry || telemetry.totalCount === 0) {
|
|
151
|
+
return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
|
|
152
|
+
"> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
|
|
153
|
+
}
|
|
154
|
+
const entitiesList = Object.entries(telemetry.entities)
|
|
155
|
+
.map(([type, count]) => `${count} ${type}`)
|
|
156
|
+
.join(', ');
|
|
157
|
+
|
|
158
|
+
const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
|
|
159
|
+
|
|
160
|
+
return `> 🛡️ **PrivacyScrubber Audit Receipt**\n` +
|
|
161
|
+
`> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n` +
|
|
162
|
+
`> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n` +
|
|
163
|
+
`> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n` +
|
|
164
|
+
`> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sanitizes input text in-memory.
|
|
169
|
+
*/
|
|
170
|
+
export function sanitize(text, options = {}) {
|
|
171
|
+
if (!text || typeof text !== 'string') {
|
|
172
|
+
return {
|
|
173
|
+
scrubbedText: '',
|
|
174
|
+
tokenMap: {},
|
|
175
|
+
count: 0,
|
|
176
|
+
executionMs: 0,
|
|
177
|
+
license: { isPro: false, tier: 'FREE', limit: 15000, wasTruncated: false },
|
|
178
|
+
telemetry: buildAuditTelemetry({}),
|
|
179
|
+
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const {
|
|
184
|
+
profile = 'General',
|
|
185
|
+
licenseKey = '',
|
|
186
|
+
customRules = [],
|
|
187
|
+
tokenLabelMap = {},
|
|
188
|
+
existingSessionMap = {},
|
|
189
|
+
generateReceipt = true,
|
|
190
|
+
throwOnLimit = true,
|
|
191
|
+
onLimit = 'throw',
|
|
192
|
+
detectSecrets = false,
|
|
193
|
+
ignoreList = null
|
|
194
|
+
} = options;
|
|
195
|
+
|
|
196
|
+
const licenseCheck = validateLicense(licenseKey);
|
|
197
|
+
const isPro = licenseCheck.valid;
|
|
198
|
+
const tier = isPro ? licenseCheck.tier : 'FREE';
|
|
199
|
+
|
|
200
|
+
const isSpecialized = profile && profile.toLowerCase() !== 'general';
|
|
201
|
+
const limit = isPro ? Infinity : (isSpecialized ? 5000 : 15000);
|
|
202
|
+
|
|
203
|
+
const shouldThrow = throwOnLimit && onLimit !== 'truncate';
|
|
204
|
+
let wasTruncated = false;
|
|
205
|
+
|
|
206
|
+
if (!isPro && text.length > limit) {
|
|
207
|
+
if (shouldThrow) {
|
|
208
|
+
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`;
|
|
209
|
+
throw new PrivacyScrubberLicenseError(errorMsg, {
|
|
210
|
+
tier: 'FREE',
|
|
211
|
+
currentLength: text.length,
|
|
212
|
+
limit: limit,
|
|
213
|
+
profile: profile
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
wasTruncated = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const activeRules = [...customRules];
|
|
220
|
+
if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
|
|
221
|
+
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
222
|
+
activeRules.push({
|
|
223
|
+
name: rule.name,
|
|
224
|
+
pattern: rule.regex.source,
|
|
225
|
+
regex: new RegExp(rule.regex.source, rule.regex.flags),
|
|
226
|
+
label: rule.label || 'SECRET'
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const startTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
232
|
+
? performance.now()
|
|
233
|
+
: Date.now();
|
|
234
|
+
|
|
235
|
+
const textToScrub = (!isPro && text.length > limit) ? text.slice(0, limit) : text;
|
|
236
|
+
|
|
237
|
+
const rawResult = scrubberPkg.scrubText(
|
|
238
|
+
textToScrub,
|
|
239
|
+
activeRules,
|
|
240
|
+
tokenLabelMap,
|
|
241
|
+
profile,
|
|
242
|
+
existingSessionMap,
|
|
243
|
+
isPro,
|
|
244
|
+
ignoreList
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
248
|
+
? performance.now()
|
|
249
|
+
: Date.now();
|
|
250
|
+
|
|
251
|
+
const executionMs = Math.round((endTime - startTime) * 100) / 100;
|
|
252
|
+
const telemetry = buildAuditTelemetry(rawResult.tokenMap || {});
|
|
253
|
+
const auditReceipt = generateReceipt ? formatAuditReceipt(telemetry) : '';
|
|
254
|
+
|
|
255
|
+
let finalScrubbedText = rawResult.scrubbedText;
|
|
256
|
+
if (wasTruncated && (onLimit === 'truncate' || throwOnLimit === false)) {
|
|
257
|
+
finalScrubbedText += `\n\n<!-- [PrivacyScrubber] Input truncated at free tier limit (${limit.toLocaleString()} chars). For unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&src=truncation -->`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
scrubbedText: finalScrubbedText,
|
|
262
|
+
tokenMap: rawResult.tokenMap || {},
|
|
263
|
+
count: rawResult.count || 0,
|
|
264
|
+
executionMs: executionMs,
|
|
265
|
+
license: {
|
|
266
|
+
isPro: isPro,
|
|
267
|
+
tier: tier,
|
|
268
|
+
limit: limit,
|
|
269
|
+
wasTruncated: wasTruncated,
|
|
270
|
+
...(!isPro && {
|
|
271
|
+
upgradeUrl: 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_telemetry&utm_campaign=dev_upsell',
|
|
272
|
+
recommendation: 'Upgrade to PRO, TEAMS, or Developer SDK for unlimited text processing, custom regex rules, and specialized compliance profiles.'
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
telemetry: telemetry,
|
|
276
|
+
auditReceipt: auditReceipt
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const scrub = sanitize;
|
|
281
|
+
|
|
282
|
+
export function clean(text, profile = 'General') {
|
|
283
|
+
if (!text || typeof text !== 'string') return '';
|
|
284
|
+
const { scrubbedText } = sanitize(text, { profile });
|
|
285
|
+
return scrubbedText;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Restores AI response back to original entities in-memory.
|
|
290
|
+
*/
|
|
291
|
+
export function restore(aiResponse, tokenMap = {}) {
|
|
292
|
+
if (!aiResponse || typeof aiResponse !== 'string') {
|
|
293
|
+
return { restoredText: '', restoredCount: 0 };
|
|
294
|
+
}
|
|
295
|
+
const result = scrubberPkg.unscrubText(aiResponse, tokenMap);
|
|
296
|
+
return {
|
|
297
|
+
restoredText: result.restoredText,
|
|
298
|
+
restoredCount: result.restoredCount || 0
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const unscrub = restore;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Recursively sanitizes all string values within an object or array.
|
|
306
|
+
*/
|
|
307
|
+
export function sanitizeObject(obj, options = {}) {
|
|
308
|
+
if (obj === null || obj === undefined) {
|
|
309
|
+
return {
|
|
310
|
+
sanitized: obj,
|
|
311
|
+
tokenMap: {},
|
|
312
|
+
count: 0,
|
|
313
|
+
executionMs: 0,
|
|
314
|
+
telemetry: buildAuditTelemetry({}),
|
|
315
|
+
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (typeof obj === 'string') {
|
|
320
|
+
const res = sanitize(obj, options);
|
|
321
|
+
return {
|
|
322
|
+
sanitized: res.scrubbedText,
|
|
323
|
+
tokenMap: res.tokenMap,
|
|
324
|
+
count: res.count,
|
|
325
|
+
executionMs: res.executionMs,
|
|
326
|
+
telemetry: res.telemetry,
|
|
327
|
+
auditReceipt: res.auditReceipt
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (typeof obj !== 'object') {
|
|
332
|
+
return {
|
|
333
|
+
sanitized: obj,
|
|
334
|
+
tokenMap: {},
|
|
335
|
+
count: 0,
|
|
336
|
+
executionMs: 0,
|
|
337
|
+
telemetry: buildAuditTelemetry({}),
|
|
338
|
+
auditReceipt: formatAuditReceipt(buildAuditTelemetry({}))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const startTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
343
|
+
? performance.now()
|
|
344
|
+
: Date.now();
|
|
345
|
+
|
|
346
|
+
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
347
|
+
let totalCount = 0;
|
|
348
|
+
|
|
349
|
+
function traverseAndSanitize(val) {
|
|
350
|
+
if (val === null || val === undefined) return val;
|
|
351
|
+
if (typeof val === 'string') {
|
|
352
|
+
if (val.trim().length === 0) return val;
|
|
353
|
+
const res = sanitize(val, {
|
|
354
|
+
...options,
|
|
355
|
+
existingSessionMap: mergedSessionMap,
|
|
356
|
+
generateReceipt: false
|
|
357
|
+
});
|
|
358
|
+
Object.assign(mergedSessionMap, res.tokenMap);
|
|
359
|
+
totalCount += res.count;
|
|
360
|
+
return res.scrubbedText;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(val)) {
|
|
363
|
+
return val.map(item => traverseAndSanitize(item));
|
|
364
|
+
}
|
|
365
|
+
if (val instanceof Date || val instanceof RegExp) {
|
|
366
|
+
return val;
|
|
367
|
+
}
|
|
368
|
+
if (typeof val === 'object') {
|
|
369
|
+
const out = {};
|
|
370
|
+
for (const [k, v] of Object.entries(val)) {
|
|
371
|
+
out[k] = traverseAndSanitize(v);
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
return val;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const sanitizedResult = traverseAndSanitize(obj);
|
|
379
|
+
|
|
380
|
+
const endTime = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
381
|
+
? performance.now()
|
|
382
|
+
: Date.now();
|
|
383
|
+
|
|
384
|
+
const executionMs = Math.round((endTime - startTime) * 100) / 100;
|
|
385
|
+
const telemetry = buildAuditTelemetry(mergedSessionMap);
|
|
386
|
+
const auditReceipt = options.generateReceipt !== false ? formatAuditReceipt(telemetry) : '';
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
sanitized: sanitizedResult,
|
|
390
|
+
tokenMap: mergedSessionMap,
|
|
391
|
+
count: totalCount,
|
|
392
|
+
executionMs: executionMs,
|
|
393
|
+
telemetry: telemetry,
|
|
394
|
+
auditReceipt: auditReceipt
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function restoreObject(obj, tokenMap = {}) {
|
|
399
|
+
if (obj === null || obj === undefined) return obj;
|
|
400
|
+
if (typeof obj === 'string') {
|
|
401
|
+
const res = restore(obj, tokenMap);
|
|
402
|
+
return res.restoredText;
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(obj)) {
|
|
405
|
+
return obj.map(item => restoreObject(item, tokenMap));
|
|
406
|
+
}
|
|
407
|
+
if (typeof obj === 'object' && !(obj instanceof Date) && !(obj instanceof RegExp)) {
|
|
408
|
+
const out = {};
|
|
409
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
410
|
+
out[k] = restoreObject(v, tokenMap);
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
return obj;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Stateful PrivacyScrubber Engine class for browser environments.
|
|
419
|
+
*/
|
|
420
|
+
export class PrivacyScrubberEngine {
|
|
421
|
+
constructor(config = {}) {
|
|
422
|
+
this.licenseKey = config.licenseKey || config.apiKey || '';
|
|
423
|
+
this.defaultProfile = config.defaultProfile || config.profile || 'General';
|
|
424
|
+
this.customRules = config.customRules || [];
|
|
425
|
+
this.tokenLabelMap = config.tokenLabelMap || {};
|
|
426
|
+
this.generateReceipt = config.generateReceipt !== false;
|
|
427
|
+
this.sessionMap = { ...(config.initialSessionMap || {}) };
|
|
428
|
+
this.ignoreList = new Set(config.ignoreList || []);
|
|
429
|
+
this.license = validateLicense(this.licenseKey);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
setLicenseKey(key) {
|
|
433
|
+
this.licenseKey = key;
|
|
434
|
+
this.license = validateLicense(this.licenseKey);
|
|
435
|
+
return this.license;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
sanitize(text, options = {}) {
|
|
439
|
+
const mergedIgnoreList = new Set(this.ignoreList);
|
|
440
|
+
if (options.ignoreList) {
|
|
441
|
+
(Array.isArray(options.ignoreList) ? options.ignoreList : [...options.ignoreList])
|
|
442
|
+
.forEach(v => mergedIgnoreList.add(v));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const mergedOptions = {
|
|
446
|
+
profile: options.profile || this.defaultProfile,
|
|
447
|
+
licenseKey: options.licenseKey || this.licenseKey,
|
|
448
|
+
customRules: [...this.customRules, ...(options.customRules || [])],
|
|
449
|
+
tokenLabelMap: { ...this.tokenLabelMap, ...(options.tokenLabelMap || {}) },
|
|
450
|
+
existingSessionMap: options.persistSession !== false ? this.sessionMap : (options.existingSessionMap || {}),
|
|
451
|
+
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
452
|
+
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
453
|
+
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
454
|
+
ignoreList: mergedIgnoreList.size > 0 ? mergedIgnoreList : null
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const result = sanitize(text, mergedOptions);
|
|
458
|
+
if (options.persistSession !== false) {
|
|
459
|
+
Object.assign(this.sessionMap, result.tokenMap);
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
restore(aiResponse, tokenMap = null) {
|
|
465
|
+
const activeMap = tokenMap || this.sessionMap;
|
|
466
|
+
return restore(aiResponse, activeMap);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
resetSession() {
|
|
470
|
+
for (const key of Object.keys(this.sessionMap)) {
|
|
471
|
+
delete this.sessionMap[key];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Browser & Web Worker Streaming Detokenizer.
|
|
478
|
+
*/
|
|
479
|
+
export class StreamingDetokenizer {
|
|
480
|
+
constructor(tokenMap = {}, options = {}) {
|
|
481
|
+
this.tokenMap = { ...tokenMap };
|
|
482
|
+
this.buffer = '';
|
|
483
|
+
this.onTokenRestored = typeof options.onTokenRestored === 'function' ? options.onTokenRestored : null;
|
|
484
|
+
this.maxTokenLength = Math.max(
|
|
485
|
+
32,
|
|
486
|
+
...Object.keys(this.tokenMap).map(k => k.length + 8)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
updateTokenMap(newTokenMap) {
|
|
491
|
+
Object.assign(this.tokenMap, newTokenMap);
|
|
492
|
+
this.maxTokenLength = Math.max(
|
|
493
|
+
this.maxTokenLength,
|
|
494
|
+
...Object.keys(this.tokenMap).map(k => k.length + 8)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
process(chunk) {
|
|
499
|
+
if (!chunk) return '';
|
|
500
|
+
this.buffer += chunk;
|
|
501
|
+
let output = '';
|
|
502
|
+
|
|
503
|
+
while (true) {
|
|
504
|
+
const openIdx = this.buffer.search(/\[|\<|\{\{|\_\_/);
|
|
505
|
+
if (openIdx === -1) {
|
|
506
|
+
output += this.buffer;
|
|
507
|
+
this.buffer = '';
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
output += this.buffer.slice(0, openIdx);
|
|
512
|
+
this.buffer = this.buffer.slice(openIdx);
|
|
513
|
+
|
|
514
|
+
const closeIdx = this.buffer.search(/\]|\>|\}\}|\_\_/);
|
|
515
|
+
if (closeIdx === -1) {
|
|
516
|
+
if (this.buffer.length > this.maxTokenLength) {
|
|
517
|
+
output += this.buffer.slice(0, this.maxTokenLength);
|
|
518
|
+
this.buffer = this.buffer.slice(this.maxTokenLength);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const candidateToken = this.buffer.slice(0, closeIdx + 1);
|
|
525
|
+
const originalValue = this.tokenMap[candidateToken];
|
|
526
|
+
|
|
527
|
+
if (originalValue !== undefined) {
|
|
528
|
+
output += originalValue;
|
|
529
|
+
if (this.onTokenRestored) {
|
|
530
|
+
this.onTokenRestored({ token: candidateToken, originalValue });
|
|
531
|
+
}
|
|
532
|
+
this.buffer = this.buffer.slice(closeIdx + 1);
|
|
533
|
+
} else {
|
|
534
|
+
output += candidateToken[0];
|
|
535
|
+
this.buffer = this.buffer.slice(1);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return output;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
flush() {
|
|
543
|
+
let output = '';
|
|
544
|
+
if (this.buffer) {
|
|
545
|
+
let flushed = this.buffer;
|
|
546
|
+
for (const [token, original] of Object.entries(this.tokenMap)) {
|
|
547
|
+
if (flushed.includes(token)) {
|
|
548
|
+
flushed = flushed.split(token).join(original);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
output = flushed;
|
|
552
|
+
this.buffer = '';
|
|
553
|
+
}
|
|
554
|
+
return output;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Wraps an async iterable AI stream to automatically de-tokenize chunks in real time.
|
|
560
|
+
*/
|
|
561
|
+
export async function* wrapAiStream(stream, tokenMap = {}, options = {}) {
|
|
562
|
+
const detokenizer = new StreamingDetokenizer(tokenMap, options);
|
|
563
|
+
for await (const chunk of stream) {
|
|
564
|
+
const text = typeof chunk === 'string' ? chunk : (chunk?.content || chunk?.text || chunk?.delta || '');
|
|
565
|
+
if (text) {
|
|
566
|
+
const detokenized = detokenizer.process(text);
|
|
567
|
+
if (detokenized) {
|
|
568
|
+
if (typeof chunk === 'string') {
|
|
569
|
+
yield detokenized;
|
|
570
|
+
} else if (typeof chunk === 'object' && chunk !== null) {
|
|
571
|
+
yield { ...chunk, content: detokenized, text: detokenized };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
yield chunk;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const remaining = detokenizer.flush();
|
|
579
|
+
if (remaining) {
|
|
580
|
+
yield remaining;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
|
|
585
|
+
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
586
|
+
if (typeof TransformStream !== 'undefined') {
|
|
587
|
+
const ts = new TransformStream({
|
|
588
|
+
transform(chunk, controller) {
|
|
589
|
+
const str = typeof chunk === 'string' ? chunk : String(chunk);
|
|
590
|
+
const out = detok.process(str);
|
|
591
|
+
if (out) controller.enqueue(out);
|
|
592
|
+
},
|
|
593
|
+
flush(controller) {
|
|
594
|
+
const rem = detok.flush();
|
|
595
|
+
if (rem) controller.enqueue(rem);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
ts.process = (c) => detok.process(c);
|
|
599
|
+
ts.flush = () => detok.flush();
|
|
600
|
+
ts.detokenizer = detok;
|
|
601
|
+
return ts;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
process: (c) => detok.process(c),
|
|
605
|
+
flush: () => detok.flush(),
|
|
606
|
+
detokenizer: detok
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function buildStreamReplacer(tokenMap = {}) {
|
|
611
|
+
const detok = new StreamingDetokenizer(tokenMap);
|
|
612
|
+
return (chunk) => detok.process(chunk);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const scrubText = scrubberPkg ? scrubberPkg.scrubText : null;
|
|
616
|
+
export const unscrubText = scrubberPkg ? scrubberPkg.unscrubText : null;
|
|
617
|
+
|
|
618
|
+
// Vision & Multimodal exports
|
|
619
|
+
export {
|
|
620
|
+
stripExifMetadata,
|
|
621
|
+
redactImageBoundingBoxes,
|
|
622
|
+
generateSvgRedactionOverlay
|
|
623
|
+
} from './vision.js';
|
|
624
|
+
import {
|
|
625
|
+
stripExifMetadata,
|
|
626
|
+
redactImageBoundingBoxes,
|
|
627
|
+
generateSvgRedactionOverlay
|
|
628
|
+
} from './vision.js';
|
|
629
|
+
|
|
630
|
+
export default {
|
|
631
|
+
sanitize,
|
|
632
|
+
sanitizeObject,
|
|
633
|
+
scrub,
|
|
634
|
+
clean,
|
|
635
|
+
restore,
|
|
636
|
+
restoreObject,
|
|
637
|
+
unscrub,
|
|
638
|
+
validateLicense,
|
|
639
|
+
validateLicenseKey,
|
|
640
|
+
buildAuditTelemetry,
|
|
641
|
+
formatAuditReceipt,
|
|
642
|
+
PrivacyScrubberEngine,
|
|
643
|
+
PrivacyScrubberLicenseError,
|
|
644
|
+
StreamingDetokenizer,
|
|
645
|
+
createStreamTokenRestorer,
|
|
646
|
+
buildStreamReplacer,
|
|
647
|
+
wrapAiStream,
|
|
648
|
+
scrubText,
|
|
649
|
+
unscrubText,
|
|
650
|
+
DEVOPS_SECRETS_DETECTOR,
|
|
651
|
+
stripExifMetadata,
|
|
652
|
+
redactImageBoundingBoxes,
|
|
653
|
+
generateSvgRedactionOverlay
|
|
654
|
+
};
|
package/index.d.ts
CHANGED
|
@@ -450,7 +450,44 @@ declare const defaultExport: {
|
|
|
450
450
|
createGuardedTools: typeof createGuardedTools;
|
|
451
451
|
scrubText: typeof scrubText;
|
|
452
452
|
unscrubText: typeof unscrubText;
|
|
453
|
+
stripExifMetadata: typeof stripExifMetadata;
|
|
454
|
+
redactImageBoundingBoxes: typeof redactImageBoundingBoxes;
|
|
455
|
+
generateSvgRedactionOverlay: typeof generateSvgRedactionOverlay;
|
|
453
456
|
};
|
|
454
457
|
|
|
458
|
+
export interface ExifStripResult {
|
|
459
|
+
cleanBuffer: Uint8Array | Buffer;
|
|
460
|
+
metadataStripped: boolean;
|
|
461
|
+
format: string;
|
|
462
|
+
removedSegments: string[];
|
|
463
|
+
bytesSaved: number;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface VisionBoundingBox {
|
|
467
|
+
x: number;
|
|
468
|
+
y: number;
|
|
469
|
+
width: number;
|
|
470
|
+
height: number;
|
|
471
|
+
label?: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface VisionRedactOptions {
|
|
475
|
+
fillColor?: string;
|
|
476
|
+
borderColor?: string;
|
|
477
|
+
showLabels?: boolean;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function stripExifMetadata(inputBuffer: Buffer | Uint8Array): ExifStripResult;
|
|
481
|
+
export function redactImageBoundingBoxes(
|
|
482
|
+
canvasOrCtx: any,
|
|
483
|
+
boxes?: VisionBoundingBox[],
|
|
484
|
+
options?: VisionRedactOptions
|
|
485
|
+
): number;
|
|
486
|
+
export function generateSvgRedactionOverlay(
|
|
487
|
+
dimensions?: { width: number; height: number },
|
|
488
|
+
boxes?: VisionBoundingBox[],
|
|
489
|
+
options?: VisionRedactOptions
|
|
490
|
+
): string;
|
|
491
|
+
|
|
455
492
|
export default defaultExport;
|
|
456
493
|
|