import Anthropic from "@anthropic-ai/sdk"; import { prisma } from "./prisma"; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); export interface LoginContext { userId: string; ip: string; country: string; city: string; device: string; browser: string; os: string; } export interface AiSecurityDecision { decision: "allow" | "refuse"; reason: string; riskLevel: "low" | "medium" | "high"; } export async function analyzeLogin(ctx: LoginContext): Promise { // Fail-open: if Anthropic is unavailable, allow the login try { // Fetch user's last 30 sessions (successful + refused) for context const history = await prisma.loginSession.findMany({ where: { userId: ctx.userId }, orderBy: { createdAt: "desc" }, take: 30, select: { ip: true, country: true, city: true, device: true, browser: true, os: true, status: true, aiDecision: true, createdAt: true, }, }); const now = new Date(); const hourUTC = now.getUTCHours(); const historyText = history.length === 0 ? "Aucun historique de connexion (premier login)." : history.map((s, i) => { const date = new Date(s.createdAt); return `${i + 1}. ${date.toISOString().split("T")[0]} ${date.toUTCString().split(" ")[4]}UTC | IP:${s.ip} | ${s.country}/${s.city} | ${s.device} ${s.browser} ${s.os} | statut:${s.status}`; }).join("\n"); const prompt = `Tu es un agent de sécurité IA pour un cloud privé personnel. Analyse cette tentative de connexion et décide si elle est légitime ou suspecte. TENTATIVE ACTUELLE: - IP: ${ctx.ip} - Pays/Ville: ${ctx.country} / ${ctx.city} - Appareil: ${ctx.device} - Navigateur: ${ctx.browser} - OS: ${ctx.os} - Heure UTC: ${hourUTC}h HISTORIQUE DES 30 DERNIÈRES CONNEXIONS: ${historyText} CRITÈRES DE SUSPICION (réfléchis à chaque point): 1. Nouveau pays jamais vu dans l'historique (surtout si compte établi avec >3 sessions) 2. Plusieurs connexions refusées récentes depuis la même IP 3. Changement simultané de pays + appareil + navigateur inconnus 4. IP connue comme suspecte ou heure très inhabituelle (ex: 2h-5h UTC si toujours connecté le jour) 5. Première connexion depuis un pays étranger sans historique local RÈGLE IMPORTANTE: Si c'est le PREMIER login (historique vide) ou si les 1-2 premières connexions, autoriser (profil pas encore établi). Réponds UNIQUEMENT en JSON valide: {"decision": "allow" ou "refuse", "reason": "explication concise en français (max 150 chars)", "riskLevel": "low" ou "medium" ou "high"}`; const response = await anthropic.messages.create({ model: "claude-opus-4-6", max_tokens: 256, messages: [{ role: "user", content: prompt }], }); const text = response.content[0].type === "text" ? response.content[0].text : ""; const jsonMatch = text.match(/\{[\s\S]*?\}/); if (!jsonMatch) throw new Error("No JSON in response"); const parsed = JSON.parse(jsonMatch[0]); return { decision: parsed.decision === "refuse" ? "refuse" : "allow", reason: String(parsed.reason || "Connexion analysée").slice(0, 200), riskLevel: ["low", "medium", "high"].includes(parsed.riskLevel) ? parsed.riskLevel : "low", }; } catch (err) { // Fail-open: log the error but do NOT block the login if AI is unavailable console.error("[AI Security] analyzeLogin failed, defaulting to allow:", (err as Error).message); return { decision: "allow", reason: "Analyse IA indisponible — connexion autorisée par défaut", riskLevel: "low" }; } }