feat(browser-ext): move browser extension to browser-ext/ and update API client to tRPC
- Create browser-ext/ with full extension code (MV3 manifest, background service worker, content script, popup, options page) - Add tRPC API client that communicates with unified monolith endpoints - Implement cache, settings, and phishing detection utilities - Create extension tRPC router in web app (getAuthStatus, linkDevice, reportPhishing) - Configure Vite build with manifest V3 support - Write unit tests for cache, phishing detector, and API client - All 20 tests passing, TypeScript lint clean
This commit is contained in:
17
browser-ext/src/lib/api-client.ts
Normal file
17
browser-ext/src/lib/api-client.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
||||
import type { TrpcClient } from "../types/trpc";
|
||||
|
||||
export function createApiClient(url: string, apiKey?: string) {
|
||||
const raw = createTRPCProxyClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url,
|
||||
headers: apiKey ? { "x-api-key": apiKey } : undefined,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return raw as unknown as TrpcClient;
|
||||
}
|
||||
|
||||
export type ApiClient = ReturnType<typeof createApiClient>;
|
||||
34
browser-ext/src/lib/cache.ts
Normal file
34
browser-ext/src/lib/cache.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export class Cache<T = unknown> {
|
||||
private store = new Map<string, CacheEntry<T>>();
|
||||
|
||||
constructor(private ttlMs: number = 5 * 60 * 1000) {}
|
||||
|
||||
get(key: string): T | null {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
set(key: string, data: T): void {
|
||||
this.store.set(key, { data, expiresAt: Date.now() + this.ttlMs });
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const spamCheckCache = new Cache<unknown>(5 * 60 * 1000);
|
||||
50
browser-ext/src/lib/phishing-detector.ts
Normal file
50
browser-ext/src/lib/phishing-detector.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PhishingResult } from "../types";
|
||||
|
||||
const SUSPICIOUS_PATTERNS = [
|
||||
/login[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/secure[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/account[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/verify[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/update[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/banking[.-]?[a-z]+\.(com|org|net)/i,
|
||||
/paypal[.-]?[a-z]+\.(com|org|net)/i,
|
||||
];
|
||||
|
||||
const KNOWN_PHISHING_DOMAINS = [
|
||||
"shieldai-secure.com",
|
||||
"shieldai-verify.com",
|
||||
"shield-ai-login.com",
|
||||
];
|
||||
|
||||
export function checkUrlForPhishing(url: string): PhishingResult {
|
||||
const reasons: string[] = [];
|
||||
const lowerUrl = url.toLowerCase();
|
||||
|
||||
try {
|
||||
const parsed = new URL(lowerUrl);
|
||||
|
||||
for (const domain of KNOWN_PHISHING_DOMAINS) {
|
||||
if (parsed.hostname === domain || parsed.hostname.endsWith("." + domain)) {
|
||||
reasons.push(`Known phishing domain: ${parsed.hostname}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of SUSPICIOUS_PATTERNS) {
|
||||
if (pattern.test(lowerUrl)) {
|
||||
reasons.push(`Suspicious URL pattern matched: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
||||
reasons.push(`Non-standard protocol: ${parsed.protocol}`);
|
||||
}
|
||||
} catch {
|
||||
reasons.push("Invalid URL format");
|
||||
}
|
||||
|
||||
return {
|
||||
isPhishing: reasons.length > 0,
|
||||
confidence: reasons.length > 2 ? 0.9 : reasons.length > 0 ? 0.6 : 0,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
25
browser-ext/src/lib/settings.ts
Normal file
25
browser-ext/src/lib/settings.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ExtensionSettings } from "../types";
|
||||
|
||||
const DEFAULT_SETTINGS: ExtensionSettings = {
|
||||
apiKey: null,
|
||||
apiUrl: "https://api.shieldai.com/api/trpc",
|
||||
notificationsEnabled: true,
|
||||
phishingDetectionEnabled: true,
|
||||
autoReportPhishing: false,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "shieldai:settings";
|
||||
|
||||
export async function getSettings(): Promise<ExtensionSettings> {
|
||||
const result = await chrome.storage.sync.get(STORAGE_KEY);
|
||||
return { ...DEFAULT_SETTINGS, ...result[STORAGE_KEY] } as ExtensionSettings;
|
||||
}
|
||||
|
||||
export async function updateSettings(
|
||||
partial: Partial<ExtensionSettings>,
|
||||
): Promise<ExtensionSettings> {
|
||||
const current = await getSettings();
|
||||
const updated = { ...current, ...partial };
|
||||
await chrome.storage.sync.set({ [STORAGE_KEY]: updated });
|
||||
return updated;
|
||||
}
|
||||
Reference in New Issue
Block a user