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:
80
packages/extension/src/lib/cache.ts
Normal file
80
packages/extension/src/lib/cache.ts
Normal 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;
|
||||
Reference in New Issue
Block a user