import "../connection-B4zVnQIa.mjs";
import { a as isSqlite } from "../dialect-helpers-B9uSp2GJ.mjs";
import { r as runMigrations } from "../runner-BraqvGYk.mjs";
import { $ as definePlugin, At as handleContentGetIncludingTrashed, Bt as handleContentUpdate, Ct as handleContentCountScheduled, Dt as handleContentDiscardDraft, Et as handleContentDelete, Ft as handleContentRestore, G as devConsoleEmailDeliver, It as handleContentSchedule, J as createHookPipeline, K as EmailPipeline, Lt as handleContentTranslations, Mt as handleContentListTrashed, Nt as handleContentPermanentDelete, Ot as handleContentDuplicate, Pt as handleContentPublish, Q as sanitizeHeadersForSandbox, Rt as handleContentUnpublish, St as handleContentCompare, Tt as handleContentCreate, U as PluginRouteRegistry, Vt as validateRev, W as DEV_CONSOLE_EMAIL_PLUGIN_ID, X as CronExecutor, Y as resolveExclusiveHooks, Z as extractRequestMeta, _t as handleRevisionList, dt as handleMediaCreate, ft as handleMediaDelete, gt as handleRevisionGet, ht as handleMediaUpdate, jt as handleContentList, kt as handleContentGet, lt as PluginStateRepository, mt as handleMediaList, ot as loadBundleFromR2, pt as handleMediaGet, vt as handleRevisionRestore, wt as handleContentCountTrashed, xt as hashString, zt as handleContentUnschedule } from "../search-C1gg67nN.mjs";
import { r as RevisionRepository } from "../content-D6C2WsZC.mjs";
import "../base64-MBPo9ozB.mjs";
import "../types-CMMN0pNg.mjs";
import { t as MediaRepository } from "../media-DqHVh136.mjs";
import { u as OptionsRepository } from "../apply-kC39ev1Z.mjs";
import { i as FTSManager, n as SchemaRegistry } from "../registry-BNYQKX_d.mjs";
import "../redirect-DIfIni3r.mjs";
import "../byline-CL847F26.mjs";
import { n as normalizeMediaValue } from "../placeholder-aiCD8aSZ.mjs";
import { a as setI18nConfig } from "../config-CKE8p9xM.mjs";
import { getRequestContext, runWithContext } from "../request-context.mjs";
import { n as getDb } from "../loader-fz8Q_3EO.mjs";
import { r as normalizeManifestRoute } from "../manifest-schema-CL8DWO9b.mjs";
import "../query-BVYN0PJ6.mjs";
import "../tokens-DpgrkrXK.mjs";
import "../bylines-C2a-2TGt.mjs";
import "../load-yOOlckBj.mjs";
import "../index.mjs";
import { t as getAuthMode } from "../mode-C2EzN1uE.mjs";
import { Kysely, sql } from "kysely";
import { defineMiddleware } from "astro:middleware";
import virtualConfig from "virtual:emdash/config";
import { createDialect, createSessionDialect, getBookmarkCookieName, getD1Binding, getDefaultConstraint, isSessionEnabled } from "virtual:emdash/dialect";
import { mediaProviders } from "virtual:emdash/media-providers";
import { plugins } from "virtual:emdash/plugins";
import { createSandboxRunner, sandboxEnabled } from "virtual:emdash/sandbox-runner";
import { sandboxedPlugins } from "virtual:emdash/sandboxed-plugins";
import { createStorage } from "virtual:emdash/storage";
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";

//#region src/auth/challenge-store.ts
/**
* Clean up expired challenges.
* Should be called periodically (e.g., on startup, or via cron).
*/
async function cleanupExpiredChallenges(db) {
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const result = await db.deleteFrom("auth_challenges").where("expires_at", "<", now).executeTakeFirst();
	return Number(result.numDeletedRows ?? 0);
}

//#endregion
//#region src/cleanup.ts
/**
* System cleanup
*
* Runs periodic maintenance tasks that prevent unbounded accumulation of
* expired or stale data. Called from cron scheduler ticks and (for latency-
* sensitive subsystems) inline during relevant requests.
*
* Each subsystem cleanup is independent and non-fatal -- if one fails, the
* rest still run. Failures are logged but never surface to callers.
*/
/** Max revisions to keep per entry during periodic pruning */
const REVISION_KEEP_COUNT = 50;
/** Only prune entries that exceed this threshold */
const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;
/**
* Run all system cleanup tasks.
*
* Safe to call frequently -- each task is a single DELETE with a WHERE clause,
* so repeated calls with nothing to clean are cheap (no-op queries).
*
* @param db - The database instance
* @param storage - Optional storage backend for deleting orphaned files.
*   When omitted, pending upload DB rows are still deleted but the
*   corresponding files in object storage are not removed.
*/
async function runSystemCleanup(db, storage) {
	const result = {
		challenges: -1,
		expiredTokens: -1,
		pendingUploads: -1,
		pendingUploadFiles: -1,
		revisionsPruned: -1
	};
	try {
		result.challenges = await cleanupExpiredChallenges(db);
	} catch (error) {
		console.error("[cleanup] Failed to clean expired challenges:", error);
	}
	try {
		await createKyselyAdapter(db).deleteExpiredTokens();
		result.expiredTokens = 0;
	} catch (error) {
		console.error("[cleanup] Failed to clean expired tokens:", error);
	}
	try {
		const orphanedKeys = await new MediaRepository(db).cleanupPendingUploads();
		result.pendingUploads = orphanedKeys.length;
		if (storage && orphanedKeys.length > 0) {
			let filesDeleted = 0;
			for (const key of orphanedKeys) try {
				await storage.delete(key);
				filesDeleted++;
			} catch (error) {
				console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
			}
			result.pendingUploadFiles = filesDeleted;
		} else result.pendingUploadFiles = 0;
	} catch (error) {
		console.error("[cleanup] Failed to clean pending uploads:", error);
	}
	try {
		result.revisionsPruned = await pruneExcessiveRevisions(db);
	} catch (error) {
		console.error("[cleanup] Failed to prune revisions:", error);
	}
	return result;
}
/**
* Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune
* them down to REVISION_KEEP_COUNT.
*/
async function pruneExcessiveRevisions(db) {
	const entries = await sql`
		SELECT collection, entry_id, COUNT(*) as cnt
		FROM revisions
		GROUP BY collection, entry_id
		HAVING cnt > ${REVISION_PRUNE_THRESHOLD}
	`.execute(db);
	if (entries.rows.length === 0) return 0;
	const revisionRepo = new RevisionRepository(db);
	let totalPruned = 0;
	for (const row of entries.rows) try {
		const pruned = await revisionRepo.pruneOldRevisions(row.collection, row.entry_id, REVISION_KEEP_COUNT);
		totalPruned += pruned;
	} catch (error) {
		console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
	}
	return totalPruned;
}

//#endregion
//#region src/comments/moderator.ts
/** Plugin ID for the built-in default comment moderator */
const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = "emdash-default-comment-moderator";
/**
* The comment:moderate handler for the built-in default moderator.
*/
async function defaultCommentModerate(event, _ctx) {
	const { comment, collectionSettings, priorApprovedCount } = event;
	if (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) return {
		status: "approved",
		reason: "Authenticated CMS user"
	};
	if (collectionSettings.commentsModeration === "none") return {
		status: "approved",
		reason: "Moderation disabled"
	};
	if (collectionSettings.commentsModeration === "first_time" && priorApprovedCount > 0) return {
		status: "approved",
		reason: "Returning commenter"
	};
	return {
		status: "pending",
		reason: "Held for review"
	};
}

