SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%
3.6 KB · 101 lines typescript
Raw Blame History
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