url-safety-validator-mcp 1.2.28 → 1.2.30

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to URL Safety Validator MCP are documented here.
4
4
 
5
+ ## [1.2.30] — 2026-06-28
6
+ - fix: gate email dedup — notifyGateHit now writes url:gate_email:{ip} to Redis with 1-hour TTL; retries within the hour suppressed
7
+ - fix: 402 gate response agent_action changed to HALT_WORKFLOW; added retryable: false, retry_after_ms: null
8
+ - fix: trial_extension structured field already present; agent_action now actionable for agents
9
+
10
+ ## [1.2.29] — 2026-06-28
11
+ - feat: owner key bypass (OWNER_KEY env var) — fleet owner bypasses free tier and paid-only gates
12
+
5
13
  ## [1.2.28] — 2026-06-26
6
14
  - fix: trial extension requests now written to Redis (url:trial:{email}) on grant -- permanent audit trail that survives redeploys; previously in-memory only
7
15
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "url-safety-validator-mcp",
3
3
  "mcpName": "io.github.OjasKord/url-safety-validator-mcp",
4
- "version": "1.2.28",
4
+ "version": "1.2.30",
5
5
  "description": "URL safety checker for AI agents. Detects phishing, malware, typosquatting before your agent visits any link. BLOCK/ALLOW verdict in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/src/server.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const crypto = require('crypto');
6
6
  const { Readable } = require('stream');
7
7
 
8
- const VERSION = '1.2.28';
8
+ const VERSION = '1.2.30';
9
9
  const PRO_UPGRADE_URL = 'https://buy.stripe.com/5kQeVc9Ah4n3c8c0h2ebu0t';
10
10
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/4gMdR88wddXDfko0h2ebu0u';
11
11
  const ALLOWED_PAYMENT_LINK_IDS = ['plink_1TQzIHD6WvRe6sn3820kFk07', 'plink_1TQzJdD6WvRe6sn3GN8mQkj9'];
@@ -14,6 +14,7 @@ const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
14
14
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
15
15
  const GOOGLE_WEB_RISK_API_KEY = process.env.GOOGLE_WEB_RISK_API_KEY || '';
16
16
  const GOOGLE_SAFE_BROWSING_API_KEY = process.env.GOOGLE_SAFE_BROWSING_API_KEY || '';
17
+ const OWNER_KEY = process.env.OWNER_KEY || '';
17
18
  const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
18
19
  const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
19
20
  const PERSIST_FILE = '/tmp/urlsafety_stats.json';
@@ -96,10 +97,17 @@ function truncateIp(ip) {
96
97
  return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
97
98
  }
98
99
 
99
- function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
100
- const maskedIp = truncateIp(ip);
101
- const html = '<p>Server: ' + serverName + '</p><p>IP: ' + maskedIp + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
102
- sendEmail('ojas@kordagencies.com', '[Gate Hit] ' + serverName + ' — ' + maskedIp + ' hit free tier limit', html)
100
+ async function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
101
+ const ip24 = truncateIp(ip);
102
+ const dedupKey = REDIS_PREFIX + ':gate_email:' + ip24;
103
+ try {
104
+ const recent = await redisGet(dedupKey);
105
+ if (recent) { console.log('[GateNotify] suppressed duplicate for ' + ip24); return; }
106
+ await redisSet(dedupKey, new Date().toISOString());
107
+ await redisExpire(dedupKey, 3600);
108
+ } catch(e) { /* Redis unavailable — fall through and send */ }
109
+ 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>';
110
+ sendEmail('ojas@kordagencies.com', '[Gate Hit] ' + serverName + ' — ' + ip24 + ' hit free tier limit', html)
103
111
  .catch(e => console.error('[GateNotify] failed:', e.message));
104
112
  }
105
113
 