//#endregion
//#region src/plugins/scheduler/node.ts
/** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */
const MIN_INTERVAL_MS = 1e3;
/** Maximum polling interval (ms) — wake up periodically to check for stale locks */
const MAX_INTERVAL_MS = 300 * 1e3;
var NodeCronScheduler = class {
	timer = null;
	running = false;
	systemCleanup = null;
	constructor(executor) {
		this.executor = executor;
	}
	setSystemCleanup(fn) {
		this.systemCleanup = fn;
	}
	start() {
		this.running = true;
		this.arm();
	}
	stop() {
		this.running = false;
		if (this.timer) {
			clearTimeout(this.timer);
			this.timer = null;
		}
	}
	reschedule() {
		if (!this.running) return;
		if (this.timer) {
			clearTimeout(this.timer);
			this.timer = null;
		}
		this.arm();
	}
	arm() {
		if (!this.running) return;
		this.executor.getNextDueTime().then((nextDue) => {
			if (!this.running) return void 0;
			let delayMs;
			if (nextDue) {
				const dueAt = new Date(nextDue).getTime();
				delayMs = Math.max(dueAt - Date.now(), MIN_INTERVAL_MS);
				delayMs = Math.min(delayMs, MAX_INTERVAL_MS);
			} else delayMs = MAX_INTERVAL_MS;
			this.timer = setTimeout(() => {
				if (!this.running) return;
				this.executeTick();
			}, delayMs);
			if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
		}).catch((error) => {
			console.error("[cron:node] Failed to get next due time:", error);
			if (this.running) {
				this.timer = setTimeout(() => this.arm(), MAX_INTERVAL_MS);
				if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
			}
		});
	}
	executeTick() {
		if (!this.running) return;
		const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
		if (this.systemCleanup) tasks.push(this.systemCleanup());
		Promise.allSettled(tasks).then((results) => {
			for (const r of results) if (r.status === "rejected") console.error("[cron:node] Tick task failed:", r.reason);
		}).finally(() => {
			if (this.running) this.arm();
		});
	}
};

//#endregion
//#region src/plugins/scheduler/piggyback.ts
/** Minimum interval between tick attempts (ms) */
const DEBOUNCE_MS = 60 * 1e3;
var PiggybackScheduler = class {
	lastTickAt = 0;
	running = false;
	systemCleanup = null;
	constructor(executor) {
		this.executor = executor;
	}
	setSystemCleanup(fn) {
		this.systemCleanup = fn;
	}
	start() {
		this.running = true;
	}
	stop() {
		this.running = false;
	}
	/**
	* No-op for piggyback — tick happens on next request.
	*/
	reschedule() {}
	/**
	* Call this from middleware on each request.
	* Debounced: only actually ticks if enough time has passed.
	*/
	onRequest() {
		if (!this.running) return;
		const now = Date.now();
		if (now - this.lastTickAt < DEBOUNCE_MS) return;
		this.lastTickAt = now;
		const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
		if (this.systemCleanup) tasks.push(this.systemCleanup());
		Promise.allSettled(tasks).then((results) => {
			for (const r of results) if (r.status === "rejected") console.error("[cron:piggyback] Tick task failed:", r.reason);
		});
	}
};

