import { v2 as cloudinary } from "cloudinary"; type AvatarTarget = "user" | "team"; interface UploadAvatarImageArgs { buffer: Buffer; contentType: string; target: AvatarTarget; entityId: string; notificationUrl: string; } interface UploadSportIconArgs { buffer: Buffer; slug?: string | null; } function requireCloudinaryConfig() { const cloudName = process.env.CLOUDINARY_CLOUD_NAME; const apiKey = process.env.CLOUDINARY_API_KEY; const apiSecret = process.env.CLOUDINARY_API_SECRET; if (!cloudName || !apiKey || !apiSecret) { throw new Error("Cloudinary environment variables are not configured"); } cloudinary.config({ cloud_name: cloudName, api_key: apiKey, api_secret: apiSecret, secure: true, }); } function toDataUri(buffer: Buffer, contentType: string): string { return `data:${contentType};base64,${buffer.toString("base64")}`; } export async function uploadAvatarImage({ buffer, contentType, target, entityId, notificationUrl, }: UploadAvatarImageArgs) { requireCloudinaryConfig(); return await cloudinary.uploader.upload(toDataUri(buffer, contentType), { resource_type: "image", folder: "avatars", public_id: `${target}-${entityId}-${Date.now()}`, overwrite: true, moderation: "aws_rek", notification_url: notificationUrl, context: { avatar_target: target, entity_id: entityId, }, transformation: [{ width: 256, height: 256, crop: "fill", gravity: "face" }], }); } function normalizePublicIdPart(value: string | null | undefined): string { const normalized = value?.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, ""); return normalized || "sport"; } export async function uploadSportIcon({ buffer, slug }: UploadSportIconArgs) { requireCloudinaryConfig(); return await cloudinary.uploader.upload(toDataUri(buffer, "image/svg+xml"), { resource_type: "image", folder: "sports-icons", public_id: `${normalizePublicIdPart(slug)}-${Date.now()}`, allowed_formats: ["svg"], overwrite: false, }); } export function cloudinaryPublicIdFromUrl(url: string | null | undefined): string | null { if (!url) return null; if (!url.includes("res.cloudinary.com") || !url.includes("/image/upload/")) return null; const path = url.split("?")[0]; const match = path.match(/\/(?:v\d+\/)?((?:avatars|sports-icons)\/[^.]+)/); return match?.[1] ?? null; } export function buildWebhookUrl(request: Request): string { const baseUrl = process.env.APP_URL ?? process.env.BETTER_AUTH_URL ?? new URL(request.url).origin; return new URL("/api/webhooks/cloudinary", baseUrl).toString(); } export async function deleteCloudinaryImageByUrl(url: string | null | undefined): Promise { const publicId = cloudinaryPublicIdFromUrl(url); if (!publicId) return; requireCloudinaryConfig(); await cloudinary.uploader.destroy(publicId, { resource_type: "image", invalidate: true }); } export function verifyCloudinaryWebhookSignature({ body, signature, timestamp, }: { body: string; signature: string | null; timestamp: string | null; }): boolean { requireCloudinaryConfig(); if (!signature || !timestamp) return false; const parsedTimestamp = Number(timestamp); if (!Number.isFinite(parsedTimestamp)) return false; return cloudinary.utils.verifyNotificationSignature(body, parsedTimestamp, signature); }