import { A as getEnvString, T as getConfigDirectoryPath, o as logger } from "./logger-CfDSdx1q.js";
import { P as CLOUD_PROVIDER_PREFIX, l as cloudConfig, t as fetchWithProxy } from "./fetch-CHx36OdV.js";
import { t as invariant } from "./invariant-vgHWClmd.js";
import { P as ProviderOptionsSchema, U as isUuid } from "./tables-hhnjOKZL.js";
import { r as checkServerFeatureSupport } from "./server-DSPBU6pS.js";
import * as fs$2 from "fs";
import * as path$1 from "path";
import * as crypto$1 from "crypto";
import dedent from "dedent";
import * as fsPromises from "fs/promises";
//#region src/util/cloud.ts
const PERMISSION_CHECK_SERVER_FEATURE_NAME = "config-permission-check-endpoint";
const PERMISSION_CHECK_SERVER_FEATURE_DATE = "2025-09-03T14:49:11Z";
/**
* Makes an authenticated HTTP request to the PromptFoo Cloud API.
* @param path - The API endpoint path (with or without leading slash)
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
* @param body - Optional request body that will be JSON stringified
* @returns Promise resolving to the fetch Response object
* @throws Error if the request fails due to network or other issues
*/
function makeRequest(path, method, body) {
	const apiHost = cloudConfig.getApiHost();
	const apiKey = cloudConfig.getApiKey();
	const url = `${apiHost}/api/v1/${path.startsWith("/") ? path.slice(1) : path}`;
	try {
		return fetchWithProxy(url, {
			method,
			body: JSON.stringify(body),
			headers: {
				Authorization: `Bearer ${apiKey}`,
				"Content-Type": "application/json"
			}
		});
	} catch (e) {
		logger.error(`[Cloud] Failed to make request to ${url}: ${e}`);
		if (e.cause) logger.error(`Cause: ${e.cause}`);
		throw e;
	}
}
/**
* Fetches a provider configuration from PromptFoo Cloud by its ID.
* @param id - The unique identifier of the cloud provider
* @returns Promise resolving to provider options with guaranteed id field
* @throws Error if cloud is not enabled, provider not found, or request fails
*/
async function getProviderFromCloud(id) {
	if (!cloudConfig.isEnabled()) throw new Error(`Could not fetch Provider ${id} from cloud. Cloud config is not enabled. Please run \`promptfoo auth login\` to login.`);
	try {
		const response = await makeRequest(`providers/${id}`, "GET");
		if (!response.ok) {
			const errorMessage = await response.text();
			logger.error(`[Cloud] Failed to fetch provider from cloud: ${errorMessage}. HTTP Status: ${response.status} -- ${response.statusText}.`);
			throw new Error(`Failed to fetch provider from cloud: ${response.statusText}`);
		}
		const body = await response.json();
		logger.debug(`Provider fetched from cloud: ${id}`);
		const provider = ProviderOptionsSchema.parse(body.config);
		invariant(provider.id, `Provider ${id} has no id in ${body.config}`);
		return {
			...provider,
			id: provider.id
		};
	} catch (e) {
		logger.error(`Failed to fetch provider from cloud: ${id}.`);
		logger.error(String(e));
		throw new Error(`Failed to fetch provider from cloud: ${id}.`);
	}
}
function isRecord(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}
async function fetchCloudConfig(path) {
	const response = await makeRequest(path, "GET");
	if (!response.ok) {
		const errorMessage = typeof response.text === "function" ? await response.text() : "";
		logger.error(`[Cloud] Failed to fetch config from cloud: ${errorMessage}. HTTP Status: ${response.status} -- ${response.statusText}.`);
		throw new Error(`Failed to fetch config from cloud: ${response.statusText}`);
	}
	return response.json();
}
function looksLikeEvalConfig(config) {
	return "providers" in config || "providerIds" in config || "prompts" in config || "tests" in config || "testCases" in config;
}
function extractEvalConfigPayload(body) {
	if (!isRecord(body)) throw new Error("Invalid cloud eval config response: expected a JSON object.");
	const bodyConfig = isRecord(body.config) ? body.config : void 0;
	if (!bodyConfig) {
		if (looksLikeEvalConfig(body)) return body;
		throw new Error("Invalid cloud eval config response: missing \"config\" object.");
	}
	const nestedConfig = isRecord(bodyConfig.config) ? bodyConfig.config : void 0;
	if (!nestedConfig) return {
		...bodyConfig,
		...typeof bodyConfig.name !== "string" && typeof body.name === "string" ? { name: body.name } : {}
	};
	return {
		...nestedConfig,
		...typeof nestedConfig.name !== "string" && typeof bodyConfig.name === "string" ? { name: bodyConfig.name } : {}
	};
}
function normalizeCloudEvalProvider(provider) {
	if (typeof provider !== "string") return provider;
	if (provider.startsWith("promptfoo://provider/") || !isUuid(provider)) return provider;
	return `${CLOUD_PROVIDER_PREFIX}${provider}`;
}
function normalizeCloudEvalPrompt(prompt) {
	if (typeof prompt === "string") return prompt;
	if (isRecord(prompt)) {
		if (typeof prompt.content === "string") return prompt.content;
		if (typeof prompt.raw === "string") return prompt.raw;
	}
	return String(prompt ?? "");
}
function normalizeEvalConfig(config) {
	const providers = Array.isArray(config.providers) ? config.providers : Array.isArray(config.providerIds) ? config.providerIds : [];
	const prompts = Array.isArray(config.prompts) ? config.prompts : [];
	const tests = Array.isArray(config.tests) ? config.tests : Array.isArray(config.testCases) ? config.testCases : [];
	const commandLineOptions = {
		...isRecord(config.commandLineOptions) ? config.commandLineOptions : {},
		...config.maxConcurrency == null ? {} : { maxConcurrency: config.maxConcurrency },
		...config.delay == null ? {} : { delay: config.delay },
		...config.verbose == null ? {} : { verbose: config.verbose }
	};
	const normalizedConfig = {
		...config,
		providers: providers.map(normalizeCloudEvalProvider),
		prompts: prompts.map(normalizeCloudEvalPrompt),
		tests
	};
	if (Object.keys(commandLineOptions).length > 0) normalizedConfig.commandLineOptions = commandLineOptions;
	else delete normalizedConfig.commandLineOptions;
	if (typeof config.description === "string" && config.description.trim().length > 0) normalizedConfig.description = config.description;
	else if (typeof config.name === "string" && config.name.trim().length > 0) normalizedConfig.description = config.name;
	delete normalizedConfig.providerIds;
	delete normalizedConfig.testCases;
	delete normalizedConfig.maxConcurrency;
	delete normalizedConfig.delay;
	delete normalizedConfig.verbose;
	return normalizedConfig;
}
/**
* Fetches an eval configuration from PromptFoo Cloud by ID.
* The response may contain legacy eval fields, which are normalized into UnifiedConfig.
* @param id - The unique identifier of the cloud eval configuration
* @returns Promise resolving to a normalized unified configuration object
* @throws Error if cloud is not enabled, config not found, or response shape is invalid
*/
async function getEvalConfigFromCloud(id) {
	if (!cloudConfig.isEnabled()) throw new Error(`Could not fetch Config ${id} from cloud. Cloud config is not enabled. Please run \`promptfoo auth login\` to login.`);
	try {
		const config = normalizeEvalConfig(extractEvalConfigPayload(await fetchCloudConfig(`configs/${id}`)));
		logger.info(`Eval config fetched from cloud: ${id}`);
		return config;
	} catch (e) {
		logger.error(`Failed to fetch eval config from cloud: ${id}.`);
		logger.error(String(e));
		if (e instanceof Error) throw e;
		throw new Error(String(e));
	}
}
/**
* Checks if a provider path represents a cloud-based provider.
* @param providerPath - The provider path to check
* @returns True if the path starts with the cloud provider prefix, false otherwise
*/
function isCloudProvider(providerPath) {
	return providerPath.startsWith(CLOUD_PROVIDER_PREFIX);
}
/**
* Extracts the database ID from a cloud provider path.
* @param providerPath - The cloud provider path
* @returns The database ID portion of the path
* @throws Error if the path is not a valid cloud provider path
*/
function getCloudDatabaseId(providerPath) {
	if (!isCloudProvider(providerPath)) throw new Error(`Provider path ${providerPath} is not a cloud provider.`);
	return providerPath.slice(CLOUD_PROVIDER_PREFIX.length);
}
/**
* Get the plugin severity overrides for a cloud provider.
* @param cloudProviderId - The cloud provider ID.
* @returns The plugin severity overrides.
*/
async function getPluginSeverityOverridesFromCloud(cloudProviderId) {
	if (!cloudConfig.isEnabled()) throw new Error(`Could not fetch plugin severity overrides from cloud. Cloud config is not enabled. Please run \`promptfoo auth login\` to login.`);
	try {
		const response = await makeRequest(`/providers/${cloudProviderId}`, "GET");
		if (!response.ok) {
			const formattedErrorMessage = `Failed to provider from cloud: ${await response.text()}. HTTP Status: ${response.status} -- ${response.statusText}.`;
			logger.error(`[Cloud] ${formattedErrorMessage}`);
			throw new Error(formattedErrorMessage);
		}
		const body = await response.json();
		if (body.pluginSeverityOverrideId) {
			const overrideRes = await makeRequest(`/redteam/plugins/severity-overrides/${body.pluginSeverityOverrideId}`, "GET");
			if (!overrideRes.ok) {
				const formattedErrorMessage = `Failed to fetch plugin severity override from cloud: ${await overrideRes.text()}. HTTP Status: ${overrideRes.status} -- ${overrideRes.statusText}.`;
				logger.error(`[Cloud] ${formattedErrorMessage}`);
				throw new Error(formattedErrorMessage);
			}
			const pluginSeverityOverride = await overrideRes.json();
			return {
				id: pluginSeverityOverride.id,
				severities: pluginSeverityOverride.members.reduce((acc, member) => ({
					...acc,
					[member.pluginId]: member.severity
				}), {})
			};
		} else {
			logger.debug(`No plugin severity overrides found for cloud provider ${cloudProviderId}`);
			return null;
		}
	} catch (e) {
		logger.error(`Failed to fetch plugin severity overrides from cloud.`);
		logger.error(String(e));
		throw new Error(`Failed to fetch plugin severity overrides from cloud.`);
	}
}
/**
* Retrieves all teams for the current user from Promptfoo Cloud.
* @returns Promise resolving to an array of team objects
* @throws Error if the request fails
*/
async function getUserTeams(apiHost, apiKey) {
	const response = apiHost && apiKey ? await fetchWithProxy(`${apiHost}/api/v1/users/me/teams`, { headers: { Authorization: `Bearer ${apiKey}` } }) : await makeRequest(`/users/me/teams`, "GET");
	if (!response.ok) throw new Error(`Failed to get user teams: ${response.statusText}`);
	return await response.json();
}
/**
* Retrieves the default team for the current user from Promptfoo Cloud.
* The default team is determined as the oldest team by creation date.
* @returns Promise resolving to an object with team id, name, organizationId, and createdAt
* @throws Error if the request fails or no teams are found
*/
async function getDefaultTeam() {
	const teams = await getUserTeams();
	if (teams.length === 0) throw new Error("No teams found for user");
	const oldestTeam = teams.sort((a, b) => {
		return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
	})[0];
	return {
		id: oldestTeam.id,
		name: oldestTeam.name,
		organizationId: oldestTeam.organizationId,
		createdAt: oldestTeam.createdAt
	};
}
/**
* Retrieves a team by its ID.
* @param teamId - The team ID to look up
* @returns Promise resolving to an object with team id, name, organizationId, and createdAt
* @throws Error if the team is not found or not accessible
*/
async function getTeamById(teamId) {
	const team = (await getUserTeams()).find((t) => t.id === teamId);
	if (!team) throw new Error(`Team with ID '${teamId}' not found or not accessible`);
	return {
		id: team.id,
		name: team.name,
		organizationId: team.organizationId,
		createdAt: team.createdAt
	};
}
/**
* Resolves a team identifier (name, slug, or ID) to a team object.
* @param identifier - The team name, slug, or ID
* @returns Promise resolving to an object with team id, name, organizationId, and createdAt
* @throws Error if the team is not found
*/
async function resolveTeamFromIdentifier(identifier) {
	const teams = await getUserTeams();
	let team = teams.find((t) => t.id === identifier);
	if (team) return {
		id: team.id,
		name: team.name,
		organizationId: team.organizationId,
		createdAt: team.createdAt
	};
	team = teams.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
	if (team) return {
		id: team.id,
		name: team.name,
		organizationId: team.organizationId,
		createdAt: team.createdAt
	};
	team = teams.find((t) => t.slug === identifier);
	if (team) return {
		id: team.id,
		name: team.name,
		organizationId: team.organizationId,
		createdAt: team.createdAt
	};
	const availableTeams = teams.map((t) => t.name).join(", ");
	throw new Error(`Team '${identifier}' not found. Available teams: ${availableTeams}`);
}
/**
* Resolves the current team context, checking stored preferences first.
* @param teamIdentifier - Optional explicit team identifier to use
* @param fallbackToDefault - Whether to fall back to server default team
* @returns Promise resolving to an object with team id and name
* @throws Error if no team can be resolved
*/
async function resolveTeamId(teamIdentifier, fallbackToDefault = true) {
	if (teamIdentifier) {
		logger.debug(`[Team Resolution] Using explicit team identifier: ${teamIdentifier}`);
		return await resolveTeamFromIdentifier(teamIdentifier);
	}
	const currentOrganizationId = cloudConfig.getCurrentOrganizationId();
	const currentTeamId = cloudConfig.getCurrentTeamId(currentOrganizationId);
	if (currentTeamId) try {
		logger.debug(`[Team Resolution] Using stored team ID: ${currentTeamId}`);
		return await getTeamById(currentTeamId);
	} catch (_error) {
		logger.warn(`[Team Resolution] Stored team ${currentTeamId} no longer accessible, falling back`);
	}
	if (fallbackToDefault) {
		logger.debug(`[Team Resolution] Using server default team`);
		const defaultTeam = await getDefaultTeam();
		cloudConfig.setCurrentTeamId(defaultTeam.id, defaultTeam.organizationId);
		logger.info(`Using team: ${defaultTeam.name} (use 'promptfoo auth teams set <name>' to change)`);
		return defaultTeam;
	}
	throw new Error("No team specified and no default available");
}
/**
* Custom error class for configuration permission-related failures.
* Thrown when users lack necessary permissions to use certain cloud features.
*/
var ConfigPermissionError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "ConfigPermissionError";
	}
};
/**
* Converts an array of structured error objects into a human-readable message.
* @param errors - Array of error objects with type, id, and message fields
* @returns A comma-separated string of formatted error messages
*/
function convertErrorsToReadableMessage(errors) {
	return errors.map((error) => `${error.type} ${error.id}: ${error.message}`).join(", ");
}
/**
* Validates that the current user has necessary permissions for the given configuration.
* Checks with PromptFoo Cloud to ensure providers and other resources can be accessed.
* Gracefully degrades if cloud is disabled or server doesn't support permission checking.
* @param config - The configuration to validate permissions for
* @throws ConfigPermissionError if permissions are insufficient (403 responses)
* @throws Error for other critical permission check failures
*/
async function checkCloudPermissions(config) {
	if (!cloudConfig.isEnabled()) return;
	if (!config.providers) {
		logger.warn("No providers specified. Skipping permission check.");
		return;
	}
	try {
		if (!await checkServerFeatureSupport(PERMISSION_CHECK_SERVER_FEATURE_NAME, PERMISSION_CHECK_SERVER_FEATURE_DATE)) {
			logger.debug(`[Config Permission Check] Server feature ${PERMISSION_CHECK_SERVER_FEATURE_NAME} is not supported. Skipping permission check.`);
			return;
		}
		const { tests, scenarios, defaultTest, evaluateOptions, ...minimalConfig } = config;
		if (minimalConfig.redteam) minimalConfig.redteam = {};
		const response = await makeRequest("permissions/check", "POST", { config: minimalConfig });
		if (!response.ok) {
			const errorData = await response.json().catch(() => ({ errors: ["Unknown error"] }));
			const errors = Array.isArray(errorData.errors) ? errorData.errors.map((error) => {
				if (typeof error === "string") return {
					type: "config",
					id: "unknown",
					message: error
				};
				return error;
			}) : [{
				type: "config",
				id: "unknown",
				message: errorData.error || "Permission check failed"
			}];
			if (response.status === 403) throw new ConfigPermissionError(`Permission denied: ${convertErrorsToReadableMessage(errors)}`);
			logger.warn(`Error checking permissions: ${convertErrorsToReadableMessage(errors)}. Continuing anyway.`);
			return;
		}
		const result = await response.json();
		if (result.errors && result.errors.length > 0) throw new ConfigPermissionError(`Not able to continue with config: ${convertErrorsToReadableMessage(result.errors)}`);
		logger.debug("Permission check passed");
	} catch (error) {
		if (error instanceof ConfigPermissionError) throw error;
		logger.warn(`Error checking permissions: ${error}. Continuing anyway.`);
	}
}
/**
* Given a list of policy IDs, fetches custom policies from Promptfoo Cloud.
* @param ids - The IDs of the policies to fetch.
* @param teamId - The ID of the team to fetch policies from. Note that all policies must belong to this team.
* @returns A map of policy IDs to their texts and severities.
*/
async function getPoliciesFromCloud(ids, teamId) {
	if (!cloudConfig.isEnabled()) throw new Error(`Could not fetch policies from cloud. Cloud config is not enabled. Please run \`promptfoo auth login\` to login.`);
	try {
		const searchParams = new URLSearchParams();
		ids.forEach((id) => {
			searchParams.append("id", id);
		});
		const response = await makeRequest(`/custom-policies/?${searchParams.toString()}&teamId=${teamId}`, "GET");
		if (!response.ok) {
			const errorMessage = await response.text();
			throw new Error(`Failed to fetch policies from cloud: ${errorMessage}. HTTP Status: ${response.status} -- ${response.statusText}.`);
		}
		const body = await response.json();
		const policiesById = /* @__PURE__ */ new Map();
		body.forEach((policy) => {
			policiesById.set(policy.id, {
				text: policy.text,
				severity: policy.severity,
				name: policy.name
			});
		});
		return policiesById;
	} catch (e) {
		logger.error(`Failed to fetch policies from cloud.`);
		logger.error(String(e));
		throw new Error(`Failed to fetch policies from cloud.`);
	}
}
/**
* Validates linkedTargetId format and existence.
* linkedTargetId is a Promptfoo Cloud feature that links custom provider results
* to an existing target instead of creating duplicates.
*
* Validates the prefix and checks existence in cloud. Format validation
* (e.g., UUID format) is deferred to the cloud API for simplicity.
*
* @param linkedTargetId - The linkedTargetId to validate
* @throws Error if validation fails
*/
async function validateLinkedTargetId(linkedTargetId) {
	if (!isCloudProvider(linkedTargetId)) {
		const appHost = cloudConfig.getApiHost().replace("/api", "").replace(":3201", "");
		throw new Error(dedent`
        Invalid linkedTargetId format: "${linkedTargetId}"

        linkedTargetId must start with "${CLOUD_PROVIDER_PREFIX}" followed by a target ID.
        Example: ${CLOUD_PROVIDER_PREFIX}12345678-1234-1234-1234-123456789abc

        linkedTargetId links your local provider configuration to a cloud target, allowing you to:
        - Consolidate findings from multiple eval runs
        - Track performance and vulnerabilities over time
        - View comprehensive reporting in the cloud dashboard

        To get a valid linkedTargetId:
        1. Log in to Promptfoo Cloud: ${appHost}
        2. Navigate to Targets page: ${appHost}/redteam/targets
        3. Find the target you want to link to and copy its ID
        4. Format as: ${CLOUD_PROVIDER_PREFIX}<target-id>
      `);
	}
	if (!cloudConfig.isEnabled()) {
		logger.warn("[Cloud] linkedTargetId specified but cloud is not configured", {
			linkedTargetId,
			suggestion: "Run 'promptfoo auth login' to enable cloud features"
		});
		return;
	}
	const providerId = getCloudDatabaseId(linkedTargetId);
	try {
		logger.debug("[Cloud] Validating linkedTargetId exists in cloud", {
			linkedTargetId,
			providerId
		});
		await getProviderFromCloud(providerId);
		logger.debug("[Cloud] linkedTargetId validation successful", { linkedTargetId });
	} catch (error) {
		logger.error("[Cloud] linkedTargetId validation failed", {
			linkedTargetId,
			error
		});
		const appHost = cloudConfig.getApiHost().replace("/api", "").replace(":3201", "");
		throw new Error(dedent`
        linkedTargetId not found: "${linkedTargetId}"

        This target doesn't exist in your Promptfoo Cloud organization or you don't have access to it.

        Troubleshooting steps:
        1. Verify you're logged in to the correct organization
           Run: promptfoo auth status

        2. Check that the target exists in your cloud dashboard:
           ${appHost}/redteam/targets

        3. Ensure you have permission to access this target
           (Targets are scoped to your organization)

        4. Verify the target ID is correct and hasn't been deleted
      `);
	}
}
/**
* Fetches the current organization and optional team context for display.
* Returns null if cloud is not enabled or if fetching fails.
* @returns Promise resolving to organization name and optional team name, or null
*/
async function getOrgContext() {
	if (!cloudConfig.isEnabled()) return null;
	try {
		const apiHost = cloudConfig.getApiHost();
		const apiKey = cloudConfig.getApiKey();
		const response = await fetchWithProxy(`${apiHost}/api/v1/users/me`, { headers: { Authorization: `Bearer ${apiKey}` } });
		if (!response.ok) return null;
		const { organization } = await response.json();
		const currentTeamId = cloudConfig.getCurrentTeamId(organization.id);
		let teamName;
		if (currentTeamId) try {
			const team = await getTeamById(currentTeamId);
			if (team.name !== organization.name) teamName = team.name;
		} catch {}
		return {
			organizationName: organization.name,
			teamName
		};
	} catch {
		return null;
	}
}
//#endregion
//#region src/storage/localFileSystemProvider.ts
/**
* Local filesystem storage provider for media files.
*
* Stores media in the local promptfoo data directory (~/.promptfoo/media).
* Uses content-based hashing for deduplication.
*/
const MEDIA_SUBDIR = "media";
const HASH_INDEX_FILE = "hash-index.json";
/**
* Get file extension from content type
*/
function getExtensionFromContentType(contentType) {
	return {
		"audio/wav": "wav",
		"audio/mp3": "mp3",
		"audio/mpeg": "mp3",
		"audio/ogg": "ogg",
		"audio/webm": "webm",
		"image/png": "png",
		"image/jpeg": "jpg",
		"image/jpg": "jpg",
		"image/gif": "gif",
		"image/webp": "webp",
		"video/mp4": "mp4",
		"video/webm": "webm",
		"video/ogg": "ogv"
	}[contentType] || "bin";
}
/**
* Compute SHA-256 hash of data
*/
function computeHash(data) {
	return crypto$1.createHash("sha256").update(data).digest("hex");
}
/**
* Local filesystem storage provider
*/
var LocalFileSystemProvider = class {
	providerId = "local";
	basePath;
	hashIndexPath;
	hashIndex = /* @__PURE__ */ new Map();
	constructor(config = {}) {
		this.basePath = config.basePath || path$1.join(getConfigDirectoryPath(true), MEDIA_SUBDIR);
		this.hashIndexPath = path$1.join(this.basePath, HASH_INDEX_FILE);
		this.ensureDirectory();
		this.loadHashIndex();
	}
	/**
	* Ensure the media directory exists
	*/
	ensureDirectory() {
		if (!fs$2.existsSync(this.basePath)) {
			fs$2.mkdirSync(this.basePath, { recursive: true });
			logger.debug(`[LocalStorage] Created media directory: ${this.basePath}`);
		}
	}
	/**
	* Load the hash index from disk
	*/
	loadHashIndex() {
		try {
			if (fs$2.existsSync(this.hashIndexPath)) {
				const data = fs$2.readFileSync(this.hashIndexPath, "utf8");
				const parsed = JSON.parse(data);
				this.hashIndex = new Map(Object.entries(parsed));
				logger.debug(`[LocalStorage] Loaded hash index with ${this.hashIndex.size} entries`);
			}
		} catch (error) {
			logger.warn(`[LocalStorage] Failed to load hash index, starting fresh`, { error });
			this.hashIndex = /* @__PURE__ */ new Map();
		}
	}
	/**
	* Save the hash index to disk
	*/
	async saveHashIndex() {
		try {
			const data = JSON.stringify(Object.fromEntries(this.hashIndex), null, 2);
			await fsPromises.writeFile(this.hashIndexPath, data, "utf8");
		} catch (error) {
			logger.warn(`[LocalStorage] Failed to save hash index`, { error });
		}
	}
	/**
	* Get the full path for a storage key
	*/
	getFilePath(key) {
		const targetPath = path$1.resolve(this.basePath, key);
		const safeBase = path$1.resolve(this.basePath) + path$1.sep;
		if (!targetPath.startsWith(safeBase)) throw new Error(`[LocalStorage] Invalid media key: path traversal attempt detected ("${key}")`);
		return targetPath;
	}
	/**
	* Generate a storage key from hash and metadata
	*/
	generateKey(hash, metadata) {
		const extension = getExtensionFromContentType(metadata.contentType);
		return `${metadata.mediaType || "media"}/${hash.slice(0, 12)}.${extension}`;
	}
	async store(data, metadata) {
		const contentHash = computeHash(data);
		const existingKey = await this.findByHash(contentHash);
		if (existingKey) {
			logger.debug(`[LocalStorage] Deduplicated media: ${existingKey}`);
			return {
				ref: {
					provider: this.providerId,
					key: existingKey,
					contentHash,
					metadata
				},
				deduplicated: true
			};
		}
		const key = this.generateKey(contentHash, metadata);
		const filePath = this.getFilePath(key);
		const dir = path$1.dirname(filePath);
		await fsPromises.mkdir(dir, { recursive: true });
		await fsPromises.writeFile(filePath, data);
		this.hashIndex.set(contentHash, key);
		await this.saveHashIndex();
		const metadataPath = `${filePath}.meta.json`;
		await fsPromises.writeFile(metadataPath, JSON.stringify({
			...metadata,
			contentHash,
			sizeBytes: data.length,
			createdAt: (/* @__PURE__ */ new Date()).toISOString()
		}, null, 2));
		logger.debug(`[LocalStorage] Stored media: ${key} (${data.length} bytes)`);
		return {
			ref: {
				provider: this.providerId,
				key,
				contentHash,
				metadata: {
					...metadata,
					sizeBytes: data.length,
					contentHash
				}
			},
			deduplicated: false
		};
	}
	async retrieve(key) {
		const filePath = this.getFilePath(key);
		try {
			return await fsPromises.readFile(filePath);
		} catch (error) {
			if (error.code === "ENOENT") throw new Error(`[LocalStorage] Media not found: ${key}`);
			throw error;
		}
	}
	async exists(key) {
		try {
			const filePath = this.getFilePath(key);
			await fsPromises.access(filePath);
			return true;
		} catch {
			return false;
		}
	}
	async delete(key) {
		const filePath = this.getFilePath(key);
		const metadataPath = `${filePath}.meta.json`;
		for (const [hash, storedKey] of this.hashIndex.entries()) if (storedKey === key) {
			this.hashIndex.delete(hash);
			break;
		}
		await this.saveHashIndex();
		try {
			await fsPromises.unlink(filePath);
		} catch (error) {
			if (error.code !== "ENOENT") throw error;
		}
		try {
			await fsPromises.unlink(metadataPath);
		} catch (error) {
			if (error.code !== "ENOENT") throw error;
		}
		logger.debug(`[LocalStorage] Deleted media: ${key}`);
	}
	async getUrl(key, _expiresIn) {
		try {
			const filePath = this.getFilePath(key);
			await fsPromises.access(filePath);
			return `file://${filePath}`;
		} catch {
			return null;
		}
	}
	async findByHash(contentHash) {
		const key = this.hashIndex.get(contentHash);
		if (key && await this.exists(key)) return key;
		if (key) {
			this.hashIndex.delete(contentHash);
			await this.saveHashIndex();
		}
		return null;
	}
	/**
	* Get the base path for this provider
	*/
	getBasePath() {
		return this.basePath;
	}
	/**
	* Get stats about stored media
	*/
	async getStats() {
		let fileCount = 0;
		let totalSizeBytes = 0;
		const walkDir = async (dir) => {
			let entries;
			try {
				entries = await fsPromises.readdir(dir, { withFileTypes: true });
			} catch (error) {
				if (error.code === "ENOENT") return;
				throw error;
			}
			for (const entry of entries) {
				const fullPath = path$1.join(dir, entry.name);
				if (entry.isDirectory()) await walkDir(fullPath);
				else if (!entry.name.endsWith(".json")) try {
					const stat = await fsPromises.stat(fullPath);
					fileCount++;
					totalSizeBytes += stat.size;
				} catch (error) {
					if (error.code !== "ENOENT") throw error;
				}
			}
		};
		await walkDir(this.basePath);
		return {
			fileCount,
			totalSizeBytes
		};
	}
};
//#endregion
//#region src/storage/index.ts
/**
* Media storage module for promptfoo.
*
* Provides abstraction for storing binary media (audio, images, video)
* separately from the main database.
*
* @example
* ```typescript
* import { getMediaStorage, storeMedia, retrieveMedia } from './storage';
*
* // Store audio data
* const { ref } = await storeMedia(audioBuffer, {
*   contentType: 'audio/wav',
*   mediaType: 'audio',
*   evalId: 'eval-123',
* });
*
* // Later, retrieve it
* const buffer = await retrieveMedia(ref.key);
* ```
*/
let defaultProvider = null;
/**
* Get the default media storage provider.
*
* For OSS, this returns a LocalFileSystemProvider.
* For cloud deployments, this can be overridden.
*/
function getMediaStorage(config) {
	if (!defaultProvider) {
		defaultProvider = new LocalFileSystemProvider({ basePath: config?.basePath || getEnvString("PROMPTFOO_MEDIA_PATH") });
		logger.debug(`[MediaStorage] Initialized local storage provider`);
	}
	return defaultProvider;
}
/**
* Store media data and return a reference
*/
async function storeMedia(data, metadata) {
	return getMediaStorage().store(data, metadata);
}
/**
* Retrieve media data by key
*/
async function retrieveMedia(key) {
	return getMediaStorage().retrieve(key);
}
/**
* Check if media exists
*/
async function mediaExists(key) {
	return getMediaStorage().exists(key);
}
/**
* Check if media storage should be used based on config/env
*
* Returns true if media storage is enabled (default for new installs).
* Set PROMPTFOO_INLINE_MEDIA=true to disable and use legacy inline base64.
*/
function isMediaStorageEnabled() {
	const inline = getEnvString("PROMPTFOO_INLINE_MEDIA");
	return inline !== "true" && inline !== "1";
}
//#endregion
export { storeMedia as a, getEvalConfigFromCloud as c, getPoliciesFromCloud as d, getProviderFromCloud as f, validateLinkedTargetId as g, resolveTeamId as h, retrieveMedia as i, getOrgContext as l, makeRequest as m, isMediaStorageEnabled as n, checkCloudPermissions as o, isCloudProvider as p, mediaExists as r, getCloudDatabaseId as s, getMediaStorage as t, getPluginSeverityOverridesFromCloud as u };

//# sourceMappingURL=storage-C5jVg4Px.js.map