//#endregion
//#region src/emdash-runtime.ts
const LEADING_SLASH_PATTERN = /^\//;
const VALID_METADATA_KINDS = new Set([
	"meta",
	"property",
	"link",
	"jsonld"
]);
/** Security-critical allowlist for link rel values from sandboxed plugins */
const VALID_LINK_REL = new Set([
	"canonical",
	"alternate",
	"author",
	"license",
	"site.standard.document"
]);
/**
* Runtime validation for sandboxed plugin metadata contributions.
* Sandboxed plugins return `unknown` across the RPC boundary — we must
* verify the shape before passing to the metadata collector.
*/
function isValidMetadataContribution(c) {
	if (!c || typeof c !== "object" || !("kind" in c)) return false;
	const obj = c;
	if (typeof obj.kind !== "string" || !VALID_METADATA_KINDS.has(obj.kind)) return false;
	switch (obj.kind) {
		case "meta": return typeof obj.name === "string" && typeof obj.content === "string";
		case "property": return typeof obj.property === "string" && typeof obj.content === "string";
		case "link": return typeof obj.href === "string" && typeof obj.rel === "string" && VALID_LINK_REL.has(obj.rel);
		case "jsonld": return obj.graph != null && typeof obj.graph === "object";
		default: return false;
	}
}
/**
* Map schema field types to editor field kinds
*/
const FIELD_TYPE_TO_KIND = {
	string: "string",
	slug: "string",
	text: "richText",
	number: "number",
	integer: "number",
	boolean: "boolean",
	datetime: "datetime",
	select: "select",
	multiSelect: "multiSelect",
	portableText: "portableText",
	image: "image",
	file: "file",
	reference: "reference",
	json: "json",
	repeater: "repeater"
};
/**
* Convert a ContentItem to Record<string, unknown> for hook consumption.
* Hooks receive the full item as a flat record.
*/
function contentItemToRecord(item) {
	return { ...item };
}
const dbCache = /* @__PURE__ */ new Map();
let dbInitPromise = null;
const storageCache = /* @__PURE__ */ new Map();
const sandboxedPluginCache = /* @__PURE__ */ new Map();
const marketplacePluginKeys = /* @__PURE__ */ new Set();
/** Manifest metadata for marketplace plugins: pluginId -> manifest admin config */
const marketplaceManifestCache = /* @__PURE__ */ new Map();
/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
let sandboxRunner = null;
/**
* EmDashRuntime - singleton per worker
*/
var EmDashRuntime = class EmDashRuntime {
	/**
	* The singleton database instance (worker-lifetime cached).
	* Use the `db` getter instead — it checks the request context first
	* for per-request overrides (D1 read replica sessions, DO multi-site).
	*/
	_db;
	storage;
	configuredPlugins;
	sandboxedPlugins;
	sandboxedPluginEntries;
	schemaRegistry;
	_hooks;
	config;
	mediaProviders;
	mediaProviderEntries;
	cronExecutor;
	email;
	cronScheduler;
	enabledPlugins;
	pluginStates;
	/** Current hook pipeline. Use the `hooks` getter for external access. */
	get hooks() {
		return this._hooks;
	}
	/** All plugins eligible for the hook pipeline (includes built-in plugins).
	*  Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
	allPipelinePlugins;
	/** Factory options for the hook pipeline context factory */
	pipelineFactoryOptions;
	/** Dependencies needed for exclusive hook resolution */
	runtimeDeps;
	/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
	pipelineRef;
	/**
	* Get the database instance for the current request.
	*
	* Checks the ALS-based request context first — middleware sets a
	* per-request Kysely instance there for D1 read replica sessions
	* or DO preview databases. Falls back to the singleton instance.
	*/
	get db() {
		const ctx = getRequestContext();
		if (ctx?.db) return ctx.db;
		return this._db;
	}
	constructor(db, storage, configuredPlugins, sandboxedPlugins, sandboxedPluginEntries, hooks, enabledPlugins, pluginStates, config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, runtimeDeps, pipelineRef) {
		this._db = db;
		this.storage = storage;
		this.configuredPlugins = configuredPlugins;
		this.sandboxedPlugins = sandboxedPlugins;
		this.sandboxedPluginEntries = sandboxedPluginEntries;
		this.schemaRegistry = new SchemaRegistry(db);
		this._hooks = hooks;
		this.enabledPlugins = enabledPlugins;
		this.pluginStates = pluginStates;
		this.config = config;
		this.mediaProviders = mediaProviders;
		this.mediaProviderEntries = mediaProviderEntries;
		this.cronExecutor = cronExecutor;
		this.cronScheduler = cronScheduler;
		this.email = emailPipeline;
		this.allPipelinePlugins = allPipelinePlugins;
		this.pipelineFactoryOptions = pipelineFactoryOptions;
		this.runtimeDeps = runtimeDeps;
		this.pipelineRef = pipelineRef;
	}
	/**
	* Get the sandbox runner instance (for marketplace install/update)
	*/
	getSandboxRunner() {
		return sandboxRunner;
	}
	/**
	* Tick the cron system from request context (piggyback mode).
	* Call this from middleware on each request to ensure cron tasks
	* execute even when no dedicated scheduler is available.
	*/
	tickCron() {
		if (this.cronScheduler instanceof PiggybackScheduler) this.cronScheduler.onRequest();
	}
	/**
	* Stop the cron scheduler gracefully.
	* Call during worker shutdown or hot-reload.
	*/
	async stopCron() {
		if (this.cronScheduler) await this.cronScheduler.stop();
	}
	/**
	* Update in-memory plugin status and rebuild the hook pipeline.
	*
	* Rebuilding the pipeline ensures disabled plugins' hooks stop firing
	* and re-enabled plugins' hooks start firing again without a restart.
	* Exclusive hook selections are re-resolved after each rebuild.
	*/
	async setPluginStatus(pluginId, status) {
		this.pluginStates.set(pluginId, status);
		if (status === "active") this.enabledPlugins.add(pluginId);
		else this.enabledPlugins.delete(pluginId);
		await this.rebuildHookPipeline();
	}
	/**
	* Rebuild the hook pipeline from the current set of enabled plugins.
	*
	* Filters `allPipelinePlugins` to only those in `enabledPlugins`,
	* creates a fresh HookPipeline, re-resolves exclusive hook selections,
	* and re-wires the context factory so existing references (cron
	* callbacks, email pipeline) use the new pipeline.
	*/
	async rebuildHookPipeline() {
		const newPipeline = createHookPipeline(this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id)), this.pipelineFactoryOptions);
		await EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);
		if (this.email) newPipeline.setContextFactory({
			db: this.db,
			emailPipeline: this.email
		});
		if (this.cronScheduler) {
			const scheduler = this.cronScheduler;
			newPipeline.setContextFactory({ cronReschedule: () => scheduler.reschedule() });
		}
		if (this.email) this.email.setPipeline(newPipeline);
		this.pipelineRef.current = newPipeline;
		this._hooks = newPipeline;
	}
	/**
	* Synchronize marketplace plugin runtime state with DB + storage.
	*
	* Ensures install/update/uninstall changes take effect immediately in the
	* current worker: loads newly active plugins and removes uninstalled ones.
	*/
	async syncMarketplacePlugins() {
		if (!this.config.marketplace || !this.storage) return;
		if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
		try {
			const marketplaceStates = await new PluginStateRepository(this.db).getMarketplacePlugins();
			const desired = /* @__PURE__ */ new Map();
			for (const state of marketplaceStates) {
				this.pluginStates.set(state.pluginId, state.status);
				if (state.status === "active") this.enabledPlugins.add(state.pluginId);
				else this.enabledPlugins.delete(state.pluginId);
				if (state.status !== "active") continue;
				desired.set(state.pluginId, state.marketplaceVersion ?? state.version);
			}
			const keysToRemove = [];
			for (const key of marketplacePluginKeys) {
				const [pluginId] = key.split(":");
				if (!pluginId) continue;
				const desiredVersion = desired.get(pluginId);
				if (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;
				keysToRemove.push(key);
			}
			for (const key of keysToRemove) {
				const [pluginId] = key.split(":");
				if (!pluginId) continue;
				if (!desired.get(pluginId)) {
					this.pluginStates.delete(pluginId);
					this.enabledPlugins.delete(pluginId);
				}
				const existing = sandboxedPluginCache.get(key);
				if (existing) try {
					await existing.terminate();
				} catch (error) {
					console.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);
				}
				sandboxedPluginCache.delete(key);
				this.sandboxedPlugins.delete(key);
				marketplacePluginKeys.delete(key);
				if (pluginId) {
					sandboxedRouteMetaCache.delete(pluginId);
					marketplaceManifestCache.delete(pluginId);
				}
			}
			for (const [pluginId, version] of desired) {
				const key = `${pluginId}:${version}`;
				if (sandboxedPluginCache.has(key)) {
					marketplacePluginKeys.add(key);
					continue;
				}
				const bundle = await loadBundleFromR2(this.storage, pluginId, version);
				if (!bundle) {
					console.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);
					continue;
				}
				const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
				sandboxedPluginCache.set(key, loaded);
				this.sandboxedPlugins.set(key, loaded);
				marketplacePluginKeys.add(key);
				marketplaceManifestCache.set(pluginId, {
					id: bundle.manifest.id,
					version: bundle.manifest.version,
					admin: bundle.manifest.admin
				});
				if (bundle.manifest.routes.length > 0) {
					const routeMetaMap = /* @__PURE__ */ new Map();
					for (const entry of bundle.manifest.routes) {
						const normalized = normalizeManifestRoute(entry);
						routeMetaMap.set(normalized.name, { public: normalized.public === true });
					}
					sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
				} else sandboxedRouteMetaCache.delete(pluginId);
			}
		} catch (error) {
			console.error("EmDash: Failed to sync marketplace plugins:", error);
		}
	}
	/**
	* Create and initialize the runtime
	*/
	static async create(deps) {
		const db = await EmDashRuntime.getDatabase(deps);
		if (isSqlite(db)) try {
			const repaired = await new FTSManager(db).verifyAndRepairAll();
			if (repaired > 0) console.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);
		} catch {}
		const storage = EmDashRuntime.getStorage(deps);
		let pluginStates = /* @__PURE__ */ new Map();
		try {
			const states = await db.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
			pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
		} catch {}
		const enabledPlugins = /* @__PURE__ */ new Set();
		for (const plugin of deps.plugins) {
			const status = pluginStates.get(plugin.id);
			if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
		}
		let siteInfo;
		try {
			const optionsRepo = new OptionsRepository(db);
			const siteName = await optionsRepo.get("emdash:site_title");
			const siteUrl = await optionsRepo.get("emdash:site_url");
			const locale = await optionsRepo.get("emdash:locale");
			siteInfo = {
				siteName: siteName ?? void 0,
				siteUrl: siteUrl ?? void 0,
				locale: locale ?? void 0
			};
		} catch {}
		const allPipelinePlugins = [...deps.plugins];
		if (import.meta.env.DEV) try {
			const devConsolePlugin = definePlugin({
				id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
				version: "0.0.0",
				capabilities: ["email:provide"],
				hooks: { "email:deliver": {
					exclusive: true,
					handler: devConsoleEmailDeliver
				} }
			});
			allPipelinePlugins.push(devConsolePlugin);
			enabledPlugins.add(devConsolePlugin.id);
		} catch (error) {
			console.warn("[email] Failed to register dev console email provider:", error);
		}
		try {
			const defaultModeratorPlugin = definePlugin({
				id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
				version: "0.0.0",
				capabilities: ["read:users"],
				hooks: { "comment:moderate": {
					exclusive: true,
					handler: defaultCommentModerate
				} }
			});
			allPipelinePlugins.push(defaultModeratorPlugin);
			enabledPlugins.add(defaultModeratorPlugin.id);
		} catch (error) {
			console.warn("[comments] Failed to register default moderator:", error);
		}
		const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
		const pipelineFactoryOptions = {
			db,
			storage: storage ?? void 0,
			siteInfo
		};
		const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
		const sandboxedPlugins = await EmDashRuntime.loadSandboxedPlugins(deps, db);
		if (deps.config.marketplace && storage) await EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins);
		const mediaProviders = /* @__PURE__ */ new Map();
		const mediaProviderEntries = deps.mediaProviderEntries ?? [];
		const providerContext = {
			db,
			storage
		};
		for (const entry of mediaProviderEntries) try {
			const provider = entry.createProvider(providerContext);
			mediaProviders.set(entry.id, provider);
		} catch (error) {
			console.warn(`Failed to initialize media provider "${entry.id}":`, error);
		}
		await EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps);
		const emailPipeline = new EmailPipeline(pipeline);
		if (sandboxRunner) sandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));
		const pipelineRef = { current: pipeline };
		const invokeCronHook = async (pluginId, event) => {
			const result = await pipelineRef.current.invokeCronHook(pluginId, event);
			if (!result.success && result.error) throw result.error;
		};
		pipeline.setContextFactory({
			db,
			emailPipeline
		});
		let cronExecutor = null;
		let cronScheduler = null;
		try {
			cronExecutor = new CronExecutor(db, invokeCronHook);
			const recovered = await cronExecutor.recoverStaleLocks();
			if (recovered > 0) console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
			if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers") cronScheduler = new PiggybackScheduler(cronExecutor);
			else cronScheduler = new NodeCronScheduler(cronExecutor);
			cronScheduler.setSystemCleanup(async () => {
				try {
					await runSystemCleanup(db, storage ?? void 0);
				} catch (error) {
					console.error("[cleanup] System cleanup failed:", error);
				}
			});
			pipeline.setContextFactory({ cronReschedule: () => cronScheduler?.reschedule() });
			await cronScheduler.start();
		} catch (error) {
			console.warn("[cron] Failed to initialize cron system:", error);
		}
		return new EmDashRuntime(db, storage, deps.plugins, sandboxedPlugins, deps.sandboxedPluginEntries, pipeline, enabledPlugins, pluginStates, deps.config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, deps, pipelineRef);
	}
	/**
	* Get a media provider by ID
	*/
	getMediaProvider(providerId) {
		return this.mediaProviders.get(providerId);
	}
	/**
	* Get all media provider entries (for admin UI)
	*/
	getMediaProviderList() {
		return this.mediaProviderEntries.map((e) => ({
			id: e.id,
			name: e.name,
			icon: e.icon,
			capabilities: e.capabilities
		}));
	}
	/**
	* Get or create database instance
	*/
	static async getDatabase(deps) {
		const ctx = getRequestContext();
		if (ctx?.db) return ctx.db;
		const dbConfig = deps.config.database;
		if (!dbConfig) try {
			return await getDb();
		} catch {
			throw new Error("EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts");
		}
		const cacheKey = dbConfig.entrypoint;
		const cached = dbCache.get(cacheKey);
		if (cached) return cached;
		if (dbInitPromise) return dbInitPromise;
		dbInitPromise = (async () => {
			const db = new Kysely({ dialect: deps.createDialect(dbConfig.config) });
			await runMigrations(db);
			try {
				const [collectionCount, setupOption] = await Promise.all([db.selectFrom("_emdash_collections").select((eb) => eb.fn.countAll().as("count")).executeTakeFirstOrThrow(), db.selectFrom("options").select("value").where("name", "=", "emdash:setup_complete").executeTakeFirst()]);
				const setupDone = (() => {
					try {
						return setupOption && JSON.parse(setupOption.value) === true;
					} catch {
						return false;
					}
				})();
				if (collectionCount.count === 0 && !setupDone) {
					const { applySeed } = await import("../apply-kC39ev1Z.mjs").then((n) => n.n);
					const { loadSeed } = await import("../load-yOOlckBj.mjs").then((n) => n.r);
					const { validateSeed } = await import("../validate-_rsF-Dx_.mjs").then((n) => n.n);
					const seed = await loadSeed();
					if (validateSeed(seed).valid) {
						await applySeed(db, seed, { onConflict: "skip" });
						console.log("Auto-seeded default collections");
					}
				}
			} catch {}
			dbCache.set(cacheKey, db);
			return db;
		})();
		try {
			return await dbInitPromise;
		} finally {
			dbInitPromise = null;
		}
	}
	/**
	* Get or create storage instance
	*/
	static getStorage(deps) {
		const storageConfig = deps.config.storage;
		if (!storageConfig || !deps.createStorage) return null;
		const cacheKey = storageConfig.entrypoint;
		const cached = storageCache.get(cacheKey);
		if (cached) return cached;
		const storage = deps.createStorage(storageConfig.config);
		storageCache.set(cacheKey, storage);
		return storage;
	}
	/**
	* Load sandboxed plugins using SandboxRunner
	*/
	static async loadSandboxedPlugins(deps, db) {
		if (sandboxedPluginCache.size > 0) return sandboxedPluginCache;
		if (!deps.sandboxEnabled || deps.sandboxedPluginEntries.length === 0) return sandboxedPluginCache;
		if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner({ db });
		if (!sandboxRunner) return sandboxedPluginCache;
		if (!sandboxRunner.isAvailable()) {
			console.debug("EmDash: Sandbox runner not available (missing bindings), skipping sandbox");
			return sandboxedPluginCache;
		}
		for (const entry of deps.sandboxedPluginEntries) {
			const pluginKey = `${entry.id}:${entry.version}`;
			if (sandboxedPluginCache.has(pluginKey)) continue;
			try {
				const manifest = {
					id: entry.id,
					version: entry.version,
					capabilities: entry.capabilities ?? [],
					allowedHosts: entry.allowedHosts ?? [],
					storage: entry.storage ?? {},
					hooks: [],
					routes: [],
					admin: {}
				};
				const plugin = await sandboxRunner.load(manifest, entry.code);
				sandboxedPluginCache.set(pluginKey, plugin);
				console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
			} catch (error) {
				console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
			}
		}
		return sandboxedPluginCache;
	}
	/**
	* Cold-start: load marketplace-installed plugins from site-local R2 storage
	*
	* Queries _plugin_state for source='marketplace' rows, fetches each bundle
	* from R2, and loads via SandboxRunner.
	*/
	static async loadMarketplacePlugins(db, storage, deps, cache) {
		if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner({ db });
		if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
		try {
			const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
			for (const plugin of marketplacePlugins) {
				if (plugin.status !== "active") continue;
				const version = plugin.marketplaceVersion ?? plugin.version;
				const pluginKey = `${plugin.pluginId}:${version}`;
				if (cache.has(pluginKey)) continue;
				try {
					const bundle = await loadBundleFromR2(storage, plugin.pluginId, version);
					if (!bundle) {
						console.warn(`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`);
						continue;
					}
					const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
					cache.set(pluginKey, loaded);
					marketplacePluginKeys.add(pluginKey);
					marketplaceManifestCache.set(plugin.pluginId, {
						id: bundle.manifest.id,
						version: bundle.manifest.version,
						admin: bundle.manifest.admin
					});
					if (bundle.manifest.routes.length > 0) {
						const routeMeta = /* @__PURE__ */ new Map();
						for (const entry of bundle.manifest.routes) {
							const normalized = normalizeManifestRoute(entry);
							routeMeta.set(normalized.name, { public: normalized.public === true });
						}
						sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
					}
					console.log(`EmDash: Loaded marketplace plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(", ")}]`);
				} catch (error) {
					console.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId}:`, error);
				}
			}
		} catch {}
	}
	/**
	* Resolve exclusive hook selections on startup.
	*
	* Delegates to the shared resolveExclusiveHooks() in hooks.ts.
	* The runtime version considers all pipeline providers as "active" since
	* the pipeline was already built from only active/enabled plugins.
	*/
	static async resolveExclusiveHooks(pipeline, db, deps) {
		if (pipeline.getRegisteredExclusiveHooks().length === 0) return;
		let optionsRepo;
		try {
			optionsRepo = new OptionsRepository(db);
		} catch {
			return;
		}
		const preferredHints = /* @__PURE__ */ new Map();
		for (const entry of deps.sandboxedPluginEntries) if (entry.preferred && entry.preferred.length > 0) preferredHints.set(entry.id, entry.preferred);
		await resolveExclusiveHooks({
			pipeline,
			isActive: () => true,
			getOption: (key) => optionsRepo.get(key),
			setOption: (key, value) => optionsRepo.set(key, value),
			deleteOption: async (key) => {
				await optionsRepo.delete(key);
			},
			preferredHints
		});
	}
	/**
	* Build the manifest (rebuilt on each request for freshness)
	*/
	async getManifest() {
		const manifestCollections = {};
		try {
			const registry = new SchemaRegistry(this.db);
			const dbCollections = await registry.listCollections();
			for (const collection of dbCollections) {
				const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
				const fields = {};
				if (collectionWithFields?.fields) for (const field of collectionWithFields.fields) {
					const entry = {
						kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
						label: field.label,
						required: field.required
					};
					if (field.widget) entry.widget = field.widget;
					if (field.validation?.options) entry.options = field.validation.options.map((v) => ({
						value: v,
						label: v.charAt(0).toUpperCase() + v.slice(1)
					}));
					if (field.type === "repeater" && field.validation) entry.validation = field.validation;
					fields[field.slug] = entry;
				}
				manifestCollections[collection.slug] = {
					label: collection.label,
					labelSingular: collection.labelSingular || collection.label,
					supports: collection.supports || [],
					hasSeo: collection.hasSeo,
					urlPattern: collection.urlPattern,
					fields
				};
			}
		} catch (error) {
			console.debug("EmDash: Could not load database collections:", error);
		}
		const manifestPlugins = {};
		for (const plugin of this.configuredPlugins) {
			const status = this.pluginStates.get(plugin.id);
			const enabled = status === void 0 || status === "active";
			const hasAdminEntry = !!plugin.admin?.entry;
			const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
			const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
			let adminMode = "none";
			if (hasAdminEntry) adminMode = "react";
			else if (hasAdminPages || hasWidgets) adminMode = "blocks";
			manifestPlugins[plugin.id] = {
				version: plugin.version,
				enabled,
				adminMode,
				adminPages: plugin.admin?.pages,
				dashboardWidgets: plugin.admin?.widgets,
				portableTextBlocks: plugin.admin?.portableTextBlocks,
				fieldWidgets: plugin.admin?.fieldWidgets
			};
		}
		for (const entry of this.sandboxedPluginEntries) {
			const status = this.pluginStates.get(entry.id);
			const enabled = status === void 0 || status === "active";
			const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
			const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
			manifestPlugins[entry.id] = {
				version: entry.version,
				enabled,
				sandboxed: true,
				adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
				adminPages: entry.adminPages,
				dashboardWidgets: entry.adminWidgets
			};
		}
		for (const [pluginId, meta] of marketplaceManifestCache) {
			if (manifestPlugins[pluginId]) continue;
			const enabled = this.pluginStates.get(pluginId) === "active";
			const pages = meta.admin?.pages;
			const widgets = meta.admin?.widgets;
			const hasAdminPages = (pages?.length ?? 0) > 0;
			const hasWidgets = (widgets?.length ?? 0) > 0;
			manifestPlugins[pluginId] = {
				version: meta.version,
				enabled,
				sandboxed: true,
				adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
				adminPages: pages,
				dashboardWidgets: widgets
			};
		}
		const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins));
		const authMode = getAuthMode(this.config);
		const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
		const { getI18nConfig, isI18nEnabled } = await import("../config-CKE8p9xM.mjs").then((n) => n.t);
		const i18nConfig = getI18nConfig();
		return {
			version: "0.1.0",
			hash: manifestHash,
			collections: manifestCollections,
			plugins: manifestPlugins,
			authMode: authModeValue,
			i18n: isI18nEnabled() && i18nConfig ? {
				defaultLocale: i18nConfig.defaultLocale,
				locales: i18nConfig.locales
			} : void 0,
			marketplace: !!this.config.marketplace
		};
	}
	/**
	* Invalidate the cached manifest (no-op now that we don't cache).
	* Kept for API compatibility.
	*/
	invalidateManifest() {}
	async handleContentList(collection, params) {
		return handleContentList(this.db, collection, params);
	}
	async handleContentGet(collection, id, locale) {
		return handleContentGet(this.db, collection, id, locale);
	}
	async handleContentGetIncludingTrashed(collection, id, locale) {
		return handleContentGetIncludingTrashed(this.db, collection, id, locale);
	}
	async handleContentCreate(collection, body) {
		let processedData = body.data;
		if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
		processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
		processedData = await this.normalizeMediaFields(collection, processedData);
		const result = await handleContentCreate(this.db, collection, {
			...body,
			data: processedData,
			authorId: body.authorId,
			bylines: body.bylines
		});
		if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);
		return result;
	}
	async handleContentUpdate(collection, id, body) {
		const { ContentRepository } = await import("../content-D6C2WsZC.mjs").then((n) => n.n);
		const repo = new ContentRepository(this.db);
		const resolvedItem = await repo.findByIdOrSlug(collection, id);
		const resolvedId = resolvedItem?.id ?? id;
		if (body._rev) {
			if (!resolvedItem) return {
				success: false,
				error: {
					code: "NOT_FOUND",
					message: `Content item not found: ${id}`
				}
			};
			const revCheck = validateRev(body._rev, resolvedItem);
			if (!revCheck.valid) return {
				success: false,
				error: {
					code: "CONFLICT",
					message: revCheck.message
				}
			};
		}
		const { _rev: _discardedRev, ...bodyWithoutRev } = body;
		let processedData = bodyWithoutRev.data;
		if (bodyWithoutRev.data) {
			if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
			processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
			processedData = await this.normalizeMediaFields(collection, processedData);
		}
		let usesDraftRevisions = false;
		if (processedData) try {
			if ((await this.schemaRegistry.getCollectionWithFields(collection))?.supports?.includes("revisions")) {
				usesDraftRevisions = true;
				const revisionRepo = new RevisionRepository(this.db);
				const existing = await repo.findById(collection, resolvedId);
				if (existing) {
					let baseData;
					if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
					else baseData = existing.data;
					const mergedData = {
						...baseData,
						...processedData
					};
					if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
					if (bodyWithoutRev.skipRevision && existing.draftRevisionId) await revisionRepo.updateData(existing.draftRevisionId, mergedData);
					else {
						const revision = await revisionRepo.create({
							collection,
							entryId: resolvedId,
							data: mergedData,
							authorId: bodyWithoutRev.authorId ?? void 0
						});
						const tableName = `ec_${collection}`;
						await sql`
								UPDATE ${sql.ref(tableName)}
								SET draft_revision_id = ${revision.id},
									updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
								WHERE id = ${resolvedId}
							`.execute(this.db);
						revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});
					}
				}
			}
		} catch {}
		const result = await handleContentUpdate(this.db, collection, resolvedId, {
			...bodyWithoutRev,
			data: usesDraftRevisions ? void 0 : processedData,
			slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
			authorId: bodyWithoutRev.authorId,
			bylines: bodyWithoutRev.bylines
		});
		if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, false);
		return result;
	}
	async handleContentDelete(collection, id) {
		if (this.hooks.hasHooks("content:beforeDelete")) {
			const { allowed } = await this.hooks.runContentBeforeDelete(id, collection);
			if (!allowed) return {
				success: false,
				error: {
					code: "DELETE_BLOCKED",
					message: "Delete blocked by plugin hook"
				}
			};
		}
		if (!await this.runSandboxedBeforeDelete(id, collection)) return {
			success: false,
			error: {
				code: "DELETE_BLOCKED",
				message: "Delete blocked by sandboxed plugin hook"
			}
		};
		const result = await handleContentDelete(this.db, collection, id);
		if (result.success) this.runAfterDeleteHooks(id, collection);
		return result;
	}
	async handleContentListTrashed(collection, params = {}) {
		return handleContentListTrashed(this.db, collection, params);
	}
	async handleContentRestore(collection, id) {
		return handleContentRestore(this.db, collection, id);
	}
	async handleContentPermanentDelete(collection, id) {
		return handleContentPermanentDelete(this.db, collection, id);
	}
	async handleContentCountTrashed(collection) {
		return handleContentCountTrashed(this.db, collection);
	}
	async handleContentDuplicate(collection, id, authorId) {
		return handleContentDuplicate(this.db, collection, id, authorId);
	}
	async handleContentPublish(collection, id) {
		const result = await handleContentPublish(this.db, collection, id);
		if (result.success && result.data) this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);
		return result;
	}
	async handleContentUnpublish(collection, id) {
		const result = await handleContentUnpublish(this.db, collection, id);
		if (result.success && result.data) this.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection);
		return result;
	}
	async handleContentSchedule(collection, id, scheduledAt) {
		return handleContentSchedule(this.db, collection, id, scheduledAt);
	}
	async handleContentUnschedule(collection, id) {
		return handleContentUnschedule(this.db, collection, id);
	}
	async handleContentCountScheduled(collection) {
		return handleContentCountScheduled(this.db, collection);
	}
	async handleContentDiscardDraft(collection, id) {
		return handleContentDiscardDraft(this.db, collection, id);
	}
	async handleContentCompare(collection, id) {
		return handleContentCompare(this.db, collection, id);
	}
	async handleContentTranslations(collection, id) {
		return handleContentTranslations(this.db, collection, id);
	}
	async handleMediaList(params) {
		return handleMediaList(this.db, params);
	}
	async handleMediaGet(id) {
		return handleMediaGet(this.db, id);
	}
	async handleMediaCreate(input) {
		let processedInput = input;
		if (this.hooks.hasHooks("media:beforeUpload")) {
			const hookResult = await this.hooks.runMediaBeforeUpload({
				name: input.filename,
				type: input.mimeType,
				size: input.size || 0
			});
			processedInput = {
				...input,
				filename: hookResult.file.name,
				mimeType: hookResult.file.type,
				size: hookResult.file.size
			};
		}
		const result = await handleMediaCreate(this.db, processedInput);
		if (result.success && this.hooks.hasHooks("media:afterUpload")) {
			const item = result.data.item;
			const mediaItem = {
				id: item.id,
				filename: item.filename,
				mimeType: item.mimeType,
				size: item.size,
				url: `/media/${item.id}/${item.filename}`,
				createdAt: item.createdAt
			};
			this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
		}
		return result;
	}
	async handleMediaUpdate(id, input) {
		return handleMediaUpdate(this.db, id, input);
	}
	async handleMediaDelete(id) {
		return handleMediaDelete(this.db, id);
	}
	async handleRevisionList(collection, entryId, params = {}) {
		return handleRevisionList(this.db, collection, entryId, params);
	}
	async handleRevisionGet(revisionId) {
		return handleRevisionGet(this.db, revisionId);
	}
	async handleRevisionRestore(revisionId, callerUserId) {
		return handleRevisionRestore(this.db, revisionId, callerUserId);
	}
	/**
	* Get route metadata for a plugin route without invoking the handler.
	* Used by the catch-all route to decide auth before dispatch.
	* Returns null if the plugin or route doesn't exist.
	*/
	getPluginRouteMeta(pluginId, path) {
		if (!this.isPluginEnabled(pluginId)) return null;
		const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
		const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
		if (trustedPlugin) {
			const route = trustedPlugin.routes[routeKey];
			if (!route) return null;
			return { public: route.public === true };
		}
		const meta = sandboxedRouteMetaCache.get(pluginId);
		if (meta) {
			const routeMeta = meta.get(routeKey);
			if (routeMeta) return routeMeta;
		}
		if (routeKey === "admin") {
			const manifestMeta = marketplaceManifestCache.get(pluginId);
			if (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) return { public: false };
			const entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);
			if (entry?.adminPages?.length || entry?.adminWidgets?.length) return { public: false };
		}
		if (this.findSandboxedPlugin(pluginId)) return { public: false };
		return null;
	}
	async handlePluginApiRoute(pluginId, _method, path, request) {
		if (!this.isPluginEnabled(pluginId)) return {
			success: false,
			error: {
				code: "NOT_FOUND",
				message: `Plugin not enabled: ${pluginId}`
			}
		};
		const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
		if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
			const routeRegistry = new PluginRouteRegistry({ db: this.db });
			routeRegistry.register(trustedPlugin);
			const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
			let body = void 0;
			try {
				body = await request.json();
			} catch {}
			return routeRegistry.invoke(pluginId, routeKey, {
				request,
				body
			});
		}
		const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
		if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
		return {
			success: false,
			error: {
				code: "NOT_FOUND",
				message: `Plugin not found: ${pluginId}`
			}
		};
	}
	findSandboxedPlugin(pluginId) {
		for (const [key, plugin] of this.sandboxedPlugins) if (key.startsWith(pluginId + ":")) return plugin;
	}
	/**
	* Normalize image/file fields in content data.
	* Fills missing dimensions, storageKey, mimeType, and filename from providers.
	*/
	async normalizeMediaFields(collection, data) {
		let collectionInfo;
		try {
			collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
		} catch {
			return data;
		}
		if (!collectionInfo?.fields) return data;
		const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
		if (imageFields.length === 0) return data;
		const getProvider = (id) => this.getMediaProvider(id);
		const result = { ...data };
		for (const field of imageFields) {
			const value = result[field.slug];
			if (value == null) continue;
			try {
				const normalized = await normalizeMediaValue(value, getProvider);
				if (normalized) result[field.slug] = normalized;
			} catch {}
		}
		return result;
	}
	async runSandboxedBeforeSave(content, collection, isNew) {
		let result = content;
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [id] = pluginKey.split(":");
			if (!id || !this.isPluginEnabled(id)) continue;
			try {
				const hookResult = await plugin.invokeHook("content:beforeSave", {
					content: result,
					collection,
					isNew
				});
				if (hookResult && typeof hookResult === "object" && !Array.isArray(hookResult)) {
					const record = {};
					for (const [k, v] of Object.entries(hookResult)) record[k] = v;
					result = record;
				}
			} catch (error) {
				console.error(`EmDash: Sandboxed plugin ${id} beforeSave hook error:`, error);
			}
		}
		return result;
	}
	async runSandboxedBeforeDelete(id, collection) {
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [pluginId] = pluginKey.split(":");
			if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
			try {
				if (await plugin.invokeHook("content:beforeDelete", {
					id,
					collection
				}) === false) return false;
			} catch (error) {
				console.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);
			}
		}
		return true;
	}
	runAfterSaveHooks(content, collection, isNew) {
		if (this.hooks.hasHooks("content:afterSave")) this.hooks.runContentAfterSave(content, collection, isNew).catch((err) => console.error("EmDash afterSave hook error:", err));
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [id] = pluginKey.split(":");
			if (!id || !this.isPluginEnabled(id)) continue;
			plugin.invokeHook("content:afterSave", {
				content,
				collection,
				isNew
			}).catch((err) => console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err));
		}
	}
	runAfterDeleteHooks(id, collection) {
		if (this.hooks.hasHooks("content:afterDelete")) this.hooks.runContentAfterDelete(id, collection).catch((err) => console.error("EmDash afterDelete hook error:", err));
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [pluginId] = pluginKey.split(":");
			if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
			plugin.invokeHook("content:afterDelete", {
				id,
				collection
			}).catch((err) => console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err));
		}
	}
	runAfterPublishHooks(content, collection) {
		if (this.hooks.hasHooks("content:afterPublish")) this.hooks.runContentAfterPublish(content, collection).catch((err) => console.error("EmDash afterPublish hook error:", err));
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [pluginId] = pluginKey.split(":");
			if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
			plugin.invokeHook("content:afterPublish", {
				content,
				collection
			}).catch((err) => console.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish error:`, err));
		}
	}
	runAfterUnpublishHooks(content, collection) {
		if (this.hooks.hasHooks("content:afterUnpublish")) this.hooks.runContentAfterUnpublish(content, collection).catch((err) => console.error("EmDash afterUnpublish hook error:", err));
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [pluginId] = pluginKey.split(":");
			if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
			plugin.invokeHook("content:afterUnpublish", {
				content,
				collection
			}).catch((err) => console.error(`EmDash: Sandboxed plugin ${pluginId} afterUnpublish error:`, err));
		}
	}
	async handleSandboxedRoute(plugin, path, request) {
		const routeName = path.replace(LEADING_SLASH_PATTERN, "");
		let body = void 0;
		try {
			body = await request.json();
		} catch {}
		try {
			const headers = sanitizeHeadersForSandbox(request.headers);
			const meta = extractRequestMeta(request);
			return {
				success: true,
				data: await plugin.invokeRoute(routeName, body, {
					url: request.url,
					method: request.method,
					headers,
					meta
				})
			};
		} catch (error) {
			console.error(`EmDash: Sandboxed plugin route error:`, error);
			return {
				success: false,
				error: {
					code: "ROUTE_ERROR",
					message: error instanceof Error ? error.message : String(error)
				}
			};
		}
	}
	/**
	* Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext
	* object so results are collected once per page context per request, even when
	* multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)
	* request contributions from the same page.
	*/
	pageContributionCache = /* @__PURE__ */ new WeakMap();
	/**
	* Collect all page contributions (metadata + fragments) in a single pass.
	* Results are cached by page context object identity.
	*/
	async collectPageContributions(page) {
		const cached = this.pageContributionCache.get(page);
		if (cached) return cached;
		const promise = this.doCollectPageContributions(page);
		this.pageContributionCache.set(page, promise);
		return promise;
	}
	async doCollectPageContributions(page) {
		const metadata = [];
		const fragments = [];
		if (this.hooks.hasHooks("page:metadata")) {
			const results = await this.hooks.runPageMetadata({ page });
			for (const r of results) metadata.push(...r.contributions);
		}
		if (this.hooks.hasHooks("page:fragments")) {
			const results = await this.hooks.runPageFragments({ page });
			for (const r of results) fragments.push(...r.contributions);
		}
		for (const [pluginKey, plugin] of this.sandboxedPlugins) {
			const [id] = pluginKey.split(":");
			if (!id || !this.isPluginEnabled(id)) continue;
			try {
				const result = await plugin.invokeHook("page:metadata", { page });
				if (result != null) {
					const items = Array.isArray(result) ? result : [result];
					for (const item of items) if (isValidMetadataContribution(item)) metadata.push(item);
				}
			} catch (error) {
				console.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);
			}
		}
		return {
			metadata,
			fragments
		};
	}
	/**
	* Collect page metadata contributions from trusted and sandboxed plugins.
	* Delegates to the single-pass collector and returns the metadata portion.
	*/
	async collectPageMetadata(page) {
		const { metadata } = await this.collectPageContributions(page);
		return metadata;
	}
	/**
	* Collect page fragment contributions from trusted plugins only.
	* Delegates to the single-pass collector and returns the fragments portion.
	*/
	async collectPageFragments(page) {
		const { fragments } = await this.collectPageContributions(page);
		return fragments;
	}
	isPluginEnabled(pluginId) {
		const status = this.pluginStates.get(pluginId);
		return status === void 0 || status === "active";
	}
};

