import { Router, type IRouter } from "express"; import { anthropic } from "@workspace/integrations-anthropic-ai"; import { DetectDarkPatternsBody, ClassifyUpsellBody, } from "@workspace/api-zod"; const router: IRouter = Router(); const DARK_PATTERN_SYSTEM_PROMPT = `You are Guardian Agent's dark pattern detection engine. Analyze webpage content and identify psychological manipulation tactics used by e-commerce sites. For each analysis, return a JSON object with this exact structure: { "falseUrgency": { "detected": boolean, "evidence": "explanation of what was found or empty string", "isTimerFake": boolean or null }, "falseScarcity": { "detected": boolean, "evidence": "explanation or empty string" }, "confirmShaming": { "detected": boolean, "shamingText": "the offending text or empty string", "rewrittenText": "neutral version like 'No thanks' or empty string" }, "hiddenFees": { "detected": boolean, "feeItems": [{"label": "fee name", "amount": 12.99}], "totalExtra": number or null }, "preCheckedAddOns": { "detected": boolean, "fieldIds": ["id1", "id2"], "addOnLabels": ["Travel Insurance", "Newsletter"] }, "misdirection": { "detected": boolean, "hiddenDeclineText": "the hard-to-find decline option text or empty string" }, "trustScore": number between 0 and 100 (100 = fully trustworthy, 0 = highly manipulative), "summary": "1-2 sentence human-readable summary of findings" } Be strict and accurate. Only flag genuine dark patterns, not normal UX. A score of 80+ means the site is mostly clean. 50-80 is neutral. Below 50 means significant manipulation detected.`; const UPSELL_SYSTEM_PROMPT = `You are Guardian Agent's upsell classification engine. Identify what type of upsell screen is being shown so the extension can automatically decline it based on user preferences. Return a JSON object with this exact structure: { "type": one of: "insurance", "breakfast", "room_upgrade", "seat_selection", "newsletter", "sms_alerts", "warranty", "refundable_rate", "other", "confidence": number between 0 and 1, "recommendedAction": one of: "decline", "accept", "ask_user", "declineButtonHint": "the text on the decline button or link, or a description of where it might be" } For insurance, seat selection, newsletters, and sms_alerts: recommend "decline". For refundable_rate: recommend "accept". For room_upgrade and breakfast: recommend "ask_user". For other: recommend "ask_user".`; router.post("/analysis/detect", async (req, res): Promise => { const parsed = DetectDarkPatternsBody.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: parsed.error.message }); return; } const { domain, pageText, timerElements, stockAlerts, buttonLabels, formFields, priceStrings } = parsed.data; const userMessage = ` Domain: ${domain} Page text (truncated to 3000 chars): ${pageText.slice(0, 3000)} Timer elements found: ${JSON.stringify(timerElements ?? [])} Stock/scarcity alerts: ${JSON.stringify(stockAlerts ?? [])} Button labels: ${JSON.stringify(buttonLabels ?? [])} Pre-checked form fields: ${JSON.stringify((formFields ?? []).filter(f => f.checked))} Price strings found: ${JSON.stringify(priceStrings ?? [])} Analyze this page for dark patterns and return the JSON analysis.`; const message = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 8192, system: DARK_PATTERN_SYSTEM_PROMPT, messages: [{ role: "user", content: userMessage }], }); const block = message.content[0]; const rawText = block.type === "text" ? block.text : "{}"; const jsonMatch = rawText.match(/\{[\s\S]*\}/); if (!jsonMatch) { res.status(500).json({ error: "Failed to parse AI response" }); return; } const result = JSON.parse(jsonMatch[0]); result.domain = domain; res.json(result); }); router.post("/analysis/classify-upsell", async (req, res): Promise => { const parsed = ClassifyUpsellBody.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: parsed.error.message }); return; } const { domain, pageText } = parsed.data; const message = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 8192, system: UPSELL_SYSTEM_PROMPT, messages: [ { role: "user", content: `Domain: ${domain}\n\nUpsell page text:\n${pageText.slice(0, 2000)}\n\nClassify this upsell and return JSON.`, }, ], }); const block = message.content[0]; const rawText = block.type === "text" ? block.text : "{}"; const jsonMatch = rawText.match(/\{[\s\S]*\}/); if (!jsonMatch) { res.status(500).json({ error: "Failed to parse AI response" }); return; } res.json(JSON.parse(jsonMatch[0])); }); export default router;