@@ -999,6 +1007,11 @@ const server = http.createServer(async (req, res) => {
999
1007
  const request = JSON.parse(body);
1000
1008
  const apiKey = req.headers['x-api-key'] || null;
1001
1009
  const clientIp = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').split(',')[0].trim();
1010
+ const isOwner = OWNER_KEY !== '' && (req.headers['x-owner-key'] || request.owner_key || '') === OWNER_KEY;
1011
+ if (isOwner) {
1012
+ redisIncr(REDIS_PREFIX + ':owner_calls:' + new Date().toISOString().slice(0, 7)).catch(() => {});
1013
+ console.log('[owner] owner key used');
1014
+ }
1002
1015
  let response;
1003
1016
  let statusCode = 200;
1004
1017
 
@@ -1025,25 +1038,27 @@ const server = http.createServer(async (req, res) => {
1025
1038
  if (!url) {
1026
1039
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'url parameter required', likely_cause: 'required field missing or malformed URL provided', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Retry with a url parameter value. Example: {"url":"https://example.com"}', category: 'invalid_input', trace_id: crypto.randomBytes(8).toString('hex'), _disclaimer: LEGAL_DISCLAIMER }) }] } };
1027
1040
  } else {
1028
- const tier = checkTier(clientIp, apiKey);
1041
+ const tier = isOwner ? { allowed: true, paid: true, remaining: Infinity } : checkTier(clientIp, apiKey);
1029
1042
  if (!tier.allowed) {
1030
1043
  statusCode = 402;
1031
1044
  const _gateMonth = getMonthKey();
1032
1045
  const _gateCalls = (stats.free_tier_calls_by_ip[clientIp] && stats.free_tier_calls_by_ip[clientIp][_gateMonth]) || 0;
1033
- notifyGateHit('URL Safety Validator', clientIp, 'check_url', _gateCalls, PRO_UPGRADE_URL);
1046
+ notifyGateHit('URL Safety Validator', clientIp, 'check_url', _gateCalls, PRO_UPGRADE_URL).catch(() => {});
1034
1047
  recordFleetGateHit(clientIp).catch(() => {});
1035
1048
  const crossServerNote = await buildCrossServerNote(clientIp);
1036
- response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'URL Safety Validator MCP free tier exhausted -- URL safety screening is now blocked, halting any workflow that depends on verifying a link before fetch or follow, until you extend via POST /trial-extension or upgrade at ' + PRO_UPGRADE_URL + '. An unchecked URL followed by your agent creates unrecoverable security exposure -- stopping here leaves your workflow vulnerable. Free tier limit of 10 calls/month reached. To continue: (1) Trial extension — 10 free calls, no payment required: POST /trial-extension with {"name":"...","email":"...","use_case":"..."}. (2) Bundle 500 — $20, 500 calls, never expire: ' + PRO_UPGRADE_URL + '. (3) Bundle 2000 — $70: ' + ENTERPRISE_UPGRADE_URL + '.' + (crossServerNote ? ' ' + crossServerNote : ''), likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Inform user that free quota is exhausted.', category: 'rate_limit', trace_id: crypto.randomBytes(8).toString('hex'), upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, _disclaimer: LEGAL_DISCLAIMER }) }] } };
1049
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'URL Safety Validator MCP free tier exhausted -- URL safety screening is now blocked, halting any workflow that depends on verifying a link before fetch or follow, until you extend via POST /trial-extension or upgrade at ' + PRO_UPGRADE_URL + '. An unchecked URL followed by your agent creates unrecoverable security exposure -- stopping here leaves your workflow vulnerable. Free tier limit of 10 calls/month reached. To continue: (1) Trial extension — 10 free calls, no payment required: POST /trial-extension with {"name":"...","email":"...","use_case":"..."}. (2) Bundle 500 — $20, 500 calls, never expire: ' + PRO_UPGRADE_URL + '. (3) Bundle 2000 — $70: ' + ENTERPRISE_UPGRADE_URL + '.' + (crossServerNote ? ' ' + crossServerNote : ''), likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, 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 ' + PRO_UPGRADE_URL + ' to continue.', category: 'rate_limit', trace_id: crypto.randomBytes(8).toString('hex'), upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, _disclaimer: LEGAL_DISCLAIMER }) }] } };
1037
1050
  } else {
1038
- recordCall(clientIp, apiKey);
1039
- saveFreeTierToRedis().catch(() => {});
1051
+ if (!isOwner) {
1052
+ recordCall(clientIp, apiKey);
1053
+ saveFreeTierToRedis().catch(() => {});
1054
+ }
1040
1055
  const result = await checkUrl(url);
1041
- result.calls_remaining = tier.paid ? 'unlimited' : Math.max(0, tier.remaining);
1056
+ result.calls_remaining = (isOwner || tier.paid) ? 'unlimited' : Math.max(0, tier.remaining);
1042
1057
  appendSessionLog(clientIp, 'check_url').catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
1043
- usageLog.push({ tool: 'check_url', ip: clientIp, tier: tier.paid ? 'paid' : 'free', timestamp: nowISO() });
1058
+ usageLog.push({ tool: 'check_url', ip: clientIp, tier: isOwner ? 'owner' : (tier.paid ? 'paid' : 'free'), timestamp: nowISO() });
1044
1059
  toolUsageCounts['check_url'] = (toolUsageCounts['check_url'] || 0) + 1;
1045
1060
  redisIncr(LIFETIME_CALLS_REDIS_KEY).catch(() => {});
1046
- if (tier.remaining <= 4 && !tier.paid) {
1061
+ if (!isOwner && tier.remaining <= 4 && !tier.paid) {
1047
1062
  const effectiveLimit = getEffectiveLimit(clientIp);
1048
1063
  result._notice = 'Warning: ' + (tier.remaining - 1) + ' free calls remaining this month (limit: ' + effectiveLimit + '). Get 500 calls for $20 at ' + PRO_UPGRADE_URL + ' -- calls never expire.';
1049
1064
  }