@privacyscrubber/mcp-server 1.0.2 ā 1.0.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/.well-known/mcp/server-card.json +60 -0
- package/README.md +25 -130
- package/bundle.mcpb +0 -0
- package/index.js +2 -44
- package/manifest.json +2 -2
- package/package.json +11 -2
- package/server.json +3 -3
- package/smithery.yaml +2 -10
- package/CONTRIBUTING.md +0 -52
- package/SECURITY.md +0 -30
- package/llms.txt +0 -37
- package/test-mcp-deep.js +0 -209
- package/test-mcp.js +0 -114
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"serverInfo": {
|
|
3
|
+
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
+
"version": "1.0.3"
|
|
5
|
+
},
|
|
6
|
+
"tools": [
|
|
7
|
+
{
|
|
8
|
+
"name": "sanitize_text",
|
|
9
|
+
"description": "Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM.",
|
|
10
|
+
"inputSchema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"text": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The raw text, code, or logs to sanitize."
|
|
16
|
+
},
|
|
17
|
+
"profile": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["text"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "reveal_text",
|
|
27
|
+
"description": "Replaces masked tokens (e.g., [EMAIL_1], [API_KEY_1]) in the LLM's response back with the original private data from the local volatile RAM-only session map.",
|
|
28
|
+
"inputSchema": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"text": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The AI generated response containing placeholders to restore."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["text"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "sanitize_file",
|
|
41
|
+
"description": "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
42
|
+
"inputSchema": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"file_path": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Absolute path to the file to sanitize."
|
|
48
|
+
},
|
|
49
|
+
"profile": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": ["file_path"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"resources": [],
|
|
59
|
+
"prompts": []
|
|
60
|
+
}
|
package/README.md
CHANGED
|
@@ -1,53 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PrivacyScrubber MCP Server v1.0.3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://smithery.ai/server/@privacyscrubber/mcp-server)
|
|
6
|
-
[](https://privacyscrubber.com)
|
|
7
|
-
[](https://privacyscrubber.com)
|
|
3
|
+
Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) server for local PII and secrets masking.
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
## What's New in v1.0.3
|
|
6
|
+
- **Robust Configuration Parsing**: Protected Smithery integration hooks from undefined configuration objects, ensuring reliable initialization across all MCP clients (Claude Desktop, Cursor, Windsurf).
|
|
7
|
+
- **Microsoft Word (.docx) Support**: Enhanced the `sanitize_file` tool to parse and redact sensitive information from Word documents locally.
|
|
8
|
+
- **Unified Branding & Metadata**: Corrected repository fields and aligned server identity across NPM, Smithery, and Glama registries.
|
|
9
|
+
- **Enhanced Scanner Discovery**: Implemented static `server-card.json` configurations to support automated registry scanning.
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
## Key Features & Security Guarantees
|
|
12
|
+
- **100% Local Processing**: All regex scanning, PII tokenization, and reverse-scrubbing occur directly in your machine's RAM.
|
|
13
|
+
- **Airplane Mode Verified**: Fully operational without an internet connection after the initial download.
|
|
14
|
+
- **Zero Server Logs**: No data, credentials, or prompts are sent to external APIs or remote databases.
|
|
13
15
|
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
All sensitive parameters, identifiers, and variables are intercepted locally inside your machine's RAM. They are replaced by tokens (e.g. `[EMAIL_1]`) before being sent to the AI. Once the AI responds, the tokens are safely swapped back to original values in your local context.
|
|
17
|
-
|
|
18
|
-
```text
|
|
19
|
-
[Raw Input / Files] āā> [MCP sanitize_text] āā> [Masked Tokens] āā> [LLM API]
|
|
20
|
-
ā ā
|
|
21
|
-
(In-Memory Map) (Result)
|
|
22
|
-
ā ā
|
|
23
|
-
[Original Output] <āāā [MCP reveal_text] <āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## š Installation
|
|
29
|
-
|
|
30
|
-
### 1. Install via Smithery
|
|
31
|
-
To automatically configure and run with your preferred client, install using Smithery:
|
|
32
|
-
```bash
|
|
33
|
-
npx -y @smithery/cli install @privacyscrubber/mcp-server --write-to-clients
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### 2. Instant Run with NPX
|
|
37
|
-
Run the server directly without local installation:
|
|
16
|
+
## Quick Start
|
|
17
|
+
Run the server instantly without local installation:
|
|
38
18
|
```bash
|
|
39
19
|
npx -y @privacyscrubber/mcp-server
|
|
40
20
|
```
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## āļø Client Integrations
|
|
22
|
+
## Client Integration Examples
|
|
45
23
|
|
|
46
24
|
### Claude Desktop
|
|
47
|
-
Add
|
|
48
|
-
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
49
|
-
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
50
|
-
|
|
25
|
+
Add to your `claude_desktop_config.json`:
|
|
51
26
|
```json
|
|
52
27
|
{
|
|
53
28
|
"mcpServers": {
|
|
@@ -63,93 +38,13 @@ Add this to your Claude Desktop config file:
|
|
|
63
38
|
```
|
|
64
39
|
|
|
65
40
|
### Cursor / Windsurf
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
### 1. `sanitize_text`
|
|
78
|
-
Redacts PII, secrets, API keys, and credentials from a text block and populates the volatile local replacement mapping.
|
|
79
|
-
|
|
80
|
-
* **Arguments:**
|
|
81
|
-
* `text` (string, required): The raw content or logs to sanitize.
|
|
82
|
-
* `profile` (string, optional): Gated industry detection profile (e.g., 'General', 'Dev', 'Medical', 'Legal', 'Compliance'). Defaults to 'General'.
|
|
83
|
-
* **JSON-RPC Call Example:**
|
|
84
|
-
```json
|
|
85
|
-
{
|
|
86
|
-
"method": "tools/call",
|
|
87
|
-
"params": {
|
|
88
|
-
"name": "sanitize_text",
|
|
89
|
-
"arguments": {
|
|
90
|
-
"text": "Contact me at dev-key-1234 or jane.doe@company.com",
|
|
91
|
-
"profile": "General"
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
* **Response Example:**
|
|
97
|
-
```json
|
|
98
|
-
{
|
|
99
|
-
"content": [
|
|
100
|
-
{
|
|
101
|
-
"type": "text",
|
|
102
|
-
"text": "Contact me at [SECRET_1] or [EMAIL_1]"
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 2. `reveal_text`
|
|
109
|
-
Detokenizes the AI response back to the original values locally.
|
|
110
|
-
|
|
111
|
-
* **Arguments:**
|
|
112
|
-
* `text` (string, required): The response from the LLM containing tokenized placeholders.
|
|
113
|
-
* **JSON-RPC Call Example:**
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"method": "tools/call",
|
|
117
|
-
"params": {
|
|
118
|
-
"name": "reveal_text",
|
|
119
|
-
"arguments": {
|
|
120
|
-
"text": "Please reach out to [EMAIL_1] regarding the update."
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
* **Response Example:**
|
|
126
|
-
```json
|
|
127
|
-
{
|
|
128
|
-
"content": [
|
|
129
|
-
{
|
|
130
|
-
"type": "text",
|
|
131
|
-
"text": "Please reach out to jane.doe@company.com regarding the update."
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### 3. `sanitize_file`
|
|
138
|
-
Reads a local file, extracts text, sanitizes it, and returns the redacted template for LLM analysis.
|
|
139
|
-
* **Supported Formats:** Plain text (source code, logs, CSV, JSON, markdown) and Microsoft Word (`.docx`) documents.
|
|
140
|
-
* **Arguments:**
|
|
141
|
-
* `filePath` (string, required): Absolute file path to read and sanitize.
|
|
142
|
-
* `profile` (string, optional): The industry detection profile.
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## š Browser Extension & Web Client
|
|
147
|
-
|
|
148
|
-
Looking for real-time protection directly inside your web browser?
|
|
149
|
-
* **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
|
|
150
|
-
* **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/).
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## š License & Commercial Upgrade
|
|
155
|
-
Standard use is free under the **Free Tier** (limits to the `General` PII profile). To unlock 22+ specialized industry profiles (DevOps, Medical, Legal, Finance) and custom regex rules, acquire a commercial license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing).
|
|
41
|
+
Add a new command-type MCP server:
|
|
42
|
+
- **Name**: `privacyscrubber`
|
|
43
|
+
- **Command**: `npx -y @privacyscrubber/mcp-server`
|
|
44
|
+
|
|
45
|
+
## Useful Links
|
|
46
|
+
- **Official Website**: https://privacyscrubber.com
|
|
47
|
+
- **Chrome Web Store Extension**: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol
|
|
48
|
+
- **MCP Documentation**: https://privacyscrubber.com/pii-mcp/
|
|
49
|
+
- **Pricing & Licensing**: https://privacyscrubber.com/pricing/
|
|
50
|
+
- **GitHub Repository**: https://github.com/moxno/privacyscrubber-mcp
|
package/bundle.mcpb
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -83,8 +83,8 @@ function checkLicenseStatus() {
|
|
|
83
83
|
// Create the MCP server
|
|
84
84
|
const server = new Server(
|
|
85
85
|
{
|
|
86
|
-
name: "privacyscrubber-mcp",
|
|
87
|
-
version: "1.0.
|
|
86
|
+
name: "privacyscrubber/pii-masking-mcp",
|
|
87
|
+
version: "1.0.3",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
capabilities: {
|
|
@@ -113,20 +113,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
required: ["text"]
|
|
116
|
-
},
|
|
117
|
-
outputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
text: {
|
|
121
|
-
type: "string",
|
|
122
|
-
description: "The sanitized text output."
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
required: ["text"]
|
|
126
|
-
},
|
|
127
|
-
annotations: {
|
|
128
|
-
priority: 1.0,
|
|
129
|
-
audience: ["developer", "user"]
|
|
130
116
|
}
|
|
131
117
|
},
|
|
132
118
|
{
|
|
@@ -141,20 +127,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
141
127
|
}
|
|
142
128
|
},
|
|
143
129
|
required: ["text"]
|
|
144
|
-
},
|
|
145
|
-
outputSchema: {
|
|
146
|
-
type: "object",
|
|
147
|
-
properties: {
|
|
148
|
-
text: {
|
|
149
|
-
type: "string",
|
|
150
|
-
description: "The detokenized text with original values restored."
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
required: ["text"]
|
|
154
|
-
},
|
|
155
|
-
annotations: {
|
|
156
|
-
priority: 1.0,
|
|
157
|
-
audience: ["developer", "user"]
|
|
158
130
|
}
|
|
159
131
|
},
|
|
160
132
|
{
|
|
@@ -173,20 +145,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
173
145
|
}
|
|
174
146
|
},
|
|
175
147
|
required: ["file_path"]
|
|
176
|
-
},
|
|
177
|
-
outputSchema: {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: {
|
|
180
|
-
text: {
|
|
181
|
-
type: "string",
|
|
182
|
-
description: "The sanitized content of the file."
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
required: ["text"]
|
|
186
|
-
},
|
|
187
|
-
annotations: {
|
|
188
|
-
priority: 1.0,
|
|
189
|
-
audience: ["developer", "user"]
|
|
190
148
|
}
|
|
191
149
|
}
|
|
192
150
|
]
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
|
-
"name": "privacyscrubber-mcp",
|
|
4
|
-
"version": "1.0.
|
|
3
|
+
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Zero-Trust Data Sanitization (ZTDS) MCP Server for AI IDEs and Claude Desktop. Locally scrubs PII, secrets, and credentials before sending context to LLMs.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
5
|
"description": "Zero-Trust Data Sanitization (ZTDS) MCP Server for AI IDEs and Claude Desktop. Locally scrubs PII, secrets, and credentials before sending context to LLMs.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
"publishConfig": {
|
|
9
9
|
"access": "public"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"scrubber-core.cjs",
|
|
14
|
+
"smithery.yaml",
|
|
15
|
+
"server.json",
|
|
16
|
+
"manifest.json",
|
|
17
|
+
"bundle.mcpb",
|
|
18
|
+
".well-known"
|
|
19
|
+
],
|
|
11
20
|
"bin": {
|
|
12
21
|
"mcp-server": "index.js",
|
|
13
22
|
"privacyscrubber-mcp": "index.js"
|
|
@@ -16,7 +25,7 @@
|
|
|
16
25
|
"start": "node index.js"
|
|
17
26
|
},
|
|
18
27
|
"dependencies": {
|
|
19
|
-
"@modelcontextprotocol/sdk": "^
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
29
|
"mammoth": "^1.8.0"
|
|
21
30
|
},
|
|
22
31
|
"keywords": [
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
4
|
"description": "Zero-Trust PII & secrets sanitizer. Locally scrubs data in-memory before sending context to LLMs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/moxno/privacyscrubber-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@privacyscrubber/mcp-server",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/smithery.yaml
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
# Smithery.ai configuration for PrivacyScrubber MCP Server
|
|
2
2
|
startCommand:
|
|
3
3
|
type: "stdio"
|
|
4
|
-
configSchema:
|
|
5
|
-
type: "object"
|
|
6
|
-
properties:
|
|
7
|
-
PRIVACYSCRUBBER_KEY:
|
|
8
|
-
type: "string"
|
|
9
|
-
title: "PrivacyScrubber License Key"
|
|
10
|
-
description: "Optional PRO or TEAMS license key. Leave blank to run on the Free tier."
|
|
11
4
|
commandFunction: |-
|
|
12
5
|
(config) => ({
|
|
13
6
|
command: 'node',
|
|
14
7
|
args: ['./index.js'],
|
|
15
8
|
env: {
|
|
16
|
-
PRIVACYSCRUBBER_KEY: config.PRIVACYSCRUBBER_KEY || ''
|
|
9
|
+
PRIVACYSCRUBBER_KEY: (config && config.PRIVACYSCRUBBER_KEY) || ''
|
|
17
10
|
}
|
|
18
11
|
})
|
|
19
|
-
exampleConfig:
|
|
20
|
-
PRIVACYSCRUBBER_KEY: ""
|
|
12
|
+
exampleConfig: {}
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Contributing to PrivacyScrubber MCP Server
|
|
2
|
-
|
|
3
|
-
We welcome contributions to the PrivacyScrubber Model Context Protocol (MCP) server integration, client configurations, and testing suites.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## š ļø Development Setup
|
|
8
|
-
|
|
9
|
-
To set up a local development environment:
|
|
10
|
-
|
|
11
|
-
1. **Clone the Repository:**
|
|
12
|
-
```bash
|
|
13
|
-
git clone https://github.com/moxno/privacyscrubber-mcp.git
|
|
14
|
-
cd privacyscrubber-mcp
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
2. **Install Dependencies:**
|
|
18
|
-
```bash
|
|
19
|
-
npm install
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
3. **Run the Server Locally:**
|
|
23
|
-
You can spin up the server transport using:
|
|
24
|
-
```bash
|
|
25
|
-
node index.js
|
|
26
|
-
```
|
|
27
|
-
*Note: The server uses StdIO transport, so it will sit waiting for JSON-RPC input on standard streams.*
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## š§Ŗ Testing Your Changes
|
|
32
|
-
|
|
33
|
-
Before submitting a Pull Request, ensure that all integration and cryptographic verification tests pass cleanly.
|
|
34
|
-
|
|
35
|
-
### 1. Cryptographic Signature Assertions
|
|
36
|
-
Verify that local license key validations and RSA signatures parse correctly:
|
|
37
|
-
```bash
|
|
38
|
-
node test-mcp.js
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 2. End-to-End JSON-RPC Subprocess Suite
|
|
42
|
-
Run the full simulation test which spawns the server, registers protocol versions, lists capabilities, processes files (TXT and DOCX), and asserts binary file rejections:
|
|
43
|
-
```bash
|
|
44
|
-
node test-mcp-deep.js
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## š Regex Rules & PII Detection Core
|
|
50
|
-
The core detection engine and regex rules are bundled inside `scrubber-core.cjs` which maintains parity with the PrivacyScrubber Chrome Extension and web application.
|
|
51
|
-
|
|
52
|
-
If you notice a false positive or an uncaught entity type (e.g. name matching edge cases), please report it directly through the main project issues or email us at `support@privacyscrubber.com` so we can sync the ruleset across all platforms.
|
package/SECURITY.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Security Policy (SECURITY.md)
|
|
2
|
-
|
|
3
|
-
PrivacyScrubber is built with a zero-server, client-side first architecture. We take data privacy and vulnerability management extremely seriously.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## š Security Posture & Guarantees
|
|
8
|
-
|
|
9
|
-
The PrivacyScrubber MCP server operates under a strict **Zero-Trust Data Sanitization (ZTDS)** design:
|
|
10
|
-
1. **100% Offline / Local Redaction:** All PII extraction, pattern matching, and tokenization logic run locally inside your system's Node.js runtime process memory (volatile RAM).
|
|
11
|
-
2. **No Telemetry & No Remote Logs:** The server contains zero network request code for telemetry, prompts transmission, or log collection.
|
|
12
|
-
3. **Strict Gating Audits:** Cryptographic key checks (for PRO/TEAMS licenses) are validated locally on-device using a signed RSA public key signature verification. No license verification calls are made to any remote databases.
|
|
13
|
-
|
|
14
|
-
### Verification (Airplane Mode Test)
|
|
15
|
-
You can verify the zero-network posture at any time:
|
|
16
|
-
1. Disconnect your machine's internet access (Wi-Fi/Ethernet).
|
|
17
|
-
2. Launch the MCP server locally using Cursor, Windsurf, or Claude Desktop.
|
|
18
|
-
3. Execute file sanitization and detokenization. All tools remain 100% operational offline.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## š Reporting a Vulnerability
|
|
23
|
-
|
|
24
|
-
If you discover a potential security vulnerability (e.g. regex engine backtracking denial of service, memory leaks, or local file access overflows), please do not report it in the public GitHub Issues tracker.
|
|
25
|
-
|
|
26
|
-
Instead, report it privately through our coordinated disclosure channel:
|
|
27
|
-
- **Email:** `security@privacyscrubber.com`
|
|
28
|
-
- **Response SLA:** Our team reviews all reports within **24 hours** and will provide a status update and remediation timeline.
|
|
29
|
-
|
|
30
|
-
Thank you for helping keep the developer ecosystem secure!
|
package/llms.txt
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# PrivacyScrubber MCP Server
|
|
2
|
-
|
|
3
|
-
> Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) Server. Locally scrubs PII, secrets, credentials, and custom patterns from text and files before they reach remote LLM APIs.
|
|
4
|
-
|
|
5
|
-
## Main URL
|
|
6
|
-
- Repository: https://github.com/moxno/privacyscrubber-mcp
|
|
7
|
-
- Web sandbox: https://privacyscrubber.com/
|
|
8
|
-
- Chrome extension: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol
|
|
9
|
-
|
|
10
|
-
## Provided Tools
|
|
11
|
-
|
|
12
|
-
The server runs locally in StdIO transport and exposes three tools:
|
|
13
|
-
|
|
14
|
-
### sanitize_text
|
|
15
|
-
Scrubs PII (names, emails, phones, locations), API keys, and passwords from input text.
|
|
16
|
-
- Inputs:
|
|
17
|
-
- `text` (string, required): Raw text block or log segment to sanitize.
|
|
18
|
-
- `profile` (string, optional): Gated industry detection profile (e.g. 'General', 'Dev', 'Medical', 'Legal', 'Compliance'). Defaults to 'General'.
|
|
19
|
-
- Output: Masked template string containing placeholders (e.g. `[EMAIL_1]`).
|
|
20
|
-
|
|
21
|
-
### reveal_text
|
|
22
|
-
Detokenizes AI responses by restoring original PII values locally in-memory.
|
|
23
|
-
- Inputs:
|
|
24
|
-
- `text` (string, required): Response from the LLM containing tokens like `[EMAIL_1]`.
|
|
25
|
-
- Output: Detokenized restored string.
|
|
26
|
-
|
|
27
|
-
### sanitize_file
|
|
28
|
-
Reads a local file, checks format compatibility, extracts raw text, and redacts PII.
|
|
29
|
-
- Supported Formats: Source code files, CSV, JSON, markdown, text logs, and Microsoft Word (`.docx`) documents.
|
|
30
|
-
- Binary formats (like PDF, ZIP) are automatically rejected with a redirection notice to prevent corrupting inputs.
|
|
31
|
-
- Inputs:
|
|
32
|
-
- `filePath` (string, required): Absolute local file path.
|
|
33
|
-
- `profile` (string, optional): Industry detection profile.
|
|
34
|
-
- Output: Sanitized file text template.
|
|
35
|
-
|
|
36
|
-
## Environment Variables
|
|
37
|
-
- `PRIVACYSCRUBBER_KEY` (string, optional): License key to unlock advanced industry detection profiles. If not set, the server runs on the Free tier and defaults all sanitizations to the 'General' profile.
|
package/test-mcp-deep.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
const indexScript = path.resolve(__dirname, 'index.js');
|
|
10
|
-
|
|
11
|
-
// Helper to send JSON-RPC and wait for response
|
|
12
|
-
async function runMcpSession() {
|
|
13
|
-
console.log("Starting MCP Server process...");
|
|
14
|
-
|
|
15
|
-
// Set license key to valid PRO key for advanced test
|
|
16
|
-
const validKey = "eyJ0eXBlIjoicHJvIiwiZXhwaXJlcyI6MjA5OTA2MjE3NCwiaXNzdWVkQXQiOjE3ODM3MDIxNzQsIm5vbmNlIjoiMmI4MTllN2ZmM2ZmOTFmYiJ9.ZmhaKDGf9vG0nTH0nyOy3EInsuUmzjBOk9IOyiqzt6Y5s3UG+2yRExuKBoeXWymbpJt3NIJNM9sVxxl+lcop5wqbi2LNtPY4MuwBRA7pO4nn3Bes5R0lxLbEYVE8Iiw3zbfK4uVYQ53BJ7El6JCeFKPJ5WbKUsPLsjb1Lr2iQzW3ODOMaM5jKdgAGcNpuWQ373D7SW7I03Jec9kvP5hL7j3u4DV8ZzuQFzy2Nh+uMH54suydg2sNIpxrRQyxpGv7rZjTO1KT+xzzqnjqX4Pein0e6GrC5E4JUEggADtXPFMKW5SEiWzeeirB5RUPMO/F927S5fFuOYHAfuar2FqErA==";
|
|
17
|
-
|
|
18
|
-
const mcpProcess = spawn('node', [indexScript], {
|
|
19
|
-
env: {
|
|
20
|
-
...process.env,
|
|
21
|
-
PRIVACYSCRUBBER_KEY: validKey
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
let buffer = '';
|
|
26
|
-
const pendingRequests = new Map();
|
|
27
|
-
let nextId = 1;
|
|
28
|
-
|
|
29
|
-
mcpProcess.stdout.on('data', (data) => {
|
|
30
|
-
buffer += data.toString();
|
|
31
|
-
// Parse JSON-RPC delimited by newlines
|
|
32
|
-
let newlineIndex;
|
|
33
|
-
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
34
|
-
const line = buffer.substring(0, newlineIndex).trim();
|
|
35
|
-
buffer = buffer.substring(newlineIndex + 1);
|
|
36
|
-
if (line) {
|
|
37
|
-
try {
|
|
38
|
-
const response = JSON.parse(line);
|
|
39
|
-
if (response.id && pendingRequests.has(response.id)) {
|
|
40
|
-
const resolve = pendingRequests.get(response.id);
|
|
41
|
-
pendingRequests.delete(response.id);
|
|
42
|
-
resolve(response);
|
|
43
|
-
}
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error("Failed to parse incoming line as JSON:", line, e);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
mcpProcess.stderr.on('data', (data) => {
|
|
52
|
-
console.error(`[Server Stderr]: ${data.toString().trim()}`);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const sendRequest = (method, params) => {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
const id = nextId++;
|
|
58
|
-
pendingRequests.set(id, resolve);
|
|
59
|
-
const requestObj = {
|
|
60
|
-
jsonrpc: '2.0',
|
|
61
|
-
id,
|
|
62
|
-
method,
|
|
63
|
-
params
|
|
64
|
-
};
|
|
65
|
-
mcpProcess.stdin.write(JSON.stringify(requestObj) + '\n');
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
// 1. Initialize
|
|
71
|
-
console.log("--> Sending 'initialize' request...");
|
|
72
|
-
const initResponse = await sendRequest('initialize', {
|
|
73
|
-
protocolVersion: '2024-11-05',
|
|
74
|
-
capabilities: {},
|
|
75
|
-
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
76
|
-
});
|
|
77
|
-
console.log("<-- Initialize response received:", JSON.stringify(initResponse).substring(0, 150) + "...");
|
|
78
|
-
|
|
79
|
-
// 2. List tools
|
|
80
|
-
console.log("--> Sending 'tools/list' request...");
|
|
81
|
-
const toolsResponse = await sendRequest('tools/list', {});
|
|
82
|
-
const tools = toolsResponse.result?.tools || [];
|
|
83
|
-
console.log(`<-- Tools list received. Total tools: ${tools.length}`);
|
|
84
|
-
tools.forEach(t => console.log(` - Tool: ${t.name} (${t.description.substring(0, 60)}...)`));
|
|
85
|
-
|
|
86
|
-
if (tools.length !== 3) {
|
|
87
|
-
throw new Error(`Expected 3 tools, got ${tools.length}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3. Test sanitize_text (General profile)
|
|
91
|
-
console.log("--> Calling 'sanitize_text' with General profile...");
|
|
92
|
-
const rawText = "Contact John Doe at john.doe@example.com or phone 555-0199.";
|
|
93
|
-
const sanitizeResponse = await sendRequest('tools/call', {
|
|
94
|
-
name: 'sanitize_text',
|
|
95
|
-
arguments: {
|
|
96
|
-
text: rawText,
|
|
97
|
-
profile: 'General'
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
const sanitizedText = sanitizeResponse.result?.content?.[0]?.text;
|
|
101
|
-
console.log("<-- Sanitized Output:", sanitizedText);
|
|
102
|
-
|
|
103
|
-
if (sanitizedText.includes("john.doe@example.com") || sanitizedText.includes("555-0199")) {
|
|
104
|
-
throw new Error("Sanitization failed to redact email or phone number.");
|
|
105
|
-
}
|
|
106
|
-
console.log("ā
Sanitize text success.");
|
|
107
|
-
|
|
108
|
-
// 4. Test reveal_text (detokenization)
|
|
109
|
-
console.log("--> Calling 'reveal_text' to restore tokens...");
|
|
110
|
-
const revealResponse = await sendRequest('tools/call', {
|
|
111
|
-
name: 'reveal_text',
|
|
112
|
-
arguments: {
|
|
113
|
-
text: `Please email ${sanitizedText.match(/\[EMAIL_\d+\]/)?.[0] || '[EMAIL_1]'} back.`
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
const revealedText = revealResponse.result?.content?.[0]?.text;
|
|
117
|
-
console.log("<-- Revealed Output:", revealedText);
|
|
118
|
-
if (!revealedText.includes("john.doe@example.com")) {
|
|
119
|
-
throw new Error("Reveal failed to restore the original email.");
|
|
120
|
-
}
|
|
121
|
-
console.log("ā
Reveal text success.");
|
|
122
|
-
|
|
123
|
-
// 5. Test sanitize_file (text file)
|
|
124
|
-
const tempFile = path.resolve(__dirname, 'temp-test-file.txt');
|
|
125
|
-
fs.writeFileSync(tempFile, "API Key: secret-key-value-12345\nUser: jane.smith@company.com", "utf8");
|
|
126
|
-
console.log(`--> Calling 'sanitize_file' on text file: ${tempFile}`);
|
|
127
|
-
|
|
128
|
-
const fileResponse = await sendRequest('tools/call', {
|
|
129
|
-
name: 'sanitize_file',
|
|
130
|
-
arguments: {
|
|
131
|
-
filePath: tempFile,
|
|
132
|
-
profile: 'Dev'
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
fs.unlinkSync(tempFile);
|
|
136
|
-
|
|
137
|
-
const fileSanitized = fileResponse.result?.content?.[0]?.text;
|
|
138
|
-
console.log("<-- Sanitized File Output:", fileSanitized);
|
|
139
|
-
if (fileSanitized.includes("secret-key-value-12345") || fileSanitized.includes("jane.smith@company.com")) {
|
|
140
|
-
throw new Error("File sanitization failed to redact credentials.");
|
|
141
|
-
}
|
|
142
|
-
console.log("ā
Sanitize file success.");
|
|
143
|
-
|
|
144
|
-
// 6. Test sanitize_file (binary file rejection)
|
|
145
|
-
const tempBinFile = path.resolve(__dirname, 'temp-binary-file.bin');
|
|
146
|
-
const binBuffer = Buffer.alloc(100);
|
|
147
|
-
binBuffer.write("GIF89a", 0);
|
|
148
|
-
binBuffer[10] = 0; // Null byte
|
|
149
|
-
fs.writeFileSync(tempBinFile, binBuffer);
|
|
150
|
-
console.log(`--> Calling 'sanitize_file' on binary file: ${tempBinFile}`);
|
|
151
|
-
|
|
152
|
-
const binFileResponse = await sendRequest('tools/call', {
|
|
153
|
-
name: 'sanitize_file',
|
|
154
|
-
arguments: {
|
|
155
|
-
filePath: tempBinFile,
|
|
156
|
-
profile: 'General'
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
fs.unlinkSync(tempBinFile);
|
|
160
|
-
|
|
161
|
-
const isBinError = binFileResponse.result?.isError || binFileResponse.error;
|
|
162
|
-
const binErrorText = binFileResponse.result?.content?.[0]?.text || binFileResponse.error?.message;
|
|
163
|
-
console.log("<-- Binary File Response Error status:", isBinError, "| message:", binErrorText);
|
|
164
|
-
|
|
165
|
-
if (!isBinError || !binErrorText?.includes("Binary file format detected")) {
|
|
166
|
-
throw new Error("Binary file check failed to reject binary file with clean message.");
|
|
167
|
-
}
|
|
168
|
-
console.log("ā
Sanitize file binary rejection success.");
|
|
169
|
-
|
|
170
|
-
// 7. Test sanitize_file (DOCX file parsing support)
|
|
171
|
-
const docxSource = path.resolve(__dirname, '../PrivacyScrubber/outreach-campaigns/guest-posts/Techbullion_GuestPost.docx');
|
|
172
|
-
const docxDest = path.resolve(__dirname, 'fixture-test.docx');
|
|
173
|
-
|
|
174
|
-
if (fs.existsSync(docxSource)) {
|
|
175
|
-
fs.copyFileSync(docxSource, docxDest);
|
|
176
|
-
console.log(`--> Calling 'sanitize_file' on DOCX document: ${docxDest}`);
|
|
177
|
-
|
|
178
|
-
const docxResponse = await sendRequest('tools/call', {
|
|
179
|
-
name: 'sanitize_file',
|
|
180
|
-
arguments: {
|
|
181
|
-
filePath: docxDest,
|
|
182
|
-
profile: 'General'
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
fs.unlinkSync(docxDest);
|
|
186
|
-
|
|
187
|
-
const docxText = docxResponse.result?.content?.[0]?.text;
|
|
188
|
-
console.log("<-- DOCX Sanitized text preview (first 150 chars):", docxText?.substring(0, 150) + "...");
|
|
189
|
-
|
|
190
|
-
if (!docxText || docxResponse.result?.isError || docxText.includes("Error") || docxText.length < 50) {
|
|
191
|
-
throw new Error("DOCX document parsing failed or returned invalid text content.");
|
|
192
|
-
}
|
|
193
|
-
console.log("ā
Sanitize DOCX document success.");
|
|
194
|
-
} else {
|
|
195
|
-
console.log("ā ļø Skipped DOCX test case: fixture file not found in main project path.");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log("\nš All deep integration tests passed successfully!");
|
|
199
|
-
mcpProcess.kill();
|
|
200
|
-
process.exit(0);
|
|
201
|
-
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error("ā Test failed:", error.message);
|
|
204
|
-
mcpProcess.kill();
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
runMcpSession();
|
package/test-mcp.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Complete Cryptographic Verification Test Suite for @privacyscrubber/mcp-server
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
|
|
13
|
-
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
14
|
-
const mcpServerIndex = path.resolve(__dirname, 'index.js');
|
|
15
|
-
|
|
16
|
-
console.log("Loading modules for verification test...");
|
|
17
|
-
try {
|
|
18
|
-
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
19
|
-
PrivacyScrubberCore.init();
|
|
20
|
-
|
|
21
|
-
// Load index.js to test its internal validate functions
|
|
22
|
-
// We can simulate its checkLicenseStatus by mocking environment variables
|
|
23
|
-
const indexModule = await import('./index.js?update=' + Date.now());
|
|
24
|
-
|
|
25
|
-
// Test Case 1: No license key
|
|
26
|
-
console.log("\n[Test 1] Testing with no license key (Free tier)...");
|
|
27
|
-
process.env.PRIVACYSCRUBBER_KEY = "";
|
|
28
|
-
const t1 = global.checkLicenseStatus ? global.checkLicenseStatus() : mockCheckLicenseStatus();
|
|
29
|
-
console.log("Status:", t1);
|
|
30
|
-
if (t1.isPro) {
|
|
31
|
-
console.error("ā Test 1 failed: keyless state should not be PRO.");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
console.log("ā
Test 1 passed.");
|
|
35
|
-
|
|
36
|
-
// Test Case 2: Expired trial key
|
|
37
|
-
console.log("\n[Test 2] Testing with expired signed trial key...");
|
|
38
|
-
// Expired key generated by generate-license.js
|
|
39
|
-
const expiredKey = "eyJ0eXBlIjoidHJpYWwiLCJleHBpcmVzIjoxNzgzMjcwMTgzLCJpc3N1ZWRBdCI6MTc4MzcwMjE4Mywibm9uY2UiOiIzYzRkYzEwNmRkOGJiZjczIn0=.Wc1cPwvRpEQxG3BarmedBYm9sUsGdwznboh8Gx3a7qQZm2I+Dha8lk1CtzmaaBt5O8pQszGuTCHapASFwZX2Ku8uZxkGipPI+tbNI39KK7cM6wA+Txc0Ufec/lnDE/27WelBswStSIMgR5LPVScfamMO5BK6jvJ9RgOrEdJeqJPcpab0mwy4jzZwSATyZ1JOS61i8ZlJ+n99zppBrrMZQQo/FJ5t70RpqZiZoMuyNbeDJB9UWGDEAELiJLBkaGeYgBo65zp7fRcTuyjmoZjINnOhCcXmKsmeB1vkHdjPg8jXaGVWFN7bJryeV7j/OkpNqO2q74x3hXDvNwlGtg4PUg==";
|
|
40
|
-
process.env.PRIVACYSCRUBBER_KEY = expiredKey;
|
|
41
|
-
const t2 = mockCheckLicenseStatus();
|
|
42
|
-
console.log("Status:", t2);
|
|
43
|
-
if (t2.isPro || !t2.error.includes("expired")) {
|
|
44
|
-
console.error("ā Test 2 failed: expired key should be blocked with expiration reason.");
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
console.log("ā
Test 2 passed.");
|
|
48
|
-
|
|
49
|
-
// Test Case 3: Valid signed PRO key
|
|
50
|
-
console.log("\n[Test 3] Testing with valid signed PRO key...");
|
|
51
|
-
// Valid key generated in this session
|
|
52
|
-
const validKey = "eyJ0eXBlIjoicHJvIiwiZXhwaXJlcyI6MjA5OTA2MjE3NCwiaXNzdWVkQXQiOjE3ODM3MDIxNzQsIm5vbmNlIjoiMmI4MTllN2ZmM2ZmOTFmYiJ9.ZmhaKDGf9vG0nTH0nyOy3EInsuUmzjBOk9IOyiqzt6Y5s3UG+2yRExuKBoeXWymbpJt3NIJNM9sVxxl+lcop5wqbi2LNtPY4MuwBRA7pO4nn3Bes5R0lxLbEYVE8Iiw3zbfK4uVYQ53BJ7El6JCeFKPJ5WbKUsPLsjb1Lr2iQzW3ODOMaM5jKdgAGcNpuWQ373D7SW7I03Jec9kvP5hL7j3u4DV8ZzuQFzy2Nh+uMH54suydg2sNIpxrRQyxpGv7rZjTO1KT+xzzqnjqX4Pein0e6GrC5E4JUEggADtXPFMKW5SEiWzeeirB5RUPMO/F927S5fFuOYHAfuar2FqErA==";
|
|
53
|
-
process.env.PRIVACYSCRUBBER_KEY = validKey;
|
|
54
|
-
const t3 = mockCheckLicenseStatus();
|
|
55
|
-
console.log("Status:", t3);
|
|
56
|
-
if (!t3.isPro) {
|
|
57
|
-
console.error("ā Test 3 failed: valid key was not accepted.");
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
console.log("ā
Test 3 passed.");
|
|
61
|
-
|
|
62
|
-
console.log("\nš All local cryptographic validation tests passed!");
|
|
63
|
-
process.exit(0);
|
|
64
|
-
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error("ā Test run crashed:", error);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Mock checker mapping index.js logic locally for direct assertion
|
|
71
|
-
function mockCheckLicenseStatus() {
|
|
72
|
-
const crypto = require('crypto');
|
|
73
|
-
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
74
|
-
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3f37srO402PU4++Baf8
|
|
75
|
-
FG8LY4l/IA3NKLlBnYmNHRTjfI/O/w5PDZn1xPcUQevojA1J+A5moKcjXsJ5b21X
|
|
76
|
-
hJoYSkE4vLpcVYOt1FhRwEHs1APDSyss0HixboLz2eW2XQf2NbwajWtNlyxvgczO
|
|
77
|
-
KE6ClnLomtsaKywwqB4alzdYnnnFJttFPjwmgPSO7D9AgN9sYaVkXOaOFrIZ90Ng
|
|
78
|
-
TRhSHUeL7ReltWlCHwz9xf5m2FrKtxr2VBlEoyPjsFzalHMey1EX+yXe81zM7IIi
|
|
79
|
-
t1Z8agLzo7WIfNBAIWmRlerTplaFFZrQgdF5g/Y0n8IIMZOtadgoY8E855psDNZV
|
|
80
|
-
7wIDAQAB
|
|
81
|
-
-----END PUBLIC KEY-----`;
|
|
82
|
-
|
|
83
|
-
const key = (process.env.PRIVACYSCRUBBER_KEY || "").trim();
|
|
84
|
-
if (!key) return { isPro: false, type: null, error: "No license key provided." };
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const [payloadBase64, signatureBase64] = key.split('.');
|
|
88
|
-
if (!payloadBase64 || !signatureBase64) {
|
|
89
|
-
return { isPro: false, type: null, error: "Invalid license key structure." };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const verifier = crypto.createVerify('SHA256');
|
|
93
|
-
verifier.update(payloadBase64);
|
|
94
|
-
const isVerified = verifier.verify(PUBLIC_KEY, signatureBase64, 'base64');
|
|
95
|
-
|
|
96
|
-
if (!isVerified) {
|
|
97
|
-
return { isPro: false, type: null, error: "Signature verification failed." };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8'));
|
|
101
|
-
|
|
102
|
-
if (payload.expires && payload.expires < Math.floor(Date.now() / 1000)) {
|
|
103
|
-
return {
|
|
104
|
-
isPro: false,
|
|
105
|
-
type: payload.type,
|
|
106
|
-
error: `License expired on ${new Date(payload.expires * 1000).toLocaleDateString()}`
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { isPro: true, type: payload.type, error: null };
|
|
111
|
-
} catch (e) {
|
|
112
|
-
return { isPro: false, type: null, error: "Error parsing license: " + e.message };
|
|
113
|
-
}
|
|
114
|
-
}
|