@privacyscrubber/sdk 2.4.4 → 2.4.6

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/cli.js CHANGED
@@ -32,7 +32,7 @@ for (const a of rawArgs) {
32
32
  }
33
33
  }
34
34
 
35
- let sdkVersion = '2.4.4';
35
+ let sdkVersion = '2.4.6';
36
36
  try {
37
37
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
38
38
  sdkVersion = pkg.version || sdkVersion;
@@ -130,12 +130,72 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
130
130
  }
131
131
 
132
132
  const defaultEnvKey = process.env.PRIVACYSCRUBBER_KEY || process.env.PRIVACYSCRUBBER_LICENSE_KEY || process.env.ZTDS_LICENSE_KEY || '';
133
+ const allowedCorsOrigin = process.env.PRIVACYSCRUBBER_CORS_ORIGIN || '';
134
+ const loopbackOriginRegex = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i;
135
+
136
+ const sidecarSessions = new Map();
137
+ const MAX_SIDECAR_SESSIONS = 200;
138
+ const MAX_SIDECAR_SESSION_TOKENS = 5000;
139
+
140
+ function getSidecarSessionMap(sessionId) {
141
+ const sid = String(sessionId || '').trim();
142
+ if (!sid) return null;
143
+ let session = sidecarSessions.get(sid);
144
+ const now = Date.now();
145
+ if (!session) {
146
+ if (sidecarSessions.size >= MAX_SIDECAR_SESSIONS) {
147
+ const oldestKey = sidecarSessions.keys().next().value;
148
+ sidecarSessions.delete(oldestKey);
149
+ }
150
+ session = { map: Object.create(null), createdAt: now, lastActive: now };
151
+ sidecarSessions.set(sid, session);
152
+ } else {
153
+ // Refresh LRU order
154
+ sidecarSessions.delete(sid);
155
+ session.lastActive = now;
156
+ sidecarSessions.set(sid, session);
157
+ }
158
+ return session.map;
159
+ }
160
+
161
+ function safeParseSidecarJson(raw) {
162
+ if (!raw) return {};
163
+ return JSON.parse(raw, (key, value) => {
164
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
165
+ return value;
166
+ });
167
+ }
168
+
169
+ function sanitizeTokenMapInput(rawMap) {
170
+ const clean = Object.create(null);
171
+ if (!rawMap || typeof rawMap !== 'object' || Array.isArray(rawMap)) return clean;
172
+ for (const [k, v] of Object.entries(rawMap)) {
173
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
174
+ if (typeof v === 'string') clean[k] = v;
175
+ }
176
+ return clean;
177
+ }
133
178
 
