import { _ as getEnvBool, g as getConfigDirectoryPath, i as logger_default } from "./logger-BudIH0qK.js";
import { n as blobReferencesTable, p as getDb, t as blobAssetsTable } from "./tables-4jScbad_.js";
import * as path$1 from "node:path";
import * as fs$1 from "node:fs";
import * as fsPromises$1 from "node:fs/promises";
import { createHash } from "crypto";
import { createHash as createHash$1, randomUUID as randomUUID$1 } from "node:crypto";
import { and, eq } from "drizzle-orm";

//#region src/util/createHash.ts
function sha256(str) {
	return createHash("sha256").update(str).digest("hex");
}
function randomSequence(length = 3) {
	const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	let result = "";
	for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * 62));
	return result;
}

//#endregion
//#region src/blobs/constants.ts
const BLOB_MIN_SIZE = 1024;
const BLOB_MAX_SIZE = 52428800;
const BLOB_SCHEME = "promptfoo://blob/";
const DEFAULT_FILESYSTEM_SUBDIR = "blobs";

//#endregion
//#region src/blobs/filesystemProvider.ts
const BLOB_HASH_REGEX$1 = /^[a-f0-9]{64}$/i;
function computeHash(data) {
	return createHash$1("sha256").update(data).digest("hex");
}
function buildUri(hash) {
	return `${BLOB_SCHEME}${hash}`;
}
var FilesystemBlobStorageProvider = class {
	providerId = "filesystem";
	basePath;
	constructor(config) {
		const defaultBase = path$1.join(getConfigDirectoryPath(true), DEFAULT_FILESYSTEM_SUBDIR);
		this.basePath = path$1.resolve(config?.basePath || defaultBase);
		this.ensureDirectory();
	}
	ensureDirectory() {
		if (!fs$1.existsSync(this.basePath)) {
			fs$1.mkdirSync(this.basePath, { recursive: true });
			logger_default.debug("[BlobFS] Created blob directory", { basePath: this.basePath });
		}
	}
	assertValidHash(hash) {
		if (!BLOB_HASH_REGEX$1.test(hash)) throw new Error(`[BlobFS] Invalid blob hash: "${hash}"`);
	}
	resolvePathInBase(unsafePath) {
		const targetPath = path$1.isAbsolute(unsafePath) ? path$1.resolve(unsafePath) : path$1.resolve(this.basePath, unsafePath);
		const safeBase = path$1.resolve(this.basePath) + path$1.sep;
		if (!targetPath.startsWith(safeBase)) throw new Error("[BlobFS] Path traversal attempt detected");
		return targetPath;
	}
	hashToPath(hash) {
		this.assertValidHash(hash);
		const dirRelative = path$1.join(hash.slice(0, 2), hash.slice(2, 4));
		const fileRelative = path$1.join(dirRelative, hash);
		return this.resolvePathInBase(fileRelative);
	}
	async ensureHashDir(hash) {
		this.assertValidHash(hash);
		const dirRelative = path$1.join(hash.slice(0, 2), hash.slice(2, 4));
		const dirPath = this.resolvePathInBase(dirRelative);
		await fsPromises$1.mkdir(dirPath, { recursive: true });
	}
	metadataPath(filePath) {
		return `${filePath}.meta.json`;
	}
	async store(data, mimeType) {
		const hash = computeHash(data);
		await this.ensureHashDir(hash);
		const filePath = this.hashToPath(hash);
		try {
			await fsPromises$1.access(filePath);
			const meta = await this.readMetadata(filePath);
			return {
				ref: this.buildRef(hash, meta?.mimeType ?? mimeType, meta?.sizeBytes ?? data.length, meta?.provider ?? this.providerId),
				deduplicated: true
			};
		} catch {}
		await fsPromises$1.writeFile(filePath, data);
		const metadata = {
			mimeType,
			sizeBytes: data.length,
			createdAt: (/* @__PURE__ */ new Date()).toISOString(),
			provider: this.providerId,
			key: filePath
		};
		await fsPromises$1.writeFile(this.metadataPath(filePath), JSON.stringify(metadata, null, 2));
		return {
			ref: this.buildRef(hash, mimeType, data.length, this.providerId),
			deduplicated: false
		};
	}
	async getByHash(hash) {
		const filePath = this.hashToPath(hash);
		let data;
		try {
			data = await fsPromises$1.readFile(filePath);
		} catch (error) {
			if (error.code === "ENOENT") throw new Error(`Blob not found: ${hash}`);
			throw error;
		}
		const metadata = await this.readMetadata(filePath) || {
			mimeType: "application/octet-stream",
			sizeBytes: data.length,
			createdAt: (/* @__PURE__ */ new Date()).toISOString(),
			provider: this.providerId,
			key: filePath
		};
		return {
			data,
			metadata
		};
	}
	async exists(hash) {
		try {
			const filePath = this.hashToPath(hash);
			await fsPromises$1.access(filePath);
			return true;
		} catch {
			return false;
		}
	}
	async deleteByHash(hash) {
		try {
			const filePath = this.hashToPath(hash);
			const metaPath = this.metadataPath(filePath);
			try {
				await fsPromises$1.unlink(filePath);
			} catch (error) {
				if (error.code !== "ENOENT") throw error;
			}
			try {
				await fsPromises$1.unlink(metaPath);
			} catch (error) {
				if (error.code !== "ENOENT") throw error;
			}
		} catch {}
	}
	async getUrl(_hash, _expiresInSeconds) {
		return null;
	}
	buildRef(hash, mimeType, sizeBytes, provider) {
		return {
			uri: buildUri(hash),
			hash,
			mimeType,
			sizeBytes,
			provider
		};
	}
	async readMetadata(filePath) {
		const safeFilePath = this.resolvePathInBase(filePath);
		const metaPath = this.metadataPath(safeFilePath);
		try {
			const raw = await fsPromises$1.readFile(metaPath, "utf8");
			return JSON.parse(raw);
		} catch (error) {
			if (error.code === "ENOENT") return null;
			logger_default.warn("[BlobFS] Failed to read metadata", { error });
			return null;
		}
	}
};

