import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
import { t as ContentRepository } from "./content-BsBoyj8G.mjs";
import { t as MediaRepository } from "./media-DqHVh136.mjs";
import { t as withTransaction } from "./transaction-Cn2rjY78.mjs";
import { t as RedirectRepository } from "./redirect-7lGhLBNZ.mjs";
import { t as BylineRepository } from "./byline-WuOq9MFJ.mjs";
import { i as FTSManager, n as SchemaRegistry } from "./registry-BgnP3ysR.mjs";
import { n as getDb } from "./loader-BYzwzORf.mjs";
import { t as validateSeed } from "./validate-DuZDIxfy.mjs";
import { sql } from "kysely";
import { ulid } from "ulidx";
import { imageSize } from "image-size";
import mime from "mime/lite";

//#region src/database/repositories/taxonomy.ts
/**
* Taxonomy repository for categories, tags, and other classification
*
* Taxonomies are hierarchical (via parentId) and can be attached to content entries.
*/
var TaxonomyRepository = class {
	constructor(db) {
		this.db = db;
	}
	/**
	* Create a new taxonomy term
	*/
	async create(input) {
		const id = ulid();
		const row = {
			id,
			name: input.name,
			slug: input.slug,
			label: input.label,
			parent_id: input.parentId ?? null,
			data: input.data ? JSON.stringify(input.data) : null
		};
		await this.db.insertInto("taxonomies").values(row).execute();
		const taxonomy = await this.findById(id);
		if (!taxonomy) throw new Error("Failed to create taxonomy");
		return taxonomy;
	}
	/**
	* Find taxonomy by ID
	*/
	async findById(id) {
		const row = await this.db.selectFrom("taxonomies").selectAll().where("id", "=", id).executeTakeFirst();
		return row ? this.rowToTaxonomy(row) : null;
	}
	/**
	* Find taxonomy by name and slug (unique constraint)
	*/
	async findBySlug(name, slug) {
		const row = await this.db.selectFrom("taxonomies").selectAll().where("name", "=", name).where("slug", "=", slug).executeTakeFirst();
		return row ? this.rowToTaxonomy(row) : null;
	}
	/**
	* Get all terms for a taxonomy (e.g., all categories)
	*/
	async findByName(name, options = {}) {
		let query = this.db.selectFrom("taxonomies").selectAll().where("name", "=", name).orderBy("label", "asc");
		if (options.parentId !== void 0) if (options.parentId === null) query = query.where("parent_id", "is", null);
		else query = query.where("parent_id", "=", options.parentId);
		return (await query.execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Get children of a taxonomy term
	*/
	async findChildren(parentId) {
		return (await this.db.selectFrom("taxonomies").selectAll().where("parent_id", "=", parentId).orderBy("label", "asc").execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Update a taxonomy term
	*/
	async update(id, input) {
		if (!await this.findById(id)) return null;
		const updates = {};
		if (input.slug !== void 0) updates.slug = input.slug;
		if (input.label !== void 0) updates.label = input.label;
		if (input.parentId !== void 0) updates.parent_id = input.parentId;
		if (input.data !== void 0) updates.data = JSON.stringify(input.data);
		if (Object.keys(updates).length > 0) await this.db.updateTable("taxonomies").set(updates).where("id", "=", id).execute();
		return this.findById(id);
	}
	/**
	* Delete a taxonomy term
	*/
	async delete(id) {
		await this.db.deleteFrom("content_taxonomies").where("taxonomy_id", "=", id).execute();
		return ((await this.db.deleteFrom("taxonomies").where("id", "=", id).executeTakeFirst()).numDeletedRows ?? 0) > 0;
	}
	/**
	* Attach a taxonomy term to a content entry
	*/
	async attachToEntry(collection, entryId, taxonomyId) {
		const row = {
			collection,
			entry_id: entryId,
			taxonomy_id: taxonomyId
		};
		await this.db.insertInto("content_taxonomies").values(row).onConflict((oc) => oc.doNothing()).execute();
	}
	/**
	* Detach a taxonomy term from a content entry
	*/
	async detachFromEntry(collection, entryId, taxonomyId) {
		await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).where("taxonomy_id", "=", taxonomyId).execute();
	}
	/**
	* Get all taxonomy terms for a content entry
	*/
	async getTermsForEntry(collection, entryId, taxonomyName) {
		let query = this.db.selectFrom("content_taxonomies").innerJoin("taxonomies", "taxonomies.id", "content_taxonomies.taxonomy_id").selectAll("taxonomies").where("content_taxonomies.collection", "=", collection).where("content_taxonomies.entry_id", "=", entryId);
		if (taxonomyName) query = query.where("taxonomies.name", "=", taxonomyName);
		return (await query.execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Set all taxonomy terms for a content entry (replaces existing)
	* Uses batch operations to avoid N+1 queries.
	*/
	async setTermsForEntry(collection, entryId, taxonomyName, taxonomyIds) {
		const current = await this.getTermsForEntry(collection, entryId, taxonomyName);
		const currentIds = new Set(current.map((t) => t.id));
		const newIds = new Set(taxonomyIds);
		const toRemove = current.filter((t) => !newIds.has(t.id)).map((t) => t.id);
		if (toRemove.length > 0) await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).where("taxonomy_id", "in", toRemove).execute();
		const toAdd = taxonomyIds.filter((id) => !currentIds.has(id));
		if (toAdd.length > 0) await this.db.insertInto("content_taxonomies").values(toAdd.map((taxonomy_id) => ({
			collection,
			entry_id: entryId,
			taxonomy_id
		}))).onConflict((oc) => oc.doNothing()).execute();
	}
	/**
	* Remove all taxonomy associations for an entry (use when entry is deleted)
	*/
	async clearEntryTerms(collection, entryId) {
		const result = await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).executeTakeFirst();
		return Number(result.numDeletedRows ?? 0);
	}
	/**
	* Count entries that have a specific taxonomy term
	*/
	async countEntriesWithTerm(taxonomyId) {
		const result = await this.db.selectFrom("content_taxonomies").select((eb) => eb.fn.count("entry_id").as("count")).where("taxonomy_id", "=", taxonomyId).executeTakeFirst();
		return Number(result?.count || 0);
	}
	/**
	* Convert database row to Taxonomy object
	*/
	rowToTaxonomy(row) {
		return {
			id: row.id,
			name: row.name,
			slug: row.slug,
			label: row.label,
			parentId: row.parent_id,
			data: row.data ? JSON.parse(row.data) : null
		};
	}
};

//#endregion
//#region src/database/repositories/options.ts
function escapeLike(value) {
	return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
}
/**
* Options repository for key-value settings storage
*
* Used for site settings, plugin configuration, and other arbitrary key-value data.
* Values are stored as JSON for flexibility.
*/
var OptionsRepository = class {
	constructor(db) {
		this.db = db;
	}
	/**
	* Get an option value
	*/
	async get(name) {
		const row = await this.db.selectFrom("options").select("value").where("name", "=", name).executeTakeFirst();
		if (!row) return null;
		return JSON.parse(row.value);
	}
	/**
	* Get an option value with a default
	*/
	async getOrDefault(name, defaultValue) {
		return await this.get(name) ?? defaultValue;
	}
	/**
	* Set an option value (creates or updates)
	*/
	async set(name, value) {
		const row = {
			name,
			value: JSON.stringify(value)
		};
		await this.db.insertInto("options").values(row).onConflict((oc) => oc.column("name").doUpdateSet({ value: row.value })).execute();
	}
	/**
	* Delete an option
	*/
	async delete(name) {
		return ((await this.db.deleteFrom("options").where("name", "=", name).executeTakeFirst()).numDeletedRows ?? 0) > 0;
	}
	/**
	* Check if an option exists
	*/
	async exists(name) {
		return !!await this.db.selectFrom("options").select("name").where("name", "=", name).executeTakeFirst();
	}
	/**
	* Get multiple options at once
	*/
	async getMany(names) {
		if (names.length === 0) return /* @__PURE__ */ new Map();
		const rows = await this.db.selectFrom("options").select(["name", "value"]).where("name", "in", names).execute();
		const result = /* @__PURE__ */ new Map();
		for (const row of rows) result.set(row.name, JSON.parse(row.value));
		return result;
	}
	/**
	* Set multiple options at once
	*/
	async setMany(options) {
		const entries = Object.entries(options);
		if (entries.length === 0) return;
		for (const [name, value] of entries) await this.set(name, value);
	}
	/**
	* Get all options (use sparingly)
	*/
	async getAll() {
		const rows = await this.db.selectFrom("options").select(["name", "value"]).execute();
		const result = /* @__PURE__ */ new Map();
		for (const row of rows) result.set(row.name, JSON.parse(row.value));
		return result;
	}
	/**
	* Get all options matching a prefix
	*/
	async getByPrefix(prefix) {
		const pattern = `${escapeLike(prefix)}%`;
		const rows = await this.db.selectFrom("options").select(["name", "value"]).where(sql`name LIKE ${pattern} ESCAPE '\\'`).execute();
		const result = /* @__PURE__ */ new Map();
		for (const row of rows) result.set(row.name, JSON.parse(row.value));
		return result;
	}
	/**
	* Delete all options matching a prefix
	*/
	async deleteByPrefix(prefix) {
		const pattern = `${escapeLike(prefix)}%`;
		const result = await this.db.deleteFrom("options").where(sql`name LIKE ${pattern} ESCAPE '\\'`).executeTakeFirst();
		return Number(result.numDeletedRows ?? 0);
	}
};

//#endregion
//#region src/settings/index.ts
/** Prefix for site settings in the options table */
const SETTINGS_PREFIX = "site:";
/**
* Type guard for MediaReference values
*/
function isMediaReference(value) {
	return typeof value === "object" && value !== null && "mediaId" in value;
}
/**
* Resolve a media reference to include the full URL
*/
async function resolveMediaReference(mediaRef, db, _storage) {
	if (!mediaRef?.mediaId) return mediaRef;
	try {
		const media = await new MediaRepository(db).findById(mediaRef.mediaId);
		if (media) return {
			...mediaRef,
			url: `/_emdash/api/media/file/${media.storageKey}`
		};
	} catch {}
	return mediaRef;
}
/**
* Get a single site setting by key
*
* Returns `undefined` if the setting has not been configured.
* For media settings (logo, favicon), the URL is resolved automatically.
*
* @param key - The setting key (e.g., "title", "logo", "social")
* @returns The setting value, or undefined if not set
*
* @example
* ```ts
* import { getSiteSetting } from "emdash";
*
* const title = await getSiteSetting("title");
* const logo = await getSiteSetting("logo");
* console.log(logo?.url); // Resolved URL
* ```
*/
async function getSiteSetting(key) {
	return getSiteSettingWithDb(key, await getDb());
}
/**
* Get a single site setting by key (with explicit db)
*
* @internal Use `getSiteSetting()` in templates. This variant is for admin routes
* that already have a database handle.
*/
async function getSiteSettingWithDb(key, db, storage = null) {
	const value = await new OptionsRepository(db).get(`${SETTINGS_PREFIX}${key}`);
	if (!value) return;
	if ((key === "logo" || key === "favicon") && isMediaReference(value)) return await resolveMediaReference(value, db, storage);
	return value;
}
/**
* Get all site settings
*
* Returns all configured settings. Unset values are undefined.
* Media references (logo/favicon) are resolved to include URLs.
*
* @example
* ```ts
* import { getSiteSettings } from "emdash";
*
* const settings = await getSiteSettings();
* console.log(settings.title); // "My Site"
* console.log(settings.logo?.url); // "/_emdash/api/media/file/abc123"
* ```
*/
async function getSiteSettings() {
	return getSiteSettingsWithDb(await getDb());
}
/**
* Get all site settings (with explicit db)
*
* @internal Use `getSiteSettings()` in templates. This variant is for admin routes
* that already have a database handle.
*/
async function getSiteSettingsWithDb(db, storage = null) {
	const allOptions = await new OptionsRepository(db).getByPrefix(SETTINGS_PREFIX);
	const settings = {};
	for (const [key, value] of allOptions) {
		const settingKey = key.replace(SETTINGS_PREFIX, "");
		settings[settingKey] = value;
	}
	const typedSettings = settings;
	if (typedSettings.logo) typedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);
	if (typedSettings.favicon) typedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);
	return typedSettings;
}
/**
* Set site settings (internal function used by admin API)
*
* Merges provided settings with existing ones. Only provided fields are updated.
* Media references should include just the mediaId; URLs are resolved on read.
*
* @param settings - Partial settings object with values to update
* @param db - Kysely database instance
* @returns Promise that resolves when settings are saved
*
* @internal
*
* @example
* ```ts
* // Update multiple settings at once
* await setSiteSettings({
*   title: "My Site",
*   tagline: "Welcome",
*   logo: { mediaId: "med_123", alt: "Logo" }
* }, db);
* ```
*/
async function setSiteSettings(settings, db) {
	const options = new OptionsRepository(db);
	const updates = {};
	for (const [key, value] of Object.entries(settings)) if (value !== void 0) updates[`${SETTINGS_PREFIX}${key}`] = value;
	await options.setMany(updates);
}
/**
* Get a single plugin setting by key.
*
* Plugin settings are stored in the options table under
* `plugin:<pluginId>:settings:<key>`.
*/
async function getPluginSetting(pluginId, key) {
	return getPluginSettingWithDb(pluginId, key, await getDb());
}
/**
* Get a single plugin setting by key (with explicit db).
*
* @internal Use `getPluginSetting()` in templates and plugin rendering code.
*/
async function getPluginSettingWithDb(pluginId, key, db) {
	return await new OptionsRepository(db).get(`plugin:${pluginId}:settings:${key}`) ?? void 0;
}
/**
* Get all persisted plugin settings for a plugin.
*
* Defaults declared in `admin.settingsSchema` are not materialized
* automatically; callers should apply their own fallback defaults.
*/
async function getPluginSettings(pluginId) {
	return getPluginSettingsWithDb(pluginId, await getDb());
}
/**
* Get all persisted plugin settings for a plugin (with explicit db).
*
* @internal Use `getPluginSettings()` in templates and plugin rendering code.
*/
async function getPluginSettingsWithDb(pluginId, db) {
	const prefix = `plugin:${pluginId}:settings:`;
	const allOptions = await new OptionsRepository(db).getByPrefix(prefix);
	const settings = {};
	for (const [key, value] of allOptions) {
		if (!key.startsWith(prefix)) continue;
		settings[key.slice(prefix.length)] = value;
	}
	return settings;
}

//#endregion
//#region src/import/ssrf.ts
/**
* SSRF protection for import URLs.
*
* Validates that URLs don't target internal/private network addresses.
* Applied before any fetch() call in the import pipeline.
*/
const IPV4_MAPPED_IPV6_DOTTED_PATTERN = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i;
const IPV4_MAPPED_IPV6_HEX_PATTERN = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
const IPV4_TRANSLATED_HEX_PATTERN = /^::ffff:0:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
const IPV6_EXPANDED_MAPPED_PATTERN = /^0{0,4}:0{0,4}:0{0,4}:0{0,4}:0{0,4}:ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
/**
* IPv4-compatible (deprecated) addresses: ::XXXX:XXXX
*
* The WHATWG URL parser normalizes [::127.0.0.1] to [::7f00:1] (no ffff prefix).
* These are deprecated but still parsed, and bypass the ffff-based checks.
*/
const IPV4_COMPATIBLE_HEX_PATTERN = /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
/**
* NAT64 prefix (RFC 6052): 64:ff9b::XXXX:XXXX
*
* Used by NAT64 gateways to embed IPv4 addresses in IPv6.
* [64:ff9b::127.0.0.1] normalizes to [64:ff9b::7f00:1].
*/
const NAT64_HEX_PATTERN = /^64:ff9b::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
const IPV6_BRACKET_PATTERN = /^\[|\]$/g;
/**
* Private and reserved IP ranges that should never be fetched.
*
* Includes:
* - Loopback (127.0.0.0/8)
* - Private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
* - Link-local (169.254.0.0/16)
* - Cloud metadata (169.254.169.254 — AWS/GCP/Azure)
* - IPv6 loopback and link-local
*/
const BLOCKED_PATTERNS = [
	{
		start: ip4ToNum(127, 0, 0, 0),
		end: ip4ToNum(127, 255, 255, 255)
	},
	{
		start: ip4ToNum(10, 0, 0, 0),
		end: ip4ToNum(10, 255, 255, 255)
	},
	{
		start: ip4ToNum(172, 16, 0, 0),
		end: ip4ToNum(172, 31, 255, 255)
	},
	{
		start: ip4ToNum(192, 168, 0, 0),
		end: ip4ToNum(192, 168, 255, 255)
	},
	{
		start: ip4ToNum(169, 254, 0, 0),
		end: ip4ToNum(169, 254, 255, 255)
	},
	{
		start: ip4ToNum(0, 0, 0, 0),
		end: ip4ToNum(0, 255, 255, 255)
	}
];
const BLOCKED_HOSTNAMES = new Set([
	"localhost",
	"metadata.google.internal",
	"metadata.google",
	"[::1]"
]);
/** Blocked URL schemes */
const ALLOWED_SCHEMES = new Set(["http:", "https:"]);
function ip4ToNum(a, b, c, d) {
	return (a << 24 | b << 16 | c << 8 | d) >>> 0;
}
function parseIpv4(ip) {
	const parts = ip.split(".");
	if (parts.length !== 4) return null;
	const nums = parts.map(Number);
	if (nums.some((n) => isNaN(n) || n < 0 || n > 255)) return null;
	return ip4ToNum(nums[0], nums[1], nums[2], nums[3]);
}
/**
* Convert IPv4-mapped/translated IPv6 addresses from hex form back to IPv4.
*
* The WHATWG URL parser normalizes dotted-decimal to hex:
*   [::ffff:127.0.0.1] -> [::ffff:7f00:1]
*   [::ffff:169.254.169.254] -> [::ffff:a9fe:a9fe]
*
* Without this conversion, the hex forms bypass isPrivateIp() regex checks.
*/
function normalizeIPv6MappedToIPv4(ip) {
	let match = ip.match(IPV4_MAPPED_IPV6_HEX_PATTERN);
	if (!match) match = ip.match(IPV4_TRANSLATED_HEX_PATTERN);
	if (!match) match = ip.match(IPV6_EXPANDED_MAPPED_PATTERN);
	if (!match) match = ip.match(IPV4_COMPATIBLE_HEX_PATTERN);
	if (!match) match = ip.match(NAT64_HEX_PATTERN);
	if (match) {
		const high = parseInt(match[1] ?? "", 16);
		const low = parseInt(match[2] ?? "", 16);
		return `${high >> 8 & 255}.${high & 255}.${low >> 8 & 255}.${low & 255}`;
	}
	return null;
}
function isPrivateIp(ip) {
	if (ip === "::1" || ip === "::ffff:127.0.0.1") return true;
	const hexIpv4 = normalizeIPv6MappedToIPv4(ip);
	if (hexIpv4) return isPrivateIp(hexIpv4);
	const v4Match = ip.match(IPV4_MAPPED_IPV6_DOTTED_PATTERN);
	const num = parseIpv4(v4Match ? v4Match[1] : ip);
	if (num === null) return ip.startsWith("fe80:") || ip.startsWith("fc") || ip.startsWith("fd");
	return BLOCKED_PATTERNS.some((range) => num >= range.start && num <= range.end);
}
/**
* Error thrown when SSRF protection blocks a URL.
*/
var SsrfError = class extends Error {
	code = "SSRF_BLOCKED";
	constructor(message) {
		super(message);
		this.name = "SsrfError";
	}
};
/**
* Validate that a URL is safe to fetch (not targeting internal networks).
*
* Checks:
* 1. URL is well-formed with http/https scheme
* 2. Hostname is not a known internal name (localhost, metadata endpoints)
* 3. If hostname is an IP literal, it's not in a private range
*
* Note: DNS rebinding attacks are not fully mitigated (hostname could resolve
* to a private IP). Full protection requires resolving DNS and checking the IP
* before connecting, which needs a custom fetch implementation. This covers
* the most common SSRF vectors.
*
* @throws SsrfError if the URL targets an internal address
*/
/** Maximum number of redirects to follow in ssrfSafeFetch */
const MAX_REDIRECTS = 5;
function validateExternalUrl(url) {
	let parsed;
	try {
		parsed = new URL(url);
	} catch {
		throw new SsrfError("Invalid URL");
	}
	if (!ALLOWED_SCHEMES.has(parsed.protocol)) throw new SsrfError(`Scheme '${parsed.protocol}' is not allowed`);
	const hostname = parsed.hostname.replace(IPV6_BRACKET_PATTERN, "");
	if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) throw new SsrfError("URLs targeting internal hosts are not allowed");
	if (isPrivateIp(hostname)) throw new SsrfError("URLs targeting private IP addresses are not allowed");
	return parsed;
}
/**
* Fetch a URL with SSRF protection on redirects.
*
* Uses `redirect: "manual"` to intercept redirects and re-validate each
* redirect target against SSRF rules before following it. This prevents
* an attacker from setting up an allowed external URL that redirects to
* an internal IP (e.g. 169.254.169.254 for cloud metadata).
*
* @throws SsrfError if the initial URL or any redirect target is internal
*/
/** Headers that must be stripped when a redirect crosses origins */
const CREDENTIAL_HEADERS = [
	"authorization",
	"cookie",
	"proxy-authorization"
];
async function ssrfSafeFetch(url, init) {
	let currentUrl = url;
	let currentInit = init;
	for (let i = 0; i <= MAX_REDIRECTS; i++) {
		validateExternalUrl(currentUrl);
		const response = await globalThis.fetch(currentUrl, {
			...currentInit,
			redirect: "manual"
		});
		if (response.status < 300 || response.status >= 400) return response;
		const location = response.headers.get("Location");
		if (!location) return response;
		const previousOrigin = new URL(currentUrl).origin;
		currentUrl = new URL(location, currentUrl).href;
		if (previousOrigin !== new URL(currentUrl).origin && currentInit) currentInit = stripCredentialHeaders(currentInit);
	}
	throw new SsrfError(`Too many redirects (max ${MAX_REDIRECTS})`);
}
/**
* Return a copy of init with credential headers removed.
*/
function stripCredentialHeaders(init) {
	if (!init.headers) return init;
	const headers = new Headers(init.headers);
	for (const name of CREDENTIAL_HEADERS) headers.delete(name);
	return {
		...init,
		headers
	};
}

