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:
2026-05-25 18:13:44 -04:00
parent 20dc5bf785
commit b03096f19d
30 changed files with 1474 additions and 5 deletions

View 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>;

View 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);

View 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,
};
}

View 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;
}