@privacyscrubber/sdk 2.3.7 → 2.4.0

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.
Files changed (45) hide show
  1. package/README.md +86 -28
  2. package/benchmark.js +347 -0
  3. package/browser.js +2 -2
  4. package/cli.js +54 -2
  5. package/examples/python-bridge/MANIFEST.in +5 -0
  6. package/examples/python-bridge/README.md +265 -0
  7. package/examples/python-bridge/bridge_daemon.cjs +241 -0
  8. package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
  9. package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
  10. package/examples/python-bridge/fastapi_example.py +206 -0
  11. package/examples/python-bridge/langchain_adapter.py +20 -0
  12. package/examples/python-bridge/langchain_example.py +126 -0
  13. package/examples/python-bridge/llamaindex_example.py +103 -0
  14. package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
  15. package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
  16. package/examples/python-bridge/privacyscrubber/cli.py +138 -0
  17. package/examples/python-bridge/privacyscrubber/client.py +542 -0
  18. package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
  19. package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
  20. package/examples/python-bridge/privacyscrubber/types.py +52 -0
  21. package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
  22. package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
  23. package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
  24. package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
  25. package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
  26. package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
  27. package/examples/python-bridge/privacyscrubber.py +36 -0
  28. package/examples/python-bridge/pyproject.toml +62 -0
  29. package/examples/python-bridge/setup.py +66 -0
  30. package/examples/python-bridge/test_python_bridge.py +320 -0
  31. package/examples/vercel-ai-stream.js +195 -0
  32. package/fastmcp.cjs +291 -2
  33. package/fastmcp.d.ts +18 -0
  34. package/fastmcp.js +4 -0
  35. package/index.cjs +211 -134
  36. package/index.js +211 -134
  37. package/package.json +15 -3
  38. package/ps-license-manager.js +1 -1
  39. package/ps-pii-engine.cjs +1 -1
  40. package/scrubber-core.cjs +1 -1
  41. package/vercel-ai.cjs +400 -0
  42. package/vercel-ai.d.ts +156 -0
  43. package/vercel-ai.js +36 -0
  44. package/vision.cjs +13 -5
  45. package/vision.js +13 -5