//#endregion
//#region src/blobs/index.ts
let defaultProvider = null;
function createDefaultProvider() {
	return new FilesystemBlobStorageProvider();
}
function getBlobStorageProvider() {
	if (!defaultProvider) {
		defaultProvider = createDefaultProvider();
		logger_default.debug("[BlobStorage] Initialized provider", { provider: defaultProvider.providerId });
	}
	return defaultProvider;
}
async function storeBlob(data, mimeType, refContext) {
	const provider = getBlobStorageProvider();
	const result = await provider.store(data, mimeType);
	const db = getDb();
	try {
		db.transaction(() => {
			const assetInsert = db.insert(blobAssetsTable).values({
				hash: result.ref.hash,
				sizeBytes: result.ref.sizeBytes,
				mimeType: result.ref.mimeType,
				provider: result.ref.provider
			}).onConflictDoNothing().run();
			return (refContext?.evalId && db.insert(blobReferencesTable).values({
				id: randomUUID$1(),
				blobHash: result.ref.hash,
				evalId: refContext.evalId,
				testIdx: refContext.testIdx,
				promptIdx: refContext.promptIdx,
				location: refContext.location,
				kind: refContext.kind
			}).onConflictDoNothing().run()) ?? assetInsert;
		});
	} catch (error) {
		try {
			await provider.deleteByHash(result.ref.hash);
		} catch (cleanupError) {
			logger_default.warn("[BlobStorage] Failed to rollback blob after DB error", {
				error: cleanupError,
				hash: result.ref.hash
			});
		}
		throw error;
	}
	return result;
}
async function getBlobByHash(hash) {
	return getBlobStorageProvider().getByHash(hash);
}
async function recordBlobReference(hash, refContext) {
	if (!refContext.evalId) return;
	if (!await getBlobStorageProvider().exists(hash).catch(() => false)) {
		logger_default.debug("[BlobStorage] Attempted to record reference for missing blob", {
			hash,
			evalId: refContext.evalId,
			location: refContext.location
		});
		return;
	}
	const db = getDb();
	if (db.select({ id: blobReferencesTable.id }).from(blobReferencesTable).where(and(eq(blobReferencesTable.blobHash, hash), eq(blobReferencesTable.evalId, refContext.evalId))).get()) return;
	db.insert(blobReferencesTable).values({
		id: randomUUID$1(),
		blobHash: hash,
		evalId: refContext.evalId,
		testIdx: refContext.testIdx,
		promptIdx: refContext.promptIdx,
		location: refContext.location,
		kind: refContext.kind
	}).run();
}