134
179
  const server = http.createServer((req, res) => {
135
- // CORS headers for local loopback & container IPC
136
- res.setHeader('Access-Control-Allow-Origin', '*');
180
+ // Strict Loopback CORS guard: block external browser websites from probing or calling local sidecar
181
+ const reqOrigin = req.headers['origin'] ? String(req.headers['origin']).trim() : '';
182
+ res.setHeader('Vary', 'Origin');
137
183
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
138
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-License-Key');
184
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-License-Key, X-Session-Id');
185
+
186
+ if (reqOrigin) {
187
+ const isAllowedOrigin = allowedCorsOrigin === '*'
188
+ || (allowedCorsOrigin && reqOrigin === allowedCorsOrigin)
189
+ || loopbackOriginRegex.test(reqOrigin);
190
+ if (!isAllowedOrigin) {
191
+ res.writeHead(403, { 'Content-Type': 'application/json' });
192
+ res.end(JSON.stringify({ error: 'Cross-Origin Request Blocked by ZTDS Local Loopback Policy' }));
193
+ return;
194
+ }
195
+ res.setHeader('Access-Control-Allow-Origin', allowedCorsOrigin === '*' ? '*' : reqOrigin);
196
+ } else if (allowedCorsOrigin === '*') {
197
+ res.setHeader('Access-Control-Allow-Origin', '*');
198
+ }
139
199
 
140
200
  if (req.method === 'OPTIONS') {
141
201
  res.writeHead(204);
@@ -165,6 +225,7 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
165
225
  tier: lic.valid ? (lic.tier || 'SDK') : 'Community (Free Tier)',
166
226
  licensed: !!lic.valid,
167
227
  version: sdkVersion,
228
+ activeSessions: sidecarSessions.size,
168
229
  networkEgress: '0 bytes (100% in-memory RAM)',
169
230
  uptimeSec: Math.floor(process.uptime()),
170
231
  memoryMb: (process.memoryUsage().heapUsed / (1024 * 1024)).toFixed(2)
@@ -181,7 +242,10 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
181
242
  `ztds_uptime_seconds ${Math.floor(process.uptime())}\n` +
182
243
  `# HELP ztds_memory_heap_bytes Process heap usage\n` +
183
244
  `# TYPE ztds_memory_heap_bytes gauge\n` +
184
- `ztds_memory_heap_bytes ${mem.heapUsed}\n`
245
+ `ztds_memory_heap_bytes ${mem.heapUsed}\n` +
246
+ `# HELP ztds_active_sessions Active in-memory sidecar sessions\n` +
247
+ `# TYPE ztds_active_sessions gauge\n` +
248
+ `ztds_active_sessions ${sidecarSessions.size}\n`
185
249
  );
186
250
  return;
187
251
  }
@@ -203,26 +267,64 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
203
267
  req.on('end', () => {
204
268
  if (aborted || res.writableEnded) return;
205
269
  try {
206
- const payload = bodyStr ? JSON.parse(bodyStr) : {};
270
+ const payload = bodyStr ? safeParseSidecarJson(bodyStr) : {};
207
271
  const isScrub = reqUrl === '/scrub' || reqUrl === '/sanitize' || reqUrl === '/v1/scrub' || reqUrl === '/v1/sanitize';
208
272
  const isUnscrub = reqUrl === '/unscrub' || reqUrl === '/restore' || reqUrl === '/v1/unscrub' || reqUrl === '/v1/restore';
273
+ const isReset = reqUrl === '/reset' || reqUrl === '/v1/reset' || reqUrl === '/v1/session/reset';
274
+ const sessionId = payload.sessionId || payload.session_id || (req.headers['x-session-id'] ? String(req.headers['x-session-id']).trim() : '');
275
+
276
+ if (isReset) {
277
+ if (payload.all === true || payload.reset_all === true || !sessionId) {
278
+ const count = sidecarSessions.size;
279
+ sidecarSessions.clear();
280
+ res.writeHead(200, { 'Content-Type': 'application/json' });
281
+ res.end(JSON.stringify({ status: 'ok', purged: true, clearedSessions: count }));
282
+ return;
283
+ }
284
+ const existed = sidecarSessions.delete(String(sessionId).trim());
285
+ res.writeHead(200, { 'Content-Type': 'application/json' });
286
+ res.end(JSON.stringify({ status: 'ok', sessionId: String(sessionId).trim(), purged: existed }));
287
+ return;
288
+ }
209
289
 
210
290
  if (isScrub) {
211
291
  const text = payload.text || '';
212
- const opts = payload.options || { profile: payload.profile || 'General', detectSecrets: true };
292
+ const rawOpts = (payload.options && typeof payload.options === 'object') ? payload.options : {};
293
+ const opts = {
294
+ ...rawOpts,
295
+ profile: rawOpts.profile || payload.profile || 'General',
296
+ detectSecrets: rawOpts.detectSecrets !== undefined ? rawOpts.detectSecrets : (payload.detectSecrets !== undefined ? payload.detectSecrets : true)
297
+ };
213
298
  if (!opts.licenseKey) {
214
299
  opts.licenseKey = payload.licenseKey || effectiveServerKey;
215
300
  }
301
+ const sessionMap = sessionId ? getSidecarSessionMap(sessionId) : null;
302
+ if (sessionMap) {
303
+ opts.existingSessionMap = { ...sessionMap, ...(opts.existingSessionMap ? sanitizeTokenMapInput(opts.existingSessionMap) : {}) };
304
+ }
216
305
 
217
306
  const start = process.hrtime.bigint();
218
307
  const result = sanitize(text, opts);
219
308
  const end = process.hrtime.bigint();
220
309
  const durationUs = Number(end - start) / 1000;
221
310
 
311
+ if (sessionMap && result.tokenMap) {
312
+ Object.assign(sessionMap, sanitizeTokenMapInput(result.tokenMap));
313
+ const keys = Object.keys(sessionMap);
314
+ if (keys.length > MAX_SIDECAR_SESSION_TOKENS) {
315
+ const pruneCount = keys.length - MAX_SIDECAR_SESSION_TOKENS;
316
+ for (let i = 0; i < pruneCount; i++) {
317
+ delete sessionMap[keys[i]];
318
+ }
319
+ }
320
+ }
321
+
222
322
  res.writeHead(200, { 'Content-Type': 'application/json' });
223
323
  res.end(JSON.stringify({
224
324
  sanitizedText: result.scrubbedText,
325
+ scrubbedText: result.scrubbedText,
225
326
  tokenMap: result.tokenMap,
327
+ ...(sessionId && { sessionId: String(sessionId).trim() }),
226
328
  license: result.license,
227
329
  telemetry: {
228
330
  ...result.telemetry,
@@ -235,15 +337,21 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
235
337
 
236
338
  if (isUnscrub) {
237
339
  const text = payload.text || '';
238
- const tokenMap = payload.tokenMap || {};
239
- const { restoredText, restoredCount } = restore(text, tokenMap);
340
+ const sessionMap = sessionId ? getSidecarSessionMap(sessionId) : null;
341
+ const explicitMap = sanitizeTokenMapInput(payload.tokenMap);
342
+ const effectiveMap = sessionMap ? Object.assign(Object.create(null), sessionMap, explicitMap) : explicitMap;
343
+ const { restoredText, restoredCount } = restore(text, effectiveMap);
240
344
  res.writeHead(200, { 'Content-Type': 'application/json' });
241
- res.end(JSON.stringify({ restoredText, restoredCount }));
345
+ res.end(JSON.stringify({
346
+ restoredText,
347
+ restoredCount,
348
+ ...(sessionId && { sessionId: String(sessionId).trim() })
349
+ }));
242
350
  return;
243
351
  }
244
352
 
245
353
  res.writeHead(404, { 'Content-Type': 'application/json' });
246
- res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /v1/scrub, /v1/unscrub, /v1/health, /v1/metrics' }));
354
+ res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /v1/scrub, /v1/unscrub, /v1/reset, /v1/health, /v1/metrics' }));
247
355
  } catch (err) {
248
356
  if (err && err.name === 'PrivacyScrubberLicenseError') {
249
357
  res.writeHead(402, { 'Content-Type': 'application/json' });
@@ -152,12 +152,12 @@ async def chat_proxy(payload: dict):
152
152
  return {"choices": [{"message": {"role": "assistant", "content": restored.restored_text}}]}
153
153
  ```
154
154
 
155
- ### LangChain (`langchain_adapter.py`)
155
+ ### LangChain (`privacyscrubber.langchain`)
156
156
 
157
157
  #### Callback Handler (Transparent Pre/Post LLM Guard)
158
158
  ```python
159
159
  from langchain_openai import ChatOpenAI
160
- from langchain_adapter import ZTDSCallbackHandler
160
+ from privacyscrubber.langchain import ZTDSCallbackHandler
161
161
 
162
162
  llm = ChatOpenAI(
163
163
  model="gpt-4o",
@@ -172,7 +172,7 @@ print(response.content)
172
172
 
173
173
  #### Vector Ingestion Guard (Pinecone, Chroma, Qdrant)
174
174
  ```python
175
- from langchain_adapter import ZTDSDocumentTransformer
175
+ from privacyscrubber.langchain import ZTDSDocumentTransformer
176
176
 
177
177
  transformer = ZTDSDocumentTransformer(
178
178
  profile="Financial",
@@ -59,7 +59,7 @@ if FASTAPI_AVAILABLE:
59
59
  app = FastAPI(
60
60
  title="PrivacyScrubber ZTDS Sidecar Proxy",
61
61
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for LLM Pipelines",
62
- version="2.4.4"
62
+ version="2.4.6"
63
63
  )
64
64
 
65
65
  @app.on_event("startup")
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import json
17
17
  from privacyscrubber import PrivacyScrubberClient
18
- from langchain_adapter import (
18
+ from privacyscrubber.langchain import (
19
19
  ZTDSCallbackHandler,
20
20
  ZTDSDocumentTransformer,
21
21
  create_ztds_lcel_runnables,
@@ -24,7 +24,7 @@ from privacyscrubber.exceptions import (
24
24
  PrivacyScrubberLicenseError,
25
25
  )
26
26
 
27
- __version__ = "2.4.4"
27
+ __version__ = "2.4.6"
28
28
  __author__ = "Ilya Sibiryakov (BrandMeWeb)"
29
29
  __license__ = "Proprietary"
30
30
 
@@ -101,7 +101,7 @@ rl.on('line', (line) => {
101
101
 
102
102
  let req;
103
103
  try {
104
- req = JSON.parse(trimmed);
104
+ req = JSON.parse(trimmed, (k, v) => (k === '__proto__' || k === 'constructor' || k === 'prototype') ? undefined : v);
105
105
  } catch (parseErr) {
106
106
  sendResponse(null, null, new Error(`Invalid JSON request: ${parseErr.message}`));
107
107
  return;
@@ -115,7 +115,7 @@ rl.on('line', (line) => {
115
115
  sendResponse(id, {
116
116
  pong: true,
117
117
  engine: 'PrivacyScrubber ZTDS',
118
- version: '2.4.4',
118
+ version: sdk.SDK_VERSION || '2.4.6',
119
119
  uptime: process.uptime()
120
120
  });
121
121
  break;
@@ -50,7 +50,7 @@ def main() -> None:
50
50
  args = parser.parse_args()
51
51
 
52
52
  if args.version:
53
- print("PrivacyScrubber ZTDS Python SDK v2.4.4")
53
+ print("PrivacyScrubber ZTDS Python SDK v2.4.6")
54
54
  sys.exit(0)
55
55
 
56
56
  if not args.command:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "privacyscrubber"
7
- version = "2.4.4"
7
+ version = "2.4.6"
8
8
  description = "Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -13,7 +13,7 @@ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
13
13
 
14
14
  setup(
15
15
  name="privacyscrubber",
16
- version="2.4.4",
16
+ version="2.4.6",
17
17
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)",
18
18
  long_description=long_description,
19
19
  long_description_content_type="text/markdown",