/** * Web upload validator. Builds on the shared media classifier so the * web upload action and the bot's send path agree on which delivery * kind a file gets routed to (image / video / audio / document). * * Policy: never reject on format alone. If a JPEG-tagged HEIC photo * gets uploaded, route it through the document path so the recipient * still gets the file (just without an inline preview). Only reject * on size — image/video/audio inline limits are tighter than the * 100 MB document cap, so the validator picks the cap based on the * RESOLVED delivery kind, not the original mime category. */ export { classifyMediaKind, isUnsupportedImageMime, isSupportedVideoMime, isSupportedAudioMime, resolveDeliveryKind, sniffUnsupportedImage, sniffUnsupportedVideo, WA_LIMITS, WA_MAX_BYTES, type WaMediaKind, } from "@cmbot/shared"; import { resolveDeliveryKind as _resolve, WA_LIMITS as _WA_LIMITS, type WaMediaKind as _WaMediaKind, } from "@cmbot/shared"; const KB = 1024; const MB = 1024 * 1024; export type WaSizeCheck = | { ok: true; kind: _WaMediaKind; limitBytes: number } | { ok: false; kind: _WaMediaKind; limitBytes: number; error: string }; /** * Validate an uploaded file. Returns the resolved delivery kind and * the cap regardless of pass/fail. The optional `bytes` arg lets the * caller pass the buffered payload so iOS-Safari-style mime spoofs * (HEIC labelled image/jpeg, .mov labelled video/mp4) get caught and * routed to the document path. */ export function validateForWhatsApp( mimeType: string, sizeBytes: number, bytes?: Uint8Array, ): WaSizeCheck { const kind = _resolve(mimeType, bytes); const limitBytes = _WA_LIMITS[kind]; if (sizeBytes <= 0) { return { ok: false, kind, limitBytes, error: "Empty file" }; } if (sizeBytes > limitBytes) { return { ok: false, kind, limitBytes, error: `${labelFor(kind)} too large (${formatBytes(sizeBytes)} > ${formatBytes(limitBytes)} limit on WhatsApp)`, }; } return { ok: true, kind, limitBytes }; } function labelFor(kind: _WaMediaKind): string { switch (kind) { case "image": return "Image"; case "video": return "Video"; case "audio": return "Audio"; case "document": return "Document"; case "sticker": return "Sticker"; } } /** Render a byte count with a sensible WhatsApp-style unit. */ export function formatBytes(n: number): string { if (n < KB) return `${n} B`; if (n < MB) return `${(n / KB).toFixed(0)} KB`; return `${(n / MB).toFixed(1)} MB`; }