url-safety-validator-mcp 1.2.20 → 1.2.22
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 +6 -0
- package/package.json +1 -1
- package/src/server.js +61 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to URL Safety Validator MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.22] — 2026-06-18
|
|
6
|
+
- feat: revoke API key on Stripe refund
|
|
7
|
+
|
|
8
|
+
## [1.2.21] — 2026-06-17
|
|
9
|
+
- fix: Stripe webhook now validates payment_link ID — ignores events not belonging to this server
|
|
10
|
+
|
|
5
11
|
## [1.2.20] — 2026-06-17
|
|
6
12
|
- feat: SmitheryBot detection on check_url — returns mock SAFE verdict without consuming Google Safe Browsing credits
|
|
7
13
|
|
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.
|
|
4
|
+
"version": "1.2.22",
|
|
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,9 +5,10 @@ const fs = require('fs');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { Readable } = require('stream');
|
|
7
7
|
|
|
8
|
-
const VERSION = '1.2.
|
|
8
|
+
const VERSION = '1.2.22';
|
|
9
9
|
const PRO_UPGRADE_URL = 'https://buy.stripe.com/5kQeVc9Ah4n3c8c0h2ebu0t';
|
|
10
10
|
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/4gMdR88wddXDfko0h2ebu0u';
|
|
11
|
+
const ALLOWED_PAYMENT_LINK_IDS = ['plink_1TQzIHD6WvRe6sn3820kFk07', 'plink_1TQzJdD6WvRe6sn3GN8mQkj9'];
|
|
11
12
|
const PORT = process.env.PORT || 3000;
|
|
12
13
|
const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
13
14
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
@@ -157,6 +158,26 @@ async function redisExpire(key, seconds) {
|
|
|
157
158
|
} catch(e) { console.error('[Redis] redisExpire failed:', e); }
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
async function redisDelete(key) {
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(
|
|
164
|
+
`${UPSTASH_URL}/del/${encodeURIComponent(key)}`,
|
|
165
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
166
|
+
);
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
if (data.error) console.error('[Redis] redisDelete error:', data.error, 'key:', key);
|
|
169
|
+
} catch(e) { console.error('[Redis] redisDelete failed:', e); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function findCheckoutSessionEmail(paymentIntentId) {
|
|
173
|
+
const res = await fetch(
|
|
174
|
+
`https://api.stripe.com/v1/checkout/sessions?payment_intent=${encodeURIComponent(paymentIntentId)}`,
|
|
175
|
+
{ headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` } }
|
|
176
|
+
);
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
return data.data?.[0]?.customer_details?.email || data.data?.[0]?.customer_email || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
160
181
|
async function redisKeys(pattern) {
|
|
161
182
|
try {
|
|
162
183
|
const res = await fetch(
|
|
@@ -695,6 +716,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
695
716
|
const event = JSON.parse(rawBody);
|
|
696
717
|
if (event.type === 'checkout.session.completed') {
|
|
697
718
|
const session = event.data.object;
|
|
719
|
+
const paymentLinkId = session.payment_link;
|
|
720
|
+
if (paymentLinkId && !ALLOWED_PAYMENT_LINK_IDS.includes(paymentLinkId)) {
|
|
721
|
+
console.log('[url-safety] Webhook received but payment link ' + paymentLinkId + ' not for this server — ignoring.');
|
|
722
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
723
|
+
}
|
|
698
724
|
const key = 'usv_' + crypto.randomBytes(16).toString('hex');
|
|
699
725
|
const email = session.customer_details?.email || session.customer_email || 'unknown';
|
|
700
726
|
const record = { email, created_at: nowISO(), plan: 'pro' };
|
|
@@ -706,6 +732,40 @@ const server = http.createServer(async (req, res) => {
|
|
|
706
732
|
sendApiKeyEmail(email, key, 'pro').catch(err => console.error('[stripe] Email send failed:', err.message));
|
|
707
733
|
}
|
|
708
734
|
}
|
|
735
|
+
if (event.type === 'charge.refunded') {
|
|
736
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
737
|
+
console.error('[url-safety] STRIPE_SECRET_KEY not set — cannot revoke key on refund');
|
|
738
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
739
|
+
}
|
|
740
|
+
const paymentIntentId = event.data.object.payment_intent;
|
|
741
|
+
if (!paymentIntentId) {
|
|
742
|
+
console.log('[url-safety] charge.refunded missing payment_intent — ignoring.');
|
|
743
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const refundEmail = await findCheckoutSessionEmail(paymentIntentId);
|
|
747
|
+
if (!refundEmail) {
|
|
748
|
+
console.log('[url-safety] No checkout session/email found for refunded payment_intent ' + paymentIntentId);
|
|
749
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
750
|
+
}
|
|
751
|
+
let revokedKey = null;
|
|
752
|
+
for (const [k, record] of apiKeys.entries()) {
|
|
753
|
+
if (record.email === refundEmail) { revokedKey = k; break; }
|
|
754
|
+
}
|
|
755
|
+
if (!revokedKey) {
|
|
756
|
+
console.log('[url-safety] No API key found for ' + refundEmail + ' — refund received, nothing to revoke');
|
|
757
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
758
|
+
}
|
|
759
|
+
apiKeys.delete(revokedKey);
|
|
760
|
+
await redisDelete(`${REDIS_PREFIX}:key:${revokedKey}`);
|
|
761
|
+
saveStats();
|
|
762
|
+
console.log('[Webhook] API key revoked for ' + refundEmail + ' — refund received');
|
|
763
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, revoked: true })); return;
|
|
764
|
+
} catch(e) {
|
|
765
|
+
console.error('[url-safety] charge.refunded handling error:', e.message);
|
|
766
|
+
res.writeHead(200, cors); res.end(JSON.stringify({ received: true, ignored: true })); return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
709
769
|
res.writeHead(200, cors); res.end(JSON.stringify({ received: true }));
|
|
710
770
|
} catch(e) {
|
|
711
771
|
res.writeHead(400, cors); res.end(JSON.stringify({ error: e.message }));
|