import { newId } from "@fetcha/core"; import { abuseEvents, db } from "@fetcha/db"; import { getEmailService } from "@fetcha/email"; import type { ApiPrincipal } from "../auth"; import type { MonthlyUsage } from "../limits"; import { getKV } from "../redis"; import { formatUsd } from "./pricing"; function monthKey(): string { const d = new Date(); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`; } /** Send soft-limit alerts once per project/org per month (idempotent via KV flags). */ export async function checkSpendAlerts(p: ApiPrincipal, usage: MonthlyUsage): Promise { const kv = getKV(); const email = getEmailService(); const checks: Array<{ key: string; spent: number; limit: number | null; label: string }> = [ { key: `fch:alert:soft:proj:${p.projectId}:${monthKey()}`, spent: usage.projectSpendUsd, limit: p.projectSoftLimitUsd, label: p.projectName }, { key: `fch:alert:soft:org:${p.organizationId}:${monthKey()}`, spent: usage.spendUsd, limit: p.orgSoftLimitUsd, label: p.organizationName }, ]; for (const c of checks) { if (c.limit === null || c.limit <= 0 || c.spent < c.limit) continue; if (await kv.get(c.key).catch(() => "1")) continue; await kv.set(c.key, "1", 40 * 86_400).catch(() => {}); email.sendUsageAlert(p.ownerEmail, { projectName: c.label, spent: formatUsd(c.spent), limit: formatUsd(c.limit), kind: "soft" }).catch(() => {}); } } /** Notify once per month when a hard limit blocks requests. */ export async function notifyHardLimit(p: ApiPrincipal, usage: MonthlyUsage): Promise { const kv = getKV(); const key = `fch:alert:hard:${p.projectId}:${monthKey()}`; if (await kv.get(key).catch(() => "1")) return; await kv.set(key, "1", 40 * 86_400).catch(() => {}); const limit = p.projectHardLimitUsd ?? p.orgHardLimitUsd ?? 0; getEmailService() .sendUsageAlert(p.ownerEmail, { projectName: p.projectName, spent: formatUsd(Math.max(usage.projectSpendUsd, usage.spendUsd)), limit: formatUsd(limit), kind: "hard" }) .catch(() => {}); } /** Record an abuse signal (SSRF attempt, prohibited target…) for admin review. */ export async function recordAbuse(p: ApiPrincipal, requestId: string, kind: "ssrf_attempt" | "prohibited_target" | "rate_abuse", detail: string): Promise { await db .insert(abuseEvents) .values({ id: newId("abuse"), organizationId: p.organizationId, projectId: p.projectId, requestId, kind, severity: kind === "ssrf_attempt" ? "medium" : "low", detail: detail.slice(0, 500) }) .catch(() => {}); }