@privacyscrubber/mcp-server 2.1.0 → 2.2.1

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/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "privacyscrubber/pii-masking-mcp",
4
- "version": "2.1.0",
4
+ "version": "2.2.1",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "author": {
7
7
  "name": "PrivacyScrubber",
@@ -12,7 +12,9 @@
12
12
  "entry_point": "index.js",
13
13
  "mcp_config": {
14
14
  "command": "node",
15
- "args": ["${__dirname}/index.js"],
15
+ "args": [
16
+ "${__dirname}/index.js"
17
+ ],
16
18
  "env": {
17
19
  "PRIVACYSCRUBBER_KEY": "${user_config.PRIVACYSCRUBBER_KEY}"
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "main": "index.js",
@@ -14,6 +14,7 @@
14
14
  "pii-masking-run.js",
15
15
  "scrubber-core.cjs",
16
16
  "ps-license-manager.js",
17
+ "ps-license-manager.cjs",
17
18
  "ps-pii-engine.cjs",
18
19
  "ps-pii-engine.js",
19
20
  "config-schema.json",
@@ -27,10 +28,14 @@
27
28
  "mcp-server": "index.js",
28
29
  "privacyscrubber-mcp": "index.js",
29
30
  "pii-masking-run": "pii-masking-run.js",
31
+ "ps-guard": "pii-masking-run.js",
32
+ "agentic-guard": "pii-masking-run.js",
30
33
  "privacyscrubber-mcp-install": "install.js"
31
34
  },
32
35
  "scripts": {
33
- "start": "node index.js"
36
+ "start": "node index.js",
37
+ "build": "echo 'No build step required for vanilla JS'",
38
+ "test": "node test-mcp.js && node test-mcp-deep.js"
34
39
  },
35
40
  "dependencies": {
36
41
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -9,7 +9,7 @@
9
9
  * Usage: pii-masking-run [options] -- <command> [args...]
10
10
  */
11
11
 
12
- import { spawn } from 'child_process';
12
+ import { spawn, execSync } from 'child_process';
13
13
  import { createRequire } from 'module';
14
14
  import fs from 'fs';
15
15
  import path from 'path';
@@ -27,24 +27,39 @@ const cmdIndex = args.indexOf("--");
27
27
 
28
28
  if (args.includes("--version") || args.includes("-v")) {
29
29
  const pkgPath = path.resolve(__dirname, './package.json');
30
- let version = '1.7.0';
30
+ let version = '2.2.0';
31
31
  try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
32
- console.log(`pii-masking-run v${version} (PrivacyScrubber MCP)`);
32
+ console.log(`ps-guard / pii-masking-run v${version} (PrivacyScrubber MCP)`);
33
33
  process.exit(0);
34
34
  }
35
35
 
36
- if (cmdIndex === -1 || args.includes("-h") || args.includes("--help")) {
37
- console.log("PrivacyScrubber CLI Command Runner: pii-masking-run");
36
+ const isDiffMode = args.includes("--diff");
37
+ const isRulesMode = args.includes("--rules");
38
+ const hasCommand = cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0;
39
+ const hasStdin = !process.stdin.isTTY;
40
+
41
+ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !hasStdin)) {
42
+ console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
38
43
  console.log("\nUsage:");
39
- console.log(" npx pii-masking-run [options] -- <command> [args...]");
44
+ console.log(" npx ps-guard [options] -- <command> [args...]");
45
+ console.log(" cat <file> | npx ps-guard [options]");
46
+ console.log(" npx ps-guard --diff [--staged] [options]");
47
+ console.log(" npx ps-guard --rules [targets]");
40
48
  console.log("\nOptions:");
41
- console.log(" --profile <name> Specify detection profile (e.g. General, Medical, Legal, Dev, HR)");
49
+ console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
42
50
  console.log(" --key <licKey> PrivacyScrubber PRO License Key");
51
+ console.log(" --diff Sanitize git diff output before committing or sending to AI");
52
+ console.log(" --staged Use staged git changes with --diff");
53
+ console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
54
+ console.log(" -q, --quiet Suppress stderr proof telemetry");
43
55
  console.log(" -v, --version Print version");
44
56
  console.log(" -h, --help Show this help screen");
45
57
  console.log("\nExamples:");
46
- console.log(" npx pii-masking-run -- cat database-dump.sql");
47
- console.log(" npx pii-masking-run --profile dev -- npm test");
58
+ console.log(" npx ps-guard -- cat database-dump.sql");
59
+ console.log(" npx ps-guard --profile dev -- npm test");
60
+ console.log(" cat .env | npx ps-guard --profile dev");
61
+ console.log(" npx ps-guard --diff --staged");
62
+ console.log(" npx ps-guard --rules cursor,windsurf");
48
63
  process.exit(0);
49
64
  }
50
65
 
@@ -69,7 +84,14 @@ const DEVOPS_SECRETS_DETECTOR = [
69
84
  { 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 }
70
85
  ];
71
86
 
72
- let LicenseManager = require('./ps-license-manager.js');
87
+ let LicenseManager;
88
+ try {
89
+ LicenseManager = require('./ps-license-manager.cjs');
90
+ } catch (e) {
91
+ try {
92
+ LicenseManager = require('./ps-license-manager.js');
93
+ } catch (err) {}
94
+ }
73
95
  if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
74
96
  LicenseManager = global.LicenseManager;
75
97
  }
@@ -126,14 +148,9 @@ function loadCustomRules() {
126
148
  // Local runtime variables
127
149
  let profile = "General";
128
150
  let key = process.env.PRIVACYSCRUBBER_KEY || "";
151
+ let rulesTargets = ["all"];
129
152
 
130
- const optionsArgs = args.slice(0, cmdIndex);
131
- const commandArgs = args.slice(cmdIndex + 1);
132
-
133
- if (commandArgs.length === 0) {
134
- console.error("Error: No command specified after '--'.");
135
- process.exit(1);
136
- }
153
+ const optionsArgs = cmdIndex !== -1 ? args.slice(0, cmdIndex) : args;
137
154
 
138
155
  // Parse custom command-line options
139
156
  for (let i = 0; i < optionsArgs.length; i++) {
@@ -143,6 +160,11 @@ for (let i = 0; i < optionsArgs.length; i++) {
143
160
  } else if (optionsArgs[i] === "--key" && optionsArgs[i + 1]) {
144
161
  key = optionsArgs[i + 1];
145
162
  i++;
163
+ } else if (optionsArgs[i] === "--rules") {
164
+ if (optionsArgs[i + 1] && !optionsArgs[i + 1].startsWith("-")) {
165
+ rulesTargets = optionsArgs[i + 1].split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
166
+ i++;
167
+ }
146
168
  }
147
169
  }
148
170
 
@@ -154,9 +176,26 @@ const customRules = loadCustomRules();
154
176
  const localSessionMap = {};
155
177
 
156
178
  function performSanitization(text) {
157
- const normalizedProfile = (profile || "general").trim().toLowerCase();
158
- const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, localSessionMap);
159
179
  const license = checkLicenseStatus();
180
+ const normalizedProfile = (profile || "general").trim().toLowerCase();
181
+ const limit = (normalizedProfile !== "general" && !license.isPro) ? 5000 : 15000;
182
+ let textToScrub = text;
183
+ let wasTruncated = false;
184
+ if (!license.isPro && textToScrub.length > limit) {
185
+ textToScrub = textToScrub.slice(0, limit);
186
+ wasTruncated = true;
187
+ }
188
+
189
+ let result;
190
+ try {
191
+ result = PrivacyScrubberCore.scrubText(textToScrub, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
192
+ } catch (err) {
193
+ return text;
194
+ }
195
+
196
+ if (result && result.tokenMap) {
197
+ Object.assign(localSessionMap, result.tokenMap);
198
+ }
160
199
 
161
200
  if (!license.isPro && result.tokenMap) {
162
201
  // Free tier: check for secrets and warn
@@ -181,40 +220,160 @@ function performSanitization(text) {
181
220
  }
182
221
  }
183
222
 
184
- return result.scrubbedText;
223
+ return result.scrubbedText + (wasTruncated ? '\n[Output truncated to Free Tier character limit. Upgrade to PRO for unlimited.]' : '');
185
224
  }
186
225
 
187
- // Spawn child process capturing output
188
- const child = spawn(commandArgs[0], commandArgs.slice(1), {
189
- stdio: ['inherit', 'pipe', 'pipe'],
190
- shell: true
191
- });
192
-
193
- const rlStdout = readline.createInterface({
194
- input: child.stdout,
195
- terminal: false
196
- });
197
-
198
- rlStdout.on('line', (line) => {
199
- const sanitized = performSanitization(line);
200
- process.stdout.write(sanitized + '\n');
201
- });
202
-
203
- const rlStderr = readline.createInterface({
204
- input: child.stderr,
205
- terminal: false
206
- });
207
-
208
- rlStderr.on('line', (line) => {
209
- const sanitized = performSanitization(line);
210
- process.stderr.write(sanitized + '\n');
211
- });
212
-
213
- child.on('close', (code) => {
214
- process.exit(code || 0);
215
- });
216
-
217
- child.on('error', (err) => {
218
- console.error(`Error spawning command: ${err.message}`);
219
- process.exit(1);
220
- });
226
+ function emitProofFeedback() {
227
+ const isQuiet = args.includes("--quiet") || args.includes("-q");
228
+ if (isQuiet) return;
229
+
230
+ const totalMasked = Object.keys(localSessionMap).length;
231
+ if (totalMasked > 0) {
232
+ process.stderr.write(`\x1b[36m[ps-guard] ${totalMasked} item(s) masked · 0 bytes egress · Zero-Trust RAM verified\x1b[0m\n`);
233
+ } else if (args.includes("--verbose")) {
234
+ process.stderr.write(`\x1b[36m[ps-guard] 0 item(s) masked · 0 bytes egress · Zero-Trust RAM verified\x1b[0m\n`);
235
+ }
236
+ }
237
+
238
+ // Mode 1: Agent Rules Generation (--rules)
239
+ if (isRulesMode) {
240
+ const targetDir = process.cwd();
241
+ const targets = rulesTargets.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : rulesTargets;
242
+ const RULE_TEXT = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
243
+ # 100% Local In-Memory Data Sanitization. Zero Network Egress.
244
+
245
+ ## Agent Mandate
246
+ You are an autonomous AI assistant operating under strict Zero-Trust Data Sanitization (ZTDS) policies.
247
+ Before reading sensitive files, running terminal commands that may print credentials, or committing code:
248
+
249
+ 1. **Terminal Commands:** When running shell commands whose output may contain API keys, database connection strings, or customer PII (e.g. \`cat .env\`, \`docker logs\`, \`env\`, \`git diff\`), invoke the \`guard_exec\` tool instead of raw terminal execution.
250
+ 2. **Reading Credentials/Config:** When reading \`.env*\`, config files, database credentials, or secret keys, always call \`guard_read_file\` so sensitive entities are tokenized locally in RAM before entering your context window.
251
+ 3. **Git Code Review:** Use \`guard_git_diff\` to inspect changes without exposing plaintext secrets to the cloud.
252
+ 4. **Applying Patches:** When writing files containing token placeholders (e.g. \`[API_KEY_1]\`, \`[DB_PASSWORD_1]\`), use \`guard_apply_patch\` to automatically restore authentic secrets locally to disk without sending them back to the LLM.
253
+ 5. **Final Output:** Pass any final synthesized text containing token placeholders through \`reveal_text\` before presenting it to the user.
254
+ `;
255
+
256
+ const generatedFiles = [];
257
+ if (targets.includes("cursor")) {
258
+ fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
259
+ generatedFiles.push('.cursorrules');
260
+ }
261
+ if (targets.includes("windsurf")) {
262
+ fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
263
+ generatedFiles.push('.windsurfrules');
264
+ }
265
+ if (targets.includes("claude_code") || targets.includes("claude")) {
266
+ const p = path.join(targetDir, 'CLAUDE.md');
267
+ if (fs.existsSync(p)) {
268
+ const existing = fs.readFileSync(p, 'utf8');
269
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
270
+ fs.writeFileSync(p, existing + "\n\n" + RULE_TEXT, 'utf8');
271
+ generatedFiles.push('CLAUDE.md (appended)');
272
+ } else {
273
+ generatedFiles.push('CLAUDE.md (already present)');
274
+ }
275
+ } else {
276
+ fs.writeFileSync(p, RULE_TEXT, 'utf8');
277
+ generatedFiles.push('CLAUDE.md');
278
+ }
279
+ }
280
+ if (targets.includes("copilot")) {
281
+ const copilotDir = path.join(targetDir, '.github');
282
+ if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
283
+ fs.writeFileSync(path.join(copilotDir, 'copilot-instructions.md'), RULE_TEXT, 'utf8');
284
+ generatedFiles.push('.github/copilot-instructions.md');
285
+ }
286
+ if (targets.includes("cline")) {
287
+ fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
288
+ generatedFiles.push('.clinerules');
289
+ }
290
+
291
+ console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
292
+ console.log(`Workspace: ${targetDir}`);
293
+ console.log(`Files Created/Updated:`);
294
+ generatedFiles.forEach(f => console.log(` - ${f}`));
295
+ process.exit(0);
296
+ }
297
+
298
+ // Mode 2: Git Diff Mode (--diff)
299
+ if (isDiffMode) {
300
+ const isStaged = args.includes("--staged") || args.includes("--cached");
301
+ const gitCmd = isStaged ? "git diff --cached" : "git diff";
302
+ try {
303
+ let diffOutput = '';
304
+ try {
305
+ diffOutput = execSync(gitCmd, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
306
+ } catch (err) {
307
+ if (err.code === 'ENOBUFS' && err.stdout) {
308
+ diffOutput = err.stdout.toString('utf8');
309
+ } else {
310
+ throw err;
311
+ }
312
+ }
313
+ if (!diffOutput.trim()) {
314
+ console.log(`[Zero-Trust Agentic Guard: Git Diff]\nNo git diff changes detected (${isStaged ? 'staged' : 'unstaged'}).`);
315
+ process.exit(0);
316
+ }
317
+ const sanitizedDiff = performSanitization(diffOutput);
318
+ process.stdout.write(sanitizedDiff.endsWith('\n') ? sanitizedDiff : sanitizedDiff + '\n');
319
+ emitProofFeedback();
320
+ process.exit(0);
321
+ } catch (err) {
322
+ console.error(`Error running git diff: ${err.message}`);
323
+ process.exit(1);
324
+ }
325
+ }
326
+
327
+ // Mode 3: Stdin Stream Mode (pipe)
328
+ if (hasStdin && !hasCommand) {
329
+ const rlStdin = readline.createInterface({
330
+ input: process.stdin,
331
+ terminal: false
332
+ });
333
+ rlStdin.on('line', (line) => {
334
+ const sanitized = performSanitization(line);
335
+ process.stdout.write(sanitized + '\n');
336
+ });
337
+ rlStdin.on('close', () => {
338
+ emitProofFeedback();
339
+ process.exit(0);
340
+ });
341
+ } else if (hasCommand) {
342
+ // Mode 4: Spawn Child Process (-- <command>)
343
+ const commandArgs = args.slice(cmdIndex + 1);
344
+ const fullCommand = commandArgs.join(' ');
345
+ const child = spawn(fullCommand, {
346
+ stdio: ['inherit', 'pipe', 'pipe'],
347
+ shell: true
348
+ });
349
+
350
+ const rlStdout = readline.createInterface({
351
+ input: child.stdout,
352
+ terminal: false
353
+ });
354
+
355
+ rlStdout.on('line', (line) => {
356
+ const sanitized = performSanitization(line);
357
+ process.stdout.write(sanitized + '\n');
358
+ });
359
+
360
+ const rlStderr = readline.createInterface({
361
+ input: child.stderr,
362
+ terminal: false
363
+ });
364
+
365
+ rlStderr.on('line', (line) => {
366
+ const sanitized = performSanitization(line);
367
+ process.stderr.write(sanitized + '\n');
368
+ });
369
+
370
+ child.on('close', (code) => {
371
+ emitProofFeedback();
372
+ process.exit(code || 0);
373
+ });
374
+
375
+ child.on('error', (err) => {
376
+ console.error(`Error spawning command: ${err.message}`);
377
+ process.exit(1);
378
+ });
379
+ }
@@ -0,0 +1,257 @@
1
+ (function(global) {
2
+ // ── START LICENSE CRYPTO ──
3
+ const PUBLIC_KEY_N = BigInt("21317979104050344678233452423803986868842680501941538057759536089609908024281729760556922188810565541201321990374637212776148857190553192389608364990754937991254608109727112734447704854882786946805547031142530865504430968628760311494129154950865158735816268612258108296781892583171421605668998979064782378729322077179022581884976242706169323540456286526420742434868559866211716347133681429395477256705746276962607312574959642065929462114149296066812527505837608212730874704915352119747022918247963886630350365119206627670591400728523813746184290857872958847087691236909900501703203494160206941648263514117706558791691");
4
+ const PUBLIC_KEY_E = 65537n;
5
+
6
+ function modPow(base, exp, mod) {
7
+ let res = 1n;
8
+ base = base % mod;
9
+ while (exp > 0n) {
10
+ if (exp % 2n === 1n) res = (res * base) % mod;
11
+ exp = exp / 2n;
12
+ base = (base * base) % mod;
13
+ }
14
+ return res;
15
+ }
16
+
17
+ function bigIntToString(bi) {
18
+ let hex = bi.toString(16);
19
+ if (hex.length % 2 !== 0) hex = '0' + hex;
20
+ let str = '';
21
+ for (let i = 0; i < hex.length; i += 2) {
22
+ str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
23
+ }
24
+ return str;
25
+ }
26
+
27
+ function stringToBigInt(str) {
28
+ let hex = '';
29
+ for (let i = 0; i < str.length; i++) {
30
+ hex += str.charCodeAt(i).toString(16).padStart(2, '0');
31
+ }
32
+ return BigInt('0x' + hex);
33
+ }
34
+
35
+ function sha256(ascii) {
36
+ function rightRotate(value, amount) { return (value>>>amount) | (value<<(32 - amount)); }
37
+ var mathPow = Math.pow; var maxWord = mathPow(2, 32);
38
+ var lengthProperty = 'length'; var i, j;
39
+ var result = ''; var words = []; var asciiBitLength = ascii[lengthProperty]*8;
40
+ var hash = sha256.h = sha256.h || [];
41
+ var k = sha256.k = sha256.k || [];
42
+ var primeCounter = k[lengthProperty];
43
+ var isComposite = {};
44
+ for (var candidate = 2; primeCounter < 64; candidate++) {
45
+ if (!isComposite[candidate]) {
46
+ for (i = 0; i < 313; i += candidate) { isComposite[i] = candidate; }
47
+ hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
48
+ k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
49
+ }
50
+ }
51
+ ascii += '\x80';
52
+ while (ascii[lengthProperty]%64 - 56) ascii += '\x00';
53
+ for (i = 0; i < ascii[lengthProperty]; i++) {
54
+ j = ascii.charCodeAt(i);
55
+ if (j>>8) return;
56
+ words[i>>2] |= j << ((3 - i)%4)*8;
57
+ }
58
+ words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
59
+ words[words[lengthProperty]] = (asciiBitLength)
60
+ for (j = 0; j < words[lengthProperty];) {
61
+ var w = words.slice(j, j += 16);
62
+ var oldHash = hash;
63
+ hash = hash.slice(0, 8);
64
+ for (i = 0; i < 64; i++) {
65
+ var w15 = w[i - 15], w2 = w[i - 2];
66
+ var a = hash[0], e = hash[4];
67
+ var temp1 = hash[7] + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) + ((e&hash[5])^((~e)&hash[6])) + k[i] + (w[i] = (i < 16) ? w[i] : (w[i - 16] + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) + w[i - 7] + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)))|0);
68
+ var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2]));
69
+ hash = [(temp1 + temp2)|0].concat(hash);
70
+ hash[4] = (hash[4] + temp1)|0;
71
+ }
72
+ for (i = 0; i < 8; i++) { hash[i] = (hash[i] + oldHash[i])|0; }
73
+ }
74
+ for (i = 0; i < 8; i++) {
75
+ for (j = 3; j + 1; j--) {
76
+ var b = (hash[i]>>(j*8))&255;
77
+ result += ((b < 16) ? 0 : '') + b.toString(16);
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ // ── END LICENSE CRYPTO ──
83
+
84
+ class LicenseManager {
85
+ /**
86
+ * Validates a license key universally across all environments.
87
+ * @param {string} key - The license key.
88
+ * @param {object} options - Options mapping to environment specifics.
89
+ * @returns {object} { valid: boolean, tier: string|null, reason?: string, expires?: number }
90
+ */
91
+ static validate(key, options = {}) {
92
+ if (!key) return { valid: false, tier: null, reason: "No key provided" };
93
+ const rawKey = key.trim();
94
+ const k = rawKey.toUpperCase();
95
+ const parts = k.split('-');
96
+
97
+ const now = options.mockTime !== undefined && options.mockTime !== null ? options.mockTime : Date.now();
98
+ let lastSeen = 0;
99
+
100
+ if (options.storage) {
101
+ lastSeen = parseInt(options.storage.getItem('ps_last_seen_time') || '0', 10);
102
+ if (options.mockTime === undefined || options.mockTime === null) {
103
+ options.storage.setItem('ps_last_seen_time', now.toString());
104
+ }
105
+ }
106
+
107
+ // Clock rollback guard
108
+ if ((options.mockTime === undefined || options.mockTime === null) && now < lastSeen) {
109
+ if (options.onAnomaly) options.onAnomaly();
110
+ return { valid: false, tier: null, reason: "Clock rollback anomaly detected" };
111
+ }
112
+
113
+ const _atob = typeof atob !== 'undefined' ? atob : (str) => Buffer.from(str, 'base64').toString('binary');
114
+
115
+ // 1. Process Pilot Keys (RSA-1024 Encrypted Timestamps)
116
+ if (k.startsWith('PS-PILOT-')) {
117
+ try {
118
+ const payload = rawKey.substring(9);
119
+ const binary = _atob(payload);
120
+ const bytes = new Uint8Array(binary.length);
121
+ for (let i = 0; i < binary.length; i++) {
122
+ bytes[i] = binary.charCodeAt(i);
123
+ }
124
+ let hex = '';
125
+ for (let i = 0; i < bytes.length; i++) {
126
+ hex += bytes[i].toString(16).padStart(2, '0');
127
+ }
128
+ const S = BigInt('0x' + hex);
129
+
130
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
131
+ const decryptedStr = bigIntToString(M);
132
+
133
+ if (decryptedStr.startsWith('ZTDS-PILOT-')) {
134
+ const expDate = parseInt(decryptedStr.substring(11), 10);
135
+ if (!isNaN(expDate)) {
136
+ if (now < expDate) return { valid: true, tier: 'TEAMS', expires: expDate };
137
+
138
+ if (options.onExpired) options.onExpired('Pilot');
139
+ return { valid: false, tier: null, reason: "Pilot expired" };
140
+ }
141
+ }
142
+ } catch(e) {
143
+ return { valid: false, tier: null, reason: "Pilot decryption failed" };
144
+ }
145
+ }
146
+
147
+ // 1.5. Process RSA Signed Keys (PS-RSA-...)
148
+ if (k.startsWith('PS-RSA-')) {
149
+ try {
150
+ const payload = rawKey.substring(7);
151
+ const binary = _atob(payload);
152
+ const bytes = new Uint8Array(binary.length);
153
+ for (let i = 0; i < binary.length; i++) {
154
+ bytes[i] = binary.charCodeAt(i);
155
+ }
156
+ let hex = '';
157
+ for (let i = 0; i < bytes.length; i++) {
158
+ hex += bytes[i].toString(16).padStart(2, '0');
159
+ }
160
+ const S = BigInt('0x' + hex);
161
+
162
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
163
+ const decryptedStr = bigIntToString(M);
164
+
165
+ if (decryptedStr.startsWith('ZTDS-LIC-')) {
166
+ const pieces = decryptedStr.split('-');
167
+ if (pieces.length >= 5) {
168
+ const tier = pieces[2];
169
+ const expires = parseInt(pieces[4], 10);
170
+ if (!isNaN(expires)) {
171
+ if (now < expires) return { valid: true, tier: tier, expires: expires };
172
+
173
+ if (options.onExpired) options.onExpired('RSA');
174
+ return { valid: false, tier: null, reason: "RSA key expired" };
175
+ }
176
+ }
177
+ }
178
+ } catch(e) {
179
+ return { valid: false, tier: null, reason: "RSA decryption failed" };
180
+ }
181
+ }
182
+
183
+ // 2. Process Modern Prefixed Keys (v1.4.2+ Salted Checksum)
184
+ if (parts.length === 4 && parts[0] === 'PS' && ['PRO', 'TEAMS', 'SDK', 'OEM', 'ENTERPRISE'].includes(parts[1])) {
185
+ const tier = parts[1];
186
+ const core = parts[2];
187
+ const checksum = parts[3];
188
+ const salt = "ZTDS_SALT_2026_!@#";
189
+ const input = core + salt + tier;
190
+ let sum = 0;
191
+ for (let i = 0; i < input.length; i++) {
192
+ sum += input.charCodeAt(i) * (i + 1);
193
+ }
194
+ const expectedChecksum = (sum % 9999).toString().padStart(4, '0');
195
+ if (checksum === expectedChecksum) {
196
+ return { valid: true, tier: tier };
197
+ }
198
+ }
199
+
200
+ // 3. Process Standard RS256 JWTs (eyJ...)
201
+ if (rawKey.startsWith('eyJ') && rawKey.split('.').length === 3) {
202
+ try {
203
+ const [b64Header, b64Payload, b64Signature] = rawKey.split('.');
204
+
205
+ let sigStr = b64Signature.replace(/-/g, '+').replace(/_/g, '/');
206
+ while (sigStr.length % 4) sigStr += '=';
207
+ const binarySig = _atob(sigStr);
208
+ let hexSig = '';
209
+ for (let i = 0; i < binarySig.length; i++) {
210
+ hexSig += binarySig.charCodeAt(i).toString(16).padStart(2, '0');
211
+ }
212
+
213
+ const S = BigInt('0x' + hexSig);
214
+ const M = modPow(S, PUBLIC_KEY_E, PUBLIC_KEY_N);
215
+
216
+ let mHex = M.toString(16);
217
+ if (mHex.length % 2 !== 0) mHex = '0' + mHex;
218
+
219
+ const expectedHash = sha256(b64Header + '.' + b64Payload);
220
+
221
+ if (mHex.endsWith(expectedHash)) {
222
+ const payloadJson = _atob(b64Payload.replace(/-/g, '+').replace(/_/g, '/'));
223
+ const payload = JSON.parse(payloadJson);
224
+
225
+ if (payload.exp && (now / 1000) < payload.exp) {
226
+ return { valid: true, tier: payload.tier || 'PRO', expires: payload.exp * 1000 };
227
+ } else {
228
+ if (options.onExpired) options.onExpired('JWT');
229
+ return { valid: false, tier: null, reason: "JWT expired" };
230
+ }
231
+ }
232
+ } catch(e) {
233
+ return { valid: false, tier: null, reason: "JWT validation failed" };
234
+ }
235
+ }
236
+
237
+ return { valid: false, tier: null, reason: "Invalid format" };
238
+ }
239
+ }
240
+
241
+ // Export module for ES modules, CommonJS, and Browser Global
242
+ if (typeof globalThis !== 'undefined') {
243
+ globalThis.LicenseManager = LicenseManager;
244
+ }
245
+ if (typeof global !== 'undefined') {
246
+ global.LicenseManager = LicenseManager;
247
+ }
248
+ if (typeof window !== 'undefined') {
249
+ window.LicenseManager = LicenseManager;
250
+ }
251
+ if (typeof exports !== 'undefined') {
252
+ if (typeof module !== 'undefined' && module.exports) {
253
+ module.exports = LicenseManager;
254
+ }
255
+ exports.LicenseManager = LicenseManager;
256
+ }
257
+ })(typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this));