const ALLOWED_IMAGE_TYPES = new Set([
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/webp',
    'image/svg+xml',
    'image/avif',
]);
const ALLOWED_DOCUMENT_TYPES = new Set([
    'application/pdf',
    'text/plain',
    'text/csv',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
/**
 * Validate a file's declared MIME type against an allowlist.
 *
 * Accepts either an array (typical caller form) or a Set (legacy form).
 * Returns a plain boolean to make call-sites read naturally:
 *
 *     if (!validateMimeType(file.type, ALLOWED)) return badRequest(...)
 */
export function validateMimeType(mimeType, allowedTypes) {
    const allowed = allowedTypes
        ? allowedTypes instanceof Set
            ? allowedTypes
            : new Set(allowedTypes)
        : new Set([...ALLOWED_IMAGE_TYPES, ...ALLOWED_DOCUMENT_TYPES]);
    return allowed.has(mimeType);
}
/**
 * Check a file's magic bytes against the declared mime type. Returns
 * `{ valid: true, detectedMimeType }` when the bytes match the declared type
 * (or when we have no signature to check), and `{ valid: false, error }`
 * otherwise.
 *
 * For container formats (WebP, AVIF) we additionally inspect the inner
 * sub-type — a generic RIFF header would otherwise let `.wav` files masquerade
 * as `.webp` and bypass image-only checks.
 *
 * For SVG (which is XML, not a binary signature) we look for `<svg` near the
 * start of the file. A leading XML declaration or BOM is allowed.
 */
export function checkMagicBytes(input, declaredMimeType) {
    const bytes = toUint8(input);
    const detected = detectMimeType(bytes);
    // No signature for the declared type — accept (caller is expected to have
    // already checked the allowlist).
    if (detected === null) {
        return { valid: true };
    }
    if (detected === declaredMimeType) {
        return { valid: true, detectedMimeType: detected };
    }
    // Some legitimate aliases:
    //   image/jpg ↔ image/jpeg
    //   image/x-png ↔ image/png
    if ((detected === 'image/jpeg' && declaredMimeType === 'image/jpg') ||
        (detected === 'image/png' && declaredMimeType === 'image/x-png')) {
        return { valid: true, detectedMimeType: detected };
    }
    return {
        valid: false,
        error: `Declared "${declaredMimeType}" but content looks like "${detected}"`,
        detectedMimeType: detected,
    };
}
function toUint8(input) {
    if (input instanceof Uint8Array)
        return input;
    return new Uint8Array(input);
}
/** Returns the detected mime type, or null when the bytes don't match a known signature. */
function detectMimeType(b) {
    if (b.length < 4)
        return null;
    if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
        return 'image/jpeg';
    if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47)
        return 'image/png';
    // GIF: full 6-byte signature ("GIF87a" or "GIF89a"), not just "GIF".
    if (b[0] === 0x47 &&
        b[1] === 0x49 &&
        b[2] === 0x46 &&
        b[3] === 0x38 &&
        (b[4] === 0x37 || b[4] === 0x39) &&
        b[5] === 0x61)
        return 'image/gif';
    // RIFF + 4-byte size + format identifier ("WEBP" / "WAVE" / "AVI ").
    if (b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46) {
        if (b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50)
            return 'image/webp';
        if (b[8] === 0x57 && b[9] === 0x41 && b[10] === 0x56 && b[11] === 0x45)
            return 'audio/wav';
    }
    // AVIF / HEIC: ISO BMFF "ftyp" box at offset 4 with brand at offset 8.
    if (b.length >= 12 && b[4] === 0x66 && b[5] === 0x74 && b[6] === 0x79 && b[7] === 0x70) {
        const brand = String.fromCharCode(b[8] ?? 0, b[9] ?? 0, b[10] ?? 0, b[11] ?? 0);
        if (brand === 'avif' || brand === 'avis')
            return 'image/avif';
        if (brand === 'mp42' || brand === 'isom' || brand === 'iso2')
            return 'video/mp4';
    }
    // PDF
    if (b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46)
        return 'application/pdf';
    // OGG
    if (b[0] === 0x4f && b[1] === 0x67 && b[2] === 0x67 && b[3] === 0x53)
        return 'audio/ogg';
    // MP3 — either "ID3" tag or a frame sync (0xFFE).
    if (b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
        return 'audio/mpeg';
    if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0)
        return 'audio/mpeg';
    // WebM / Matroska EBML header
    if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
        return 'video/webm';
    // SVG: scan the first 1024 bytes for a "<svg" tag. Accept optional XML
    // declaration / BOM / whitespace / comments.
    const head = new TextDecoder('utf-8', { fatal: false }).decode(b.slice(0, 1024)).trimStart();
    if (head.toLowerCase().includes('<svg') ||
        (head.startsWith('<?xml') && head.toLowerCase().includes('<svg'))) {
        return 'image/svg+xml';
    }
    return null;
}
export { ALLOWED_IMAGE_TYPES, ALLOWED_DOCUMENT_TYPES };
//# sourceMappingURL=upload.js.map