@@ -0,0 +1,265 @@
1
+ # PrivacyScrubber Python Bridge (ZTDS Zero-Egress Sidecar)
2
+
3
+ Sub-millisecond, zero-dependency Python adapter for **PrivacyScrubber SDK** (`@privacyscrubber/sdk`).
4
+
5
+ Enforces **Zero-Trust Data Sanitization (ZTDS RFC v1.0)** across Python AI and data pipelines:
6
+ - **FastAPI / Flask / Django**: Pre-LLM sanitization and post-LLM in-memory rehydration.
7
+ - **LangChain**: `ZTDSCallbackHandler` and `ZTDSDocumentTransformer`.
8
+ - **LlamaIndex**: Pre-embedding chunk and metadata sanitization.
9
+ - **Raw OpenAI / Anthropic / Gemini SDKs**: Direct string and dictionary tokenization.
10
+
11
+ ---
12
+
13
+ ## Architectural Comparison: PrivacyScrubber vs. Microsoft Presidio
14
+
15
+ | Metric | Microsoft Presidio | PrivacyScrubber ZTDS Bridge |
16
+ | :--- | :--- | :--- |
17
+ | **Inference Latency** | ~35ms – 65ms per request | **~0.15ms – 0.40ms per request (<1ms SLA)** |
18
+ | **Memory Footprint** | 1.8 GB – 3.2 GB RAM (PyTorch / spaCy) | **< 25 MB RAM (Zero PyTorch / zero spaCy)** |
19
+ | **Python Dependencies** | `presidio-analyzer`, `presidio-anonymizer`, `spacy`, `torch` | **0 Dependencies (Python Standard Library Only)** |
20
+ | **Round-Trip Re-hydration** | Non-trivial (custom hash table mapping required) | **Native 100% Round-Trip (`client.restore()`)** |
21
+ | **Pre-Configured Profiles** | Basic PII only (requires custom YAML recognizers) | **30 Built-in Industry Profiles (HIPAA, Financial, Legal, HR, etc.)** |
22
+ | **Network Egress** | Potential telemetry unless air-gapped | **0.00 Bytes Network Egress (RAM-only volatile mapping)** |
23
+ | **Container Startup** | 15 – 30 seconds model loading time | **< 100 milliseconds** |
24
+
25
+ ---
26
+
27
+ ---
28
+
29
+ ## Installation & Prerequisites
30
+
31
+ 1. Ensure **Node.js** (v18+) and **Python** (3.8+) are available on your system.
32
+ 2. Install `@privacyscrubber/sdk`:
33
+ ```bash
34
+ npm install @privacyscrubber/sdk
35
+ ```
36
+ 3. Install the Python package via `pip`:
37
+ ```bash
38
+ # Editable local development:
39
+ pip install -e npm-sdk/examples/python-bridge
40
+
41
+ # Or standard pip install:
42
+ pip install ./npm-sdk/examples/python-bridge
43
+ ```
44
+ *Note: Zero external Python dependencies are required.*
45
+
46
+ ---
47
+
48
+ ## Command Line Interface (CLI)
49
+
50
+ The package bundles a terminal CLI utility `privacyscrubber` (alias: `ps-bridge`):
51
+
52
+ ```bash
53
+ # Check ZTDS engine status and uptime
54
+ privacyscrubber ping
55
+ privacyscrubber status
56
+
57
+ # Sanitize text directly
58
+ privacyscrubber sanitize "Customer Alice Cooper (alice@hospital.org)" --profile Healthcare
59
+
60
+ # Pipe text through standard input:
61
+ echo "Analyze wire for John Doe john@corp.com" | privacyscrubber sanitize
62
+
63
+ # Output structured JSON with tokens and compliance telemetry:
64
+ privacyscrubber sanitize "Patient John Doe" --json
65
+
66
+ # Restore surrogate tokens:
67
+ privacyscrubber restore "Patient [NAME_1]" --token-map '{"[NAME_1]": "John Doe"}'
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Quickstart
73
+
74
+ ### 1. Synchronous Client
75
+
76
+ ```python
77
+ from privacyscrubber import PrivacyScrubber
78
+
79
+ # Initialize sidecar in local RAM (auto-starts daemon)
80
+ with PrivacyScrubber(default_profile="General") as ps:
81
+ # Sanitize prompt before sending to cloud LLM
82
+ result = ps.sanitize("Please review intake for patient John Doe (john@acme.com, phone +1-555-0199).")
83
+
84
+ print("Sanitized Prompt:", result.sanitized)
85
+ # Output: Please review intake for patient [NAME_1] ([EMAIL_1], phone [PHONE_1]).
86
+
87
+ # Simulate LLM operating strictly on tokens
88
+ ai_response = "I have noted records for [NAME_1] ([EMAIL_1])."
89
+
90
+ # Restore cleartext tokens in local RAM
91
+ restored = ps.restore(ai_response)
92
+ print("Restored Answer:", restored.restored_text)
93
+ # Output: I have noted records for John Doe (john@acme.com).
94
+ ```
95
+
96
+ ### 2. Asynchronous Client (FastAPI / asyncio)
97
+
98
+ ```python
99
+ import asyncio
100
+ from privacyscrubber import AsyncPrivacyScrubber
101
+
102
+ async def main():
103
+ async with AsyncPrivacyScrubber(default_profile="Healthcare") as ps:
104
+ res = await ps.sanitize("Patient Alice Cooper (DOB: 1984-05-12, SSN: 000-11-2222).")
105
+ print("Sanitized:", res.sanitized)
106
+ # Patient [NAME_1] (DOB: [DATE_1], SSN: [ID_1]).
107
+
108
+ restored = await ps.restore("Prescription confirmed for [NAME_1].")
109
+ print("Restored:", restored.restored_text)
110
+
111
+ asyncio.run(main())
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Framework Integrations
117
+
118
+ ### FastAPI Zero-Egress Proxy (`fastapi_example.py`)
119
+
120
+ Run an OpenAI-compatible proxy that guarantees no raw PII ever touches OpenAI or external model APIs:
121
+
122
+ ```python
123
+ from fastapi import FastAPI
124
+ from privacyscrubber import AsyncPrivacyScrubber
125
+
126
+ app = FastAPI()
127
+ scrubber = AsyncPrivacyScrubber()
128
+
129
+ @app.on_event("startup")
130
+ async def on_startup():
131
+ await scrubber.start()
132
+
133
+ @app.on_event("shutdown")
134
+ async def on_shutdown():
135
+ await scrubber.close()
136
+
137
+ @app.post("/v1/chat/completions")
138
+ async def chat_proxy(payload: dict):
139
+ # 1. Sanitize user prompt in client memory (<0.2ms)
140
+ messages = payload.get("messages", [])
141
+ user_prompt = messages[-1]["content"]
142
+
143
+ sanitized = await scrubber.sanitize(user_prompt)
144
+ messages[-1]["content"] = sanitized.sanitized
145
+
146
+ # 2. Forward sanitized payload to cloud LLM (httpx call)
147
+ # raw_llm_response = await client.post("https://api.openai.com/v1/chat/completions", json=payload)
148
+ mock_llm_reply = f"Acknowledged request for {sanitized.sanitized}"
149
+
150
+ # 3. Restore cleartext in RAM before returning to end-user
151
+ restored = await scrubber.restore(mock_llm_reply, token_map=sanitized.token_map)
152
+ return {"choices": [{"message": {"role": "assistant", "content": restored.restored_text}}]}
153
+ ```
154
+
155
+ ### LangChain (`langchain_adapter.py`)
156
+
157
+ #### Callback Handler (Transparent Pre/Post LLM Guard)
158
+ ```python
159
+ from langchain_openai import ChatOpenAI
160
+ from langchain_adapter import ZTDSCallbackHandler
161
+
162
+ llm = ChatOpenAI(
163
+ model="gpt-4o",
164
+ callbacks=[ZTDSCallbackHandler(profile="Financial")]
165
+ )
166
+
167
+ # Prompts are automatically sanitized before leaving process memory;
168
+ # LLM outputs are automatically restored before returning to your code.
169
+ response = llm.invoke("What is the credit rating for customer David Miller (SSN 987-65-4321)?")
170
+ print(response.content)
171
+ ```
172
+
173
+ #### Vector Ingestion Guard (Pinecone, Chroma, Qdrant)
174
+ ```python
175
+ from langchain_adapter import ZTDSDocumentTransformer
176
+
177
+ transformer = ZTDSDocumentTransformer(
178
+ profile="Financial",
179
+ sensitive_metadata_keys=["customer_email", "account_id"]
180
+ )
181
+
182
+ # Sanitizes both page_content and sensitive metadata dictionaries
183
+ safe_docs = transformer.transform_documents(raw_docs)
184
+ vector_store.add_documents(safe_docs)
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Nested Dictionary & JSON Sanitization
190
+
191
+ Sanitize complex structured payloads (REST API payloads, MongoDB documents, JSON logs):
192
+
193
+ ```python
194
+ payload = {
195
+ "user_id": 9012,
196
+ "customer": {
197
+ "name": "Sarah Connor",
198
+ "email": "sconnor@cyberdyne.org",
199
+ "phones": ["+1-555-0199", "+1-555-0188"]
200
+ },
201
+ "status": "active"
202
+ }
203
+
204
+ # Recursively sanitizes strings, preserving keys and types intact:
205
+ result = ps.sanitize_object(payload)
206
+ print(result.sanitized)
207
+ # {
208
+ # "user_id": 9012,
209
+ # "customer": {
210
+ # "name": "[NAME_1]",
211
+ # "email": "[EMAIL_1]",
212
+ # "phones": ["[PHONE_1]", "[PHONE_2]"]
213
+ # },
214
+ # "status": "active"
215
+ # }
216
+
217
+ # Restore structure:
218
+ restored = ps.restore_object(result.sanitized, token_map=result.token_map)
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Production Docker Deployment
224
+
225
+ Deploying Python applications alongside the PrivacyScrubber sidecar in a single multi-runtime container:
226
+
227
+ ```dockerfile
228
+ FROM node:22-bookworm-slim AS node-base
229
+
230
+ FROM python:3.11-slim-bookworm
231
+
232
+ # Copy Node.js runtime into Python container
233
+ COPY --from=node-base /usr/local/bin/node /usr/local/bin/node
234
+ COPY --from=node-base /usr/local/lib/node_modules /usr/local/lib/node_modules
235
+
236
+ WORKDIR /app
237
+
238
+ # Install SDK bridge
239
+ COPY package*.json ./
240
+ RUN npm install @privacyscrubber/sdk --production
241
+
242
+ # Install Python requirements
243
+ COPY requirements.txt ./
244
+ RUN pip install --no-cache-dir -r requirements.txt
245
+
246
+ COPY . .
247
+
248
+ CMD ["uvicorn", "fastapi_example:app", "--host", "0.0.0.0", "--port", "8000"]
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Running the Verification Suite
254
+
255
+ Run all tests, benchmarks, and concurrency assertions:
256
+
257
+ ```bash
258
+ # Automated Test Suite (10 tests, 0.3s)
259
+ python3 test_python_bridge.py
260
+
261
+ # Interactive Examples
262
+ python3 fastapi_example.py
263
+ python3 langchain_example.py
264
+ python3 llamaindex_example.py
265
+ ```
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PrivacyScrubber ZTDS Python Bridge Daemon
4
+ *
5
+ * Provides a low-latency, zero-network-egress stdio NDJSON RPC daemon
6
+ * for Python, FastAPI, LangChain, and LlamaIndex environments.
7
+ *
8
+ * Communication protocol:
9
+ * - Request (stdin): Single-line JSON {"id": number|string, "action": string, ...}\n
10
+ * - Response (stdout): Single-line JSON {"id": number|string, "result": {...}}\n
11
+ * or {"id": number|string, "error": {"message": string, ...}}\n
12
+ *
13
+ * Zero external network egress. 100% in-memory RAM execution.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const readline = require('readline');
19
+ const path = require('path');
20
+
21
+ // Resolve @privacyscrubber/sdk across local monorepo, parent dirs, cwd, and node_modules
22
+ let sdk;
23
+ const candidatePaths = [
24
+ process.env.PRIVACYSCRUBBER_SDK_PATH,
25
+ path.resolve(process.cwd(), 'npm-sdk/index.cjs'),
26
+ path.resolve(process.cwd(), 'node_modules/@privacyscrubber/sdk'),
27
+ path.resolve(__dirname, '../../index.cjs'),
28
+ path.resolve(__dirname, '../index.cjs'),
29
+ path.resolve(__dirname, './index.cjs'),
30
+ '@privacyscrubber/sdk'
31
+ ];
32
+
33
+ for (const p of candidatePaths) {
34
+ if (!p) continue;
35
+ try {
36
+ sdk = require(p);
37
+ break;
38
+ } catch (e) {
39
+ // try next candidate
40
+ }
41
+ }
42
+
43
+ if (!sdk) {
44
+ try {
45
+ const resolved = require.resolve('@privacyscrubber/sdk', { paths: [process.cwd(), __dirname] });
46
+ sdk = require(resolved);
47
+ } catch (e2) {}
48
+ }
49
+
50
+ if (!sdk) {
51
+ process.stderr.write('Fatal: Could not load @privacyscrubber/sdk or index.cjs\n');
52
+ process.exit(1);
53
+ }
54
+
55
+ // Parse initial CLI options
56
+ const args = process.argv.slice(2);
57
+ let initialProfile = process.env.PRIVACYSCRUBBER_PROFILE || 'General';
58
+ let licenseKey = process.env.PRIVACYSCRUBBER_KEY || process.env.PRIVACYSCRUBBER_LICENSE_KEY || '';
59
+
60
+ for (let i = 0; i < args.length; i++) {
61
+ if (args[i] === '--profile' && args[i + 1]) {
62
+ initialProfile = args[++i];
63
+ } else if (args[i] === '--license-key' && args[i + 1]) {
64
+ licenseKey = args[++i];
65
+ }
66
+ }
67
+
68
+ // Instantiate long-lived engine in local RAM
69
+ const engine = new sdk.PrivacyScrubberEngine({
70
+ licenseKey,
71
+ defaultProfile: initialProfile,
72
+ generateReceipt: true
73
+ });
74
+
75
+ function sendResponse(id, result, error = null) {
76
+ const payload = { id };
77
+ if (error) {
78
+ payload.error = {
79
+ message: error.message || String(error),
80
+ name: error.name || 'Error',
81
+ tier: error.tier,
82
+ limit: error.limit,
83
+ profile: error.profile
84
+ };
85
+ } else {
86
+ payload.result = result;
87
+ }
88
+ process.stdout.write(JSON.stringify(payload) + '\n');
89
+ }
90
+
91
+ // Setup readline interface on stdin
92
+ const rl = readline.createInterface({
93
+ input: process.stdin,
94
+ output: process.stdout,
95
+ terminal: false
96
+ });
97
+
98
+ rl.on('line', (line) => {
99
+ const trimmed = line.trim();
100
+ if (!trimmed) return;
101
+
102
+ let req;
103
+ try {
104
+ req = JSON.parse(trimmed);
105
+ } catch (parseErr) {
106
+ sendResponse(null, null, new Error(`Invalid JSON request: ${parseErr.message}`));
107
+ return;
108
+ }
109
+
110
+ const { id, action } = req;
111
+
112
+ try {
113
+ switch (action) {
114
+ case 'ping': {
115
+ sendResponse(id, {
116
+ pong: true,
117
+ engine: 'PrivacyScrubber ZTDS',
118
+ version: '2.4.0',
119
+ uptime: process.uptime()
120
+ });
121
+ break;
122
+ }
123
+
124
+ case 'sanitize': {
125
+ const text = req.text || '';
126
+ const options = {
127
+ profile: req.profile || engine.defaultProfile,
128
+ detectSecrets: req.detect_secrets ?? req.detectSecrets ?? false,
129
+ licenseKey: req.license_key || req.licenseKey || engine.licenseKey,
130
+ persistSession: req.persist_session ?? req.persistSession ?? true,
131
+ generateReceipt: req.generate_receipt ?? req.generateReceipt ?? true,
132
+ customRules: req.custom_rules || req.customRules || [],
133
+ ignoreList: req.ignore_list || req.ignoreList || null,
134
+ throwOnLimit: req.throw_on_limit ?? req.throwOnLimit ?? true
135
+ };
136
+
137
+ const res = engine.sanitize(text, options);
138
+ sendResponse(id, {
139
+ sanitized: res.scrubbedText,
140
+ scrubbed_text: res.scrubbedText,
141
+ token_map: res.tokenMap,
142
+ matches: res.matches,
143
+ count: res.count,
144
+ execution_ms: res.executionMs,
145
+ telemetry: res.telemetry,
146
+ audit_receipt: res.auditReceipt,
147
+ license: res.license
148
+ });
149
+ break;
150
+ }
151
+
152
+ case 'restore': {
153
+ const text = req.text || '';
154
+ const tokenMap = req.token_map || req.tokenMap || null;
155
+ const res = engine.restore(text, tokenMap);
156
+ sendResponse(id, {
157
+ restored_text: res.restoredText,
158
+ restored_count: res.restoredCount
159
+ });
160
+ break;
161
+ }
162
+
163
+ case 'sanitize_object': {
164
+ const data = req.data ?? {};
165
+ const options = {
166
+ profile: req.profile || engine.defaultProfile,
167
+ detectSecrets: req.detect_secrets ?? req.detectSecrets ?? false,
168
+ licenseKey: req.license_key || req.licenseKey || engine.licenseKey,
169
+ customRules: req.custom_rules || req.customRules || [],
170
+ throwOnLimit: req.throw_on_limit ?? req.throwOnLimit ?? true
171
+ };
172
+
173
+ const res = sdk.sanitizeObject(data, options);
174
+ // Also merge into engine sessionMap if requested
175
+ if (req.persist_session !== false && res.tokenMap) {
176
+ Object.assign(engine.sessionMap, res.tokenMap);
177
+ }
178
+ sendResponse(id, {
179
+ sanitized: res.sanitized,
180
+ token_map: res.tokenMap,
181
+ count: res.count,
182
+ execution_ms: res.executionMs,
183
+ telemetry: res.telemetry,
184
+ audit_receipt: res.auditReceipt
185
+ });
186
+ break;
187
+ }
188
+
189
+ case 'restore_object': {
190
+ const data = req.data ?? {};
191
+ const tokenMap = req.token_map || req.tokenMap || engine.sessionMap;
192
+ const restored = sdk.restoreObject(data, tokenMap);
193
+ sendResponse(id, {
194
+ restored
195
+ });
196
+ break;
197
+ }
198
+
199
+ case 'reset_session': {
200
+ engine.resetSession();
201
+ sendResponse(id, { success: true });
202
+ break;
203
+ }
204
+
205
+ case 'mark_false_positive': {
206
+ const token = req.token;
207
+ if (!token) {
208
+ throw new Error('Missing token argument for mark_false_positive');
209
+ }
210
+ const fpRes = engine.markFalsePositive(token);
211
+ sendResponse(id, fpRes);
212
+ break;
213
+ }
214
+
215
+ case 'status': {
216
+ sendResponse(id, {
217
+ profile: engine.defaultProfile,
218
+ license_tier: engine.license?.tier || 'FREE',
219
+ is_pro: engine.license?.isPro || false,
220
+ session_tokens_count: Object.keys(engine.sessionMap).length,
221
+ ignore_list_size: engine.ignoreList.size,
222
+ memory_usage: process.memoryUsage()
223
+ });
224
+ break;
225
+ }
226
+
227
+ default:
228
+ sendResponse(id, null, new Error(`Unknown action: ${action}`));
229
+ break;
230
+ }
231
+ } catch (err) {
232
+ sendResponse(id, null, err);
233
+ }
234
+ });
235
+
236
+ rl.on('close', () => {
237
+ process.exit(0);
238
+ });
239
+
240
+ process.on('SIGINT', () => process.exit(0));
241
+ process.on('SIGTERM', () => process.exit(0));