//#endregion
//#region src/seed/apply.ts
/**
* Seed engine - applies seed files to database
*
* This is the core implementation that bootstraps an EmDash site from a seed file.
* Apply order is critical for foreign keys and references.
*/
var apply_exports = /* @__PURE__ */ __exportAll({ applySeed: () => applySeed });
const FILE_EXTENSION_PATTERN = /\.([a-z0-9]+)(?:\?|$)/i;
/** Pattern to remove file extensions */
const EXTENSION_PATTERN = /\.[^.]+$/;
/** Pattern to remove query parameters */
const QUERY_PARAM_PATTERN = /\?.*$/;
/** Pattern to remove non-alphanumeric characters (except dash and underscore) */
const SANITIZE_PATTERN = /[^a-zA-Z0-9_-]/g;
/** Pattern to collapse multiple hyphens */
const MULTIPLE_HYPHENS_PATTERN = /-+/g;
/**
* Apply a seed file to the database
*
* This function is idempotent - safe to run multiple times.
*
* @param db - Kysely database instance
* @param seed - Seed file to apply
* @param options - Application options
* @returns Result summary
*/
async function applySeed(db, seed, options = {}) {
	const validation = validateSeed(seed);
	if (!validation.valid) throw new Error(`Invalid seed file:\n${validation.errors.join("\n")}`);
	const { includeContent = false, storage, skipMediaDownload = false, onConflict = "skip" } = options;
	const result = {
		collections: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		fields: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		taxonomies: {
			created: 0,
			terms: 0
		},
		bylines: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		menus: {
			created: 0,
			items: 0
		},
		redirects: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		widgetAreas: {
			created: 0,
			widgets: 0
		},
		sections: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		settings: { applied: 0 },
		content: {
			created: 0,
			skipped: 0,
			updated: 0
		},
		media: {
			created: 0,
			skipped: 0
		}
	};
	const mediaContext = {
		db,
		storage: storage ?? null,
		skipMediaDownload,
		mediaCache: /* @__PURE__ */ new Map()
	};
	const seedIdMap = /* @__PURE__ */ new Map();
	const seedBylineIdMap = /* @__PURE__ */ new Map();
	if (seed.settings) {
		await setSiteSettings(seed.settings, db);
		result.settings.applied = Object.keys(seed.settings).length;
	}
	if (seed.collections) {
		const registry = new SchemaRegistry(db);
		for (const collection of seed.collections) {
			if (await registry.getCollection(collection.slug)) {
				if (onConflict === "error") throw new Error(`Conflict: collection "${collection.slug}" already exists`);
				if (onConflict === "update") {
					await registry.updateCollection(collection.slug, {
						label: collection.label,
						labelSingular: collection.labelSingular,
						description: collection.description,
						icon: collection.icon,
						supports: collection.supports || [],
						urlPattern: collection.urlPattern,
						commentsEnabled: collection.commentsEnabled
					});
					result.collections.updated++;
					for (const field of collection.fields) if (await registry.getField(collection.slug, field.slug)) {
						await registry.updateField(collection.slug, field.slug, {
							label: field.label,
							required: field.required || false,
							unique: field.unique || false,
							searchable: field.searchable || false,
							defaultValue: field.defaultValue,
							validation: field.validation,
							widget: field.widget,
							options: field.options
						});
						result.fields.updated++;
					} else {
						await registry.createField(collection.slug, {
							slug: field.slug,
							label: field.label,
							type: field.type,
							required: field.required || false,
							unique: field.unique || false,
							searchable: field.searchable || false,
							defaultValue: field.defaultValue,
							validation: field.validation,
							widget: field.widget,
							options: field.options
						});
						result.fields.created++;
					}
					continue;
				}
				result.collections.skipped++;
				result.fields.skipped += collection.fields.length;
				continue;
			}
			await registry.createCollection({
				slug: collection.slug,
				label: collection.label,
				labelSingular: collection.labelSingular,
				description: collection.description,
				icon: collection.icon,
				supports: collection.supports || [],
				source: "seed",
				urlPattern: collection.urlPattern,
				commentsEnabled: collection.commentsEnabled
			});
			result.collections.created++;
			for (const field of collection.fields) {
				await registry.createField(collection.slug, {
					slug: field.slug,
					label: field.label,
					type: field.type,
					required: field.required || false,
					unique: field.unique || false,
					searchable: field.searchable || false,
					defaultValue: field.defaultValue,
					validation: field.validation,
					widget: field.widget,
					options: field.options
				});
				result.fields.created++;
			}
		}
	}
	if (seed.taxonomies) for (const taxonomy of seed.taxonomies) {
		const existingDef = await db.selectFrom("_emdash_taxonomy_defs").selectAll().where("name", "=", taxonomy.name).executeTakeFirst();
		if (existingDef) {
			if (onConflict === "error") throw new Error(`Conflict: taxonomy "${taxonomy.name}" already exists`);
			if (onConflict === "update") await db.updateTable("_emdash_taxonomy_defs").set({
				label: taxonomy.label,
				label_singular: taxonomy.labelSingular ?? null,
				hierarchical: taxonomy.hierarchical ? 1 : 0,
				collections: JSON.stringify(taxonomy.collections)
			}).where("id", "=", existingDef.id).execute();
		} else {
			await db.insertInto("_emdash_taxonomy_defs").values({
				id: ulid(),
				name: taxonomy.name,
				label: taxonomy.label,
				label_singular: taxonomy.labelSingular ?? null,
				hierarchical: taxonomy.hierarchical ? 1 : 0,
				collections: JSON.stringify(taxonomy.collections)
			}).execute();
			result.taxonomies.created++;
		}
		if (taxonomy.terms && taxonomy.terms.length > 0) {
			const termRepo = new TaxonomyRepository(db);
			if (taxonomy.hierarchical) await applyHierarchicalTerms(termRepo, taxonomy.name, taxonomy.terms, result, onConflict);
			else for (const term of taxonomy.terms) {
				const existing = await termRepo.findBySlug(taxonomy.name, term.slug);
				if (existing) {
					if (onConflict === "error") throw new Error(`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" already exists`);
					if (onConflict === "update") {
						await termRepo.update(existing.id, {
							label: term.label,
							data: term.description ? { description: term.description } : {}
						});
						result.taxonomies.terms++;
					}
				} else {
					await termRepo.create({
						name: taxonomy.name,
						slug: term.slug,
						label: term.label,
						data: term.description ? { description: term.description } : void 0
					});
					result.taxonomies.terms++;
				}
			}
		}
	}
	if (seed.bylines) {
		const bylineRepo = new BylineRepository(db);
		for (const byline of seed.bylines) {
			const existing = await bylineRepo.findBySlug(byline.slug);
			if (existing) {
				if (onConflict === "error") throw new Error(`Conflict: byline "${byline.slug}" already exists`);
				if (onConflict === "update") {
					await bylineRepo.update(existing.id, {
						displayName: byline.displayName,
						bio: byline.bio ?? null,
						websiteUrl: byline.websiteUrl ?? null,
						isGuest: byline.isGuest
					});
					seedBylineIdMap.set(byline.id, existing.id);
					result.bylines.updated++;
					continue;
				}
				seedBylineIdMap.set(byline.id, existing.id);
				result.bylines.skipped++;
				continue;
			}
			const created = await bylineRepo.create({
				slug: byline.slug,
				displayName: byline.displayName,
				bio: byline.bio ?? null,
				websiteUrl: byline.websiteUrl ?? null,
				isGuest: byline.isGuest
			});
			seedBylineIdMap.set(byline.id, created.id);
			result.bylines.created++;
		}
	}
	if (includeContent && seed.content) {
		const contentRepo = new ContentRepository(db);
		for (const [collectionSlug, entries] of Object.entries(seed.content)) for (const entry of entries) {
			const existing = await contentRepo.findBySlug(collectionSlug, entry.slug, entry.locale);
			if (existing) {
				if (onConflict === "error") throw new Error(`Conflict: content "${entry.slug}" in "${collectionSlug}" already exists`);
				if (onConflict === "update") {
					const resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
					const status = entry.status || "published";
					await withTransaction(db, async (trx) => {
						const trxContentRepo = new ContentRepository(trx);
						const trxBylineRepo = new BylineRepository(trx);
						await trxContentRepo.update(collectionSlug, existing.id, {
							status,
							data: resolvedData
						});
						await applyContentBylines(trxBylineRepo, collectionSlug, existing.id, entry, seedBylineIdMap, true);
						await applyContentTaxonomies(trx, collectionSlug, existing.id, entry, true);
					});
					seedIdMap.set(entry.id, existing.id);
					result.content.updated++;
					continue;
				}
				result.content.skipped++;
				seedIdMap.set(entry.id, existing.id);
				continue;
			}
			const resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
			let translationOf;
			if (entry.translationOf) {
				const sourceId = seedIdMap.get(entry.translationOf);
				if (!sourceId) console.warn(`content.${collectionSlug}: translationOf "${entry.translationOf}" not found (not yet created or missing). Skipping translation link.`);
				else translationOf = sourceId;
			}
			const status = entry.status || "published";
			const created = await withTransaction(db, async (trx) => {
				const trxContentRepo = new ContentRepository(trx);
				const trxBylineRepo = new BylineRepository(trx);
				const item = await trxContentRepo.create({
					type: collectionSlug,
					slug: entry.slug,
					status,
					data: resolvedData,
					locale: entry.locale,
					translationOf,
					publishedAt: status === "published" ? (/* @__PURE__ */ new Date()).toISOString() : null
				});
				await applyContentBylines(trxBylineRepo, collectionSlug, item.id, entry, seedBylineIdMap);
				await applyContentTaxonomies(trx, collectionSlug, item.id, entry, false);
				return item;
			});
			seedIdMap.set(entry.id, created.id);
			result.content.created++;
		}
	}
	if (seed.menus) for (const menu of seed.menus) {
		const existingMenu = await db.selectFrom("_emdash_menus").selectAll().where("name", "=", menu.name).executeTakeFirst();
		let menuId;
		if (existingMenu) {
			menuId = existingMenu.id;
			await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
		} else {
			menuId = ulid();
			await db.insertInto("_emdash_menus").values({
				id: menuId,
				name: menu.name,
				label: menu.label,
				created_at: (/* @__PURE__ */ new Date()).toISOString(),
				updated_at: (/* @__PURE__ */ new Date()).toISOString()
			}).execute();
			result.menus.created++;
		}
		const itemCount = await applyMenuItems(db, menuId, menu.items, null, 0, seedIdMap);
		result.menus.items += itemCount;
	}
	if (seed.redirects) {
		const redirectRepo = new RedirectRepository(db);
		for (const redirect of seed.redirects) {
			const existing = await redirectRepo.findBySource(redirect.source);
			if (existing) {
				if (onConflict === "error") throw new Error(`Conflict: redirect "${redirect.source}" already exists`);
				if (onConflict === "update") {
					await redirectRepo.update(existing.id, {
						destination: redirect.destination,
						type: redirect.type,
						enabled: redirect.enabled,
						groupName: redirect.groupName
					});
					result.redirects.updated++;
					continue;
				}
				result.redirects.skipped++;
				continue;
			}
			await redirectRepo.create({
				source: redirect.source,
				destination: redirect.destination,
				type: redirect.type,
				enabled: redirect.enabled,
				groupName: redirect.groupName
			});
			result.redirects.created++;
		}
	}
	if (seed.widgetAreas) for (const area of seed.widgetAreas) {
		const existingArea = await db.selectFrom("_emdash_widget_areas").selectAll().where("name", "=", area.name).executeTakeFirst();
		let areaId;
		if (existingArea) {
			areaId = existingArea.id;
			await db.deleteFrom("_emdash_widgets").where("area_id", "=", areaId).execute();
		} else {
			areaId = ulid();
			await db.insertInto("_emdash_widget_areas").values({
				id: areaId,
				name: area.name,
				label: area.label,
				description: area.description ?? null
			}).execute();
			result.widgetAreas.created++;
		}
		for (let i = 0; i < area.widgets.length; i++) {
			const widget = area.widgets[i];
			await applyWidget(db, areaId, widget, i);
			result.widgetAreas.widgets++;
		}
	}
	if (seed.sections) for (const section of seed.sections) {
		const existing = await db.selectFrom("_emdash_sections").select("id").where("slug", "=", section.slug).executeTakeFirst();
		if (existing) {
			if (onConflict === "error") throw new Error(`Conflict: section "${section.slug}" already exists`);
			if (onConflict === "update") {
				await db.updateTable("_emdash_sections").set({
					title: section.title,
					description: section.description ?? null,
					keywords: section.keywords ? JSON.stringify(section.keywords) : null,
					content: JSON.stringify(section.content),
					source: section.source || "theme",
					updated_at: (/* @__PURE__ */ new Date()).toISOString()
				}).where("id", "=", existing.id).execute();
				result.sections.updated++;
				continue;
			}
			result.sections.skipped++;
			continue;
		}
		const id = ulid();
		const now = (/* @__PURE__ */ new Date()).toISOString();
		await db.insertInto("_emdash_sections").values({
			id,
			slug: section.slug,
			title: section.title,
			description: section.description ?? null,
			keywords: section.keywords ? JSON.stringify(section.keywords) : null,
			content: JSON.stringify(section.content),
			preview_media_id: null,
			source: section.source || "theme",
			theme_id: section.source === "theme" ? section.slug : null,
			created_at: now,
			updated_at: now
		}).execute();
		result.sections.created++;
	}
	if (seed.collections) {
		const ftsManager = new FTSManager(db);
		for (const collection of seed.collections) if (collection.supports?.includes("search")) {
			if ((await ftsManager.getSearchableFields(collection.slug)).length > 0) try {
				await ftsManager.enableSearch(collection.slug);
			} catch (err) {
				console.warn(`Failed to enable search for ${collection.slug}:`, err);
			}
		}
	}
	const { invalidateBylineCache } = await import("./bylines-C_Wsnz4L.mjs").then((n) => n.t);
	const { invalidateRedirectCache } = await import("./cache-E3Dts-yT.mjs").then((n) => n.t);
	const { invalidateUrlPatternCache } = await import("./query-B6Vu0d2i.mjs").then((n) => n.o);
	invalidateBylineCache();
	invalidateRedirectCache();
	invalidateUrlPatternCache();
	return result;
}
/**
* Apply hierarchical taxonomy terms (parents before children)
*/
async function applyHierarchicalTerms(termRepo, taxonomyName, terms, result, onConflict = "skip") {
	const slugToId = /* @__PURE__ */ new Map();
	let remaining = [...terms];
	let maxPasses = 10;
	while (remaining.length > 0 && maxPasses > 0) {
		const processedThisPass = [];
		for (const term of remaining) if (!term.parent || slugToId.has(term.parent)) {
			const parentId = term.parent ? slugToId.get(term.parent) : void 0;
			const existing = await termRepo.findBySlug(taxonomyName, term.slug);
			if (existing) {
				if (onConflict === "error") throw new Error(`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" already exists`);
				if (onConflict === "update") {
					await termRepo.update(existing.id, {
						label: term.label,
						parentId,
						data: term.description ? { description: term.description } : {}
					});
					result.taxonomies.terms++;
				}
				slugToId.set(term.slug, existing.id);
			} else {
				const created = await termRepo.create({
					name: taxonomyName,
					slug: term.slug,
					label: term.label,
					parentId,
					data: term.description ? { description: term.description } : void 0
				});
				slugToId.set(term.slug, created.id);
				result.taxonomies.terms++;
			}
			processedThisPass.push(term.slug);
		}
		remaining = remaining.filter((t) => !processedThisPass.includes(t.slug));
		maxPasses--;
	}
	if (remaining.length > 0) console.warn(`Could not process ${remaining.length} terms due to missing parents`);
}
/**
* Apply byline credits to a content entry.
* In update mode, clears existing credits even if the seed has none.
*/
async function applyContentBylines(bylineRepo, collectionSlug, contentId, entry, seedBylineIdMap, isUpdate = false) {
	if (!entry.bylines || entry.bylines.length === 0) {
		if (isUpdate) await bylineRepo.setContentBylines(collectionSlug, contentId, []);
		return;
	}
	const credits = entry.bylines.map((credit) => {
		const bylineId = seedBylineIdMap.get(credit.byline);
		if (!bylineId) return null;
		return {
			bylineId,
			roleLabel: credit.roleLabel ?? null
		};
	}).filter((credit) => Boolean(credit));
	if (credits.length !== entry.bylines.length) console.warn(`content.${collectionSlug}.${entry.slug}: one or more byline refs could not be resolved`);
	if (credits.length > 0 || isUpdate) await bylineRepo.setContentBylines(collectionSlug, contentId, credits);
}
/**
* Apply taxonomy term assignments to a content entry.
* In update mode, clears existing assignments before re-attaching.
*/
async function applyContentTaxonomies(db, collectionSlug, contentId, entry, isUpdate) {
	if (isUpdate) await db.deleteFrom("content_taxonomies").where("collection", "=", collectionSlug).where("entry_id", "=", contentId).execute();
	if (!entry.taxonomies) return;
	for (const [taxonomyName, termSlugs] of Object.entries(entry.taxonomies)) {
		const termRepo = new TaxonomyRepository(db);
		for (const termSlug of termSlugs) {
			const term = await termRepo.findBySlug(taxonomyName, termSlug);
			if (term) await termRepo.attachToEntry(collectionSlug, contentId, term.id);
		}
	}
}
/**
* Apply menu items recursively
*/
async function applyMenuItems(db, menuId, items, parentId, startOrder, seedIdMap) {
	let count = 0;
	let order = startOrder;
	for (const item of items) {
		const itemId = ulid();
		let referenceId = null;
		let referenceCollection = null;
		if (item.type === "page" || item.type === "post") {
			if (item.ref && seedIdMap.has(item.ref)) {
				referenceId = seedIdMap.get(item.ref);
				referenceCollection = item.collection || `${item.type}s`;
			}
		}
		await db.insertInto("_emdash_menu_items").values({
			id: itemId,
			menu_id: menuId,
			parent_id: parentId,
			sort_order: order,
			type: item.type,
			reference_collection: referenceCollection,
			reference_id: referenceId,
			custom_url: item.url ?? null,
			label: item.label || "",
			title_attr: item.titleAttr ?? null,
			target: item.target ?? null,
			css_classes: item.cssClasses ?? null,
			created_at: (/* @__PURE__ */ new Date()).toISOString()
		}).execute();
		count++;
		order++;
		if (item.children && item.children.length > 0) {
			const childCount = await applyMenuItems(db, menuId, item.children, itemId, 0, seedIdMap);
			count += childCount;
		}
	}
	return count;
}
/**
* Apply a widget
*/
async function applyWidget(db, areaId, widget, sortOrder) {
	await db.insertInto("_emdash_widgets").values({
		id: ulid(),
		area_id: areaId,
		sort_order: sortOrder,
		type: widget.type,
		title: widget.title ?? null,
		content: widget.content ? JSON.stringify(widget.content) : null,
		menu_name: widget.menuName ?? null,
		component_id: widget.componentId ?? null,
		component_props: widget.props ? JSON.stringify(widget.props) : null
	}).execute();
}
/**
* Type guard for $media reference
*/
function isSeedMediaReference(value) {
	if (typeof value !== "object" || value === null || !("$media" in value)) return false;
	const media = value.$media;
	return typeof media === "object" && media !== null && "url" in media && typeof media.url === "string";
}
/**
* Resolve $ref: and $media references in content data
*/
async function resolveReferences(data, seedIdMap, mediaContext, result) {
	const resolved = {};
	for (const [key, value] of Object.entries(data)) resolved[key] = await resolveValue(value, seedIdMap, mediaContext, result);
	return resolved;
}
/**
* Resolve a single value recursively
*/
async function resolveValue(value, seedIdMap, mediaContext, result) {
	if (typeof value === "string" && value.startsWith("$ref:")) {
		const seedId = value.slice(5);
		return seedIdMap.get(seedId) ?? value;
	}
	if (isSeedMediaReference(value)) return resolveMedia(value, mediaContext, result);
	if (Array.isArray(value)) return Promise.all(value.map((item) => resolveValue(item, seedIdMap, mediaContext, result)));
	if (typeof value === "object" && value !== null) {
		const resolved = {};
		for (const [k, v] of Object.entries(value)) resolved[k] = await resolveValue(v, seedIdMap, mediaContext, result);
		return resolved;
	}
	return value;
}
/**
* Resolve a $media reference by downloading and uploading the media
*/
async function resolveMedia(ref, ctx, result) {
	const { url, alt, filename, caption } = ref.$media;
	const cached = ctx.mediaCache.get(url);
	if (cached) {
		result.media.skipped++;
		return {
			...cached,
			alt: alt ?? cached.alt
		};
	}
	if (ctx.skipMediaDownload) {
		const mediaValue = {
			provider: "external",
			id: ulid(),
			src: url,
			alt: alt ?? void 0,
			filename: filename ?? void 0
		};
		ctx.mediaCache.set(url, mediaValue);
		result.media.created++;
		return mediaValue;
	}
	if (!ctx.storage) {
		console.warn(`Skipping $media reference (no storage configured): ${url}`);
		result.media.skipped++;
		return null;
	}
	try {
		validateExternalUrl(url);
		console.log(`  📥 Downloading: ${url}`);
		const response = await ssrfSafeFetch(url, { headers: { "User-Agent": "EmDash-CMS/1.0" } });
		if (!response.ok) {
			console.warn(`  ⚠️ Failed to download ${url}: ${response.status}`);
			result.media.skipped++;
			return null;
		}
		const contentType = response.headers.get("content-type") || "application/octet-stream";
		const ext = getExtensionFromContentType(contentType) || getExtensionFromUrl(url) || ".bin";
		const id = ulid();
		const finalFilename = filename || generateFilename(url, ext);
		const storageKey = `${id}${ext}`;
		const arrayBuffer = await response.arrayBuffer();
		const body = new Uint8Array(arrayBuffer);
		let width;
		let height;
		if (contentType.startsWith("image/")) {
			const dimensions = getImageDimensions(body);
			width = dimensions?.width;
			height = dimensions?.height;
		}
		await ctx.storage.upload({
			key: storageKey,
			body,
			contentType
		});
		await new MediaRepository(ctx.db).create({
			filename: finalFilename,
			mimeType: contentType,
			size: body.length,
			width,
			height,
			alt,
			caption,
			storageKey,
			status: "ready"
		});
		const mediaValue = {
			provider: "local",
			id,
			alt: alt ?? void 0,
			width,
			height,
			mimeType: contentType,
			filename: finalFilename,
			meta: { storageKey }
		};
		ctx.mediaCache.set(url, mediaValue);
		result.media.created++;
		console.log(`  ✅ Uploaded: ${finalFilename}`);
		return mediaValue;
	} catch (error) {
		console.warn(`  ⚠️ Error processing $media ${url}:`, error instanceof Error ? error.message : error);
		result.media.skipped++;
		return null;
	}
}
/**
* Get file extension from content type
*/
function getExtensionFromContentType(contentType) {
	const baseMime = contentType.split(";")[0].trim();
	const ext = mime.getExtension(baseMime);
	return ext ? `.${ext}` : null;
}
/**
* Get file extension from URL
*/
function getExtensionFromUrl(url) {
	try {
		const match = new URL(url).pathname.match(FILE_EXTENSION_PATTERN);
		return match ? `.${match[1]}` : null;
	} catch {
		return null;
	}
}
/**
* Generate a filename from URL
*/
function generateFilename(url, ext) {
	try {
		return `${(new URL(url).pathname.split("/").pop() || "media").replace(EXTENSION_PATTERN, "").replace(QUERY_PARAM_PATTERN, "").replace(SANITIZE_PATTERN, "-").replace(MULTIPLE_HYPHENS_PATTERN, "-") || "media"}${ext}`;
	} catch {
		return `media${ext}`;
	}
}
/**
* Get image dimensions from buffer using image-size.
* Supports PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, and more.
*/
function getImageDimensions(buffer) {
	try {
		const result = imageSize(buffer);
		if (result.width != null && result.height != null) return {
			width: result.width,
			height: result.height
		};
		return null;
	} catch {
		return null;
	}
}

//#endregion
export { stripCredentialHeaders as a, getPluginSettings as c, setSiteSettings as d, OptionsRepository as f, ssrfSafeFetch as i, getSiteSetting as l, apply_exports as n, validateExternalUrl as o, TaxonomyRepository as p, SsrfError as r, getPluginSetting as s, applySeed as t, getSiteSettings as u };
//# sourceMappingURL=apply-Cma_PiF6.mjs.map