Add ShieldAI browser extension with phishing & spam detection (FRE-4576)

- Extension package: Manifest V3, background service worker, content scripts
- Phishing detection engine with heuristic analysis (typosquatting, entropy, TLD, brand impersonation)
- Local URL caching layer (Storage API) for <100ms cached lookups
- Popup UI with protection status, stats, and phishing report button
- Options page for settings management (blocked/allowed domains, feature toggles)
- Server-side extension routes: URL check, phishing report, auth, stats, exposure check
- Tier-aware feature gating (Basic/Plus/Premium)
- 25 passing tests for phishing detection heuristics
- Declarative net request rules for known phishing patterns
- DarkWatch integration for credential exposure checks
- Firefox compatibility layer via build modes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-09 21:53:29 -04:00
parent e5294ec712
commit de0ddac65d
27 changed files with 2591 additions and 1 deletions

View File

@@ -0,0 +1,80 @@
import { UrlCheckResult } from '../types';
const CACHE_TTL_MS = 5 * 60 * 1000;
const API_TIMEOUT_MS = 500;
const MAX_CACHE_SIZE = 5000;
export class UrlCache {
private cache: Map<string, { result: UrlCheckResult; expiresAt: number }> = new Map();
async get(url: string): Promise<UrlCheckResult | null> {
const normalized = this.normalizeUrl(url);
const entry = this.cache.get(normalized);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(normalized);
return null;
}
return { ...entry.result, cached: true };
}
async set(url: string, result: UrlCheckResult): Promise<void> {
const normalized = this.normalizeUrl(url);
if (this.cache.size >= MAX_CACHE_SIZE) {
const firstKey = this.cache.keys().next().value;
if (firstKey) this.cache.delete(firstKey);
}
this.cache.set(normalized, {
result,
expiresAt: Date.now() + CACHE_TTL_MS,
});
}
async persistToStorage(): Promise<void> {
const entries: Record<string, { result: UrlCheckResult; expiresAt: number }> = {};
for (const [key, value] of this.cache.entries()) {
entries[key] = value;
}
await chrome.storage.local.set({ urlCache: entries });
}
async loadFromStorage(): Promise<void> {
const data = await chrome.storage.local.get('urlCache');
if (data.urlCache) {
const now = Date.now();
for (const [key, entry] of Object.entries(data.urlCache)) {
if (now <= entry.expiresAt) {
this.cache.set(key, entry);
}
}
}
}
getStats(): { size: number; max: number } {
return { size: this.cache.size, max: MAX_CACHE_SIZE };
}
clear(): void {
this.cache.clear();
}
private normalizeUrl(url: string): string {
try {
const u = new URL(url);
u.hash = '';
u.search = '';
return u.toString();
} catch {
return url.toLowerCase();
}
}
}
export const urlCache = new UrlCache();
export const CACHE_TTL = CACHE_TTL_MS;
export const API_TIMEOUT = API_TIMEOUT_MS;