globalThis.process ??= {};
globalThis.process.env ??= {};
import { r as __exportAll } from "./rolldown-runtime_-UtK6b3T.mjs";
import { l as jsonError, u as jsonOk } from "./crypto_CY2zBtG3.mjs";
import { c as reserveAndInsertMedia, l as reserveAndInsertMediaD1, n as getFullDb, r as getMutationDb, s as listMedia } from "./db_BQ9NAt8x.mjs";
import { n as recordAudit, t as clientIp } from "./audit_D9EAE4GF.mjs";
import { a as requireEditor, c as assertCsrf } from "./auth_B0DWM0G6.mjs";
import { i as quotaBytes, n as maxBytes, r as publicBaseUrl, t as getStorage } from "./storage_CReXjlye.mjs";
import { env } from "cloudflare:workers";
//#region ../../packages/uploads/dist/size-limit.js
var SizeLimitExceededError = class extends Error {
	name = "SizeLimitExceededError";
	constructor(bytes, limit) {
		super(`Upload size ${bytes} exceeds limit ${limit}`);
	}
};
/** Throws when `bytes` is strictly greater than `limit`. */
function assertSizeLimit(bytes, limit) {
	if (bytes > limit) throw new SizeLimitExceededError(bytes, limit);
}
//#endregion
//#region ../../packages/uploads/dist/magic-bytes.js
var TEXT_DECODER = new TextDecoder("latin1");
function bytesToAscii(bytes, start, length) {
	return TEXT_DECODER.decode(bytes.subarray(start, start + length));
}
function startsWithAscii(bytes, start, literal) {
	if (bytes.length < start + literal.length) return false;
	for (let i = 0; i < literal.length; i++) if (bytes[start + i] !== literal.charCodeAt(i)) return false;
	return true;
}
function isDangerousTextHeader(header) {
	const sample = TEXT_DECODER.decode(header.subarray(0, Math.min(header.length, 256))).trimStart().toLowerCase();
	if (sample.startsWith("<svg")) return true;
	if (sample.startsWith("<html")) return true;
	if (sample.startsWith("<!doctype html")) return true;
	if (sample.startsWith("<!doctype svg")) return true;
	if (sample.startsWith("<?xml")) return true;
	if (sample.includes("<script")) return true;
	return false;
}
function isJpeg(header) {
	return header.length >= 3 && header[0] === 255 && header[1] === 216 && header[2] === 255;
}
function isPng(header) {
	return header.length >= 8 && header[0] === 137 && header[1] === 80 && header[2] === 78 && header[3] === 71 && header[4] === 13 && header[5] === 10 && header[6] === 26 && header[7] === 10;
}
function isGif(header) {
	if (header.length < 6) return false;
	const tag = bytesToAscii(header, 0, 6);
	return tag === "GIF87a" || tag === "GIF89a";
}
function isWebp(header) {
	return header.length >= 12 && bytesToAscii(header, 0, 4) === "RIFF" && bytesToAscii(header, 8, 4) === "WEBP";
}
/** Strict AVIF brand check: major or compatible brand must be `avif` or `avis`. Bare `ftyp` alone is rejected. */
function isStrictAvif(header) {
	if (header.length < 12 || !startsWithAscii(header, 4, "ftyp")) return false;
	const brands = [bytesToAscii(header, 8, 4)];
	for (let offset = 16; offset + 4 <= header.length; offset += 4) brands.push(bytesToAscii(header, offset, 4));
	return brands.some((brand) => brand === "avif" || brand === "avis");
}
/**
* Detect image MIME type from the first bytes of a file.
* Header-only, zero-dep. Rejects SVG/HTML/script/XML and loose bare-`ftyp` AVIF.
*/
function detectMimeFromMagicBytes(header) {
	if (header.length === 0) return null;
	if (isDangerousTextHeader(header)) return null;
	if (isJpeg(header)) return "image/jpeg";
	if (isPng(header)) return "image/png";
	if (isGif(header)) return "image/gif";
	if (isWebp(header)) return "image/webp";
	if (isStrictAvif(header)) return "image/avif";
	return null;
}
//#endregion
//#region ../../packages/uploads/dist/image-dimensions.js
function readUint32BE(bytes, offset) {
	return ((bytes[offset] ?? 0) << 24 | (bytes[offset + 1] ?? 0) << 16 | (bytes[offset + 2] ?? 0) << 8 | (bytes[offset + 3] ?? 0)) >>> 0;
}
function readUint16BE(bytes, offset) {
	return ((bytes[offset] ?? 0) << 8 | (bytes[offset + 1] ?? 0)) >>> 0;
}
function readUint24LE(bytes, offset) {
	return ((bytes[offset] ?? 0) | (bytes[offset + 1] ?? 0) << 8 | (bytes[offset + 2] ?? 0) << 16) >>> 0;
}
function parsePngDimensions(header) {
	if (header.length < 24) return null;
	const pngSig = [
		137,
		80,
		78,
		71,
		13,
		10,
		26,
		10
	];
	for (let i = 0; i < pngSig.length; i++) if (header[i] !== pngSig[i]) return null;
	if (String.fromCharCode(header[12] ?? 0, header[13] ?? 0, header[14] ?? 0, header[15] ?? 0) !== "IHDR") return null;
	return {
		width: readUint32BE(header, 16),
		height: readUint32BE(header, 20)
	};
}
function parseJpegDimensions(header) {
	let offset = 2;
	while (offset + 9 < header.length) {
		if (header[offset] !== 255) {
			offset++;
			continue;
		}
		const marker = header[offset + 1] ?? 0;
		if (marker === 216 || marker === 217) {
			offset += 2;
			continue;
		}
		if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) return {
			height: readUint16BE(header, offset + 5),
			width: readUint16BE(header, offset + 7)
		};
		const segmentLength = readUint16BE(header, offset + 2);
		if (segmentLength < 2) return null;
		offset += 2 + segmentLength;
	}
	return null;
}
function parseWebpDimensions(header) {
	if (header.length < 30) return null;
	const riff = String.fromCharCode(header[0] ?? 0, header[1] ?? 0, header[2] ?? 0, header[3] ?? 0);
	const webp = String.fromCharCode(header[8] ?? 0, header[9] ?? 0, header[10] ?? 0, header[11] ?? 0);
	if (riff !== "RIFF" || webp !== "WEBP") return null;
	const chunk = String.fromCharCode(header[12] ?? 0, header[13] ?? 0, header[14] ?? 0, header[15] ?? 0);
	if (chunk === "VP8X" && header.length >= 30) return {
		width: readUint24LE(header, 24) + 1,
		height: readUint24LE(header, 27) + 1
	};
	if (chunk === "VP8 " && header.length >= 30) return {
		width: readUint16BE(header, 26) & 16383,
		height: readUint16BE(header, 28) & 16383
	};
	if (chunk === "VP8L" && header.length >= 25) {
		const bits = readUint32BE(header, 21);
		return {
			width: (bits & 16383) + 1,
			height: (bits >> 14 & 16383) + 1
		};
	}
	return null;
}
/** Parse PNG/WebP/JPEG dimensions from header bytes only. */
function parseImageDimensions(header) {
	const png = parsePngDimensions(header);
	if (png) return png;
	const webp = parseWebpDimensions(header);
	if (webp) return webp;
	return parseJpegDimensions(header);
}
//#endregion
//#region src/pages/api/admin/media.ts
var media_exports = /* @__PURE__ */ __exportAll({
	GET: () => GET,
	POST: () => POST,
	prerender: () => false
});
var EXT = {
	"image/jpeg": "jpg",
	"image/png": "png",
	"image/webp": "webp",
	"image/avif": "avif",
	"image/gif": "gif"
};
function ulid() {
	const bytes = /* @__PURE__ */ new Uint8Array(10);
	crypto.getRandomValues(bytes);
	const rand = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
	return `${Date.now().toString(16)}${rand}`;
}
var MULTIPART_OVERHEAD = 8192;
var POST = async ({ request, cookies }) => {
	try {
		const { actor, authMode } = await requireEditor(request, env, cookies);
		assertCsrf(request, {
			authMode,
			siteOrigin: env.SITE_ORIGIN
		});
		const contentLengthHeader = request.headers.get("content-length");
		const contentLength = contentLengthHeader === null ? NaN : Number(contentLengthHeader);
		if (!Number.isFinite(contentLength) || contentLength > maxBytes(env) + MULTIPART_OVERHEAD) return jsonError(413, "file_too_large", "request body exceeds the upload limit or has no declared length");
		const file = (await request.formData()).get("file");
		if (!(file instanceof File)) return jsonError(400, "validation_error", "file is required");
		assertSizeLimit(file.size, maxBytes(env));
		const mutationDb = getMutationDb(env);
		const queryDb = getFullDb(env).db;
		const bytes = new Uint8Array(await file.arrayBuffer());
		const detected = detectMimeFromMagicBytes(bytes);
		if (!detected) return jsonError(415, "unsupported_type", "file is not a supported image");
		const dims = parseImageDimensions(bytes);
		const now = /* @__PURE__ */ new Date();
		const key = `media/${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}/${ulid()}.${EXT[detected]}`;
		const storage = getStorage(env);
		await storage.put(key, bytes, { contentType: detected });
		const url = `${publicBaseUrl(env)}/${key}`;
		let row;
		try {
			row = mutationDb.dialect === "sqlite" ? await reserveAndInsertMediaD1(mutationDb.db, {
				key,
				url,
				contentType: detected,
				size: bytes.length,
				width: dims?.width,
				height: dims?.height,
				uploaderId: actor.id
			}, quotaBytes(env)) : await reserveAndInsertMedia(mutationDb.db, {
				key,
				url,
				contentType: detected,
				size: bytes.length,
				width: dims?.width,
				height: dims?.height,
				uploaderId: actor.id
			}, quotaBytes(env));
		} catch (insertError) {
			if (insertError.name === "QuotaExceededError") {
				try {
					await storage.delete(key);
				} catch {}
				return jsonError(413, "quota_exceeded", "media quota exceeded");
			}
			try {
				await storage.delete(key);
			} catch {}
			throw insertError;
		}
		await recordAudit(queryDb, {
			actorId: actor.id,
			ip: clientIp(request)
		}, {
			action: "media.upload",
			entityType: "media",
			entityId: key
		});
		return jsonOk({ item: {
			key: row.key,
			url: row.url,
			size: row.size,
			contentType: row.contentType,
			width: row.width ?? void 0,
			height: row.height ?? void 0,
			uploadedAt: row.createdAt.getTime()
		} }, 201);
	} catch (error) {
		const name = error?.name;
		if (name === "SizeLimitExceededError") return jsonError(413, "file_too_large", error.message);
		if (name === "QuotaExceededError") return jsonError(413, "quota_exceeded", error.message);
		if (name === "InvalidSessionError") return jsonError(401, "unauthorized", error.message);
		if (name === "RoleDeniedError") return jsonError(403, "forbidden", error.message);
		if (name === "CsrfError") return jsonError(403, "forbidden", error.message);
		if (name === "RateLimitedError") return jsonError(429, "rate_limited", error.message);
		return jsonError(500, "internal_error", "internal error");
	}
};
var GET = async ({ request, locals, url, cookies }) => {
	try {
		await requireEditor(request, env, cookies);
		const { db } = getFullDb(env);
		const cursor = url.searchParams.get("cursor") ?? void 0;
		const limitParamRaw = url.searchParams.get("limit");
		const limit = limitParamRaw ? Number(limitParamRaw) : void 0;
		const { rows, cursor: next } = await listMedia(db, {
			cursor,
			limit: Number.isFinite(limit) ? limit : void 0
		});
		return jsonOk({
			items: rows.map((r) => ({
				key: r.key,
				url: r.url,
				size: r.size,
				contentType: r.contentType,
				width: r.width ?? void 0,
				height: r.height ?? void 0,
				uploadedAt: r.createdAt.getTime()
			})),
			cursor: next
		});
	} catch (error) {
		const name = error?.name;
		if (name === "InvalidSessionError") return jsonError(401, "unauthorized", error.message);
		if (name === "RoleDeniedError") return jsonError(403, "forbidden", error.message);
		if (name === "RateLimitedError") return jsonError(429, "rate_limited", error.message);
		return jsonError(500, "internal_error", "internal error");
	}
};
//#endregion
//#region \0virtual:astro:page:src/pages/api/admin/media@_@ts
var page = () => media_exports;
//#endregion
export { page };
