import sharp from 'sharp'; import { env } from '../env'; import { HttpError } from './http'; export const AVATAR_SIZE = 512; export const AVATAR_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; /** * Uploaded avatars are stored as a path (`/avatars/?v=`) so they keep * working if the API moves; older rows may hold a full external URL. */ export const publicAvatarUrl = (stored: string | null): string | null => stored?.startsWith('/') ? env.API_PUBLIC_URL.replace(/\/$/, '') + stored : stored; /** * Square-crops, resizes and re-encodes to WebP. Re-encoding also drops EXIF * (phone photos carry GPS location) and anything that isn't image data. */ export async function processAvatar(input: Buffer): Promise { try { return await sharp(input, { limitInputPixels: 50_000_000, animated: false }) .rotate() // apply the EXIF orientation before it's stripped .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'attention' }) .webp({ quality: 82 }) .toBuffer(); } catch { throw new HttpError(400, "That file isn't an image we can read. Try a JPEG, PNG, WebP or HEIC."); } }