JavaScript 56.7%
TypeScript 42.6%
1import Anthropic from "@anthropic-ai/sdk";2import { prisma } from "./prisma";34const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });56export interface LoginContext {7 userId: string;8 ip: string;9 country: string;10 city: string;11 device: string;12 browser: string;13 os: string;14}1516export interface AiSecurityDecision {17 decision: "allow" | "refuse";18 reason: string;19 riskLevel: "low" | "medium" | "high";20}2122export async function analyzeLogin(ctx: LoginContext): Promise<AiSecurityDecision> {23 // Fail-open: if Anthropic is unavailable, allow the login24 try {25 // Fetch user's last 30 sessions (successful + refused) for context26 const history = await prisma.loginSession.findMany({27 where: { userId: ctx.userId },28 orderBy: { createdAt: "desc" },29 take: 30,30 select: {31 ip: true,32 country: true,33 city: true,34 device: true,35 browser: true,36 os: true,37 status: true,38 aiDecision: true,39 createdAt: true,40 },41 });4243 const now = new Date();44 const hourUTC = now.getUTCHours();4546 const historyText = history.length === 047 ? "Aucun historique de connexion (premier login)."48 : history.map((s, i) => {49 const date = new Date(s.createdAt);50 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}`;51 }).join("\n");5253 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.5455TENTATIVE ACTUELLE:56- IP: ${ctx.ip}57- Pays/Ville: ${ctx.country} / ${ctx.city}58- Appareil: ${ctx.device}59- Navigateur: ${ctx.browser}60- OS: ${ctx.os}61- Heure UTC: ${hourUTC}h6263HISTORIQUE DES 30 DERNIÈRES CONNEXIONS:64${historyText}6566CRITÈRES DE SUSPICION (réfléchis à chaque point):671. Nouveau pays jamais vu dans l'historique (surtout si compte établi avec >3 sessions)682. Plusieurs connexions refusées récentes depuis la même IP693. Changement simultané de pays + appareil + navigateur inconnus704. IP connue comme suspecte ou heure très inhabituelle (ex: 2h-5h UTC si toujours connecté le jour)715. Première connexion depuis un pays étranger sans historique local7273RÈGLE IMPORTANTE: Si c'est le PREMIER login (historique vide) ou si les 1-2 premières connexions, autoriser (profil pas encore établi).7475Réponds UNIQUEMENT en JSON valide:76{"decision": "allow" ou "refuse", "reason": "explication concise en français (max 150 chars)", "riskLevel": "low" ou "medium" ou "high"}`;7778 const response = await anthropic.messages.create({79 model: "claude-opus-4-6",80 max_tokens: 256,81 messages: [{ role: "user", content: prompt }],82 });8384 const text = response.content[0].type === "text" ? response.content[0].text : "";85 const jsonMatch = text.match(/\{[\s\S]*?\}/);86 if (!jsonMatch) throw new Error("No JSON in response");8788 const parsed = JSON.parse(jsonMatch[0]);8990 return {91 decision: parsed.decision === "refuse" ? "refuse" : "allow",92 reason: String(parsed.reason || "Connexion analysée").slice(0, 200),93 riskLevel: ["low", "medium", "high"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",94 };95 } catch (err) {96 // Fail-open: log the error but do NOT block the login if AI is unavailable97 console.error("[AI Security] analyzeLogin failed, defaulting to allow:", (err as Error).message);98 return { decision: "allow", reason: "Analyse IA indisponible — connexion autorisée par défaut", riskLevel: "low" };99 }100}101