// SHA-1 hex (uppercase, 40 chars) — required as the `cs` (checksum) field // on SHIP requests and on the inbound photo `extra` JSON. Verified by inbound // capture of a real KakaoTalk client photo (2026-05). export async function sha1Hex(data: Uint8Array): Promise { const hashBuf = await crypto.subtle.digest('SHA-1', data) return Array.from(new Uint8Array(hashBuf)) .map((b) => b.toString(16).padStart(2, '0').toUpperCase()) .join('') } const MIME_BY_EXT: Record = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', mkv: 'video/x-matroska', m4v: 'video/x-m4v', m4a: 'audio/m4a', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', pdf: 'application/pdf', csv: 'text/csv', txt: 'text/plain', json: 'application/json', zip: 'application/zip', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', } export function guessMimeFromFilename(filename: string): string { const dot = filename.lastIndexOf('.') if (dot < 0 || dot === filename.length - 1) return 'application/octet-stream' const ext = filename.slice(dot + 1).toLowerCase() return MIME_BY_EXT[ext] ?? 'application/octet-stream' }