//#endregion
//#region src/blobs/extractor.ts
const BLOB_URI_REGEX = /^promptfoo:\/\/blob\/([a-f0-9]{64})$/i;
const BLOB_HASH_REGEX = /^[a-f0-9]{64}$/i;
function isDataUrl(value) {
	return /^data:(audio|image)\/[^;]+;base64,/.test(value);
}
function extractBase64(value) {
	const match = value.match(/^data:([^;]+);base64,(.+)$/);
	if (!match) return null;
	const mimeType = match[1];
	try {
		return {
			buffer: Buffer.from(match[2], "base64"),
			mimeType
		};
	} catch (error) {
		logger_default.warn("[BlobExtractor] Failed to parse base64 data URL", { error });
		return null;
	}
}
function shouldExternalize(buffer) {
	const size = buffer.length;
	return size >= BLOB_MIN_SIZE && size <= BLOB_MAX_SIZE;
}
function getKindFromMimeType(mimeType) {
	return mimeType.startsWith("audio/") ? "audio" : "image";
}
/**
* Normalize audio format to proper MIME type.
* Some providers return just 'wav' instead of 'audio/wav'.
* @internal Exported for testing
*/
function normalizeAudioMimeType(format) {
	if (!format) return "audio/wav";
	const trimmedFormat = format.trim();
	if (/^audio\/[a-z0-9_+-]+$/i.test(trimmedFormat)) return trimmedFormat;
	const formatLower = trimmedFormat.toLowerCase();
	const mimeMap = {
		wav: "audio/wav",
		mp3: "audio/mpeg",
		ogg: "audio/ogg",
		flac: "audio/flac",
		aac: "audio/aac",
		m4a: "audio/mp4",
		webm: "audio/webm"
	};
	if (mimeMap[formatLower]) return mimeMap[formatLower];
	if (!/^[a-z0-9_-]+$/i.test(formatLower)) {
		logger_default.warn("[BlobExtractor] Invalid audio format, using default", { format });
		return "audio/wav";
	}
	return `audio/${formatLower}`;
}
function parseBinary(base64OrDataUrl, defaultMimeType) {
	if (isDataUrl(base64OrDataUrl)) {
		const parsed = extractBase64(base64OrDataUrl);
		if (!parsed) return null;
		return parsed;
	}
	try {
		return {
			buffer: Buffer.from(base64OrDataUrl, "base64"),
			mimeType: defaultMimeType
		};
	} catch (error) {
		logger_default.warn("[BlobExtractor] Failed to parse base64 data", { error });
		return null;
	}
}
async function maybeStore(base64OrDataUrl, defaultMimeType, context, location, kind) {
	const parsed = parseBinary(base64OrDataUrl, defaultMimeType);
	if (!parsed || !shouldExternalize(parsed.buffer)) return null;
	if (!isBlobStorageEnabled()) return null;
	const { ref } = await storeBlob(parsed.buffer, parsed.mimeType || "application/octet-stream", {
		...context,
		location,
		kind
	});
	return ref;
}
async function externalizeDataUrls(value, context, location) {
	if (typeof value === "string") {
		if (!isDataUrl(value)) return {
			value,
			mutated: false
		};
		const parsed = extractBase64(value);
		if (!parsed || !shouldExternalize(parsed.buffer)) return {
			value,
			mutated: false
		};
		const storedRef = await maybeStore(parsed.buffer.toString("base64"), parsed.mimeType, context, location, getKindFromMimeType(parsed.mimeType)) || null;
		if (!storedRef) return {
			value,
			mutated: false
		};
		return {
			value: storedRef.uri,
			mutated: true
		};
	}
	if (Array.isArray(value)) {
		let mutated = false;
		const nextValues = await Promise.all(value.map(async (item, idx) => {
			const { value: nextValue, mutated: childMutated } = await externalizeDataUrls(item, context, `${location}[${idx}]`);
			mutated ||= childMutated;
			return nextValue;
		}));
		return mutated ? {
			value: nextValues,
			mutated
		} : {
			value,
			mutated: false
		};
	}
	if (value && typeof value === "object") {
		let mutated = false;
		const nextObject = { ...value };
		for (const [key, child] of Object.entries(value)) {
			const { value: nextValue, mutated: childMutated } = await externalizeDataUrls(child, context, location ? `${location}.${key}` : key);
			if (childMutated) {
				nextObject[key] = nextValue;
				mutated = true;
			}
		}
		return mutated ? {
			value: nextObject,
			mutated: true
		} : {
			value,
			mutated: false
		};
	}
	return {
		value,
		mutated: false
	};
}
/**
* Best-effort extraction of binary data from provider responses.
* Currently focuses on audio.data fields and data URL outputs.
*/
async function extractAndStoreBinaryData(response, context) {
	if (!response) return response;
	let mutated = false;
	const next = { ...response };
	const blobContext = context || {};
	if (response.audio?.data && typeof response.audio.data === "string") {
		const stored = await maybeStore(response.audio.data, normalizeAudioMimeType(response.audio.format), blobContext, "response.audio.data", "audio");
		if (stored) {
			next.audio = {
				...response.audio,
				data: void 0,
				blobRef: stored
			};
			mutated = true;
			logger_default.debug("[BlobExtractor] Stored audio blob", {
				...context,
				hash: stored.hash
			});
		}
	}
	const turns = response.turns;
	if (Array.isArray(turns)) next.turns = await Promise.all(turns.map(async (turn, idx) => {
		if (turn?.audio?.data && typeof turn.audio.data === "string") {
			const stored = await maybeStore(turn.audio.data, normalizeAudioMimeType(turn.audio.format), blobContext, `response.turns[${idx}].audio.data`, "audio");
			if (stored) {
				mutated = true;
				return {
					...turn,
					audio: {
						...turn.audio,
						data: void 0,
						blobRef: stored
					}
				};
			}
		}
		return turn;
	}));
	if (typeof response.output === "string" && isDataUrl(response.output)) {
		const parsed = extractBase64(response.output);
		if (parsed && shouldExternalize(parsed.buffer)) {
			const stored = await maybeStore(parsed.buffer.toString("base64"), parsed.mimeType, blobContext, "response.output", getKindFromMimeType(parsed.mimeType));
			if (stored) {
				next.output = stored.uri;
				mutated = true;
				logger_default.debug("[BlobExtractor] Stored output blob", {
					...context,
					hash: stored.hash
				});
			}
		}
	}
	if (typeof response.output === "string" && response.output.trim().startsWith("{") && (response.isBase64 && response.format === "json" || response.output.includes("\"b64_json\"") || response.output.includes("b64_json"))) try {
		const parsed = JSON.parse(response.output);
		if (Array.isArray(parsed.data)) {
			let jsonMutated = false;
			const storedUris = [];
			for (const item of parsed.data) if (item?.b64_json && typeof item.b64_json === "string") {
				const stored = await maybeStore(item.b64_json, "image/png", blobContext, "response.output.data[].b64_json", "image");
				if (stored) {
					item.b64_json = stored.uri;
					storedUris.push(stored.uri);
					jsonMutated = true;
					mutated = true;
					logger_default.debug("[BlobExtractor] Stored image blob from b64_json", {
						...context,
						hash: stored.hash
					});
				}
			}
			if (jsonMutated) {
				if (storedUris.length === 1) next.output = storedUris[0];
				else if (storedUris.length > 1) next.output = JSON.stringify(storedUris);
				else next.output = JSON.stringify(parsed);
				next.metadata = {
					...response.metadata || {},
					blobUris: storedUris,
					originalFormat: response.format
				};
			}
		}
	} catch (err) {
		logger_default.debug("[BlobExtractor] Failed to parse base64 JSON output", {
			error: err instanceof Error ? err.message : String(err),
			location: "response.output"
		});
	}
	if (response.metadata) {
		const { value, mutated: metadataMutated } = await externalizeDataUrls(response.metadata, blobContext, "response.metadata");
		if (metadataMutated) {
			next.metadata = value;
			mutated = true;
		}
	}
	const finalResponse = mutated ? next : response;
	if (blobContext.evalId) await recordExistingBlobReferences(finalResponse, blobContext, "response");
	return finalResponse;
}
function isBlobStorageEnabled() {
	return !getEnvBool("PROMPTFOO_INLINE_MEDIA", false);
}
function parseBlobHashFromValue(value) {
	if (!value) return null;
	if (typeof value === "string") {
		const match = value.match(BLOB_URI_REGEX);
		return match ? match[1] : null;
	}
	if (typeof value === "object") {
		const candidate = value;
		if (candidate.hash && BLOB_HASH_REGEX.test(candidate.hash)) return candidate.hash;
		if (candidate.uri && typeof candidate.uri === "string") {
			const match = candidate.uri.match(BLOB_URI_REGEX);
			if (match) return match[1];
		}
	}
	return null;
}
async function recordExistingBlobReferences(value, context, location) {
	const hash = parseBlobHashFromValue(value);
	if (hash) {
		await recordBlobReference(hash, {
			...context,
			location
		});
		return;
	}
	if (Array.isArray(value)) {
		await Promise.all(value.map((child, idx) => recordExistingBlobReferences(child, context, `${location}[${idx}]`)));
		return;
	}
	if (value && typeof value === "object") for (const [key, child] of Object.entries(value)) await recordExistingBlobReferences(child, context, location ? `${location}.${key}` : key);
}

//#endregion
export { randomSequence as a, storeBlob as i, isBlobStorageEnabled as n, sha256 as o, getBlobByHash as r, extractAndStoreBinaryData as t };
//# sourceMappingURL=extractor-DJ7JYpPo.js.map