import { _ as resolveStateDir } from "./paths-B2cMK-wd.js";
import { p as resolveUserPath } from "./utils-DvkbxKCZ.js";
import { n as authorizeOperatorScopesForMethod } from "./method-scopes-C931ndUQ.js";
import { o as getLatestSubagentRunByChildSessionKey } from "./subagent-registry-qfvEQLZY.js";
import { a as getImageMetadata, l as resizeToJpeg, o as hasAlphaChannel, u as resizeToPng } from "./image-ops-ZGfs-8RW.js";
import { a as safeFileURLToPath } from "./local-file-access-pjYL9RYy.js";
import { t as isPassThroughRemoteMediaSource } from "./media-source-url-Q_d0u7eY.js";
import { i as readSessionMessages } from "./session-utils.fs-bNM54VNU.js";
import { a as loadSessionEntry } from "./session-utils-B30-zGfl.js";
import { l as saveMediaBuffer, t as MEDIA_MAX_BYTES, u as saveMediaSource } from "./store-CkuXJAhD.js";
import { n as assertLocalMediaAllowed } from "./local-media-access-gxO9AjdC.js";
import { a as sendMethodNotAllowed, i as sendJson } from "./http-common-Cxvs6DvV.js";
import { l as resolveOpenAiCompatibleHttpOperatorScopes, t as authorizeGatewayHttpRequestOrReply } from "./http-auth-utils-B3L3BM1B.js";
import "./http-utils-CUOnlbrY.js";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/gateway/managed-image-attachments.ts
const OUTGOING_IMAGE_ROUTE_PREFIX = "/api/chat/media/outgoing";
const DEFAULT_TRANSIENT_OUTGOING_IMAGE_TTL_MS = 900 * 1e3;
const MANAGED_OUTGOING_ATTACHMENT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DATA_URL_RE = /^data:/i;
const WINDOWS_DRIVE_RE = /^[A-Za-z]:[\\/]/;
const DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS = {
	maxBytes: 12 * 1024 * 1024,
	maxWidth: 4096,
	maxHeight: 4096,
	maxPixels: 2e7
};
const sessionManagedOutgoingAttachmentIndexCache = /* @__PURE__ */ new Map();
const MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES = 500;
function resolveManagedImageAttachmentLimits(config) {
	return {
		maxBytes: config?.maxBytes ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxBytes,
		maxWidth: config?.maxWidth ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxWidth,
		maxHeight: config?.maxHeight ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxHeight,
		maxPixels: config?.maxPixels ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxPixels
	};
}
function formatLimitMiB(bytes) {
	if (bytes < 1024 * 1024) return `${bytes} bytes`;
	return Number.isInteger(bytes / (1024 * 1024)) ? `${bytes / (1024 * 1024)} MiB` : `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
}
function createManagedImageAttachmentError(message) {
	const error = new Error(message);
	error.name = "ManagedImageAttachmentError";
	return error;
}
function isManagedImageAttachmentSafeError(error) {
	if (!(error instanceof Error)) return false;
	if (error.name === "ManagedImageAttachmentError") return true;
	return error.message.startsWith("Managed image attachment ") || error.message.startsWith("Invalid image data URL");
}
function getSanitizedManagedImageAttachmentError(error, alt) {
	if (isManagedImageAttachmentSafeError(error)) return error;
	return createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} could not be prepared`);
}
function validateManagedImageBuffer(buffer, alt, limits) {
	if (buffer.byteLength > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
}
function estimateBase64DecodedByteLength(base64) {
	const normalized = base64.replace(/\s+/g, "");
	const paddingMatch = /=+$/u.exec(normalized);
	const padding = Math.min(paddingMatch?.[0].length ?? 0, 2);
	return Math.floor(normalized.length * 3 / 4) - padding;
}
function getManagedImageMetadataLimitError(metadata, alt, limits) {
	if (!metadata) return `Managed image attachment ${JSON.stringify(alt)} is missing readable dimensions`;
	if (metadata.width > limits.maxWidth) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxWidth}px width limit`;
	if (metadata.height > limits.maxHeight) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxHeight}px height limit`;
	if (metadata.width * metadata.height > limits.maxPixels) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxPixels.toLocaleString("en-US")} pixel limit`;
	return null;
}
function computeManagedImageResizeTarget(metadata, limits) {
	const scale = Math.min(1, limits.maxWidth / metadata.width, limits.maxHeight / metadata.height, Math.sqrt(limits.maxPixels / (metadata.width * metadata.height)));
	if (!Number.isFinite(scale) || scale >= 1) return null;
	let width = Math.max(1, Math.floor(metadata.width * scale));
	let height = Math.max(1, Math.floor(metadata.height * scale));
	while (width > limits.maxWidth || height > limits.maxHeight || width * height > limits.maxPixels) if (width >= height && width > 1) width -= 1;
	else if (height > 1) height -= 1;
	else break;
	return {
		width,
		height
	};
}
async function resizeManagedImageBufferToLimits(params) {
	const target = computeManagedImageResizeTarget(params.metadata, params.limits);
	if (!target) return {
		buffer: params.buffer,
		contentType: params.contentType,
		width: params.metadata.width,
		height: params.metadata.height
	};
	const preserveAlpha = await hasAlphaChannel(params.buffer).catch(() => false);
	return {
		buffer: preserveAlpha ? await resizeToPng({
			buffer: params.buffer,
			maxSide: Math.max(target.width, target.height),
			compressionLevel: 9,
			withoutEnlargement: true
		}) : await resizeToJpeg({
			buffer: params.buffer,
			maxSide: Math.max(target.width, target.height),
			quality: 92,
			withoutEnlargement: true
		}),
		contentType: preserveAlpha ? "image/png" : "image/jpeg",
		width: target.width,
		height: target.height
	};
}
function resolveOutgoingRecordsDir(stateDir = resolveStateDir()) {
	return path.join(stateDir, "media", "outgoing", "records");
}
function resolveOutgoingOriginalsDir(stateDir = resolveStateDir()) {
	return path.join(stateDir, "media", "outgoing", "originals");
}
function resolveOutgoingRecordPath(attachmentId, stateDir = resolveStateDir()) {
	return path.join(resolveOutgoingRecordsDir(stateDir), `${attachmentId}.json`);
}
function buildOutgoingVariantUrl(sessionKey, attachmentId, variant) {
	return `${OUTGOING_IMAGE_ROUTE_PREFIX}/${encodeURIComponent(sessionKey)}/${attachmentId}/${variant}`;
}
function resolveRequesterSessionKey(req) {
	const raw = req.headers["x-openclaw-requester-session-key"];
	if (Array.isArray(raw)) return raw[0]?.trim() || null;
	return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null;
}
async function requesterOwnsManagedImageSession(params) {
	if (params.requesterSessionKey === params.targetSessionKey) return true;
	const subagentRun = getLatestSubagentRunByChildSessionKey(params.targetSessionKey);
	if (!subagentRun) return false;
	return subagentRun.requesterSessionKey === params.requesterSessionKey || subagentRun.controllerSessionKey === params.requesterSessionKey;
}
function deriveAltText(source, index) {
	const fallback = `Generated image ${index + 1}`;
	try {
		if (/^https?:\/\//i.test(source)) {
			const parsed = new URL(source);
			return path.basename(parsed.pathname || "").trim() || fallback;
		}
	} catch {}
	return path.basename(source).trim() || fallback;
}
function resolveLocalMediaPath(source) {
	const trimmed = source.trim();
	if (!trimmed || isPassThroughRemoteMediaSource(trimmed) || DATA_URL_RE.test(trimmed)) return;
	if (trimmed.startsWith("file://")) try {
		return safeFileURLToPath(trimmed);
	} catch {
		return;
	}
	if (trimmed.startsWith("~")) return resolveUserPath(trimmed);
	if (path.isAbsolute(trimmed) || WINDOWS_DRIVE_RE.test(trimmed)) return path.resolve(trimmed);
}
function parseImageDataUrl(source, alt, limits) {
	const trimmed = source.trim();
	if (!trimmed.startsWith("data:")) return { kind: "not-data-url" };
	const match = /^data:([^;,]+)(?:;[^,]*)*;base64,([A-Za-z0-9+/=\s]+)$/i.exec(trimmed);
	if (!match) throw new Error("Invalid image data URL");
	const contentType = match[1]?.trim().toLowerCase() ?? "";
	if (!contentType.startsWith("image/")) return { kind: "non-image-data-url" };
	if (estimateBase64DecodedByteLength(match[2]) > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
	return {
		kind: "image-data-url",
		buffer: Buffer.from(match[2].replace(/\s+/g, ""), "base64"),
		contentType
	};
}
async function getVariantStats(filePath) {
	const [stats, metadataBuffer] = await Promise.all([fs.stat(filePath), fs.readFile(filePath)]);
	const metadata = await getImageMetadata(metadataBuffer).catch(() => null) ?? {
		width: null,
		height: null
	};
	return {
		width: metadata.width ?? null,
		height: metadata.height ?? null,
		sizeBytes: Number.isFinite(stats.size) ? stats.size : null
	};
}
async function writeManagedImageRecord(record, stateDir = resolveStateDir()) {
	const recordPath = resolveOutgoingRecordPath(record.attachmentId, stateDir);
	await fs.mkdir(path.dirname(recordPath), { recursive: true });
	await fs.writeFile(recordPath, JSON.stringify(record, null, 2), "utf-8");
}
async function deleteManagedImageRecordArtifacts(record, stateDir = resolveStateDir()) {
	const files = /* @__PURE__ */ new Set();
	if (record.original?.path) files.add(record.original.path);
	let deletedFileCount = 0;
	for (const filePath of files) try {
		await fs.rm(filePath, { force: true });
		deletedFileCount += 1;
	} catch {}
	try {
		await fs.rm(resolveOutgoingRecordPath(record.attachmentId, stateDir), { force: true });
	} catch {}
	return deletedFileCount;
}
async function deleteOrphanManagedImageFiles(params) {
	let deletedFileCount = 0;
	for (const dir of [resolveOutgoingOriginalsDir(params.stateDir)]) {
		let names = [];
		try {
			names = await fs.readdir(dir);
		} catch {
			continue;
		}
		for (const name of names) {
			const filePath = path.join(dir, name);
			if (params.referencedPaths.has(filePath)) continue;
			try {
				if (!(await fs.stat(filePath)).isFile()) continue;
			} catch {
				continue;
			}
			try {
				await fs.rm(filePath, { force: true });
				deletedFileCount += 1;
			} catch {}
		}
	}
	return deletedFileCount;
}
async function cleanupManagedOutgoingImageRecords(params) {
	const stateDir = params?.stateDir ?? resolveStateDir();
	const nowMs = params?.nowMs ?? Date.now();
	const transientMaxAgeMs = params?.transientMaxAgeMs ?? DEFAULT_TRANSIENT_OUTGOING_IMAGE_TTL_MS;
	const sessionKeyFilter = params?.sessionKey ?? null;
	const forceDeleteSessionRecords = params?.forceDeleteSessionRecords === true;
	const recordsDir = resolveOutgoingRecordsDir(stateDir);
	let names = [];
	try {
		names = await fs.readdir(recordsDir);
	} catch {
		names = [];
	}
	let deletedRecordCount = 0;
	let deletedFileCount = 0;
	let retainedCount = 0;
	const retainedReferencedPaths = /* @__PURE__ */ new Set();
	const transcriptAttachmentIndexCache = /* @__PURE__ */ new Map();
	for (const name of names) {
		if (!name.endsWith(".json")) continue;
		const recordPath = path.join(recordsDir, name);
		let record;
		try {
			record = JSON.parse(await fs.readFile(recordPath, "utf-8"));
		} catch {
			try {
				await fs.rm(recordPath, { force: true });
			} catch {}
			deletedRecordCount += 1;
			continue;
		}
		if (sessionKeyFilter && record.sessionKey !== sessionKeyFilter) {
			if (record.original?.path) retainedReferencedPaths.add(record.original.path);
			retainedCount += 1;
			continue;
		}
		let shouldDelete = false;
		if (forceDeleteSessionRecords && (!sessionKeyFilter || record.sessionKey === sessionKeyFilter)) shouldDelete = true;
		else if (record.messageId) shouldDelete = !await recordMatchesTranscriptMessage(record, transcriptAttachmentIndexCache);
		else {
			const createdAtMs = Date.parse(record.createdAt);
			shouldDelete = Number.isFinite(createdAtMs) && nowMs - createdAtMs >= transientMaxAgeMs;
		}
		if (shouldDelete) {
			deletedRecordCount += 1;
			deletedFileCount += await deleteManagedImageRecordArtifacts(record, stateDir);
		} else {
			if (record.original?.path) retainedReferencedPaths.add(record.original.path);
			retainedCount += 1;
		}
	}
	deletedFileCount += await deleteOrphanManagedImageFiles({
		stateDir,
		referencedPaths: retainedReferencedPaths
	});
	return {
		deletedRecordCount,
		deletedFileCount,
		retainedCount
	};
}
async function readManagedImageRecord(attachmentId, stateDir = resolveStateDir()) {
	try {
		const raw = await fs.readFile(resolveOutgoingRecordPath(attachmentId, stateDir), "utf-8");
		return JSON.parse(raw);
	} catch {
		return null;
	}
}
function buildManagedImageBlock(record) {
	const fullUrl = buildOutgoingVariantUrl(record.sessionKey, record.attachmentId, "full");
	return {
		type: "image",
		url: fullUrl,
		openUrl: fullUrl,
		alt: record.alt,
		mimeType: record.original.contentType,
		width: record.original.width,
		height: record.original.height
	};
}
function buildManagedOutgoingAttachmentRefKey(messageId, attachmentId) {
	return `${messageId}::${attachmentId}`;
}
function buildManagedImageResizeWarningBlock(params) {
	return {
		type: "text",
		text: `[Image warning] ${params.alt} exceeded gateway dimension/pixel limits and was resized from ${params.originalWidth}×${params.originalHeight} to ${params.resizedWidth}×${params.resizedHeight}.`
	};
}
function toRecordFilename(filePath) {
	return path.basename(filePath).trim() || null;
}
function asArray(value) {
	return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim()) : [];
}
function parseManagedOutgoingRoute(value) {
	try {
		const match = new URL(value, "http://localhost").pathname.match(/^\/api\/chat\/media\/outgoing\/([^/]+)\/([^/]+)\/full$/);
		if (!match) return null;
		if (!MANAGED_OUTGOING_ATTACHMENT_ID_RE.test(match[2])) return null;
		return {
			sessionKey: decodeURIComponent(match[1]),
			attachmentId: match[2]
		};
	} catch {
		return null;
	}
}
function collectManagedOutgoingAttachmentRefs(blocks, expectedSessionKey) {
	const refs = /* @__PURE__ */ new Map();
	for (const block of blocks ?? []) {
		if (block?.type !== "image") continue;
		for (const candidate of [block.url, block.openUrl]) {
			if (typeof candidate !== "string") continue;
			const parsed = parseManagedOutgoingRoute(candidate);
			if (!parsed) continue;
			if (expectedSessionKey && parsed.sessionKey !== expectedSessionKey) continue;
			refs.set(parsed.attachmentId, {
				attachmentId: parsed.attachmentId,
				sessionKey: parsed.sessionKey
			});
		}
	}
	return [...refs.values()];
}
function getCachedSessionManagedOutgoingAttachmentIndex(sessionKey, stat) {
	const cached = sessionManagedOutgoingAttachmentIndexCache.get(sessionKey);
	if (!cached) return null;
	if (cached.transcriptPath !== stat.transcriptPath || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
		sessionManagedOutgoingAttachmentIndexCache.delete(sessionKey);
		return null;
	}
	sessionManagedOutgoingAttachmentIndexCache.delete(sessionKey);
	sessionManagedOutgoingAttachmentIndexCache.set(sessionKey, cached);
	return cached.index;
}
function setCachedSessionManagedOutgoingAttachmentIndex(sessionKey, stat, index) {
	sessionManagedOutgoingAttachmentIndexCache.set(sessionKey, {
		transcriptPath: stat.transcriptPath,
		mtimeMs: stat.mtimeMs,
		size: stat.size,
		index
	});
	while (sessionManagedOutgoingAttachmentIndexCache.size > MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES) {
		const oldestKey = sessionManagedOutgoingAttachmentIndexCache.keys().next().value;
		if (!oldestKey) break;
		sessionManagedOutgoingAttachmentIndexCache.delete(oldestKey);
	}
}
async function getSessionManagedOutgoingAttachmentIndex(sessionKey, cache) {
	if (cache?.has(sessionKey)) return cache.get(sessionKey) ?? null;
	const { storePath, entry } = loadSessionEntry(sessionKey);
	const sessionId = entry?.sessionId;
	if (!sessionId) {
		cache?.set(sessionKey, null);
		return null;
	}
	let transcriptStat = null;
	const transcriptPath = typeof entry?.sessionFile === "string" ? entry.sessionFile.trim() : "";
	if (transcriptPath) try {
		const stat = await fs.stat(transcriptPath);
		transcriptStat = {
			transcriptPath,
			mtimeMs: stat.mtimeMs,
			size: stat.size
		};
		const cachedIndex = getCachedSessionManagedOutgoingAttachmentIndex(sessionKey, transcriptStat);
		if (cachedIndex) {
			cache?.set(sessionKey, cachedIndex);
			return cachedIndex;
		}
	} catch {
		sessionManagedOutgoingAttachmentIndexCache.delete(sessionKey);
	}
	const messages = readSessionMessages(sessionId, storePath, entry.sessionFile);
	const index = /* @__PURE__ */ new Set();
	for (const message of messages) {
		const messageId = (message?.__openclaw)?.id;
		if (typeof messageId !== "string" || !messageId) continue;
		for (const ref of collectManagedOutgoingAttachmentRefs(Array.isArray(message?.content) ? message.content : [], sessionKey)) index.add(buildManagedOutgoingAttachmentRefKey(messageId, ref.attachmentId));
	}
	if (transcriptStat) setCachedSessionManagedOutgoingAttachmentIndex(sessionKey, transcriptStat, index);
	cache?.set(sessionKey, index);
	return index;
}
async function recordMatchesTranscriptMessage(record, cache) {
	if (!record.messageId) return false;
	return (await getSessionManagedOutgoingAttachmentIndex(record.sessionKey, cache))?.has(buildManagedOutgoingAttachmentRefKey(record.messageId, record.attachmentId)) ?? false;
}
async function attachManagedOutgoingImagesToMessage(params) {
	const messageId = params.messageId.trim();
	if (!messageId) return;
	const refs = collectManagedOutgoingAttachmentRefs(params.blocks);
	if (refs.length === 0) return;
	await Promise.all(refs.map(async ({ attachmentId, sessionKey }) => {
		const record = await readManagedImageRecord(attachmentId, params.stateDir);
		if (!record || record.sessionKey !== sessionKey) return;
		if (record.messageId === messageId && record.retentionClass === "history") return;
		await writeManagedImageRecord({
			...record,
			messageId,
			retentionClass: "history",
			updatedAt: (/* @__PURE__ */ new Date()).toISOString()
		}, params.stateDir);
	}));
}
async function createManagedOutgoingImageBlocks(params) {
	const sessionKey = params.sessionKey.trim();
	if (!sessionKey) return [];
	const mediaUrls = asArray(params.mediaUrls);
	if (mediaUrls.length === 0) return [];
	const stateDir = params.stateDir ?? resolveStateDir();
	const limits = resolveManagedImageAttachmentLimits(params.limits);
	const blocks = [];
	for (const [index, mediaUrl] of mediaUrls.entries()) {
		const fallbackAlt = `Generated image ${index + 1}`;
		const parsedDataUrl = parseImageDataUrl(mediaUrl, fallbackAlt, limits);
		const alt = parsedDataUrl.kind === "image-data-url" ? fallbackAlt : deriveAltText(mediaUrl, index);
		if (parsedDataUrl.kind === "non-image-data-url") continue;
		let savedOriginalPath = null;
		try {
			let resizeWarning = null;
			if (parsedDataUrl.kind === "image-data-url") validateManagedImageBuffer(parsedDataUrl.buffer, alt, limits);
			let savedOriginal = parsedDataUrl.kind === "image-data-url" ? await saveMediaBuffer(parsedDataUrl.buffer, parsedDataUrl.contentType, "outgoing/originals", limits.maxBytes, `generated-image-${index + 1}`) : await (async () => {
				const localMediaPath = resolveLocalMediaPath(mediaUrl);
				if (localMediaPath) await assertLocalMediaAllowed(localMediaPath, params.localRoots);
				return await saveMediaSource(mediaUrl, void 0, "outgoing/originals", Math.max(limits.maxBytes, MEDIA_MAX_BYTES));
			})();
			savedOriginalPath = savedOriginal.path;
			let savedOriginalContentType = savedOriginal.contentType;
			if (!savedOriginalContentType?.startsWith("image/")) {
				await fs.rm(savedOriginal.path, { force: true }).catch(() => {});
				savedOriginalPath = null;
				continue;
			}
			if (savedOriginal.size > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
			let originalBuffer = parsedDataUrl.kind === "image-data-url" ? parsedDataUrl.buffer : await fs.readFile(savedOriginal.path);
			validateManagedImageBuffer(originalBuffer, alt, limits);
			let originalStats = await getVariantStats(savedOriginal.path);
			if (originalStats.sizeBytes != null && originalStats.sizeBytes > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
			const originalMetadata = originalStats.width != null && originalStats.height != null ? {
				width: originalStats.width,
				height: originalStats.height
			} : await getImageMetadata(originalBuffer);
			let effectiveMetadata = originalMetadata;
			let metadataLimitError = getManagedImageMetadataLimitError(effectiveMetadata, alt, limits);
			for (let resizeAttempt = 0; metadataLimitError; resizeAttempt += 1) {
				if (!effectiveMetadata) throw createManagedImageAttachmentError(metadataLimitError);
				if (resizeAttempt >= 3) throw createManagedImageAttachmentError(metadataLimitError);
				const resized = await resizeManagedImageBufferToLimits({
					buffer: originalBuffer,
					contentType: savedOriginalContentType,
					metadata: effectiveMetadata,
					limits
				});
				validateManagedImageBuffer(resized.buffer, alt, limits);
				const replacement = await saveMediaBuffer(resized.buffer, resized.contentType, "outgoing/originals", limits.maxBytes, toRecordFilename(savedOriginal.path) ?? `generated-image-${index + 1}`);
				await fs.rm(savedOriginal.path, { force: true }).catch(() => {});
				savedOriginal = replacement;
				savedOriginalContentType = replacement.contentType ?? resized.contentType;
				savedOriginalPath = savedOriginal.path;
				originalBuffer = resized.buffer;
				originalStats = await getVariantStats(savedOriginal.path);
				effectiveMetadata = originalStats.width != null && originalStats.height != null ? {
					width: originalStats.width,
					height: originalStats.height
				} : await getImageMetadata(originalBuffer);
				metadataLimitError = getManagedImageMetadataLimitError(effectiveMetadata, alt, limits);
				if (!metadataLimitError) resizeWarning = buildManagedImageResizeWarningBlock({
					alt,
					originalWidth: originalMetadata?.width ?? effectiveMetadata?.width ?? resized.width,
					originalHeight: originalMetadata?.height ?? effectiveMetadata?.height ?? resized.height,
					resizedWidth: effectiveMetadata?.width ?? resized.width,
					resizedHeight: effectiveMetadata?.height ?? resized.height
				});
			}
			const record = {
				attachmentId: randomUUID(),
				sessionKey,
				messageId: params.messageId ?? null,
				createdAt: (/* @__PURE__ */ new Date()).toISOString(),
				retentionClass: params.messageId ? "history" : "transient",
				alt,
				original: {
					path: savedOriginal.path,
					contentType: savedOriginalContentType,
					width: originalStats.width,
					height: originalStats.height,
					sizeBytes: originalStats.sizeBytes,
					filename: toRecordFilename(savedOriginal.path)
				}
			};
			await writeManagedImageRecord(record, stateDir);
			blocks.push(buildManagedImageBlock(record));
			if (resizeWarning) blocks.push(resizeWarning);
		} catch (error) {
			if (savedOriginalPath) await fs.rm(savedOriginalPath, { force: true }).catch(() => {});
			const sanitizedError = getSanitizedManagedImageAttachmentError(error, alt);
			if (params.continueOnPrepareError) {
				params.onPrepareError?.(sanitizedError);
				continue;
			}
			throw sanitizedError;
		}
	}
	return blocks;
}
function sendStatus(res, statusCode, body) {
	if (res.writableEnded) return;
	res.statusCode = statusCode;
	res.setHeader("content-type", "text/plain; charset=utf-8");
	res.end(body);
}
function safeAttachmentFilename(value) {
	const fallback = "generated-image";
	return (value ?? fallback).replace(/[\r\n"\\]/g, "_").trim() || fallback;
}
async function handleManagedOutgoingImageHttpRequest(req, res, opts) {
	const match = new URL(req.url ?? "/", "http://localhost").pathname.match(/^\/api\/chat\/media\/outgoing\/([^/]+)\/([^/]+)\/full$/);
	if (!match) return false;
	if (req.method !== "GET") {
		sendMethodNotAllowed(res, "GET");
		return true;
	}
	const requestAuth = await authorizeGatewayHttpRequestOrReply({
		req,
		res,
		auth: opts.auth,
		trustedProxies: opts.trustedProxies,
		allowRealIpFallback: opts.allowRealIpFallback,
		rateLimiter: opts.rateLimiter
	});
	if (!requestAuth) return true;
	const privilegedAccess = requestAuth.trustDeclaredOperatorScopes || requestAuth.authMethod === "device-token";
	const scopeAuth = authorizeOperatorScopesForMethod("chat.history", resolveOpenAiCompatibleHttpOperatorScopes(req, requestAuth));
	if (!scopeAuth.allowed) {
		sendJson(res, 403, {
			ok: false,
			error: {
				type: "forbidden",
				message: `missing scope: ${scopeAuth.missingScope}`
			}
		});
		return true;
	}
	const encodedSessionKey = match[1];
	const attachmentId = match[2];
	if (!encodedSessionKey || !attachmentId) return false;
	if (!MANAGED_OUTGOING_ATTACHMENT_ID_RE.test(attachmentId)) {
		sendStatus(res, 404, "not found");
		return true;
	}
	let sessionKey;
	try {
		sessionKey = decodeURIComponent(encodedSessionKey);
	} catch {
		sendStatus(res, 404, "not found");
		return true;
	}
	const record = await readManagedImageRecord(attachmentId, opts.stateDir);
	if (!record || record.sessionKey !== sessionKey) {
		sendStatus(res, 404, "not found");
		return true;
	}
	if (!privilegedAccess) {
		const requesterSessionKey = resolveRequesterSessionKey(req);
		if (!requesterSessionKey) {
			sendJson(res, 403, {
				ok: false,
				error: {
					type: "forbidden",
					message: "requester session ownership required"
				}
			});
			return true;
		}
		if (!await requesterOwnsManagedImageSession({
			requesterSessionKey,
			targetSessionKey: record.sessionKey
		})) {
			sendJson(res, 403, {
				ok: false,
				error: {
					type: "forbidden",
					message: "requester session does not own attachment session"
				}
			});
			return true;
		}
	}
	if (!await recordMatchesTranscriptMessage(record)) {
		sendStatus(res, 404, "not found");
		return true;
	}
	let body;
	try {
		body = await fs.readFile(record.original.path);
	} catch {
		sendStatus(res, 404, "not found");
		return true;
	}
	res.statusCode = 200;
	res.setHeader("content-type", record.original.contentType || "application/octet-stream");
	res.setHeader("content-length", String(body.byteLength));
	res.setHeader("cache-control", "private, max-age=31536000, immutable");
	res.setHeader("content-disposition", `inline; filename="${safeAttachmentFilename(record.original.filename)}"`);
	res.end(body);
	return true;
}
//#endregion
export { handleManagedOutgoingImageHttpRequest as a, createManagedOutgoingImageBlocks as i, attachManagedOutgoingImagesToMessage as n, resolveManagedImageAttachmentLimits as o, cleanupManagedOutgoingImageRecords as r, DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS as t };
