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