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

@@ -10,6 +10,7 @@ import { removebrokersRouter } from "./routers/removebrokers";
import { correlationRouter } from "./routers/correlation";
import { reportsRouter } from "./routers/reports";
import { schedulerRouter } from "./routers/scheduler";
import { extensionRouter } from "./routers/extension";
import { createTRPCRouter } from "./utils";
export const appRouter = createTRPCRouter({
@@ -25,6 +26,7 @@ export const appRouter = createTRPCRouter({
correlation: correlationRouter,
reports: reportsRouter,
scheduler: schedulerRouter,
extension: extensionRouter,
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,55 @@
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { wrap } from "@typeschema/valibot";
import { createTRPCRouter, publicProcedure } from "../utils";
import { GetAuthStatusSchema, LinkDeviceSchema, ReportPhishingSchema } from "../schemas/extension";
import { db } from "~/server/db";
import { deviceTokens } from "~/server/db/schema/auth";
export const extensionRouter = createTRPCRouter({
getAuthStatus: publicProcedure.input(wrap(GetAuthStatusSchema)).query(async ({ ctx }) => {
if (ctx.user) {
return { linked: true, userId: ctx.user.id, email: ctx.user.email };
}
if (ctx.apiKey) {
return { linked: true, apiKey: ctx.apiKey };
}
return { linked: false };
}),
linkDevice: publicProcedure.input(wrap(LinkDeviceSchema)).mutation(async ({ ctx, input }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required to link device" });
}
const existing = await db.query.deviceTokens.findFirst({
where: eq(deviceTokens.token, input.extensionId),
});
if (existing) {
await db
.update(deviceTokens)
.set({ lastUsedAt: new Date(), appName: input.deviceName ?? null })
.where(eq(deviceTokens.id, existing.id));
return { linked: true, deviceId: existing.id };
}
const [newDevice] = await db
.insert(deviceTokens)
.values({
userId: ctx.user.id,
deviceType: "desktop",
platform: "web",
token: input.extensionId,
appName: input.deviceName ?? "ShieldAI Browser Extension",
})
.returning();
return { linked: true, deviceId: newDevice.id };
}),
reportPhishing: publicProcedure.input(wrap(ReportPhishingSchema)).mutation(async ({ input }) => {
console.log(`[Phishing Report] URL: ${input.url}, Source: ${input.source ?? "unknown"}`);
return { reported: true, url: input.url };
}),
});

View File

@@ -0,0 +1,15 @@
import { object, string, optional } from "valibot";
export const GetAuthStatusSchema = object({
apiKey: optional(string()),
});
export const LinkDeviceSchema = object({
extensionId: string(),
deviceName: optional(string()),
});
export const ReportPhishingSchema = object({
url: string(),
source: optional(string()),
});