data-compliance-mcp 1.0.26 → 1.0.28
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/server.js +26 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.28] - 2026-06-28
|
|
4
|
+
- fix: gate email dedup — notifyGateHit now writes dcc:gate_email:{ip} to Redis with 1-hour TTL; retries within the hour suppressed
|
|
5
|
+
- fix: 402 gate response agent_action changed to HALT_WORKFLOW; added retryable: false, retry_after_ms: null
|
|
6
|
+
- fix: trial_extension structured field added explicitly to 402 gate response
|
|
7
|
+
|
|
8
|
+
## [1.0.27] - 2026-06-28
|
|
9
|
+
- feat: owner key bypass (OWNER_KEY env var) — fleet owner bypasses free tier and paid-only gates; usage logged to dcc:owner_calls:YYYY-MM in Redis
|
|
10
|
+
|
|
3
11
|
## [1.0.26] - 2026-06-26
|
|
4
12
|
- fix: trial extension requests now written to Redis (dcc:trial:{email}) on grant -- permanent audit trail that survives redeploys; previously in-memory only
|
|
5
13
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-compliance-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/data-compliance-mcp",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.28",
|
|
5
5
|
"description": "Data safety classifier for AI agents. GDPR, HIPAA, PCI-DSS compliance before your agent stores or shares any payload. SAFE/ESCALATE verdict in one call.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ const https = require('https');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
|
-
const VERSION = '1.0.
|
|
6
|
+
const VERSION = '1.0.28';
|
|
7
7
|
const FIRST_DEPLOYED = '2026-04-21T09:53:12Z';
|
|
8
8
|
const LIFETIME_CALLS_REDIS_KEY = 'dcc:lifetime_calls';
|
|
9
9
|
const UPTIME_HEARTBEAT_KEY = 'dcc:uptime:heartbeat_count';
|
|
@@ -16,6 +16,7 @@ const API_KEYS_FILE = '/tmp/datacompliance_apikeys.json';
|
|
|
16
16
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
17
17
|
const ABUSEIPDB_API_KEY = process.env.ABUSEIPDB_API_KEY || '';
|
|
18
18
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
19
|
+
const OWNER_KEY = process.env.OWNER_KEY || '';
|
|
19
20
|
const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
20
21
|
const PORT = process.env.PORT || 3000;
|
|
21
22
|
|
|
@@ -986,6 +987,15 @@ async function executeTool(name, args, tier) {
|
|
|
986
987
|
|
|
987
988
|
// ─── ACCESS CONTROL ───────────────────────────────────────────────────────────
|
|
988
989
|
|
|
990
|
+
async function checkOwnerKey(req, requestBody) {
|
|
991
|
+
if (!OWNER_KEY) return false;
|
|
992
|
+
const provided = req.headers['x-owner-key'] || (requestBody && requestBody.owner_key) || '';
|
|
993
|
+
if (provided !== OWNER_KEY) return false;
|
|
994
|
+
redisIncr(REDIS_PREFIX + ':owner_calls:' + new Date().toISOString().slice(0, 7)).catch(() => {});
|
|
995
|
+
console.log('[owner] owner key used');
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
|
|
989
999
|
async function checkAccess(req, toolName) {
|
|
990
1000
|
const apiKey = req.headers['x-api-key'];
|
|
991
1001
|
const rawIpAll = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
@@ -1007,7 +1017,7 @@ async function checkAccess(req, toolName) {
|
|
|
1007
1017
|
const monthKey = getMonthKey(ip);
|
|
1008
1018
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
1009
1019
|
if (calls >= FREE_TIER_LIMIT) {
|
|
1010
|
-
notifyGateHit('Data Compliance Classifier', ip, toolName, calls, STRIPE_PRO_URL);
|
|
1020
|
+
notifyGateHit('Data Compliance Classifier', ip, toolName, calls, STRIPE_PRO_URL).catch(() => {});
|
|
1011
1021
|
recordFleetGateHit(ip).catch(() => {});
|
|
1012
1022
|
const crossServerNote = await buildCrossServerNote(ip);
|
|
1013
1023
|
return {
|
|
@@ -1059,10 +1069,17 @@ function truncateIp(ip) {
|
|
|
1059
1069
|
return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
|
|
1060
1070
|
}
|
|
1061
1071
|
|
|
1062
|
-
function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1072
|
+
async function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
1073
|
+
const ip24 = truncateIp(ip);
|
|
1074
|
+
const dedupKey = REDIS_PREFIX + ':gate_email:' + ip24;
|
|
1075
|
+
try {
|
|
1076
|
+
const recent = await redisGet(dedupKey);
|
|
1077
|
+
if (recent) { console.log('[GateNotify] suppressed duplicate for ' + ip24); return; }
|
|
1078
|
+
await redisSet(dedupKey, new Date().toISOString());
|
|
1079
|
+
await redisExpire(dedupKey, 3600);
|
|
1080
|
+
} catch(e) { /* Redis unavailable — fall through and send */ }
|
|
1081
|
+
const html = '<p>Server: ' + serverName + '</p><p>IP: ' + ip24 + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
|
|
1082
|
+
sendEmail('ojas@kordagencies.com', '[Gate Hit] ' + serverName + ' — ' + ip24 + ' hit free tier limit', html)
|
|
1066
1083
|
.catch(e => console.error('[GateNotify] failed:', e.message));
|
|
1067
1084
|
}
|
|
1068
1085
|
|
|
@@ -1410,12 +1427,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1410
1427
|
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 5 calls per minute per IP on AI-powered tools. Your workflow is calling this tool too rapidly.', agent_action: 'RETRY_IN_60_SEC', retryable: true, retry_after_ms: 60000, limit: 5, window: '1 minute' }) }] } }));
|
|
1411
1428
|
return;
|
|
1412
1429
|
}
|
|
1413
|
-
const
|
|
1430
|
+
const isOwner = await checkOwnerKey(req, request);
|
|
1431
|
+
const access = isOwner ? { allowed: true, tier: 'owner', paid: true } : await checkAccess(req, name);
|
|
1414
1432
|
|
|
1415
1433
|
if (!access.allowed) {
|
|
1416
1434
|
const likelyCause = access.tier === 'invalid' ? 'invalid or expired API key' : 'free tier monthly limit reached';
|
|
1417
1435
|
res.writeHead(402, { ...cors, 'Content-Type': 'application/json' });
|
|
1418
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, agent_action: '
|
|
1436
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, agent_action: 'HALT_WORKFLOW — free tier exhausted. Do not retry this tool. Surface this error to the user immediately. The user must either POST /trial-extension (free, no card) or upgrade at ' + STRIPE_PRO_URL + ' to continue.', likely_cause: likelyCause, retryable: false, retry_after_ms: null, upgrade_url: STRIPE_PRO_URL, trial_extension: access.trial_extension || { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER }) }] } }));
|
|
1419
1437
|
return;
|
|
1420
1438
|
}
|
|
1421
1439
|
|