//#endregion
//#region src/astro/middleware.ts
/**
* EmDash middleware
*
* Thin wrapper that initializes EmDashRuntime and attaches it to locals.
* All heavy lifting happens in EmDashRuntime.
*/
let runtimeInstance = null;
let runtimeInitializing = false;
/** Whether i18n config has been initialized from the virtual module */
let i18nInitialized = false;
/**
* Whether we've verified the database has been set up.
* On a fresh deployment the first request may hit a public page, bypassing
* runtime init. Without this check, template helpers like getSiteSettings()
* would query an empty database and crash. Once verified (or once the runtime
* has initialized via an admin/API request), this stays true for the worker's
* lifetime.
*/
let setupVerified = false;
/**
* Get EmDash configuration from virtual module
*/
function getConfig() {
	if (virtualConfig && typeof virtualConfig === "object") {
		if (!i18nInitialized) {
			i18nInitialized = true;
			const config = virtualConfig;
			if (config.i18n && typeof config.i18n === "object") setI18nConfig(config.i18n);
			else setI18nConfig(null);
		}
		return virtualConfig;
	}
	return null;
}
/**
* Get plugins from virtual module
*/
function getPlugins() {
	return plugins || [];
}
/**
* Build runtime dependencies from virtual modules
*/
function buildDependencies(config) {
	return {
		config,
		plugins: getPlugins(),
		createDialect,
		createStorage,
		sandboxEnabled,
		sandboxedPluginEntries: sandboxedPlugins || [],
		createSandboxRunner,
		mediaProviderEntries: mediaProviders || []
	};
}
/**
* Get or create the runtime instance
*/
async function getRuntime(config) {
	if (runtimeInstance) return runtimeInstance;
	if (runtimeInitializing) {
		await new Promise((resolve) => setTimeout(resolve, 50));
		return getRuntime(config);
	}
	runtimeInitializing = true;
	try {
		const deps = buildDependencies(config);
		const runtime = await EmDashRuntime.create(deps);
		runtimeInstance = runtime;
		return runtime;
	} finally {
		runtimeInitializing = false;
	}
}
/**
* Baseline security headers applied to all responses.
* Admin routes get additional headers (strict CSP) from auth middleware.
*/
function setBaselineSecurityHeaders(response) {
	response.headers.set("X-Content-Type-Options", "nosniff");
	response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
	response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
	if (!response.headers.has("Content-Security-Policy")) response.headers.set("X-Frame-Options", "SAMEORIGIN");
}
/** Public routes that require the runtime (sitemap, robots.txt, etc.) */
const PUBLIC_RUNTIME_ROUTES = new Set(["/sitemap.xml", "/robots.txt"]);
const onRequest = defineMiddleware(async (context, next) => {
	const { request, locals, cookies } = context;
	const url = context.url;
	const isEmDashRoute = url.pathname.startsWith("/_emdash");
	const isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname);
	const hasEditCookie = cookies.get("emdash-edit-mode")?.value === "true";
	const hasPreviewToken = url.searchParams.has("_preview");
	const playgroundDb = locals.__playgroundDb;
	if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
		if (!await context.session?.get("user") && !playgroundDb) {
			if (!setupVerified) try {
				const { getDb } = await import("../loader-fz8Q_3EO.mjs").then((n) => n.r);
				await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
				setupVerified = true;
			} catch {
				return context.redirect("/_emdash/admin/setup");
			}
			const response = await next();
			setBaselineSecurityHeaders(response);
			return response;
		}
	}
	const config = getConfig();
	if (!config) {
		console.error("EmDash: No configuration found");
		return next();
	}
	const doInit = async () => {
		try {
			const runtime = await getRuntime(config);
			setupVerified = true;
			locals.emdashManifest = await runtime.getManifest();
			locals.emdash = {
				handleContentList: runtime.handleContentList.bind(runtime),
				handleContentGet: runtime.handleContentGet.bind(runtime),
				handleContentCreate: runtime.handleContentCreate.bind(runtime),
				handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
				handleContentDelete: runtime.handleContentDelete.bind(runtime),
				handleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),
				handleContentRestore: runtime.handleContentRestore.bind(runtime),
				handleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),
				handleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),
				handleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),
				handleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),
				handleContentPublish: runtime.handleContentPublish.bind(runtime),
				handleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),
				handleContentSchedule: runtime.handleContentSchedule.bind(runtime),
				handleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),
				handleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),
				handleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),
				handleContentCompare: runtime.handleContentCompare.bind(runtime),
				handleContentTranslations: runtime.handleContentTranslations.bind(runtime),
				handleMediaList: runtime.handleMediaList.bind(runtime),
				handleMediaGet: runtime.handleMediaGet.bind(runtime),
				handleMediaCreate: runtime.handleMediaCreate.bind(runtime),
				handleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),
				handleMediaDelete: runtime.handleMediaDelete.bind(runtime),
				handleRevisionList: runtime.handleRevisionList.bind(runtime),
				handleRevisionGet: runtime.handleRevisionGet.bind(runtime),
				handleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),
				handlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),
				getPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),
				getMediaProvider: runtime.getMediaProvider.bind(runtime),
				getMediaProviderList: runtime.getMediaProviderList.bind(runtime),
				storage: runtime.storage,
				db: runtime.db,
				hooks: runtime.hooks,
				email: runtime.email,
				configuredPlugins: runtime.configuredPlugins,
				config,
				invalidateManifest: runtime.invalidateManifest.bind(runtime),
				getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
				syncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),
				setPluginStatus: runtime.setPluginStatus.bind(runtime)
			};
		} catch (error) {
			console.error("EmDash middleware error:", error);
		}
		const dbConfig = config?.database?.config;
		if (dbConfig && typeof isSessionEnabled === "function" && isSessionEnabled(dbConfig) && typeof getD1Binding === "function" && createSessionDialect) {
			const d1Binding = getD1Binding(dbConfig);
			if (d1Binding && typeof d1Binding === "object" && "withSession" in d1Binding) {
				const isAuthenticated = !!await context.session?.get("user");
				const isWrite = request.method !== "GET" && request.method !== "HEAD";
				const configConstraint = getDefaultConstraint(dbConfig);
				const cookieName = getBookmarkCookieName(dbConfig);
				let constraint = configConstraint;
				if (isAuthenticated && isWrite) constraint = "first-primary";
				else if (isAuthenticated) {
					const bookmarkCookie = context.cookies.get(cookieName);
					if (bookmarkCookie?.value) constraint = bookmarkCookie.value;
				}
				const session = d1Binding.withSession.call(d1Binding, constraint);
				return runWithContext({
					editMode: false,
					db: new Kysely({ dialect: createSessionDialect(session) })
				}, async () => {
					const response = await next();
					setBaselineSecurityHeaders(response);
					if (isAuthenticated && session && typeof session === "object" && "getBookmark" in session) {
						const newBookmark = session.getBookmark.call(session);
						if (newBookmark) response.headers.append("Set-Cookie", `${cookieName}=${newBookmark}; Path=/; HttpOnly; SameSite=Lax; Secure`);
					}
					return response;
				});
			}
		}
		const response = await next();
		setBaselineSecurityHeaders(response);
		return response;
	};
	if (playgroundDb) return runWithContext({
		editMode: context.cookies.get("emdash-edit-mode")?.value === "true",
		db: playgroundDb
	}, doInit);
	return doInit();
});

//#endregion
export { onRequest as default, onRequest };
//# sourceMappingURL=middleware.mjs.map