emdash 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;AA0KA;;;cAAa,SAAA,EAiOX,KAAA,CAjOoB,iBAAA"}
1
+ {"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;AAoLA;;;cAAa,SAAA,EA0PX,KAAA,CA1PoB,iBAAA"}
@@ -1432,6 +1432,15 @@ let runtimeInitializing = false;
1432
1432
  /** Whether i18n config has been initialized from the virtual module */
1433
1433
  let i18nInitialized = false;
1434
1434
  /**
1435
+ * Whether we've verified the database has been set up.
1436
+ * On a fresh deployment the first request may hit a public page, bypassing
1437
+ * runtime init. Without this check, template helpers like getSiteSettings()
1438
+ * would query an empty database and crash. Once verified (or once the runtime
1439
+ * has initialized via an admin/API request), this stays true for the worker's
1440
+ * lifetime.
1441
+ */
1442
+ let setupVerified = false;
1443
+ /**
1435
1444
  * Get EmDash configuration from virtual module
1436
1445
  */
1437
1446
  function getConfig() {
@@ -1508,6 +1517,13 @@ const onRequest = defineMiddleware(async (context, next) => {
1508
1517
  const playgroundDb = locals.__playgroundDb;
1509
1518
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
1510
1519
  if (!await context.session?.get("user") && !playgroundDb) {
1520
+ if (!setupVerified) try {
1521
+ const { getDb } = await import("../loader-fz8Q_3EO.mjs").then((n) => n.r);
1522
+ await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
1523
+ setupVerified = true;
1524
+ } catch {
1525
+ return context.redirect("/_emdash/admin/setup");
1526
+ }
1511
1527
  const response = await next();
1512
1528
  setBaselineSecurityHeaders(response);
1513
1529
  return response;
@@ -1521,6 +1537,7 @@ const onRequest = defineMiddleware(async (context, next) => {
1521
1537
  const doInit = async () => {
1522
1538
  try {
1523
1539
  const runtime = await getRuntime(config);
1540
+ setupVerified = true;
1524
1541
  locals.emdashManifest = await runtime.getManifest();
1525
1542
  locals.emdash = {
1526
1543
  handleContentList: runtime.handleContentList.bind(runtime),
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","virtualPlugins","virtualCreateDialect","virtualCreateStorage","virtualSandboxEnabled","virtualSandboxedPlugins","virtualCreateSandboxRunner","virtualMediaProviders","virtualIsSessionEnabled","virtualGetD1Binding","virtualCreateSessionDialect","virtualGetDefaultConstraint","virtualGetBookmarkCookieName"],"sources":["../../src/auth/challenge-store.ts","../../src/cleanup.ts","../../src/comments/moderator.ts","../../src/plugins/scheduler/node.ts","../../src/plugins/scheduler/piggyback.ts","../../src/emdash-runtime.ts","../../src/astro/middleware.ts"],"sourcesContent":["/**\n * Challenge store for WebAuthn\n *\n * Stores WebAuthn challenges in a dedicated table with automatic expiration.\n */\n\nimport type { ChallengeStore, ChallengeData } from \"@emdash-cms/auth/passkey\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nexport function createChallengeStore(db: Kysely<Database>): ChallengeStore {\n\treturn {\n\t\tasync set(challenge: string, data: ChallengeData): Promise<void> {\n\t\t\tconst expiresAt = new Date(data.expiresAt).toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"auth_challenges\")\n\t\t\t\t.values({\n\t\t\t\t\tchallenge,\n\t\t\t\t\ttype: data.type,\n\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\tdata: null, // Could store additional context if needed\n\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc.column(\"challenge\").doUpdateSet({\n\t\t\t\t\t\ttype: data.type,\n\t\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync get(challenge: string): Promise<ChallengeData | null> {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"auth_challenges\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"challenge\", \"=\", challenge)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!row) return null;\n\n\t\t\tconst expiresAt = new Date(row.expires_at).getTime();\n\n\t\t\t// Check expiration\n\t\t\tif (expiresAt < Date.now()) {\n\t\t\t\t// Expired, delete and return null\n\t\t\t\tawait this.delete(challenge);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: row.type === \"registration\" ? \"registration\" : \"authentication\",\n\t\t\t\tuserId: row.user_id ?? undefined,\n\t\t\t\texpiresAt,\n\t\t\t};\n\t\t},\n\n\t\tasync delete(challenge: string): Promise<void> {\n\t\t\tawait db.deleteFrom(\"auth_challenges\").where(\"challenge\", \"=\", challenge).execute();\n\t\t},\n\t};\n}\n\n/**\n * Clean up expired challenges.\n * Should be called periodically (e.g., on startup, or via cron).\n */\nexport async function cleanupExpiredChallenges(db: Kysely<Database>): Promise<number> {\n\tconst now = new Date().toISOString();\n\n\tconst result = await db\n\t\t.deleteFrom(\"auth_challenges\")\n\t\t.where(\"expires_at\", \"<\", now)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n","/**\n * System cleanup\n *\n * Runs periodic maintenance tasks that prevent unbounded accumulation of\n * expired or stale data. Called from cron scheduler ticks and (for latency-\n * sensitive subsystems) inline during relevant requests.\n *\n * Each subsystem cleanup is independent and non-fatal -- if one fails, the\n * rest still run. Failures are logged but never surface to callers.\n */\n\nimport { createKyselyAdapter, type AuthTables } from \"@emdash-cms/auth/adapters/kysely\";\nimport { sql, type Kysely } from \"kysely\";\n\nimport { cleanupExpiredChallenges } from \"./auth/challenge-store.js\";\nimport { MediaRepository } from \"./database/repositories/media.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { Database } from \"./database/types.js\";\nimport type { Storage } from \"./storage/types.js\";\n\n/**\n * Result of a system cleanup run.\n * Each field is the number of rows deleted, or -1 if the cleanup failed.\n */\nexport interface CleanupResult {\n\tchallenges: number;\n\texpiredTokens: number;\n\tpendingUploads: number;\n\tpendingUploadFiles: number;\n\trevisionsPruned: number;\n}\n\n/** Max revisions to keep per entry during periodic pruning */\nconst REVISION_KEEP_COUNT = 50;\n\n/** Only prune entries that exceed this threshold */\nconst REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;\n\n/**\n * Run all system cleanup tasks.\n *\n * Safe to call frequently -- each task is a single DELETE with a WHERE clause,\n * so repeated calls with nothing to clean are cheap (no-op queries).\n *\n * @param db - The database instance\n * @param storage - Optional storage backend for deleting orphaned files.\n * When omitted, pending upload DB rows are still deleted but the\n * corresponding files in object storage are not removed.\n */\nexport async function runSystemCleanup(\n\tdb: Kysely<Database>,\n\tstorage?: Storage,\n): Promise<CleanupResult> {\n\tconst result: CleanupResult = {\n\t\tchallenges: -1,\n\t\texpiredTokens: -1,\n\t\tpendingUploads: -1,\n\t\tpendingUploadFiles: -1,\n\t\trevisionsPruned: -1,\n\t};\n\n\t// 1. Passkey challenges (expire after 60s, clean anything past 5 min)\n\ttry {\n\t\tresult.challenges = await cleanupExpiredChallenges(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired challenges:\", error);\n\t}\n\n\t// 2. Magic link / invite / signup tokens\n\ttry {\n\t\t// Cast needed: Database extends AuthTables but uses Generated<> wrappers\n\t\t// that confuse structural checks. The adapter casts internally anyway.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Database uses Generated<> wrappers incompatible with AuthTables structurally; safe at runtime\n\t\tconst authAdapter = createKyselyAdapter(db as unknown as Kysely<AuthTables>);\n\t\tawait authAdapter.deleteExpiredTokens();\n\t\tresult.expiredTokens = 0; // deleteExpiredTokens returns void\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired tokens:\", error);\n\t}\n\n\t// 3. Pending media uploads (abandoned after 1 hour)\n\t// Delete DB rows first, then remove corresponding files from storage.\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst orphanedKeys = await mediaRepo.cleanupPendingUploads();\n\t\tresult.pendingUploads = orphanedKeys.length;\n\n\t\t// Delete orphaned files from object storage\n\t\tif (storage && orphanedKeys.length > 0) {\n\t\t\tlet filesDeleted = 0;\n\t\t\tfor (const key of orphanedKeys) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tfilesDeleted++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Log per-file failures but continue -- storage.delete is\n\t\t\t\t\t// documented as idempotent, so this is an unexpected error.\n\t\t\t\t\tconsole.error(`[cleanup] Failed to delete storage file ${key}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.pendingUploadFiles = filesDeleted;\n\t\t} else {\n\t\t\tresult.pendingUploadFiles = 0;\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean pending uploads:\", error);\n\t}\n\n\t// 4. Revision pruning -- trim entries with excessive revision counts\n\ttry {\n\t\tresult.revisionsPruned = await pruneExcessiveRevisions(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to prune revisions:\", error);\n\t}\n\n\treturn result;\n}\n\n/**\n * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune\n * them down to REVISION_KEEP_COUNT.\n */\nasync function pruneExcessiveRevisions(db: Kysely<Database>): Promise<number> {\n\tconst entries = await sql<{ collection: string; entry_id: string; cnt: number }>`\n\t\tSELECT collection, entry_id, COUNT(*) as cnt\n\t\tFROM revisions\n\t\tGROUP BY collection, entry_id\n\t\tHAVING cnt > ${REVISION_PRUNE_THRESHOLD}\n\t`.execute(db);\n\n\tif (entries.rows.length === 0) return 0;\n\n\tconst revisionRepo = new RevisionRepository(db);\n\tlet totalPruned = 0;\n\n\tfor (const row of entries.rows) {\n\t\ttry {\n\t\t\tconst pruned = await revisionRepo.pruneOldRevisions(\n\t\t\t\trow.collection,\n\t\t\t\trow.entry_id,\n\t\t\t\tREVISION_KEEP_COUNT,\n\t\t\t);\n\t\t\ttotalPruned += pruned;\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn totalPruned;\n}\n","/**\n * Built-in Default Comment Moderator\n *\n * Registers comment:moderate as an exclusive hook.\n * Implements the 4-step decision logic:\n * 1. Auto-approve authenticated CMS users (if configured)\n * 2. If moderation is \"none\" → approved\n * 3. If moderation is \"first_time\" and returning commenter → approved\n * 4. Otherwise → pending\n *\n * This moderator does not read `metadata` — it only uses collection settings\n * and prior approval count. Plugin moderators (AI, Akismet) replace this.\n */\n\nimport type { CommentModerateEvent, ModerationDecision, PluginContext } from \"../plugins/types.js\";\n\n/** Plugin ID for the built-in default comment moderator */\nexport const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = \"emdash-default-comment-moderator\";\n\n/**\n * The comment:moderate handler for the built-in default moderator.\n */\nexport async function defaultCommentModerate(\n\tevent: CommentModerateEvent,\n\t_ctx: PluginContext,\n): Promise<ModerationDecision> {\n\tconst { comment, collectionSettings, priorApprovedCount } = event;\n\n\t// 1. Auto-approve authenticated CMS users if configured\n\tif (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) {\n\t\treturn { status: \"approved\", reason: \"Authenticated CMS user\" };\n\t}\n\n\t// 2. If moderation is \"none\" → approved\n\tif (collectionSettings.commentsModeration === \"none\") {\n\t\treturn { status: \"approved\", reason: \"Moderation disabled\" };\n\t}\n\n\t// 3. If moderation is \"first_time\" and returning commenter → approved\n\tif (collectionSettings.commentsModeration === \"first_time\" && priorApprovedCount > 0) {\n\t\treturn { status: \"approved\", reason: \"Returning commenter\" };\n\t}\n\n\t// 4. Otherwise → pending\n\treturn { status: \"pending\", reason: \"Held for review\" };\n}\n","/**\n * Node.js cron scheduler — setTimeout-based.\n *\n * Queries the executor for the next due time and sets a timeout. Re-arms\n * after each tick and when reschedule() is called (new task scheduled or\n * cancelled).\n *\n * Suitable for single-process deployments (local dev, single-node).\n *\n */\n\nimport type { CronExecutor } from \"../cron.js\";\nimport type { CronScheduler, SystemCleanupFn } from \"./types.js\";\n\n/** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */\nconst MIN_INTERVAL_MS = 1000;\n\n/** Maximum polling interval (ms) — wake up periodically to check for stale locks */\nconst MAX_INTERVAL_MS = 5 * 60 * 1000;\n\nexport class NodeCronScheduler implements CronScheduler {\n\tprivate timer: ReturnType<typeof setTimeout> | null = null;\n\tprivate running = false;\n\tprivate systemCleanup: SystemCleanupFn | null = null;\n\n\tconstructor(private executor: CronExecutor) {}\n\n\tsetSystemCleanup(fn: SystemCleanupFn): void {\n\t\tthis.systemCleanup = fn;\n\t}\n\n\tstart(): void {\n\t\tthis.running = true;\n\t\tthis.arm();\n\t}\n\n\tstop(): void {\n\t\tthis.running = false;\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t}\n\n\treschedule(): void {\n\t\tif (!this.running) return;\n\t\t// Clear existing timer and re-arm with fresh next due time\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tthis.arm();\n\t}\n\n\tprivate arm(): void {\n\t\tif (!this.running) return;\n\n\t\t// Query the next due time, then schedule a wake-up\n\t\tvoid this.executor\n\t\t\t.getNextDueTime()\n\t\t\t.then((nextDue) => {\n\t\t\t\tif (!this.running) return undefined;\n\n\t\t\t\tlet delayMs: number;\n\t\t\t\tif (nextDue) {\n\t\t\t\t\tconst dueAt = new Date(nextDue).getTime();\n\t\t\t\t\tdelayMs = Math.max(dueAt - Date.now(), MIN_INTERVAL_MS);\n\t\t\t\t\tdelayMs = Math.min(delayMs, MAX_INTERVAL_MS);\n\t\t\t\t} else {\n\t\t\t\t\t// No tasks scheduled — poll at max interval for stale lock recovery\n\t\t\t\t\tdelayMs = MAX_INTERVAL_MS;\n\t\t\t\t}\n\n\t\t\t\tthis.timer = setTimeout(() => {\n\t\t\t\t\tif (!this.running) return;\n\t\t\t\t\tthis.executeTick();\n\t\t\t\t}, delayMs);\n\n\t\t\t\t// Don't prevent process exit\n\t\t\t\tif (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n\t\t\t\t\tthis.timer.unref();\n\t\t\t\t}\n\n\t\t\t\treturn undefined;\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tconsole.error(\"[cron:node] Failed to get next due time:\", error);\n\t\t\t\t// Retry after max interval\n\t\t\t\tif (this.running) {\n\t\t\t\t\tthis.timer = setTimeout(() => this.arm(), MAX_INTERVAL_MS);\n\t\t\t\t\tif (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n\t\t\t\t\t\tthis.timer.unref();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t}\n\n\tprivate executeTick(): void {\n\t\tif (!this.running) return;\n\n\t\t// Run tick + stale lock recovery + system cleanup, then re-arm\n\t\tconst tasks: Promise<unknown>[] = [this.executor.tick(), this.executor.recoverStaleLocks()];\n\t\tif (this.systemCleanup) {\n\t\t\ttasks.push(this.systemCleanup());\n\t\t}\n\n\t\tvoid Promise.allSettled(tasks)\n\t\t\t.then((results) => {\n\t\t\t\tfor (const r of results) {\n\t\t\t\t\tif (r.status === \"rejected\") {\n\t\t\t\t\t\tconsole.error(\"[cron:node] Tick task failed:\", r.reason);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tif (this.running) {\n\t\t\t\t\tthis.arm();\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n","/**\n * Piggyback cron scheduler — request-driven fallback.\n *\n * Checks for overdue tasks on each incoming request, debounced to at most\n * once per 60 seconds. Fire-and-forget (does not block the request).\n *\n * Used on Cloudflare when no Durable Object binding is available, or\n * during development when DO bindings aren't configured.\n *\n */\n\nimport type { CronExecutor } from \"../cron.js\";\nimport type { CronScheduler, SystemCleanupFn } from \"./types.js\";\n\n/** Minimum interval between tick attempts (ms) */\nconst DEBOUNCE_MS = 60 * 1000;\n\nexport class PiggybackScheduler implements CronScheduler {\n\tprivate lastTickAt = 0;\n\tprivate running = false;\n\tprivate systemCleanup: SystemCleanupFn | null = null;\n\n\tconstructor(private executor: CronExecutor) {}\n\n\tsetSystemCleanup(fn: SystemCleanupFn): void {\n\t\tthis.systemCleanup = fn;\n\t}\n\n\tstart(): void {\n\t\tthis.running = true;\n\t}\n\n\tstop(): void {\n\t\tthis.running = false;\n\t}\n\n\t/**\n\t * No-op for piggyback — tick happens on next request.\n\t */\n\treschedule(): void {\n\t\t// Nothing to do — next request will check\n\t}\n\n\t/**\n\t * Call this from middleware on each request.\n\t * Debounced: only actually ticks if enough time has passed.\n\t */\n\tonRequest(): void {\n\t\tif (!this.running) return;\n\n\t\tconst now = Date.now();\n\t\tif (now - this.lastTickAt < DEBOUNCE_MS) return;\n\n\t\tthis.lastTickAt = now;\n\n\t\t// Fire-and-forget — don't block the request\n\t\tconst tasks: Promise<unknown>[] = [this.executor.tick(), this.executor.recoverStaleLocks()];\n\t\tif (this.systemCleanup) {\n\t\t\ttasks.push(this.systemCleanup());\n\t\t}\n\n\t\tvoid Promise.allSettled(tasks).then((results) => {\n\t\t\tfor (const r of results) {\n\t\t\t\tif (r.status === \"rejected\") {\n\t\t\t\t\tconsole.error(\"[cron:piggyback] Tick task failed:\", r.reason);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\t}\n}\n","/**\n * EmDashRuntime - Core runtime for EmDash CMS\n *\n * Manages database, storage, plugins (trusted + sandboxed), hooks, and\n * provides handlers for content/media operations.\n *\n * Created once per worker lifetime, cached and reused across requests.\n */\n\nimport type { Element } from \"@emdash-cms/blocks\";\nimport { Kysely, sql, type Dialect } from \"kysely\";\n\nimport { validateRev } from \"./api/rev.js\";\nimport type {\n\tEmDashConfig,\n\tPluginAdminPage,\n\tPluginDashboardWidget,\n} from \"./astro/integration/runtime.js\";\nimport type { EmDashManifest, ManifestCollection } from \"./astro/types.js\";\nimport { getAuthMode } from \"./auth/mode.js\";\nimport { isSqlite } from \"./database/dialect-helpers.js\";\nimport { runMigrations } from \"./database/migrations/runner.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { ContentItem as ContentItemInternal } from \"./database/repositories/types.js\";\nimport { normalizeMediaValue } from \"./media/normalize.js\";\nimport type { MediaProvider, MediaProviderCapabilities } from \"./media/types.js\";\nimport type { SandboxedPlugin, SandboxRunner } from \"./plugins/sandbox/types.js\";\nimport type {\n\tResolvedPlugin,\n\tMediaItem,\n\tPluginManifest,\n\tPluginCapability,\n\tPluginStorageConfig,\n\tPublicPageContext,\n\tPageMetadataContribution,\n\tPageFragmentContribution,\n} from \"./plugins/types.js\";\nimport type { FieldType } from \"./schema/types.js\";\nimport { hashString } from \"./utils/hash.js\";\n\nconst LEADING_SLASH_PATTERN = /^\\//;\n\n/** Combined result from a single-pass page contribution collection */\ninterface PageContributions {\n\tmetadata: PageMetadataContribution[];\n\tfragments: PageFragmentContribution[];\n}\n\nconst VALID_METADATA_KINDS = new Set([\"meta\", \"property\", \"link\", \"jsonld\"]);\n\n/** Security-critical allowlist for link rel values from sandboxed plugins */\nconst VALID_LINK_REL = new Set([\n\t\"canonical\",\n\t\"alternate\",\n\t\"author\",\n\t\"license\",\n\t\"site.standard.document\",\n]);\n\n/**\n * Runtime validation for sandboxed plugin metadata contributions.\n * Sandboxed plugins return `unknown` across the RPC boundary — we must\n * verify the shape before passing to the metadata collector.\n */\nfunction isValidMetadataContribution(c: unknown): c is PageMetadataContribution {\n\tif (!c || typeof c !== \"object\" || !(\"kind\" in c)) return false;\n\tconst obj = c as Record<string, unknown>;\n\tif (typeof obj.kind !== \"string\" || !VALID_METADATA_KINDS.has(obj.kind)) return false;\n\n\tswitch (obj.kind) {\n\t\tcase \"meta\":\n\t\t\treturn typeof obj.name === \"string\" && typeof obj.content === \"string\";\n\t\tcase \"property\":\n\t\t\treturn typeof obj.property === \"string\" && typeof obj.content === \"string\";\n\t\tcase \"link\":\n\t\t\treturn (\n\t\t\t\ttypeof obj.href === \"string\" && typeof obj.rel === \"string\" && VALID_LINK_REL.has(obj.rel)\n\t\t\t);\n\t\tcase \"jsonld\":\n\t\t\treturn obj.graph != null && typeof obj.graph === \"object\";\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nimport { loadBundleFromR2 } from \"./api/handlers/marketplace.js\";\nimport { runSystemCleanup } from \"./cleanup.js\";\nimport {\n\tDEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\tdefaultCommentModerate,\n} from \"./comments/moderator.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport {\n\thandleContentList,\n\thandleContentGet,\n\thandleContentGetIncludingTrashed,\n\thandleContentCreate,\n\thandleContentUpdate,\n\thandleContentDelete,\n\thandleContentDuplicate,\n\thandleContentRestore,\n\thandleContentPermanentDelete,\n\thandleContentListTrashed,\n\thandleContentCountTrashed,\n\thandleContentPublish,\n\thandleContentUnpublish,\n\thandleContentSchedule,\n\thandleContentUnschedule,\n\thandleContentCountScheduled,\n\thandleContentDiscardDraft,\n\thandleContentCompare,\n\thandleContentTranslations,\n\thandleMediaList,\n\thandleMediaGet,\n\thandleMediaCreate,\n\thandleMediaUpdate,\n\thandleMediaDelete,\n\thandleRevisionList,\n\thandleRevisionGet,\n\thandleRevisionRestore,\n\tSchemaRegistry,\n\ttype Database,\n\ttype Storage,\n} from \"./index.js\";\nimport { getDb } from \"./loader.js\";\nimport { CronExecutor, type InvokeCronHookFn } from \"./plugins/cron.js\";\nimport { definePlugin } from \"./plugins/define-plugin.js\";\nimport { DEV_CONSOLE_EMAIL_PLUGIN_ID, devConsoleEmailDeliver } from \"./plugins/email-console.js\";\nimport { EmailPipeline } from \"./plugins/email.js\";\nimport {\n\tcreateHookPipeline,\n\tresolveExclusiveHooks as resolveExclusiveHooksShared,\n\ttype HookPipeline,\n} from \"./plugins/hooks.js\";\nimport { normalizeManifestRoute } from \"./plugins/manifest-schema.js\";\nimport { extractRequestMeta, sanitizeHeadersForSandbox } from \"./plugins/request-meta.js\";\nimport { PluginRouteRegistry, type RouteMeta } from \"./plugins/routes.js\";\nimport { NodeCronScheduler } from \"./plugins/scheduler/node.js\";\nimport { PiggybackScheduler } from \"./plugins/scheduler/piggyback.js\";\nimport type { CronScheduler } from \"./plugins/scheduler/types.js\";\nimport { PluginStateRepository } from \"./plugins/state.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { FTSManager } from \"./search/fts-manager.js\";\n\n/**\n * Map schema field types to editor field kinds\n */\nconst FIELD_TYPE_TO_KIND: Record<FieldType, string> = {\n\tstring: \"string\",\n\tslug: \"string\",\n\ttext: \"richText\",\n\tnumber: \"number\",\n\tinteger: \"number\",\n\tboolean: \"boolean\",\n\tdatetime: \"datetime\",\n\tselect: \"select\",\n\tmultiSelect: \"multiSelect\",\n\tportableText: \"portableText\",\n\timage: \"image\",\n\tfile: \"file\",\n\treference: \"reference\",\n\tjson: \"json\",\n};\n\n/**\n * Sandboxed plugin entry from virtual module\n */\nexport interface SandboxedPluginEntry {\n\tid: string;\n\tversion: string;\n\toptions: Record<string, unknown>;\n\tcode: string;\n\t/** Capabilities the plugin requests */\n\tcapabilities: PluginCapability[];\n\t/** Allowed hosts for network:fetch */\n\tallowedHosts: string[];\n\t/** Declared storage collections */\n\tstorage: PluginStorageConfig;\n\t/** Admin pages */\n\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t/** Dashboard widgets */\n\tadminWidgets?: Array<{ id: string; title?: string; size?: string }>;\n\t/** Admin entry module */\n\tadminEntry?: string;\n\t/**\n\t * Exclusive hooks this plugin should be auto-selected for.\n\t * Weaker than an existing admin DB selection — config order wins when no selection exists.\n\t */\n\tpreferred?: string[];\n}\n\n/**\n * Media provider entry from virtual module\n */\nexport interface MediaProviderEntry {\n\tid: string;\n\tname: string;\n\ticon?: string;\n\tcapabilities: MediaProviderCapabilities;\n\t/** Factory function to create the provider instance */\n\tcreateProvider: (ctx: MediaProviderContext) => MediaProvider;\n}\n\n/**\n * Context passed to media provider factory functions\n */\nexport interface MediaProviderContext {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * Dependencies injected from virtual modules (middleware reads these)\n */\nexport interface RuntimeDependencies {\n\tconfig: EmDashConfig;\n\tplugins: ResolvedPlugin[];\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateDialect: (config: any) => Dialect;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateStorage: ((config: any) => Storage) | null;\n\tsandboxEnabled: boolean;\n\t/** Media provider entries from virtual module */\n\tmediaProviderEntries?: MediaProviderEntry[];\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\t/** Factory function matching SandboxRunnerFactory signature */\n\tcreateSandboxRunner: ((opts: { db: Kysely<Database> }) => SandboxRunner) | null;\n}\n\n/**\n * Convert a ContentItem to Record<string, unknown> for hook consumption.\n * Hooks receive the full item as a flat record.\n */\nfunction contentItemToRecord(item: ContentItemInternal): Record<string, unknown> {\n\treturn { ...item };\n}\n\n// Module-level caches (persist across requests within worker)\nconst dbCache = new Map<string, Kysely<Database>>();\nlet dbInitPromise: Promise<Kysely<Database>> | null = null;\nconst storageCache = new Map<string, Storage>();\nconst sandboxedPluginCache = new Map<string, SandboxedPlugin>();\nconst marketplacePluginKeys = new Set<string>();\n/** Manifest metadata for marketplace plugins: pluginId -> manifest admin config */\nconst marketplaceManifestCache = new Map<\n\tstring,\n\t{\n\t\tid: string;\n\t\tversion: string;\n\t\tadmin?: { pages?: PluginAdminPage[]; widgets?: PluginDashboardWidget[] };\n\t}\n>();\n/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */\nconst sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();\nlet sandboxRunner: SandboxRunner | null = null;\n\n/**\n * EmDashRuntime - singleton per worker\n */\nexport class EmDashRuntime {\n\t/**\n\t * The singleton database instance (worker-lifetime cached).\n\t * Use the `db` getter instead — it checks the request context first\n\t * for per-request overrides (D1 read replica sessions, DO multi-site).\n\t */\n\tprivate readonly _db: Kysely<Database>;\n\treadonly storage: Storage | null;\n\treadonly configuredPlugins: ResolvedPlugin[];\n\treadonly sandboxedPlugins: Map<string, SandboxedPlugin>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\treadonly schemaRegistry: SchemaRegistry;\n\tprivate _hooks!: HookPipeline;\n\treadonly config: EmDashConfig;\n\treadonly mediaProviders: Map<string, MediaProvider>;\n\treadonly mediaProviderEntries: MediaProviderEntry[];\n\treadonly cronExecutor: CronExecutor | null;\n\treadonly email: EmailPipeline | null;\n\n\tprivate cronScheduler: CronScheduler | null;\n\tprivate enabledPlugins: Set<string>;\n\tprivate pluginStates: Map<string, string>;\n\n\t/** Current hook pipeline. Use the `hooks` getter for external access. */\n\tget hooks(): HookPipeline {\n\t\treturn this._hooks;\n\t}\n\n\t/** All plugins eligible for the hook pipeline (includes built-in plugins).\n\t * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */\n\tprivate allPipelinePlugins: ResolvedPlugin[];\n\t/** Factory options for the hook pipeline context factory */\n\tprivate pipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tstorage?: Storage;\n\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t};\n\t/** Dependencies needed for exclusive hook resolution */\n\tprivate runtimeDeps: RuntimeDependencies;\n\t/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */\n\tprivate pipelineRef!: { current: HookPipeline };\n\n\t/**\n\t * Get the database instance for the current request.\n\t *\n\t * Checks the ALS-based request context first — middleware sets a\n\t * per-request Kysely instance there for D1 read replica sessions\n\t * or DO preview databases. Falls back to the singleton instance.\n\t */\n\tget db(): Kysely<Database> {\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is set by middleware with correct type\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\t\treturn this._db;\n\t}\n\n\tprivate constructor(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage | null,\n\t\tconfiguredPlugins: ResolvedPlugin[],\n\t\tsandboxedPlugins: Map<string, SandboxedPlugin>,\n\t\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\t\thooks: HookPipeline,\n\t\tenabledPlugins: Set<string>,\n\t\tpluginStates: Map<string, string>,\n\t\tconfig: EmDashConfig,\n\t\tmediaProviders: Map<string, MediaProvider>,\n\t\tmediaProviderEntries: MediaProviderEntry[],\n\t\tcronExecutor: CronExecutor | null,\n\t\tcronScheduler: CronScheduler | null,\n\t\temailPipeline: EmailPipeline | null,\n\t\tallPipelinePlugins: ResolvedPlugin[],\n\t\tpipelineFactoryOptions: {\n\t\t\tdb: Kysely<Database>;\n\t\t\tstorage?: Storage;\n\t\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t\t},\n\t\truntimeDeps: RuntimeDependencies,\n\t\tpipelineRef: { current: HookPipeline },\n\t) {\n\t\tthis._db = db;\n\t\tthis.storage = storage;\n\t\tthis.configuredPlugins = configuredPlugins;\n\t\tthis.sandboxedPlugins = sandboxedPlugins;\n\t\tthis.sandboxedPluginEntries = sandboxedPluginEntries;\n\t\tthis.schemaRegistry = new SchemaRegistry(db);\n\t\tthis._hooks = hooks;\n\t\tthis.enabledPlugins = enabledPlugins;\n\t\tthis.pluginStates = pluginStates;\n\t\tthis.config = config;\n\t\tthis.mediaProviders = mediaProviders;\n\t\tthis.mediaProviderEntries = mediaProviderEntries;\n\t\tthis.cronExecutor = cronExecutor;\n\t\tthis.cronScheduler = cronScheduler;\n\t\tthis.email = emailPipeline;\n\t\tthis.allPipelinePlugins = allPipelinePlugins;\n\t\tthis.pipelineFactoryOptions = pipelineFactoryOptions;\n\t\tthis.runtimeDeps = runtimeDeps;\n\t\tthis.pipelineRef = pipelineRef;\n\t}\n\n\t/**\n\t * Get the sandbox runner instance (for marketplace install/update)\n\t */\n\tgetSandboxRunner(): SandboxRunner | null {\n\t\treturn sandboxRunner;\n\t}\n\n\t/**\n\t * Tick the cron system from request context (piggyback mode).\n\t * Call this from middleware on each request to ensure cron tasks\n\t * execute even when no dedicated scheduler is available.\n\t */\n\ttickCron(): void {\n\t\tif (this.cronScheduler instanceof PiggybackScheduler) {\n\t\t\tthis.cronScheduler.onRequest();\n\t\t}\n\t}\n\n\t/**\n\t * Stop the cron scheduler gracefully.\n\t * Call during worker shutdown or hot-reload.\n\t */\n\tasync stopCron(): Promise<void> {\n\t\tif (this.cronScheduler) {\n\t\t\tawait this.cronScheduler.stop();\n\t\t}\n\t}\n\n\t/**\n\t * Update in-memory plugin status and rebuild the hook pipeline.\n\t *\n\t * Rebuilding the pipeline ensures disabled plugins' hooks stop firing\n\t * and re-enabled plugins' hooks start firing again without a restart.\n\t * Exclusive hook selections are re-resolved after each rebuild.\n\t */\n\tasync setPluginStatus(pluginId: string, status: \"active\" | \"inactive\"): Promise<void> {\n\t\tthis.pluginStates.set(pluginId, status);\n\t\tif (status === \"active\") {\n\t\t\tthis.enabledPlugins.add(pluginId);\n\t\t} else {\n\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t}\n\n\t\tawait this.rebuildHookPipeline();\n\t}\n\n\t/**\n\t * Rebuild the hook pipeline from the current set of enabled plugins.\n\t *\n\t * Filters `allPipelinePlugins` to only those in `enabledPlugins`,\n\t * creates a fresh HookPipeline, re-resolves exclusive hook selections,\n\t * and re-wires the context factory so existing references (cron\n\t * callbacks, email pipeline) use the new pipeline.\n\t */\n\tprivate async rebuildHookPipeline(): Promise<void> {\n\t\tconst enabledList = this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id));\n\t\tconst newPipeline = createHookPipeline(enabledList, this.pipelineFactoryOptions);\n\n\t\t// Re-resolve exclusive hooks against the new pipeline\n\t\tawait EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);\n\n\t\t// Carry over context factory options from the old pipeline so that\n\t\t// email, cron reschedule, and other wired-in options are preserved.\n\t\t// The old pipeline's contextFactoryOptions were built up incrementally\n\t\t// via setContextFactory calls during create(). We replay them here.\n\t\tif (this.email) {\n\t\t\tnewPipeline.setContextFactory({ db: this.db, emailPipeline: this.email });\n\t\t}\n\t\tif (this.cronScheduler) {\n\t\t\tconst scheduler = this.cronScheduler;\n\t\t\tnewPipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => scheduler.reschedule(),\n\t\t\t});\n\t\t}\n\n\t\t// Update the email pipeline to use the new hook pipeline\n\t\tif (this.email) {\n\t\t\tthis.email.setPipeline(newPipeline);\n\t\t}\n\n\t\t// Update the mutable ref so the cron closure dispatches through\n\t\t// the new pipeline without needing to reconstruct the CronExecutor.\n\t\tthis.pipelineRef.current = newPipeline;\n\n\t\tthis._hooks = newPipeline;\n\t}\n\n\t/**\n\t * Synchronize marketplace plugin runtime state with DB + storage.\n\t *\n\t * Ensures install/update/uninstall changes take effect immediately in the\n\t * current worker: loads newly active plugins and removes uninstalled ones.\n\t */\n\tasync syncMarketplacePlugins(): Promise<void> {\n\t\tif (!this.config.marketplace || !this.storage) return;\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) return;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst marketplaceStates = await stateRepo.getMarketplacePlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of marketplaceStates) {\n\t\t\t\tthis.pluginStates.set(state.pluginId, state.status);\n\t\t\t\tif (state.status === \"active\") {\n\t\t\t\t\tthis.enabledPlugins.add(state.pluginId);\n\t\t\t\t} else {\n\t\t\t\t\tthis.enabledPlugins.delete(state.pluginId);\n\t\t\t\t}\n\t\t\t\tif (state.status !== \"active\") continue;\n\t\t\t\tdesired.set(state.pluginId, state.marketplaceVersion ?? state.version);\n\t\t\t}\n\n\t\t\t// Remove uninstalled or no-longer-active marketplace plugins from memory.\n\t\t\tconst keysToRemove: string[] = [];\n\t\t\tfor (const key of marketplacePluginKeys) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;\n\t\t\t\tkeysToRemove.push(key);\n\t\t\t}\n\n\t\t\tfor (const key of keysToRemove) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (!desiredVersion) {\n\t\t\t\t\tthis.pluginStates.delete(pluginId);\n\t\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t\t}\n\n\t\t\t\tconst existing = sandboxedPluginCache.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait existing.terminate();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsandboxedPluginCache.delete(key);\n\t\t\t\tthis.sandboxedPlugins.delete(key);\n\t\t\t\tmarketplacePluginKeys.delete(key);\n\t\t\t\tif (pluginId) {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Load newly active marketplace plugins.\n\t\t\tfor (const [pluginId, version] of desired) {\n\t\t\t\tconst key = `${pluginId}:${version}`;\n\t\t\t\tif (sandboxedPluginCache.has(key)) {\n\t\t\t\t\tmarketplacePluginKeys.add(key);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst bundle = await loadBundleFromR2(this.storage, pluginId, version);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\tsandboxedPluginCache.set(key, loaded);\n\t\t\t\tthis.sandboxedPlugins.set(key, loaded);\n\t\t\t\tmarketplacePluginKeys.add(key);\n\n\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\tmarketplaceManifestCache.set(pluginId, {\n\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t});\n\n\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\tconst routeMetaMap = new Map<string, RouteMeta>();\n\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\trouteMetaMap.set(normalized.name, { public: normalized.public === true });\n\t\t\t\t\t}\n\t\t\t\t\tsandboxedRouteMetaCache.set(pluginId, routeMetaMap);\n\t\t\t\t} else {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash: Failed to sync marketplace plugins:\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create and initialize the runtime\n\t */\n\tstatic async create(deps: RuntimeDependencies): Promise<EmDashRuntime> {\n\t\t// Initialize database\n\t\tconst db = await EmDashRuntime.getDatabase(deps);\n\n\t\t// Verify and repair FTS indexes (auto-heal crash corruption)\n\t\t// FTS5 is SQLite-only; on other dialects, search is a no-op until\n\t\t// the pluggable SearchProvider work lands.\n\t\tif (isSqlite(db)) {\n\t\t\ttry {\n\t\t\t\tconst ftsManager = new FTSManager(db);\n\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\tif (repaired > 0) {\n\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal.\n\t\t\t}\n\t\t}\n\n\t\t// Initialize storage\n\t\tconst storage = EmDashRuntime.getStorage(deps);\n\n\t\t// Fetch plugin states from database\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\ttry {\n\t\t\tconst states = await db.selectFrom(\"_plugin_state\").select([\"plugin_id\", \"status\"]).execute();\n\t\t\tpluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));\n\t\t} catch {\n\t\t\t// Plugin state table may not exist yet\n\t\t}\n\n\t\t// Build set of enabled plugins\n\t\tconst enabledPlugins = new Set<string>();\n\t\tfor (const plugin of deps.plugins) {\n\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t}\n\t\t}\n\n\t\t// Load site info for plugin context extensions\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\ttry {\n\t\t\tconst optionsRepo = new OptionsRepository(db);\n\t\t\tconst siteName = await optionsRepo.get<string>(\"emdash:site_title\");\n\t\t\tconst siteUrl = await optionsRepo.get<string>(\"emdash:site_url\");\n\t\t\tconst locale = await optionsRepo.get<string>(\"emdash:locale\");\n\t\t\tsiteInfo = {\n\t\t\t\tsiteName: siteName ?? undefined,\n\t\t\t\tsiteUrl: siteUrl ?? undefined,\n\t\t\t\tlocale: locale ?? undefined,\n\t\t\t};\n\t\t} catch {\n\t\t\t// Options table may not exist yet (pre-setup)\n\t\t}\n\n\t\t// Build the full list of pipeline-eligible plugins: all configured\n\t\t// plugins (regardless of current enabled status) plus built-in plugins.\n\t\t// rebuildHookPipeline() filters this to only enabled plugins.\n\t\tconst allPipelinePlugins: ResolvedPlugin[] = [...deps.plugins];\n\n\t\t// In dev mode, register a built-in console email provider.\n\t\t// It participates in exclusive hook resolution like any other plugin —\n\t\t// auto-selected when it's the sole provider, overridden when a real one is configured.\n\t\t// Gated by import.meta.env.DEV to prevent silent email loss in production.\n\t\tif (import.meta.env.DEV) {\n\t\t\ttry {\n\t\t\t\tconst devConsolePlugin = definePlugin({\n\t\t\t\t\tid: DEV_CONSOLE_EMAIL_PLUGIN_ID,\n\t\t\t\t\tversion: \"0.0.0\",\n\t\t\t\t\tcapabilities: [\"email:provide\"],\n\t\t\t\t\thooks: {\n\t\t\t\t\t\t\"email:deliver\": {\n\t\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\t\thandler: devConsoleEmailDeliver,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tallPipelinePlugins.push(devConsolePlugin);\n\t\t\t\t// Built-in plugins are always enabled\n\t\t\t\tenabledPlugins.add(devConsolePlugin.id);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[email] Failed to register dev console email provider:\", error);\n\t\t\t}\n\t\t}\n\n\t\t// Register built-in default comment moderator.\n\t\t// Always present — auto-selected as the sole comment:moderate provider\n\t\t// unless a plugin (e.g. AI moderation) provides its own.\n\t\ttry {\n\t\t\tconst defaultModeratorPlugin = definePlugin({\n\t\t\t\tid: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\t\t\t\tversion: \"0.0.0\",\n\t\t\t\tcapabilities: [\"read:users\"],\n\t\t\t\thooks: {\n\t\t\t\t\t\"comment:moderate\": {\n\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\thandler: defaultCommentModerate,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t\tallPipelinePlugins.push(defaultModeratorPlugin);\n\t\t\t// Built-in plugins are always enabled\n\t\t\tenabledPlugins.add(defaultModeratorPlugin.id);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[comments] Failed to register default moderator:\", error);\n\t\t}\n\n\t\t// Filter to currently enabled plugins for the initial pipeline\n\t\tconst enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));\n\n\t\t// Create hook pipeline\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tstorage: storage ?? undefined,\n\t\t\tsiteInfo,\n\t\t};\n\t\tconst pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);\n\n\t\t// Load sandboxed plugins (build-time)\n\t\tconst sandboxedPlugins = await EmDashRuntime.loadSandboxedPlugins(deps, db);\n\n\t\t// Cold-start: load marketplace-installed plugins from site R2\n\t\tif (deps.config.marketplace && storage) {\n\t\t\tawait EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins);\n\t\t}\n\n\t\t// Initialize media providers\n\t\tconst mediaProviders = new Map<string, MediaProvider>();\n\t\tconst mediaProviderEntries = deps.mediaProviderEntries ?? [];\n\t\tconst providerContext: MediaProviderContext = { db, storage };\n\n\t\tfor (const entry of mediaProviderEntries) {\n\t\t\ttry {\n\t\t\t\tconst provider = entry.createProvider(providerContext);\n\t\t\t\tmediaProviders.set(entry.id, provider);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Failed to initialize media provider \"${entry.id}\":`, error);\n\t\t\t}\n\t\t}\n\n\t\t// Resolve exclusive hooks — auto-select providers and sync with DB\n\t\tawait EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps);\n\n\t\t// ── Email pipeline ───────────────────────────────────────────────\n\t\t// The email pipeline orchestrates beforeSend → deliver → afterSend.\n\t\t// The dev console provider was registered above and will be auto-selected\n\t\t// by resolveExclusiveHooks if it's the sole email:deliver provider.\n\t\tconst emailPipeline = new EmailPipeline(pipeline);\n\n\t\t// Wire email send into sandbox runner (created earlier but without\n\t\t// email pipeline since it didn't exist yet)\n\t\tif (sandboxRunner) {\n\t\t\tsandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));\n\t\t}\n\n\t\t// ── Cron system ──────────────────────────────────────────────────\n\t\t// Create executor with a hook dispatch function that uses the pipeline.\n\t\t// The callback reads from a mutable ref so that rebuildHookPipeline()\n\t\t// can swap the pipeline without reconstructing the CronExecutor.\n\t\tconst pipelineRef = { current: pipeline };\n\t\tconst invokeCronHook: InvokeCronHookFn = async (pluginId, event) => {\n\t\t\tconst result = await pipelineRef.current.invokeCronHook(pluginId, event);\n\t\t\tif (!result.success && result.error) {\n\t\t\t\tthrow result.error;\n\t\t\t}\n\t\t};\n\n\t\t// Wire email pipeline into context factory (independent of cron —\n\t\t// must not be inside the cron try/catch or ctx.email breaks when cron fails)\n\t\tpipeline.setContextFactory({ db, emailPipeline });\n\n\t\tlet cronExecutor: CronExecutor | null = null;\n\t\tlet cronScheduler: CronScheduler | null = null;\n\n\t\ttry {\n\t\t\tcronExecutor = new CronExecutor(db, invokeCronHook);\n\n\t\t\t// Recover stale locks from previous crashes\n\t\t\tconst recovered = await cronExecutor.recoverStaleLocks();\n\t\t\tif (recovered > 0) {\n\t\t\t\tconsole.log(`[cron] Recovered ${recovered} stale task lock(s)`);\n\t\t\t}\n\n\t\t\t// Detect platform and create appropriate scheduler.\n\t\t\t// On Cloudflare Workers, setTimeout is available but unreliable for\n\t\t\t// long durations — use PiggybackScheduler as default.\n\t\t\t// In Node/Bun, use NodeCronScheduler with real timers.\n\t\t\tconst isWorkersRuntime =\n\t\t\t\ttypeof globalThis.navigator !== \"undefined\" &&\n\t\t\t\tglobalThis.navigator.userAgent === \"Cloudflare-Workers\";\n\n\t\t\tif (isWorkersRuntime) {\n\t\t\t\tcronScheduler = new PiggybackScheduler(cronExecutor);\n\t\t\t} else {\n\t\t\t\tcronScheduler = new NodeCronScheduler(cronExecutor);\n\t\t\t}\n\n\t\t\t// Register system cleanup to run alongside each scheduler tick.\n\t\t\t// Pass storage so cleanupPendingUploads can delete orphaned files.\n\t\t\tcronScheduler.setSystemCleanup(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runSystemCleanup(db, storage ?? undefined);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Non-fatal -- individual cleanup failures are already logged\n\t\t\t\t\t// by runSystemCleanup. This catches unexpected errors.\n\t\t\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Add cron reschedule callback (merges with existing factory options)\n\t\t\tpipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => cronScheduler?.reschedule(),\n\t\t\t});\n\n\t\t\t// Start the scheduler\n\t\t\tawait cronScheduler.start();\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[cron] Failed to initialize cron system:\", error);\n\t\t\t// Non-fatal — CMS works without cron\n\t\t}\n\n\t\treturn new EmDashRuntime(\n\t\t\tdb,\n\t\t\tstorage,\n\t\t\tdeps.plugins,\n\t\t\tsandboxedPlugins,\n\t\t\tdeps.sandboxedPluginEntries,\n\t\t\tpipeline,\n\t\t\tenabledPlugins,\n\t\t\tpluginStates,\n\t\t\tdeps.config,\n\t\t\tmediaProviders,\n\t\t\tmediaProviderEntries,\n\t\t\tcronExecutor,\n\t\t\tcronScheduler,\n\t\t\temailPipeline,\n\t\t\tallPipelinePlugins,\n\t\t\tpipelineFactoryOptions,\n\t\t\tdeps,\n\t\t\tpipelineRef,\n\t\t);\n\t}\n\n\t/**\n\t * Get a media provider by ID\n\t */\n\tgetMediaProvider(providerId: string): MediaProvider | undefined {\n\t\treturn this.mediaProviders.get(providerId);\n\t}\n\n\t/**\n\t * Get all media provider entries (for admin UI)\n\t */\n\tgetMediaProviderList(): Array<{\n\t\tid: string;\n\t\tname: string;\n\t\ticon?: string;\n\t\tcapabilities: MediaProviderCapabilities;\n\t}> {\n\t\treturn this.mediaProviderEntries.map((e) => ({\n\t\t\tid: e.id,\n\t\t\tname: e.name,\n\t\t\ticon: e.icon,\n\t\t\tcapabilities: e.capabilities,\n\t\t}));\n\t}\n\n\t/**\n\t * Get or create database instance\n\t */\n\tprivate static async getDatabase(deps: RuntimeDependencies): Promise<Kysely<Database>> {\n\t\t// If a per-request DB override is set (e.g. by the playground middleware\n\t\t// which runs before the runtime init), use that directly. This allows\n\t\t// the runtime to initialize against the real DO database instead of\n\t\t// the dummy singleton dialect.\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is typed as unknown to avoid circular deps\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\n\t\tconst dbConfig = deps.config.database;\n\n\t\t// If no database configured in integration, try to get from loader\n\t\tif (!dbConfig) {\n\t\t\ttry {\n\t\t\t\treturn await getDb();\n\t\t\t} catch {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst cacheKey = dbConfig.entrypoint;\n\n\t\t// Return cached instance if available\n\t\tconst cached = dbCache.get(cacheKey);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Use initialization lock to prevent race conditions.\n\t\t// Sharing this promise across requests is safe because the Kysely instance\n\t\t// doesn't hold a request-scoped resource — the DO dialect uses a getStub()\n\t\t// factory that creates a fresh stub per query execution.\n\t\tif (dbInitPromise) {\n\t\t\treturn dbInitPromise;\n\t\t}\n\n\t\tdbInitPromise = (async () => {\n\t\t\tconst dialect = deps.createDialect(dbConfig.config);\n\t\t\tconst db = new Kysely<Database>({ dialect });\n\n\t\t\tawait runMigrations(db);\n\n\t\t\t// Auto-seed schema if no collections exist and setup hasn't run.\n\t\t\t// This covers first-load on sites that skip the setup wizard.\n\t\t\t// Dev-bypass and the wizard apply seeds explicitly.\n\t\t\ttry {\n\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\tdb\n\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\tdb\n\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t]);\n\n\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t})();\n\n\t\t\t\tif (collectionCount.count === 0 && !setupDone) {\n\t\t\t\t\tconst { applySeed } = await import(\"./seed/apply.js\");\n\t\t\t\t\tconst { loadSeed } = await import(\"./seed/load.js\");\n\t\t\t\t\tconst { validateSeed } = await import(\"./seed/validate.js\");\n\n\t\t\t\t\tconst seed = await loadSeed();\n\t\t\t\t\tconst validation = validateSeed(seed);\n\t\t\t\t\tif (validation.valid) {\n\t\t\t\t\t\tawait applySeed(db, seed, { onConflict: \"skip\" });\n\t\t\t\t\t\tconsole.log(\"Auto-seeded default collections\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Tables may not exist yet. Non-fatal.\n\t\t\t}\n\n\t\t\tdbCache.set(cacheKey, db);\n\t\t\treturn db;\n\t\t})();\n\n\t\ttry {\n\t\t\treturn await dbInitPromise;\n\t\t} finally {\n\t\t\tdbInitPromise = null;\n\t\t}\n\t}\n\n\t/**\n\t * Get or create storage instance\n\t */\n\tprivate static getStorage(deps: RuntimeDependencies): Storage | null {\n\t\tconst storageConfig = deps.config.storage;\n\t\tif (!storageConfig || !deps.createStorage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst cacheKey = storageConfig.entrypoint;\n\t\tconst cached = storageCache.get(cacheKey);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\tconst storage = deps.createStorage(storageConfig.config);\n\t\tstorageCache.set(cacheKey, storage);\n\t\treturn storage;\n\t}\n\n\t/**\n\t * Load sandboxed plugins using SandboxRunner\n\t */\n\tprivate static async loadSandboxedPlugins(\n\t\tdeps: RuntimeDependencies,\n\t\tdb: Kysely<Database>,\n\t): Promise<Map<string, SandboxedPlugin>> {\n\t\t// Return cached plugins if already loaded\n\t\tif (sandboxedPluginCache.size > 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Check if sandboxing is enabled\n\t\tif (!deps.sandboxEnabled || deps.sandboxedPluginEntries.length === 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Create sandbox runner if not exists\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({ db });\n\t\t}\n\n\t\tif (!sandboxRunner) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Check if the runner is actually available (has required bindings)\n\t\tif (!sandboxRunner.isAvailable()) {\n\t\t\tconsole.debug(\"EmDash: Sandbox runner not available (missing bindings), skipping sandbox\");\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Load each sandboxed plugin\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tconst pluginKey = `${entry.id}:${entry.version}`;\n\t\t\tif (sandboxedPluginCache.has(pluginKey)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Build manifest from entry's declared config\n\t\t\t\tconst manifest: PluginManifest = {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tcapabilities: entry.capabilities ?? [],\n\t\t\t\t\tallowedHosts: entry.allowedHosts ?? [],\n\t\t\t\t\tstorage: entry.storage ?? {},\n\t\t\t\t\thooks: [],\n\t\t\t\t\troutes: [],\n\t\t\t\t\tadmin: {},\n\t\t\t\t};\n\n\t\t\t\tconst plugin = await sandboxRunner.load(manifest, entry.code);\n\t\t\t\tsandboxedPluginCache.set(pluginKey, plugin);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(\", \")}]`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn sandboxedPluginCache;\n\t}\n\n\t/**\n\t * Cold-start: load marketplace-installed plugins from site-local R2 storage\n\t *\n\t * Queries _plugin_state for source='marketplace' rows, fetches each bundle\n\t * from R2, and loads via SandboxRunner.\n\t */\n\tprivate static async loadMarketplacePlugins(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t\tdeps: RuntimeDependencies,\n\t\tcache: Map<string, SandboxedPlugin>,\n\t): Promise<void> {\n\t\t// Ensure sandbox runner exists\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({ db });\n\t\t}\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\n\t\t\tfor (const plugin of marketplacePlugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\n\t\t\t\tconst version = plugin.marketplaceVersion ?? plugin.version;\n\t\t\t\tconst pluginKey = `${plugin.pluginId}:${version}`;\n\n\t\t\t\t// Skip if already loaded (shouldn't happen, but guard)\n\t\t\t\tif (cache.has(pluginKey)) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst bundle = await loadBundleFromR2(storage, plugin.pluginId, version);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\t\tcache.set(pluginKey, loaded);\n\t\t\t\t\tmarketplacePluginKeys.add(pluginKey);\n\n\t\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\t\tmarketplaceManifestCache.set(plugin.pluginId, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\t\tconst routeMeta = new Map<string, RouteMeta>();\n\t\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\t\trouteMeta.set(normalized.name, { public: normalized.public === true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);\n\t\t\t\t\t}\n\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded marketplace plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(\", \")}]`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet (pre-migration)\n\t\t}\n\t}\n\n\t/**\n\t * Resolve exclusive hook selections on startup.\n\t *\n\t * Delegates to the shared resolveExclusiveHooks() in hooks.ts.\n\t * The runtime version considers all pipeline providers as \"active\" since\n\t * the pipeline was already built from only active/enabled plugins.\n\t */\n\tprivate static async resolveExclusiveHooks(\n\t\tpipeline: HookPipeline,\n\t\tdb: Kysely<Database>,\n\t\tdeps: RuntimeDependencies,\n\t): Promise<void> {\n\t\tconst exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();\n\t\tif (exclusiveHookNames.length === 0) return;\n\n\t\tlet optionsRepo: OptionsRepository;\n\t\ttry {\n\t\t\toptionsRepo = new OptionsRepository(db);\n\t\t} catch {\n\t\t\treturn; // Options table may not exist yet\n\t\t}\n\n\t\t// Build preferred hints from sandboxed plugin entries\n\t\tconst preferredHints = new Map<string, string[]>();\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tif (entry.preferred && entry.preferred.length > 0) {\n\t\t\t\tpreferredHints.set(entry.id, entry.preferred);\n\t\t\t}\n\t\t}\n\n\t\t// The pipeline was created from only enabled plugins, so all providers\n\t\t// in it are active. The isActive check always returns true.\n\t\tawait resolveExclusiveHooksShared({\n\t\t\tpipeline,\n\t\t\tisActive: () => true,\n\t\t\tgetOption: (key) => optionsRepo.get<string>(key),\n\t\t\tsetOption: (key, value) => optionsRepo.set(key, value),\n\t\t\tdeleteOption: async (key) => {\n\t\t\t\tawait optionsRepo.delete(key);\n\t\t\t},\n\t\t\tpreferredHints,\n\t\t});\n\t}\n\n\t// =========================================================================\n\t// Manifest\n\t// =========================================================================\n\n\t/**\n\t * Build the manifest (rebuilt on each request for freshness)\n\t */\n\tasync getManifest(): Promise<EmDashManifest> {\n\t\t// Build collections from database.\n\t\t// Use this.db (ALS-aware getter) so playground mode picks up the\n\t\t// per-session DO database instead of the hardcoded singleton.\n\t\tconst manifestCollections: Record<string, ManifestCollection> = {};\n\t\ttry {\n\t\t\tconst registry = new SchemaRegistry(this.db);\n\t\t\tconst dbCollections = await registry.listCollections();\n\t\t\tfor (const collection of dbCollections) {\n\t\t\t\tconst collectionWithFields = await registry.getCollectionWithFields(collection.slug);\n\t\t\t\tconst fields: Record<\n\t\t\t\t\tstring,\n\t\t\t\t\t{\n\t\t\t\t\t\tkind: string;\n\t\t\t\t\t\tlabel?: string;\n\t\t\t\t\t\trequired?: boolean;\n\t\t\t\t\t\twidget?: string;\n\t\t\t\t\t\toptions?: Array<{ value: string; label: string }>;\n\t\t\t\t\t}\n\t\t\t\t> = {};\n\n\t\t\t\tif (collectionWithFields?.fields) {\n\t\t\t\t\tfor (const field of collectionWithFields.fields) {\n\t\t\t\t\t\tconst entry: (typeof fields)[string] = {\n\t\t\t\t\t\t\tkind: FIELD_TYPE_TO_KIND[field.type] ?? \"string\",\n\t\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\t\trequired: field.required,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (field.widget) entry.widget = field.widget;\n\t\t\t\t\t\t// Include select/multiSelect options from validation\n\t\t\t\t\t\tif (field.validation?.options) {\n\t\t\t\t\t\t\tentry.options = field.validation.options.map((v) => ({\n\t\t\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfields[field.slug] = entry;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tmanifestCollections[collection.slug] = {\n\t\t\t\t\tlabel: collection.label,\n\t\t\t\t\tlabelSingular: collection.labelSingular || collection.label,\n\t\t\t\t\tsupports: collection.supports || [],\n\t\t\t\t\thasSeo: collection.hasSeo,\n\t\t\t\t\tfields,\n\t\t\t\t};\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.debug(\"EmDash: Could not load database collections:\", error);\n\t\t}\n\n\t\t// Build plugins manifest\n\t\tconst manifestPlugins: Record<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tversion?: string;\n\t\t\t\tenabled?: boolean;\n\t\t\t\tsandboxed?: boolean;\n\t\t\t\tadminMode?: \"react\" | \"blocks\" | \"none\";\n\t\t\t\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t\t\t\tdashboardWidgets?: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\ttitle?: string;\n\t\t\t\t\tsize?: string;\n\t\t\t\t}>;\n\t\t\t\tportableTextBlocks?: Array<{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\ticon?: string;\n\t\t\t\t\tdescription?: string;\n\t\t\t\t\tplaceholder?: string;\n\t\t\t\t\tfields?: Element[];\n\t\t\t\t}>;\n\t\t\t\tfieldWidgets?: Array<{\n\t\t\t\t\tname: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\tfieldTypes: string[];\n\t\t\t\t\telements?: Element[];\n\t\t\t\t}>;\n\t\t\t}\n\t\t> = {};\n\n\t\tfor (const plugin of this.configuredPlugins) {\n\t\t\tconst status = this.pluginStates.get(plugin.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\t// Determine admin mode: has admin entry → react, has pages/widgets → blocks, else none\n\t\t\tconst hasAdminEntry = !!plugin.admin?.entry;\n\t\t\tconst hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;\n\t\t\tlet adminMode: \"react\" | \"blocks\" | \"none\" = \"none\";\n\t\t\tif (hasAdminEntry) {\n\t\t\t\tadminMode = \"react\";\n\t\t\t} else if (hasAdminPages || hasWidgets) {\n\t\t\t\tadminMode = \"blocks\";\n\t\t\t}\n\n\t\t\tmanifestPlugins[plugin.id] = {\n\t\t\t\tversion: plugin.version,\n\t\t\t\tenabled,\n\t\t\t\tadminMode,\n\t\t\t\tadminPages: plugin.admin?.pages,\n\t\t\t\tdashboardWidgets: plugin.admin?.widgets,\n\t\t\t\tportableTextBlocks: plugin.admin?.portableTextBlocks,\n\t\t\t\tfieldWidgets: plugin.admin?.fieldWidgets,\n\t\t\t};\n\t\t}\n\n\t\t// Add sandboxed plugins (use entries for admin config)\n\t\t// TODO: sandboxed plugins need fieldWidgets extracted from their manifest\n\t\t// to support Block Kit field widgets. Currently only trusted plugins carry\n\t\t// fieldWidgets through the admin.fieldWidgets path.\n\t\tfor (const entry of this.sandboxedPluginEntries) {\n\t\t\tconst status = this.pluginStates.get(entry.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\tconst hasAdminPages = (entry.adminPages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[entry.id] = {\n\t\t\t\tversion: entry.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: entry.adminPages,\n\t\t\t\tdashboardWidgets: entry.adminWidgets,\n\t\t\t};\n\t\t}\n\n\t\t// Add marketplace-installed plugins (dynamically loaded from R2)\n\t\tfor (const [pluginId, meta] of marketplaceManifestCache) {\n\t\t\t// Skip if already included from build-time config\n\t\t\tif (manifestPlugins[pluginId]) continue;\n\n\t\t\tconst status = this.pluginStates.get(pluginId);\n\t\t\tconst enabled = status === \"active\";\n\n\t\t\tconst pages = meta.admin?.pages;\n\t\t\tconst widgets = meta.admin?.widgets;\n\t\t\tconst hasAdminPages = (pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (widgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[pluginId] = {\n\t\t\t\tversion: meta.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: pages,\n\t\t\t\tdashboardWidgets: widgets,\n\t\t\t};\n\t\t}\n\n\t\t// Generate hash from both collections and plugins so cache invalidates\n\t\t// when plugins are enabled/disabled or their config changes\n\t\tconst manifestHash = await hashString(\n\t\t\tJSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins),\n\t\t);\n\n\t\t// Determine auth mode\n\t\tconst authMode = getAuthMode(this.config);\n\t\tconst authModeValue = authMode.type === \"external\" ? authMode.providerType : \"passkey\";\n\n\t\t// Include i18n config if enabled\n\t\tconst { getI18nConfig, isI18nEnabled } = await import(\"./i18n/config.js\");\n\t\tconst i18nConfig = getI18nConfig();\n\t\tconst i18n =\n\t\t\tisI18nEnabled() && i18nConfig\n\t\t\t\t? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }\n\t\t\t\t: undefined;\n\n\t\treturn {\n\t\t\tversion: \"0.1.0\",\n\t\t\thash: manifestHash,\n\t\t\tcollections: manifestCollections,\n\t\t\tplugins: manifestPlugins,\n\t\t\tauthMode: authModeValue,\n\t\t\ti18n,\n\t\t\tmarketplace: !!this.config.marketplace,\n\t\t};\n\t}\n\n\t/**\n\t * Invalidate the cached manifest (no-op now that we don't cache).\n\t * Kept for API compatibility.\n\t */\n\tinvalidateManifest(): void {\n\t\t// No-op - manifest is rebuilt on each request\n\t}\n\n\t// =========================================================================\n\t// Content Handlers\n\t// =========================================================================\n\n\tasync handleContentList(\n\t\tcollection: string,\n\t\tparams: {\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t\tstatus?: string;\n\t\t\torderBy?: string;\n\t\t\torder?: \"asc\" | \"desc\";\n\t\t\tlocale?: string;\n\t\t},\n\t) {\n\t\treturn handleContentList(this.db, collection, params);\n\t}\n\n\tasync handleContentGet(collection: string, id: string, locale?: string) {\n\t\treturn handleContentGet(this.db, collection, id, locale);\n\t}\n\n\tasync handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {\n\t\treturn handleContentGetIncludingTrashed(this.db, collection, id, locale);\n\t}\n\n\tasync handleContentCreate(\n\t\tcollection: string,\n\t\tbody: {\n\t\t\tdata: Record<string, unknown>;\n\t\t\tslug?: string;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\tlocale?: string;\n\t\t\ttranslationOf?: string;\n\t\t},\n\t) {\n\t\t// Run beforeSave hooks (trusted plugins)\n\t\tlet processedData = body.data;\n\t\tif (this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(body.data, collection, true);\n\t\t\tprocessedData = hookResult.content;\n\t\t}\n\n\t\t// Run beforeSave hooks (sandboxed plugins)\n\t\tprocessedData = await this.runSandboxedBeforeSave(processedData, collection, true);\n\n\t\t// Normalize media fields (fill dimensions, storageKey, etc.)\n\t\tprocessedData = await this.normalizeMediaFields(collection, processedData);\n\n\t\t// Create the content\n\t\tconst result = await handleContentCreate(this.db, collection, {\n\t\t\t...body,\n\t\t\tdata: processedData,\n\t\t\tauthorId: body.authorId,\n\t\t\tbylines: body.bylines,\n\t\t});\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUpdate(\n\t\tcollection: string,\n\t\tid: string,\n\t\tbody: {\n\t\t\tdata?: Record<string, unknown>;\n\t\t\tslug?: string;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string | null;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\t/** Skip revision creation (used by autosave) */\n\t\t\tskipRevision?: boolean;\n\t\t\t_rev?: string;\n\t\t},\n\t) {\n\t\t// Resolve slug → ID if needed (before any lookups)\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(this.db);\n\t\tconst resolvedItem = await repo.findByIdOrSlug(collection, id);\n\t\tconst resolvedId = resolvedItem?.id ?? id;\n\n\t\t// Validate _rev early — before draft revision writes which modify updated_at.\n\t\t// After validation, strip _rev so the handler doesn't double-check against\n\t\t// the now-modified timestamp.\n\t\tif (body._rev) {\n\t\t\tif (!resolvedItem) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: `Content item not found: ${id}` },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst revCheck = validateRev(body._rev, resolvedItem);\n\t\t\tif (!revCheck.valid) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"CONFLICT\", message: revCheck.message },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tconst { _rev: _discardedRev, ...bodyWithoutRev } = body;\n\n\t\t// Run beforeSave hooks if data is provided\n\t\tlet processedData = bodyWithoutRev.data;\n\t\tif (bodyWithoutRev.data) {\n\t\t\tif (this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(\n\t\t\t\t\tbodyWithoutRev.data,\n\t\t\t\t\tcollection,\n\t\t\t\t\tfalse,\n\t\t\t\t);\n\t\t\t\tprocessedData = hookResult.content;\n\t\t\t}\n\n\t\t\t// Run sandboxed beforeSave hooks\n\t\t\tprocessedData = await this.runSandboxedBeforeSave(processedData!, collection, false);\n\n\t\t\t// Normalize media fields (fill dimensions, storageKey, etc.)\n\t\t\tprocessedData = await this.normalizeMediaFields(collection, processedData);\n\t\t}\n\n\t\t// Draft-aware revision handling (if collection supports revisions)\n\t\t// Content table columns = published data (never written by saves).\n\t\t// Draft data lives only in the revisions table.\n\t\tlet usesDraftRevisions = false;\n\t\tif (processedData) {\n\t\t\ttry {\n\t\t\t\tconst collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);\n\t\t\t\tif (collectionInfo?.supports?.includes(\"revisions\")) {\n\t\t\t\t\tusesDraftRevisions = true;\n\t\t\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\t\t\t// Re-fetch to get latest state (resolvedItem may be stale after _rev check)\n\t\t\t\t\tconst existing = await repo.findById(collection, resolvedId);\n\n\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t// Build the draft data: merge with existing draft revision if one exists,\n\t\t\t\t\t\t// otherwise merge with the published data from the content table\n\t\t\t\t\t\tlet baseData: Record<string, unknown>;\n\t\t\t\t\t\tif (existing.draftRevisionId) {\n\t\t\t\t\t\t\tconst draftRevision = await revisionRepo.findById(existing.draftRevisionId);\n\t\t\t\t\t\t\tbaseData = draftRevision?.data ?? existing.data;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbaseData = existing.data;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Include slug in the revision data if it changed\n\t\t\t\t\t\tconst mergedData = { ...baseData, ...processedData };\n\t\t\t\t\t\tif (bodyWithoutRev.slug !== undefined) {\n\t\t\t\t\t\t\tmergedData._slug = bodyWithoutRev.slug;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (bodyWithoutRev.skipRevision && existing.draftRevisionId) {\n\t\t\t\t\t\t\t// Autosave: update existing draft revision in place\n\t\t\t\t\t\t\tawait revisionRepo.updateData(existing.draftRevisionId, mergedData);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create new draft revision\n\t\t\t\t\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\tentryId: resolvedId,\n\t\t\t\t\t\t\t\tdata: mergedData,\n\t\t\t\t\t\t\t\tauthorId: bodyWithoutRev.authorId ?? undefined,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Update entry to point to new draft (metadata only, not data columns)\n\t\t\t\t\t\t\tconst tableName = `ec_${collection}`;\n\t\t\t\t\t\t\tawait sql`\n\t\t\t\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\t\t\t\tSET draft_revision_id = ${revision.id},\n\t\t\t\t\t\t\t\t\tupdated_at = ${new Date().toISOString()}\n\t\t\t\t\t\t\t\tWHERE id = ${resolvedId}\n\t\t\t\t\t\t\t`.execute(this.db);\n\n\t\t\t\t\t\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\t\t\t\t\t\tvoid revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Don't fail the update if revision creation fails\n\t\t\t}\n\t\t}\n\n\t\t// Update the content table:\n\t\t// - If collection uses draft revisions: only update metadata (no data fields, no slug)\n\t\t// - Otherwise: update everything as before\n\t\tconst result = await handleContentUpdate(this.db, collection, resolvedId, {\n\t\t\t...bodyWithoutRev,\n\t\t\tdata: usesDraftRevisions ? undefined : processedData,\n\t\t\tslug: usesDraftRevisions ? undefined : bodyWithoutRev.slug,\n\t\t\tauthorId: bodyWithoutRev.authorId,\n\t\t\tbylines: bodyWithoutRev.bylines,\n\t\t});\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, false);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentDelete(collection: string, id: string) {\n\t\t// Run beforeDelete hooks (trusted plugins)\n\t\tif (this.hooks.hasHooks(\"content:beforeDelete\")) {\n\t\t\tconst { allowed } = await this.hooks.runContentBeforeDelete(id, collection);\n\t\t\tif (!allowed) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"DELETE_BLOCKED\",\n\t\t\t\t\t\tmessage: \"Delete blocked by plugin hook\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Run sandboxed beforeDelete hooks\n\t\tconst sandboxAllowed = await this.runSandboxedBeforeDelete(id, collection);\n\t\tif (!sandboxAllowed) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"DELETE_BLOCKED\",\n\t\t\t\t\tmessage: \"Delete blocked by sandboxed plugin hook\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Delete the content\n\t\tconst result = await handleContentDelete(this.db, collection, id);\n\n\t\t// Run afterDelete hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Trash Handlers\n\t// =========================================================================\n\n\tasync handleContentListTrashed(\n\t\tcollection: string,\n\t\tparams: { cursor?: string; limit?: number } = {},\n\t) {\n\t\treturn handleContentListTrashed(this.db, collection, params);\n\t}\n\n\tasync handleContentRestore(collection: string, id: string) {\n\t\treturn handleContentRestore(this.db, collection, id);\n\t}\n\n\tasync handleContentPermanentDelete(collection: string, id: string) {\n\t\treturn handleContentPermanentDelete(this.db, collection, id);\n\t}\n\n\tasync handleContentCountTrashed(collection: string) {\n\t\treturn handleContentCountTrashed(this.db, collection);\n\t}\n\n\tasync handleContentDuplicate(collection: string, id: string, authorId?: string) {\n\t\treturn handleContentDuplicate(this.db, collection, id, authorId);\n\t}\n\n\t// =========================================================================\n\t// Publishing & Scheduling Handlers\n\t// =========================================================================\n\n\tasync handleContentPublish(collection: string, id: string) {\n\t\treturn handleContentPublish(this.db, collection, id);\n\t}\n\n\tasync handleContentUnpublish(collection: string, id: string) {\n\t\treturn handleContentUnpublish(this.db, collection, id);\n\t}\n\n\tasync handleContentSchedule(collection: string, id: string, scheduledAt: string) {\n\t\treturn handleContentSchedule(this.db, collection, id, scheduledAt);\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\treturn handleContentUnschedule(this.db, collection, id);\n\t}\n\n\tasync handleContentCountScheduled(collection: string) {\n\t\treturn handleContentCountScheduled(this.db, collection);\n\t}\n\n\tasync handleContentDiscardDraft(collection: string, id: string) {\n\t\treturn handleContentDiscardDraft(this.db, collection, id);\n\t}\n\n\tasync handleContentCompare(collection: string, id: string) {\n\t\treturn handleContentCompare(this.db, collection, id);\n\t}\n\n\tasync handleContentTranslations(collection: string, id: string) {\n\t\treturn handleContentTranslations(this.db, collection, id);\n\t}\n\n\t// =========================================================================\n\t// Media Handlers\n\t// =========================================================================\n\n\tasync handleMediaList(params: { cursor?: string; limit?: number; mimeType?: string }) {\n\t\treturn handleMediaList(this.db, params);\n\t}\n\n\tasync handleMediaGet(id: string) {\n\t\treturn handleMediaGet(this.db, id);\n\t}\n\n\tasync handleMediaCreate(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t}) {\n\t\t// Run beforeUpload hooks\n\t\tlet processedInput = input;\n\t\tif (this.hooks.hasHooks(\"media:beforeUpload\")) {\n\t\t\tconst hookResult = await this.hooks.runMediaBeforeUpload({\n\t\t\t\tname: input.filename,\n\t\t\t\ttype: input.mimeType,\n\t\t\t\tsize: input.size || 0,\n\t\t\t});\n\t\t\tprocessedInput = {\n\t\t\t\t...input,\n\t\t\t\tfilename: hookResult.file.name,\n\t\t\t\tmimeType: hookResult.file.type,\n\t\t\t\tsize: hookResult.file.size,\n\t\t\t};\n\t\t}\n\n\t\t// Create the media record\n\t\tconst result = await handleMediaCreate(this.db, processedInput);\n\n\t\t// Run afterUpload hooks (fire-and-forget)\n\t\tif (result.success && this.hooks.hasHooks(\"media:afterUpload\")) {\n\t\t\tconst item = result.data.item;\n\t\t\tconst mediaItem: MediaItem = {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t\tthis.hooks\n\t\t\t\t.runMediaAfterUpload(mediaItem)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUpload hook error:\", err));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleMediaUpdate(\n\t\tid: string,\n\t\tinput: { alt?: string; caption?: string; width?: number; height?: number },\n\t) {\n\t\treturn handleMediaUpdate(this.db, id, input);\n\t}\n\n\tasync handleMediaDelete(id: string) {\n\t\treturn handleMediaDelete(this.db, id);\n\t}\n\n\t// =========================================================================\n\t// Revision Handlers\n\t// =========================================================================\n\n\tasync handleRevisionList(collection: string, entryId: string, params: { limit?: number } = {}) {\n\t\treturn handleRevisionList(this.db, collection, entryId, params);\n\t}\n\n\tasync handleRevisionGet(revisionId: string) {\n\t\treturn handleRevisionGet(this.db, revisionId);\n\t}\n\n\tasync handleRevisionRestore(revisionId: string, callerUserId: string) {\n\t\treturn handleRevisionRestore(this.db, revisionId, callerUserId);\n\t}\n\n\t// =========================================================================\n\t// Plugin Routes\n\t// =========================================================================\n\n\t/**\n\t * Get route metadata for a plugin route without invoking the handler.\n\t * Used by the catch-all route to decide auth before dispatch.\n\t * Returns null if the plugin or route doesn't exist.\n\t */\n\tgetPluginRouteMeta(pluginId: string, path: string): RouteMeta | null {\n\t\tif (!this.isPluginEnabled(pluginId)) return null;\n\n\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\t// Check trusted plugins first\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin) {\n\t\t\tconst route = trustedPlugin.routes[routeKey];\n\t\t\tif (!route) return null;\n\t\t\treturn { public: route.public === true };\n\t\t}\n\n\t\t// Check sandboxed plugin route metadata cache\n\t\tconst meta = sandboxedRouteMetaCache.get(pluginId);\n\t\tif (meta) {\n\t\t\tconst routeMeta = meta.get(routeKey);\n\t\t\tif (routeMeta) return routeMeta;\n\t\t}\n\n\t\t// The \"admin\" route is implicitly available for any sandboxed plugin\n\t\t// that declares admin pages or widgets. This handles plugins installed\n\t\t// from bundles that predate the explicit admin route requirement.\n\t\tif (routeKey === \"admin\") {\n\t\t\tconst manifestMeta = marketplaceManifestCache.get(pluginId);\n\t\t\tif (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t\t// Also check build-time sandboxed entries\n\t\t\tconst entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\t\tif (entry?.adminPages?.length || entry?.adminWidgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: if the plugin exists in the sandbox cache, allow the route.\n\t\t// The sandbox runner will return an error if the route doesn't actually exist.\n\t\tif (this.findSandboxedPlugin(pluginId)) {\n\t\t\treturn { public: false };\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tasync handlePluginApiRoute(pluginId: string, _method: string, path: string, request: Request) {\n\t\tif (!this.isPluginEnabled(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not enabled: ${pluginId}` },\n\t\t\t};\n\t\t}\n\n\t\t// Check trusted (configured) plugins first — this must match the\n\t\t// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {\n\t\t\tconst routeRegistry = new PluginRouteRegistry({ db: this.db });\n\t\t\trouteRegistry.register(trustedPlugin);\n\n\t\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\t\tlet body: unknown = undefined;\n\t\t\ttry {\n\t\t\t\tbody = await request.json();\n\t\t\t} catch {\n\t\t\t\t// No body or not JSON\n\t\t\t}\n\n\t\t\treturn routeRegistry.invoke(pluginId, routeKey, { request, body });\n\t\t}\n\n\t\t// Check sandboxed (marketplace) plugins second\n\t\tconst sandboxedPlugin = this.findSandboxedPlugin(pluginId);\n\t\tif (sandboxedPlugin) {\n\t\t\treturn this.handleSandboxedRoute(sandboxedPlugin, path, request);\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Sandboxed Plugin Helpers\n\t// =========================================================================\n\n\tprivate findSandboxedPlugin(pluginId: string): SandboxedPlugin | undefined {\n\t\tfor (const [key, plugin] of this.sandboxedPlugins) {\n\t\t\tif (key.startsWith(pluginId + \":\")) {\n\t\t\t\treturn plugin;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Normalize image/file fields in content data.\n\t * Fills missing dimensions, storageKey, mimeType, and filename from providers.\n\t */\n\tprivate async normalizeMediaFields(\n\t\tcollection: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<Record<string, unknown>> {\n\t\tlet collectionInfo;\n\t\ttry {\n\t\t\tcollectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);\n\t\t} catch {\n\t\t\treturn data;\n\t\t}\n\t\tif (!collectionInfo?.fields) return data;\n\n\t\tconst imageFields = collectionInfo.fields.filter(\n\t\t\t(f) => f.type === \"image\" || f.type === \"file\",\n\t\t);\n\t\tif (imageFields.length === 0) return data;\n\n\t\tconst getProvider = (id: string) => this.getMediaProvider(id);\n\t\tconst result = { ...data };\n\n\t\tfor (const field of imageFields) {\n\t\t\tconst value = result[field.slug];\n\t\t\tif (value == null) continue;\n\n\t\t\ttry {\n\t\t\t\tconst normalized = await normalizeMediaValue(value, getProvider);\n\t\t\t\tif (normalized) {\n\t\t\t\t\tresult[field.slug] = normalized;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Don't fail the save if normalization fails for a single field\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate async runSandboxedBeforeSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<Record<string, unknown>> {\n\t\tlet result = content;\n\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst hookResult = await plugin.invokeHook(\"content:beforeSave\", {\n\t\t\t\t\tcontent: result,\n\t\t\t\t\tcollection,\n\t\t\t\t\tisNew,\n\t\t\t\t});\n\t\t\t\tif (hookResult && typeof hookResult === \"object\" && !Array.isArray(hookResult)) {\n\t\t\t\t\t// Sandbox returns unknown; convert to record by iterating own properties\n\t\t\t\t\tconst record: Record<string, unknown> = {};\n\t\t\t\t\tfor (const [k, v] of Object.entries(hookResult)) {\n\t\t\t\t\t\trecord[k] = v;\n\t\t\t\t\t}\n\t\t\t\t\tresult = record;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} beforeSave hook error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate async runSandboxedBeforeDelete(id: string, collection: string): Promise<boolean> {\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst result = await plugin.invokeHook(\"content:beforeDelete\", {\n\t\t\t\t\tid,\n\t\t\t\t\tcollection,\n\t\t\t\t});\n\t\t\t\tif (result === false) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate runAfterSaveHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterSave\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterSave(content, collection, isNew)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterSave hook error:\", err));\n\t\t}\n\n\t\t// Sandboxed plugins\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\tplugin\n\t\t\t\t.invokeHook(\"content:afterSave\", { content, collection, isNew })\n\t\t\t\t.catch((err) => console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err));\n\t\t}\n\t}\n\n\tprivate runAfterDeleteHooks(id: string, collection: string): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterDelete\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterDelete(id, collection)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterDelete hook error:\", err));\n\t\t}\n\n\t\t// Sandboxed plugins\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\tplugin\n\t\t\t\t.invokeHook(\"content:afterDelete\", { id, collection })\n\t\t\t\t.catch((err) =>\n\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err),\n\t\t\t\t);\n\t\t}\n\t}\n\n\tprivate async handleSandboxedRoute(\n\t\tplugin: SandboxedPlugin,\n\t\tpath: string,\n\t\trequest: Request,\n\t): Promise<{\n\t\tsuccess: boolean;\n\t\tdata?: unknown;\n\t\terror?: { code: string; message: string };\n\t}> {\n\t\tconst routeName = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\tlet body: unknown = undefined;\n\t\ttry {\n\t\t\tbody = await request.json();\n\t\t} catch {\n\t\t\t// No body or not JSON\n\t\t}\n\n\t\ttry {\n\t\t\tconst headers = sanitizeHeadersForSandbox(request.headers);\n\t\t\tconst meta = extractRequestMeta(request);\n\t\t\tconst result = await plugin.invokeRoute(routeName, body, {\n\t\t\t\turl: request.url,\n\t\t\t\tmethod: request.method,\n\t\t\t\theaders,\n\t\t\t\tmeta,\n\t\t\t});\n\t\t\treturn { success: true, data: result };\n\t\t} catch (error) {\n\t\t\tconsole.error(`EmDash: Sandboxed plugin route error:`, error);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_ERROR\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Public Page Contributions\n\t// =========================================================================\n\n\t/**\n\t * Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext\n\t * object so results are collected once per page context per request, even when\n\t * multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)\n\t * request contributions from the same page.\n\t */\n\tprivate pageContributionCache = new WeakMap<PublicPageContext, Promise<PageContributions>>();\n\n\t/**\n\t * Collect all page contributions (metadata + fragments) in a single pass.\n\t * Results are cached by page context object identity.\n\t */\n\tasync collectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst cached = this.pageContributionCache.get(page);\n\t\tif (cached) return cached;\n\n\t\tconst promise = this.doCollectPageContributions(page);\n\t\tthis.pageContributionCache.set(page, promise);\n\t\treturn promise;\n\t}\n\n\tprivate async doCollectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst metadata: PageMetadataContribution[] = [];\n\t\tconst fragments: PageFragmentContribution[] = [];\n\n\t\t// Trusted plugins via HookPipeline — both metadata and fragments\n\t\tif (this.hooks.hasHooks(\"page:metadata\")) {\n\t\t\tconst results = await this.hooks.runPageMetadata({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tmetadata.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\tif (this.hooks.hasHooks(\"page:fragments\")) {\n\t\t\tconst results = await this.hooks.runPageFragments({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tfragments.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\t// Sandboxed plugins — metadata only, never fragments\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst result = await plugin.invokeHook(\"page:metadata\", { page });\n\t\t\t\tif (result != null) {\n\t\t\t\t\tconst items = Array.isArray(result) ? result : [result];\n\t\t\t\t\tfor (const item of items) {\n\t\t\t\t\t\tif (isValidMetadataContribution(item)) {\n\t\t\t\t\t\t\tmetadata.push(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn { metadata, fragments };\n\t}\n\n\t/**\n\t * Collect page metadata contributions from trusted and sandboxed plugins.\n\t * Delegates to the single-pass collector and returns the metadata portion.\n\t */\n\tasync collectPageMetadata(page: PublicPageContext): Promise<PageMetadataContribution[]> {\n\t\tconst { metadata } = await this.collectPageContributions(page);\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Collect page fragment contributions from trusted plugins only.\n\t * Delegates to the single-pass collector and returns the fragments portion.\n\t */\n\tasync collectPageFragments(page: PublicPageContext): Promise<PageFragmentContribution[]> {\n\t\tconst { fragments } = await this.collectPageContributions(page);\n\t\treturn fragments;\n\t}\n\n\tprivate isPluginEnabled(pluginId: string): boolean {\n\t\tconst status = this.pluginStates.get(pluginId);\n\t\treturn status === undefined || status === \"active\";\n\t}\n}\n","/**\n * EmDash middleware\n *\n * Thin wrapper that initializes EmDashRuntime and attaches it to locals.\n * All heavy lifting happens in EmDashRuntime.\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\nimport { Kysely } from \"kysely\";\n// Import from virtual modules (populated by integration at build time)\n// @ts-ignore - virtual module\nimport virtualConfig from \"virtual:emdash/config\";\n// @ts-ignore - virtual module\nimport {\n\tcreateDialect as virtualCreateDialect,\n\tisSessionEnabled as virtualIsSessionEnabled,\n\tgetD1Binding as virtualGetD1Binding,\n\tgetDefaultConstraint as virtualGetDefaultConstraint,\n\tgetBookmarkCookieName as virtualGetBookmarkCookieName,\n\tcreateSessionDialect as virtualCreateSessionDialect,\n} from \"virtual:emdash/dialect\";\n// @ts-ignore - virtual module\nimport { mediaProviders as virtualMediaProviders } from \"virtual:emdash/media-providers\";\n// @ts-ignore - virtual module\nimport { plugins as virtualPlugins } from \"virtual:emdash/plugins\";\nimport {\n\tcreateSandboxRunner as virtualCreateSandboxRunner,\n\tsandboxEnabled as virtualSandboxEnabled,\n\t// @ts-ignore - virtual module\n} from \"virtual:emdash/sandbox-runner\";\n// @ts-ignore - virtual module\nimport { sandboxedPlugins as virtualSandboxedPlugins } from \"virtual:emdash/sandboxed-plugins\";\n// @ts-ignore - virtual module\nimport { createStorage as virtualCreateStorage } from \"virtual:emdash/storage\";\n\nimport {\n\tEmDashRuntime,\n\ttype RuntimeDependencies,\n\ttype SandboxedPluginEntry,\n\ttype MediaProviderEntry,\n} from \"../emdash-runtime.js\";\nimport { setI18nConfig } from \"../i18n/config.js\";\nimport type { Database, Storage } from \"../index.js\";\nimport type { SandboxRunner } from \"../plugins/sandbox/types.js\";\nimport type { ResolvedPlugin } from \"../plugins/types.js\";\nimport { runWithContext } from \"../request-context.js\";\nimport type { EmDashConfig } from \"./integration/runtime.js\";\n\n// Cached runtime instance (persists across requests within worker)\nlet runtimeInstance: EmDashRuntime | null = null;\n// Whether initialization is in progress (prevents concurrent init attempts)\nlet runtimeInitializing = false;\n\n/** Whether i18n config has been initialized from the virtual module */\nlet i18nInitialized = false;\n\n/**\n * Get EmDash configuration from virtual module\n */\nfunction getConfig(): EmDashConfig | null {\n\tif (virtualConfig && typeof virtualConfig === \"object\") {\n\t\t// Initialize i18n config on first access (once per worker lifetime)\n\t\tif (!i18nInitialized) {\n\t\t\ti18nInitialized = true;\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module checked as object above\n\t\t\tconst config = virtualConfig as Record<string, unknown>;\n\t\t\tif (config.i18n && typeof config.i18n === \"object\") {\n\t\t\t\tsetI18nConfig(\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- runtime-checked above\n\t\t\t\t\tconfig.i18n as {\n\t\t\t\t\t\tdefaultLocale: string;\n\t\t\t\t\t\tlocales: string[];\n\t\t\t\t\t\tfallback?: Record<string, string>;\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tsetI18nConfig(null);\n\t\t\t}\n\t\t}\n\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\treturn virtualConfig as EmDashConfig;\n\t}\n\treturn null;\n}\n\n/**\n * Get plugins from virtual module\n */\nfunction getPlugins(): ResolvedPlugin[] {\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\treturn (virtualPlugins as ResolvedPlugin[]) || [];\n}\n\n/**\n * Build runtime dependencies from virtual modules\n */\nfunction buildDependencies(config: EmDashConfig): RuntimeDependencies {\n\treturn {\n\t\tconfig,\n\t\tplugins: getPlugins(),\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateDialect: virtualCreateDialect as (config: Record<string, unknown>) => unknown,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateStorage: virtualCreateStorage as ((config: Record<string, unknown>) => Storage) | null,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tsandboxEnabled: virtualSandboxEnabled as boolean,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tsandboxedPluginEntries: (virtualSandboxedPlugins as SandboxedPluginEntry[]) || [],\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateSandboxRunner: virtualCreateSandboxRunner as\n\t\t\t| ((opts: { db: Kysely<Database> }) => SandboxRunner)\n\t\t\t| null,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tmediaProviderEntries: (virtualMediaProviders as MediaProviderEntry[]) || [],\n\t};\n}\n\n/**\n * Get or create the runtime instance\n */\nasync function getRuntime(config: EmDashConfig): Promise<EmDashRuntime> {\n\t// Return cached instance if available\n\tif (runtimeInstance) {\n\t\treturn runtimeInstance;\n\t}\n\n\t// If another request is already initializing, wait and retry.\n\t// We don't share the promise across requests because workerd flags\n\t// cross-request promise resolution (causes warnings + potential hangs).\n\tif (runtimeInitializing) {\n\t\t// Poll until the initializing request finishes\n\t\tawait new Promise((resolve) => setTimeout(resolve, 50));\n\t\treturn getRuntime(config);\n\t}\n\n\truntimeInitializing = true;\n\ttry {\n\t\tconst deps = buildDependencies(config);\n\t\tconst runtime = await EmDashRuntime.create(deps);\n\t\truntimeInstance = runtime;\n\t\treturn runtime;\n\t} finally {\n\t\truntimeInitializing = false;\n\t}\n}\n\n/**\n * Baseline security headers applied to all responses.\n * Admin routes get additional headers (strict CSP) from auth middleware.\n */\nfunction setBaselineSecurityHeaders(response: Response): void {\n\t// Prevent MIME type sniffing\n\tresponse.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\t// Control referrer information\n\tresponse.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\t// Restrict access to sensitive browser APIs\n\tresponse.headers.set(\n\t\t\"Permissions-Policy\",\n\t\t\"camera=(), microphone=(), geolocation=(), payment=()\",\n\t);\n\t// Prevent clickjacking (non-admin routes; admin CSP uses frame-ancestors)\n\tif (!response.headers.has(\"Content-Security-Policy\")) {\n\t\tresponse.headers.set(\"X-Frame-Options\", \"SAMEORIGIN\");\n\t}\n}\n\n/** Public routes that require the runtime (sitemap, robots.txt, etc.) */\nconst PUBLIC_RUNTIME_ROUTES = new Set([\"/sitemap.xml\", \"/robots.txt\"]);\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { request, locals, cookies } = context;\n\tconst url = context.url;\n\n\t// Process /_emdash routes and public routes with an active session\n\t// (logged-in editors need the runtime for toolbar/visual editing on public pages)\n\tconst isEmDashRoute = url.pathname.startsWith(\"/_emdash\");\n\tconst isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname);\n\n\t// Check for edit mode cookie - editors viewing public pages need the runtime\n\t// so auth middleware can verify their session for visual editing\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// Playground mode: the playground middleware stashes the per-session DO database\n\t// on locals.__playgroundDb. When present, use runWithContext() to make it\n\t// available to getDb() and the runtime's db getter via the correct ALS instance.\n\tconst playgroundDb = locals.__playgroundDb;\n\n\tif (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {\n\t\tconst sessionUser = await context.session?.get(\"user\");\n\t\tif (!sessionUser && !playgroundDb) {\n\t\t\tconst response = await next();\n\t\t\tsetBaselineSecurityHeaders(response);\n\t\t\treturn response;\n\t\t}\n\t}\n\n\tconst config = getConfig();\n\tif (!config) {\n\t\tconsole.error(\"EmDash: No configuration found\");\n\t\treturn next();\n\t}\n\n\t// In playground mode, wrap the entire runtime init + request handling in\n\t// runWithContext so that getDatabase() and all init queries use the real\n\t// DO database via the same AsyncLocalStorage instance as the loader.\n\tconst doInit = async () => {\n\t\ttry {\n\t\t\t// Get or create runtime\n\t\t\tconst runtime = await getRuntime(config);\n\n\t\t\t// Get manifest (cached after first call)\n\t\t\tconst manifest = await runtime.getManifest();\n\n\t\t\t// Attach to locals for route handlers\n\t\t\tlocals.emdashManifest = manifest;\n\t\t\tlocals.emdash = {\n\t\t\t\t// Content handlers\n\t\t\t\thandleContentList: runtime.handleContentList.bind(runtime),\n\t\t\t\thandleContentGet: runtime.handleContentGet.bind(runtime),\n\t\t\t\thandleContentCreate: runtime.handleContentCreate.bind(runtime),\n\t\t\t\thandleContentUpdate: runtime.handleContentUpdate.bind(runtime),\n\t\t\t\thandleContentDelete: runtime.handleContentDelete.bind(runtime),\n\n\t\t\t\t// Trash handlers\n\t\t\t\thandleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),\n\t\t\t\thandleContentRestore: runtime.handleContentRestore.bind(runtime),\n\t\t\t\thandleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),\n\t\t\t\thandleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),\n\t\t\t\thandleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),\n\n\t\t\t\t// Duplicate handler\n\t\t\t\thandleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),\n\n\t\t\t\t// Publishing & Scheduling handlers\n\t\t\t\thandleContentPublish: runtime.handleContentPublish.bind(runtime),\n\t\t\t\thandleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),\n\t\t\t\thandleContentSchedule: runtime.handleContentSchedule.bind(runtime),\n\t\t\t\thandleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),\n\t\t\t\thandleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),\n\t\t\t\thandleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),\n\t\t\t\thandleContentCompare: runtime.handleContentCompare.bind(runtime),\n\t\t\t\thandleContentTranslations: runtime.handleContentTranslations.bind(runtime),\n\n\t\t\t\t// Media handlers\n\t\t\t\thandleMediaList: runtime.handleMediaList.bind(runtime),\n\t\t\t\thandleMediaGet: runtime.handleMediaGet.bind(runtime),\n\t\t\t\thandleMediaCreate: runtime.handleMediaCreate.bind(runtime),\n\t\t\t\thandleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),\n\t\t\t\thandleMediaDelete: runtime.handleMediaDelete.bind(runtime),\n\n\t\t\t\t// Revision handlers\n\t\t\t\thandleRevisionList: runtime.handleRevisionList.bind(runtime),\n\t\t\t\thandleRevisionGet: runtime.handleRevisionGet.bind(runtime),\n\t\t\t\thandleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),\n\n\t\t\t\t// Plugin routes\n\t\t\t\thandlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),\n\t\t\t\tgetPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),\n\n\t\t\t\t// Media provider methods\n\t\t\t\tgetMediaProvider: runtime.getMediaProvider.bind(runtime),\n\t\t\t\tgetMediaProviderList: runtime.getMediaProviderList.bind(runtime),\n\n\t\t\t\t// Direct access (for advanced use cases)\n\t\t\t\tstorage: runtime.storage,\n\t\t\t\tdb: runtime.db,\n\t\t\t\thooks: runtime.hooks,\n\t\t\t\temail: runtime.email,\n\t\t\t\tconfiguredPlugins: runtime.configuredPlugins,\n\n\t\t\t\t// Configuration (for checking database type, auth mode, etc.)\n\t\t\t\tconfig,\n\n\t\t\t\t// Manifest invalidation (call after schema changes)\n\t\t\t\tinvalidateManifest: runtime.invalidateManifest.bind(runtime),\n\n\t\t\t\t// Sandbox runner (for marketplace plugin install/update)\n\t\t\t\tgetSandboxRunner: runtime.getSandboxRunner.bind(runtime),\n\n\t\t\t\t// Sync marketplace plugin states (after install/update/uninstall)\n\t\t\t\tsyncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),\n\n\t\t\t\t// Update plugin enabled/disabled status and rebuild hook pipeline\n\t\t\t\tsetPluginStatus: runtime.setPluginStatus.bind(runtime),\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash middleware error:\", error);\n\t\t}\n\n\t\t// =========================================================================\n\t\t// D1 Read Replica Session Management\n\t\t//\n\t\t// When D1 sessions are enabled, we create a per-request D1 session and\n\t\t// Kysely instance. The session is wrapped in ALS so `runtime.db` (a getter)\n\t\t// picks up the per-request instance instead of the singleton.\n\t\t//\n\t\t// After the response, we extract the bookmark from the session and set\n\t\t// it as a cookie for authenticated users (read-your-writes consistency).\n\t\t// =========================================================================\n\t\tconst dbConfig = config?.database?.config;\n\t\tconst sessionEnabled =\n\t\t\tdbConfig &&\n\t\t\ttypeof virtualIsSessionEnabled === \"function\" &&\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t(virtualIsSessionEnabled as (config: unknown) => boolean)(dbConfig);\n\n\t\tif (\n\t\t\tsessionEnabled &&\n\t\t\ttypeof virtualGetD1Binding === \"function\" &&\n\t\t\tvirtualCreateSessionDialect\n\t\t) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\tconst d1Binding = (virtualGetD1Binding as (config: unknown) => unknown)(dbConfig);\n\n\t\t\tif (d1Binding && typeof d1Binding === \"object\" && \"withSession\" in d1Binding) {\n\t\t\t\tconst isAuthenticated = !!(await context.session?.get(\"user\"));\n\t\t\t\tconst isWrite = request.method !== \"GET\" && request.method !== \"HEAD\";\n\n\t\t\t\t// Determine session constraint:\n\t\t\t\t// - Config says \"primary-first\" → always \"first-primary\"\n\t\t\t\t// - Authenticated writes → \"first-primary\" (need to hit primary)\n\t\t\t\t// - Authenticated reads with bookmark → resume from bookmark\n\t\t\t\t// - Otherwise → \"first-unconstrained\" (nearest replica)\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\tconst configConstraint = (virtualGetDefaultConstraint as (config: unknown) => string)(\n\t\t\t\t\tdbConfig,\n\t\t\t\t);\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\tconst cookieName = (virtualGetBookmarkCookieName as (config: unknown) => string)(dbConfig);\n\n\t\t\t\tlet constraint: string = configConstraint;\n\t\t\t\tif (isAuthenticated && isWrite) {\n\t\t\t\t\tconstraint = \"first-primary\";\n\t\t\t\t} else if (isAuthenticated) {\n\t\t\t\t\tconst bookmarkCookie = context.cookies.get(cookieName);\n\t\t\t\t\tif (bookmarkCookie?.value) {\n\t\t\t\t\t\tconstraint = bookmarkCookie.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Create the D1 session and per-request Kysely instance.\n\t\t\t\t// D1DatabaseSession has the same prepare()/batch() interface as D1Database,\n\t\t\t\t// so createSessionDialect passes it straight to D1Dialect.\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- D1 binding with Sessions API, checked via \"withSession\" in d1Binding above\n\t\t\t\tconst withSession = (d1Binding as { withSession: (c: string) => unknown }).withSession;\n\t\t\t\tconst session = withSession.call(d1Binding, constraint);\n\t\t\t\tconst sessionDialect =\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\t\t(virtualCreateSessionDialect as (db: unknown) => import(\"kysely\").Dialect)(session);\n\t\t\t\tconst sessionDb = new Kysely<Database>({ dialect: sessionDialect });\n\n\t\t\t\t// Wrap the request in ALS with the per-request db\n\t\t\t\treturn runWithContext({ editMode: false, db: sessionDb }, async () => {\n\t\t\t\t\tconst response = await next();\n\t\t\t\t\tsetBaselineSecurityHeaders(response);\n\n\t\t\t\t\t// Set bookmark cookie for authenticated users only — they need\n\t\t\t\t\t// read-your-writes consistency across requests. Anonymous visitors\n\t\t\t\t\t// don't write, so they get \"first-unconstrained\" every time.\n\t\t\t\t\tif (\n\t\t\t\t\t\tisAuthenticated &&\n\t\t\t\t\t\tsession &&\n\t\t\t\t\t\ttypeof session === \"object\" &&\n\t\t\t\t\t\t\"getBookmark\" in session\n\t\t\t\t\t) {\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- D1DatabaseSession with getBookmark()\n\t\t\t\t\t\tconst getBookmark = (session as { getBookmark: () => string | null }).getBookmark;\n\t\t\t\t\t\tconst newBookmark = getBookmark.call(session);\n\t\t\t\t\t\tif (newBookmark) {\n\t\t\t\t\t\t\tresponse.headers.append(\n\t\t\t\t\t\t\t\t\"Set-Cookie\",\n\t\t\t\t\t\t\t\t`${cookieName}=${newBookmark}; Path=/; HttpOnly; SameSite=Lax; Secure`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn response;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst response = await next();\n\t\tsetBaselineSecurityHeaders(response);\n\t\treturn response;\n\t}; // end doInit\n\n\tif (playgroundDb) {\n\t\t// Read the edit-mode cookie to determine if visual editing is active.\n\t\t// Default to false -- editing is opt-in via the playground toolbar toggle.\n\t\tconst editMode = context.cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode, db: playgroundDb }, doInit);\n\t}\n\treturn doInit();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","virtualPlugins","virtualCreateDialect","virtualCreateStorage","virtualSandboxEnabled","virtualSandboxedPlugins","virtualCreateSandboxRunner","virtualMediaProviders","virtualIsSessionEnabled","virtualGetD1Binding","virtualCreateSessionDialect","virtualGetDefaultConstraint","virtualGetBookmarkCookieName"],"sources":["../../src/auth/challenge-store.ts","../../src/cleanup.ts","../../src/comments/moderator.ts","../../src/plugins/scheduler/node.ts","../../src/plugins/scheduler/piggyback.ts","../../src/emdash-runtime.ts","../../src/astro/middleware.ts"],"sourcesContent":["/**\n * Challenge store for WebAuthn\n *\n * Stores WebAuthn challenges in a dedicated table with automatic expiration.\n */\n\nimport type { ChallengeStore, ChallengeData } from \"@emdash-cms/auth/passkey\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nexport function createChallengeStore(db: Kysely<Database>): ChallengeStore {\n\treturn {\n\t\tasync set(challenge: string, data: ChallengeData): Promise<void> {\n\t\t\tconst expiresAt = new Date(data.expiresAt).toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"auth_challenges\")\n\t\t\t\t.values({\n\t\t\t\t\tchallenge,\n\t\t\t\t\ttype: data.type,\n\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\tdata: null, // Could store additional context if needed\n\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc.column(\"challenge\").doUpdateSet({\n\t\t\t\t\t\ttype: data.type,\n\t\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync get(challenge: string): Promise<ChallengeData | null> {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"auth_challenges\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"challenge\", \"=\", challenge)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!row) return null;\n\n\t\t\tconst expiresAt = new Date(row.expires_at).getTime();\n\n\t\t\t// Check expiration\n\t\t\tif (expiresAt < Date.now()) {\n\t\t\t\t// Expired, delete and return null\n\t\t\t\tawait this.delete(challenge);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: row.type === \"registration\" ? \"registration\" : \"authentication\",\n\t\t\t\tuserId: row.user_id ?? undefined,\n\t\t\t\texpiresAt,\n\t\t\t};\n\t\t},\n\n\t\tasync delete(challenge: string): Promise<void> {\n\t\t\tawait db.deleteFrom(\"auth_challenges\").where(\"challenge\", \"=\", challenge).execute();\n\t\t},\n\t};\n}\n\n/**\n * Clean up expired challenges.\n * Should be called periodically (e.g., on startup, or via cron).\n */\nexport async function cleanupExpiredChallenges(db: Kysely<Database>): Promise<number> {\n\tconst now = new Date().toISOString();\n\n\tconst result = await db\n\t\t.deleteFrom(\"auth_challenges\")\n\t\t.where(\"expires_at\", \"<\", now)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n","/**\n * System cleanup\n *\n * Runs periodic maintenance tasks that prevent unbounded accumulation of\n * expired or stale data. Called from cron scheduler ticks and (for latency-\n * sensitive subsystems) inline during relevant requests.\n *\n * Each subsystem cleanup is independent and non-fatal -- if one fails, the\n * rest still run. Failures are logged but never surface to callers.\n */\n\nimport { createKyselyAdapter, type AuthTables } from \"@emdash-cms/auth/adapters/kysely\";\nimport { sql, type Kysely } from \"kysely\";\n\nimport { cleanupExpiredChallenges } from \"./auth/challenge-store.js\";\nimport { MediaRepository } from \"./database/repositories/media.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { Database } from \"./database/types.js\";\nimport type { Storage } from \"./storage/types.js\";\n\n/**\n * Result of a system cleanup run.\n * Each field is the number of rows deleted, or -1 if the cleanup failed.\n */\nexport interface CleanupResult {\n\tchallenges: number;\n\texpiredTokens: number;\n\tpendingUploads: number;\n\tpendingUploadFiles: number;\n\trevisionsPruned: number;\n}\n\n/** Max revisions to keep per entry during periodic pruning */\nconst REVISION_KEEP_COUNT = 50;\n\n/** Only prune entries that exceed this threshold */\nconst REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;\n\n/**\n * Run all system cleanup tasks.\n *\n * Safe to call frequently -- each task is a single DELETE with a WHERE clause,\n * so repeated calls with nothing to clean are cheap (no-op queries).\n *\n * @param db - The database instance\n * @param storage - Optional storage backend for deleting orphaned files.\n * When omitted, pending upload DB rows are still deleted but the\n * corresponding files in object storage are not removed.\n */\nexport async function runSystemCleanup(\n\tdb: Kysely<Database>,\n\tstorage?: Storage,\n): Promise<CleanupResult> {\n\tconst result: CleanupResult = {\n\t\tchallenges: -1,\n\t\texpiredTokens: -1,\n\t\tpendingUploads: -1,\n\t\tpendingUploadFiles: -1,\n\t\trevisionsPruned: -1,\n\t};\n\n\t// 1. Passkey challenges (expire after 60s, clean anything past 5 min)\n\ttry {\n\t\tresult.challenges = await cleanupExpiredChallenges(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired challenges:\", error);\n\t}\n\n\t// 2. Magic link / invite / signup tokens\n\ttry {\n\t\t// Cast needed: Database extends AuthTables but uses Generated<> wrappers\n\t\t// that confuse structural checks. The adapter casts internally anyway.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Database uses Generated<> wrappers incompatible with AuthTables structurally; safe at runtime\n\t\tconst authAdapter = createKyselyAdapter(db as unknown as Kysely<AuthTables>);\n\t\tawait authAdapter.deleteExpiredTokens();\n\t\tresult.expiredTokens = 0; // deleteExpiredTokens returns void\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired tokens:\", error);\n\t}\n\n\t// 3. Pending media uploads (abandoned after 1 hour)\n\t// Delete DB rows first, then remove corresponding files from storage.\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst orphanedKeys = await mediaRepo.cleanupPendingUploads();\n\t\tresult.pendingUploads = orphanedKeys.length;\n\n\t\t// Delete orphaned files from object storage\n\t\tif (storage && orphanedKeys.length > 0) {\n\t\t\tlet filesDeleted = 0;\n\t\t\tfor (const key of orphanedKeys) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tfilesDeleted++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Log per-file failures but continue -- storage.delete is\n\t\t\t\t\t// documented as idempotent, so this is an unexpected error.\n\t\t\t\t\tconsole.error(`[cleanup] Failed to delete storage file ${key}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.pendingUploadFiles = filesDeleted;\n\t\t} else {\n\t\t\tresult.pendingUploadFiles = 0;\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean pending uploads:\", error);\n\t}\n\n\t// 4. Revision pruning -- trim entries with excessive revision counts\n\ttry {\n\t\tresult.revisionsPruned = await pruneExcessiveRevisions(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to prune revisions:\", error);\n\t}\n\n\treturn result;\n}\n\n/**\n * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune\n * them down to REVISION_KEEP_COUNT.\n */\nasync function pruneExcessiveRevisions(db: Kysely<Database>): Promise<number> {\n\tconst entries = await sql<{ collection: string; entry_id: string; cnt: number }>`\n\t\tSELECT collection, entry_id, COUNT(*) as cnt\n\t\tFROM revisions\n\t\tGROUP BY collection, entry_id\n\t\tHAVING cnt > ${REVISION_PRUNE_THRESHOLD}\n\t`.execute(db);\n\n\tif (entries.rows.length === 0) return 0;\n\n\tconst revisionRepo = new RevisionRepository(db);\n\tlet totalPruned = 0;\n\n\tfor (const row of entries.rows) {\n\t\ttry {\n\t\t\tconst pruned = await revisionRepo.pruneOldRevisions(\n\t\t\t\trow.collection,\n\t\t\t\trow.entry_id,\n\t\t\t\tREVISION_KEEP_COUNT,\n\t\t\t);\n\t\t\ttotalPruned += pruned;\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn totalPruned;\n}\n","/**\n * Built-in Default Comment Moderator\n *\n * Registers comment:moderate as an exclusive hook.\n * Implements the 4-step decision logic:\n * 1. Auto-approve authenticated CMS users (if configured)\n * 2. If moderation is \"none\" → approved\n * 3. If moderation is \"first_time\" and returning commenter → approved\n * 4. Otherwise → pending\n *\n * This moderator does not read `metadata` — it only uses collection settings\n * and prior approval count. Plugin moderators (AI, Akismet) replace this.\n */\n\nimport type { CommentModerateEvent, ModerationDecision, PluginContext } from \"../plugins/types.js\";\n\n/** Plugin ID for the built-in default comment moderator */\nexport const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = \"emdash-default-comment-moderator\";\n\n/**\n * The comment:moderate handler for the built-in default moderator.\n */\nexport async function defaultCommentModerate(\n\tevent: CommentModerateEvent,\n\t_ctx: PluginContext,\n): Promise<ModerationDecision> {\n\tconst { comment, collectionSettings, priorApprovedCount } = event;\n\n\t// 1. Auto-approve authenticated CMS users if configured\n\tif (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) {\n\t\treturn { status: \"approved\", reason: \"Authenticated CMS user\" };\n\t}\n\n\t// 2. If moderation is \"none\" → approved\n\tif (collectionSettings.commentsModeration === \"none\") {\n\t\treturn { status: \"approved\", reason: \"Moderation disabled\" };\n\t}\n\n\t// 3. If moderation is \"first_time\" and returning commenter → approved\n\tif (collectionSettings.commentsModeration === \"first_time\" && priorApprovedCount > 0) {\n\t\treturn { status: \"approved\", reason: \"Returning commenter\" };\n\t}\n\n\t// 4. Otherwise → pending\n\treturn { status: \"pending\", reason: \"Held for review\" };\n}\n","/**\n * Node.js cron scheduler — setTimeout-based.\n *\n * Queries the executor for the next due time and sets a timeout. Re-arms\n * after each tick and when reschedule() is called (new task scheduled or\n * cancelled).\n *\n * Suitable for single-process deployments (local dev, single-node).\n *\n */\n\nimport type { CronExecutor } from \"../cron.js\";\nimport type { CronScheduler, SystemCleanupFn } from \"./types.js\";\n\n/** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */\nconst MIN_INTERVAL_MS = 1000;\n\n/** Maximum polling interval (ms) — wake up periodically to check for stale locks */\nconst MAX_INTERVAL_MS = 5 * 60 * 1000;\n\nexport class NodeCronScheduler implements CronScheduler {\n\tprivate timer: ReturnType<typeof setTimeout> | null = null;\n\tprivate running = false;\n\tprivate systemCleanup: SystemCleanupFn | null = null;\n\n\tconstructor(private executor: CronExecutor) {}\n\n\tsetSystemCleanup(fn: SystemCleanupFn): void {\n\t\tthis.systemCleanup = fn;\n\t}\n\n\tstart(): void {\n\t\tthis.running = true;\n\t\tthis.arm();\n\t}\n\n\tstop(): void {\n\t\tthis.running = false;\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t}\n\n\treschedule(): void {\n\t\tif (!this.running) return;\n\t\t// Clear existing timer and re-arm with fresh next due time\n\t\tif (this.timer) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tthis.arm();\n\t}\n\n\tprivate arm(): void {\n\t\tif (!this.running) return;\n\n\t\t// Query the next due time, then schedule a wake-up\n\t\tvoid this.executor\n\t\t\t.getNextDueTime()\n\t\t\t.then((nextDue) => {\n\t\t\t\tif (!this.running) return undefined;\n\n\t\t\t\tlet delayMs: number;\n\t\t\t\tif (nextDue) {\n\t\t\t\t\tconst dueAt = new Date(nextDue).getTime();\n\t\t\t\t\tdelayMs = Math.max(dueAt - Date.now(), MIN_INTERVAL_MS);\n\t\t\t\t\tdelayMs = Math.min(delayMs, MAX_INTERVAL_MS);\n\t\t\t\t} else {\n\t\t\t\t\t// No tasks scheduled — poll at max interval for stale lock recovery\n\t\t\t\t\tdelayMs = MAX_INTERVAL_MS;\n\t\t\t\t}\n\n\t\t\t\tthis.timer = setTimeout(() => {\n\t\t\t\t\tif (!this.running) return;\n\t\t\t\t\tthis.executeTick();\n\t\t\t\t}, delayMs);\n\n\t\t\t\t// Don't prevent process exit\n\t\t\t\tif (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n\t\t\t\t\tthis.timer.unref();\n\t\t\t\t}\n\n\t\t\t\treturn undefined;\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tconsole.error(\"[cron:node] Failed to get next due time:\", error);\n\t\t\t\t// Retry after max interval\n\t\t\t\tif (this.running) {\n\t\t\t\t\tthis.timer = setTimeout(() => this.arm(), MAX_INTERVAL_MS);\n\t\t\t\t\tif (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n\t\t\t\t\t\tthis.timer.unref();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t}\n\n\tprivate executeTick(): void {\n\t\tif (!this.running) return;\n\n\t\t// Run tick + stale lock recovery + system cleanup, then re-arm\n\t\tconst tasks: Promise<unknown>[] = [this.executor.tick(), this.executor.recoverStaleLocks()];\n\t\tif (this.systemCleanup) {\n\t\t\ttasks.push(this.systemCleanup());\n\t\t}\n\n\t\tvoid Promise.allSettled(tasks)\n\t\t\t.then((results) => {\n\t\t\t\tfor (const r of results) {\n\t\t\t\t\tif (r.status === \"rejected\") {\n\t\t\t\t\t\tconsole.error(\"[cron:node] Tick task failed:\", r.reason);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tif (this.running) {\n\t\t\t\t\tthis.arm();\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n","/**\n * Piggyback cron scheduler — request-driven fallback.\n *\n * Checks for overdue tasks on each incoming request, debounced to at most\n * once per 60 seconds. Fire-and-forget (does not block the request).\n *\n * Used on Cloudflare when no Durable Object binding is available, or\n * during development when DO bindings aren't configured.\n *\n */\n\nimport type { CronExecutor } from \"../cron.js\";\nimport type { CronScheduler, SystemCleanupFn } from \"./types.js\";\n\n/** Minimum interval between tick attempts (ms) */\nconst DEBOUNCE_MS = 60 * 1000;\n\nexport class PiggybackScheduler implements CronScheduler {\n\tprivate lastTickAt = 0;\n\tprivate running = false;\n\tprivate systemCleanup: SystemCleanupFn | null = null;\n\n\tconstructor(private executor: CronExecutor) {}\n\n\tsetSystemCleanup(fn: SystemCleanupFn): void {\n\t\tthis.systemCleanup = fn;\n\t}\n\n\tstart(): void {\n\t\tthis.running = true;\n\t}\n\n\tstop(): void {\n\t\tthis.running = false;\n\t}\n\n\t/**\n\t * No-op for piggyback — tick happens on next request.\n\t */\n\treschedule(): void {\n\t\t// Nothing to do — next request will check\n\t}\n\n\t/**\n\t * Call this from middleware on each request.\n\t * Debounced: only actually ticks if enough time has passed.\n\t */\n\tonRequest(): void {\n\t\tif (!this.running) return;\n\n\t\tconst now = Date.now();\n\t\tif (now - this.lastTickAt < DEBOUNCE_MS) return;\n\n\t\tthis.lastTickAt = now;\n\n\t\t// Fire-and-forget — don't block the request\n\t\tconst tasks: Promise<unknown>[] = [this.executor.tick(), this.executor.recoverStaleLocks()];\n\t\tif (this.systemCleanup) {\n\t\t\ttasks.push(this.systemCleanup());\n\t\t}\n\n\t\tvoid Promise.allSettled(tasks).then((results) => {\n\t\t\tfor (const r of results) {\n\t\t\t\tif (r.status === \"rejected\") {\n\t\t\t\t\tconsole.error(\"[cron:piggyback] Tick task failed:\", r.reason);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\t}\n}\n","/**\n * EmDashRuntime - Core runtime for EmDash CMS\n *\n * Manages database, storage, plugins (trusted + sandboxed), hooks, and\n * provides handlers for content/media operations.\n *\n * Created once per worker lifetime, cached and reused across requests.\n */\n\nimport type { Element } from \"@emdash-cms/blocks\";\nimport { Kysely, sql, type Dialect } from \"kysely\";\n\nimport { validateRev } from \"./api/rev.js\";\nimport type {\n\tEmDashConfig,\n\tPluginAdminPage,\n\tPluginDashboardWidget,\n} from \"./astro/integration/runtime.js\";\nimport type { EmDashManifest, ManifestCollection } from \"./astro/types.js\";\nimport { getAuthMode } from \"./auth/mode.js\";\nimport { isSqlite } from \"./database/dialect-helpers.js\";\nimport { runMigrations } from \"./database/migrations/runner.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { ContentItem as ContentItemInternal } from \"./database/repositories/types.js\";\nimport { normalizeMediaValue } from \"./media/normalize.js\";\nimport type { MediaProvider, MediaProviderCapabilities } from \"./media/types.js\";\nimport type { SandboxedPlugin, SandboxRunner } from \"./plugins/sandbox/types.js\";\nimport type {\n\tResolvedPlugin,\n\tMediaItem,\n\tPluginManifest,\n\tPluginCapability,\n\tPluginStorageConfig,\n\tPublicPageContext,\n\tPageMetadataContribution,\n\tPageFragmentContribution,\n} from \"./plugins/types.js\";\nimport type { FieldType } from \"./schema/types.js\";\nimport { hashString } from \"./utils/hash.js\";\n\nconst LEADING_SLASH_PATTERN = /^\\//;\n\n/** Combined result from a single-pass page contribution collection */\ninterface PageContributions {\n\tmetadata: PageMetadataContribution[];\n\tfragments: PageFragmentContribution[];\n}\n\nconst VALID_METADATA_KINDS = new Set([\"meta\", \"property\", \"link\", \"jsonld\"]);\n\n/** Security-critical allowlist for link rel values from sandboxed plugins */\nconst VALID_LINK_REL = new Set([\n\t\"canonical\",\n\t\"alternate\",\n\t\"author\",\n\t\"license\",\n\t\"site.standard.document\",\n]);\n\n/**\n * Runtime validation for sandboxed plugin metadata contributions.\n * Sandboxed plugins return `unknown` across the RPC boundary — we must\n * verify the shape before passing to the metadata collector.\n */\nfunction isValidMetadataContribution(c: unknown): c is PageMetadataContribution {\n\tif (!c || typeof c !== \"object\" || !(\"kind\" in c)) return false;\n\tconst obj = c as Record<string, unknown>;\n\tif (typeof obj.kind !== \"string\" || !VALID_METADATA_KINDS.has(obj.kind)) return false;\n\n\tswitch (obj.kind) {\n\t\tcase \"meta\":\n\t\t\treturn typeof obj.name === \"string\" && typeof obj.content === \"string\";\n\t\tcase \"property\":\n\t\t\treturn typeof obj.property === \"string\" && typeof obj.content === \"string\";\n\t\tcase \"link\":\n\t\t\treturn (\n\t\t\t\ttypeof obj.href === \"string\" && typeof obj.rel === \"string\" && VALID_LINK_REL.has(obj.rel)\n\t\t\t);\n\t\tcase \"jsonld\":\n\t\t\treturn obj.graph != null && typeof obj.graph === \"object\";\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nimport { loadBundleFromR2 } from \"./api/handlers/marketplace.js\";\nimport { runSystemCleanup } from \"./cleanup.js\";\nimport {\n\tDEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\tdefaultCommentModerate,\n} from \"./comments/moderator.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport {\n\thandleContentList,\n\thandleContentGet,\n\thandleContentGetIncludingTrashed,\n\thandleContentCreate,\n\thandleContentUpdate,\n\thandleContentDelete,\n\thandleContentDuplicate,\n\thandleContentRestore,\n\thandleContentPermanentDelete,\n\thandleContentListTrashed,\n\thandleContentCountTrashed,\n\thandleContentPublish,\n\thandleContentUnpublish,\n\thandleContentSchedule,\n\thandleContentUnschedule,\n\thandleContentCountScheduled,\n\thandleContentDiscardDraft,\n\thandleContentCompare,\n\thandleContentTranslations,\n\thandleMediaList,\n\thandleMediaGet,\n\thandleMediaCreate,\n\thandleMediaUpdate,\n\thandleMediaDelete,\n\thandleRevisionList,\n\thandleRevisionGet,\n\thandleRevisionRestore,\n\tSchemaRegistry,\n\ttype Database,\n\ttype Storage,\n} from \"./index.js\";\nimport { getDb } from \"./loader.js\";\nimport { CronExecutor, type InvokeCronHookFn } from \"./plugins/cron.js\";\nimport { definePlugin } from \"./plugins/define-plugin.js\";\nimport { DEV_CONSOLE_EMAIL_PLUGIN_ID, devConsoleEmailDeliver } from \"./plugins/email-console.js\";\nimport { EmailPipeline } from \"./plugins/email.js\";\nimport {\n\tcreateHookPipeline,\n\tresolveExclusiveHooks as resolveExclusiveHooksShared,\n\ttype HookPipeline,\n} from \"./plugins/hooks.js\";\nimport { normalizeManifestRoute } from \"./plugins/manifest-schema.js\";\nimport { extractRequestMeta, sanitizeHeadersForSandbox } from \"./plugins/request-meta.js\";\nimport { PluginRouteRegistry, type RouteMeta } from \"./plugins/routes.js\";\nimport { NodeCronScheduler } from \"./plugins/scheduler/node.js\";\nimport { PiggybackScheduler } from \"./plugins/scheduler/piggyback.js\";\nimport type { CronScheduler } from \"./plugins/scheduler/types.js\";\nimport { PluginStateRepository } from \"./plugins/state.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { FTSManager } from \"./search/fts-manager.js\";\n\n/**\n * Map schema field types to editor field kinds\n */\nconst FIELD_TYPE_TO_KIND: Record<FieldType, string> = {\n\tstring: \"string\",\n\tslug: \"string\",\n\ttext: \"richText\",\n\tnumber: \"number\",\n\tinteger: \"number\",\n\tboolean: \"boolean\",\n\tdatetime: \"datetime\",\n\tselect: \"select\",\n\tmultiSelect: \"multiSelect\",\n\tportableText: \"portableText\",\n\timage: \"image\",\n\tfile: \"file\",\n\treference: \"reference\",\n\tjson: \"json\",\n};\n\n/**\n * Sandboxed plugin entry from virtual module\n */\nexport interface SandboxedPluginEntry {\n\tid: string;\n\tversion: string;\n\toptions: Record<string, unknown>;\n\tcode: string;\n\t/** Capabilities the plugin requests */\n\tcapabilities: PluginCapability[];\n\t/** Allowed hosts for network:fetch */\n\tallowedHosts: string[];\n\t/** Declared storage collections */\n\tstorage: PluginStorageConfig;\n\t/** Admin pages */\n\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t/** Dashboard widgets */\n\tadminWidgets?: Array<{ id: string; title?: string; size?: string }>;\n\t/** Admin entry module */\n\tadminEntry?: string;\n\t/**\n\t * Exclusive hooks this plugin should be auto-selected for.\n\t * Weaker than an existing admin DB selection — config order wins when no selection exists.\n\t */\n\tpreferred?: string[];\n}\n\n/**\n * Media provider entry from virtual module\n */\nexport interface MediaProviderEntry {\n\tid: string;\n\tname: string;\n\ticon?: string;\n\tcapabilities: MediaProviderCapabilities;\n\t/** Factory function to create the provider instance */\n\tcreateProvider: (ctx: MediaProviderContext) => MediaProvider;\n}\n\n/**\n * Context passed to media provider factory functions\n */\nexport interface MediaProviderContext {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * Dependencies injected from virtual modules (middleware reads these)\n */\nexport interface RuntimeDependencies {\n\tconfig: EmDashConfig;\n\tplugins: ResolvedPlugin[];\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateDialect: (config: any) => Dialect;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateStorage: ((config: any) => Storage) | null;\n\tsandboxEnabled: boolean;\n\t/** Media provider entries from virtual module */\n\tmediaProviderEntries?: MediaProviderEntry[];\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\t/** Factory function matching SandboxRunnerFactory signature */\n\tcreateSandboxRunner: ((opts: { db: Kysely<Database> }) => SandboxRunner) | null;\n}\n\n/**\n * Convert a ContentItem to Record<string, unknown> for hook consumption.\n * Hooks receive the full item as a flat record.\n */\nfunction contentItemToRecord(item: ContentItemInternal): Record<string, unknown> {\n\treturn { ...item };\n}\n\n// Module-level caches (persist across requests within worker)\nconst dbCache = new Map<string, Kysely<Database>>();\nlet dbInitPromise: Promise<Kysely<Database>> | null = null;\nconst storageCache = new Map<string, Storage>();\nconst sandboxedPluginCache = new Map<string, SandboxedPlugin>();\nconst marketplacePluginKeys = new Set<string>();\n/** Manifest metadata for marketplace plugins: pluginId -> manifest admin config */\nconst marketplaceManifestCache = new Map<\n\tstring,\n\t{\n\t\tid: string;\n\t\tversion: string;\n\t\tadmin?: { pages?: PluginAdminPage[]; widgets?: PluginDashboardWidget[] };\n\t}\n>();\n/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */\nconst sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();\nlet sandboxRunner: SandboxRunner | null = null;\n\n/**\n * EmDashRuntime - singleton per worker\n */\nexport class EmDashRuntime {\n\t/**\n\t * The singleton database instance (worker-lifetime cached).\n\t * Use the `db` getter instead — it checks the request context first\n\t * for per-request overrides (D1 read replica sessions, DO multi-site).\n\t */\n\tprivate readonly _db: Kysely<Database>;\n\treadonly storage: Storage | null;\n\treadonly configuredPlugins: ResolvedPlugin[];\n\treadonly sandboxedPlugins: Map<string, SandboxedPlugin>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\treadonly schemaRegistry: SchemaRegistry;\n\tprivate _hooks!: HookPipeline;\n\treadonly config: EmDashConfig;\n\treadonly mediaProviders: Map<string, MediaProvider>;\n\treadonly mediaProviderEntries: MediaProviderEntry[];\n\treadonly cronExecutor: CronExecutor | null;\n\treadonly email: EmailPipeline | null;\n\n\tprivate cronScheduler: CronScheduler | null;\n\tprivate enabledPlugins: Set<string>;\n\tprivate pluginStates: Map<string, string>;\n\n\t/** Current hook pipeline. Use the `hooks` getter for external access. */\n\tget hooks(): HookPipeline {\n\t\treturn this._hooks;\n\t}\n\n\t/** All plugins eligible for the hook pipeline (includes built-in plugins).\n\t * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */\n\tprivate allPipelinePlugins: ResolvedPlugin[];\n\t/** Factory options for the hook pipeline context factory */\n\tprivate pipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tstorage?: Storage;\n\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t};\n\t/** Dependencies needed for exclusive hook resolution */\n\tprivate runtimeDeps: RuntimeDependencies;\n\t/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */\n\tprivate pipelineRef!: { current: HookPipeline };\n\n\t/**\n\t * Get the database instance for the current request.\n\t *\n\t * Checks the ALS-based request context first — middleware sets a\n\t * per-request Kysely instance there for D1 read replica sessions\n\t * or DO preview databases. Falls back to the singleton instance.\n\t */\n\tget db(): Kysely<Database> {\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is set by middleware with correct type\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\t\treturn this._db;\n\t}\n\n\tprivate constructor(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage | null,\n\t\tconfiguredPlugins: ResolvedPlugin[],\n\t\tsandboxedPlugins: Map<string, SandboxedPlugin>,\n\t\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\t\thooks: HookPipeline,\n\t\tenabledPlugins: Set<string>,\n\t\tpluginStates: Map<string, string>,\n\t\tconfig: EmDashConfig,\n\t\tmediaProviders: Map<string, MediaProvider>,\n\t\tmediaProviderEntries: MediaProviderEntry[],\n\t\tcronExecutor: CronExecutor | null,\n\t\tcronScheduler: CronScheduler | null,\n\t\temailPipeline: EmailPipeline | null,\n\t\tallPipelinePlugins: ResolvedPlugin[],\n\t\tpipelineFactoryOptions: {\n\t\t\tdb: Kysely<Database>;\n\t\t\tstorage?: Storage;\n\t\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t\t},\n\t\truntimeDeps: RuntimeDependencies,\n\t\tpipelineRef: { current: HookPipeline },\n\t) {\n\t\tthis._db = db;\n\t\tthis.storage = storage;\n\t\tthis.configuredPlugins = configuredPlugins;\n\t\tthis.sandboxedPlugins = sandboxedPlugins;\n\t\tthis.sandboxedPluginEntries = sandboxedPluginEntries;\n\t\tthis.schemaRegistry = new SchemaRegistry(db);\n\t\tthis._hooks = hooks;\n\t\tthis.enabledPlugins = enabledPlugins;\n\t\tthis.pluginStates = pluginStates;\n\t\tthis.config = config;\n\t\tthis.mediaProviders = mediaProviders;\n\t\tthis.mediaProviderEntries = mediaProviderEntries;\n\t\tthis.cronExecutor = cronExecutor;\n\t\tthis.cronScheduler = cronScheduler;\n\t\tthis.email = emailPipeline;\n\t\tthis.allPipelinePlugins = allPipelinePlugins;\n\t\tthis.pipelineFactoryOptions = pipelineFactoryOptions;\n\t\tthis.runtimeDeps = runtimeDeps;\n\t\tthis.pipelineRef = pipelineRef;\n\t}\n\n\t/**\n\t * Get the sandbox runner instance (for marketplace install/update)\n\t */\n\tgetSandboxRunner(): SandboxRunner | null {\n\t\treturn sandboxRunner;\n\t}\n\n\t/**\n\t * Tick the cron system from request context (piggyback mode).\n\t * Call this from middleware on each request to ensure cron tasks\n\t * execute even when no dedicated scheduler is available.\n\t */\n\ttickCron(): void {\n\t\tif (this.cronScheduler instanceof PiggybackScheduler) {\n\t\t\tthis.cronScheduler.onRequest();\n\t\t}\n\t}\n\n\t/**\n\t * Stop the cron scheduler gracefully.\n\t * Call during worker shutdown or hot-reload.\n\t */\n\tasync stopCron(): Promise<void> {\n\t\tif (this.cronScheduler) {\n\t\t\tawait this.cronScheduler.stop();\n\t\t}\n\t}\n\n\t/**\n\t * Update in-memory plugin status and rebuild the hook pipeline.\n\t *\n\t * Rebuilding the pipeline ensures disabled plugins' hooks stop firing\n\t * and re-enabled plugins' hooks start firing again without a restart.\n\t * Exclusive hook selections are re-resolved after each rebuild.\n\t */\n\tasync setPluginStatus(pluginId: string, status: \"active\" | \"inactive\"): Promise<void> {\n\t\tthis.pluginStates.set(pluginId, status);\n\t\tif (status === \"active\") {\n\t\t\tthis.enabledPlugins.add(pluginId);\n\t\t} else {\n\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t}\n\n\t\tawait this.rebuildHookPipeline();\n\t}\n\n\t/**\n\t * Rebuild the hook pipeline from the current set of enabled plugins.\n\t *\n\t * Filters `allPipelinePlugins` to only those in `enabledPlugins`,\n\t * creates a fresh HookPipeline, re-resolves exclusive hook selections,\n\t * and re-wires the context factory so existing references (cron\n\t * callbacks, email pipeline) use the new pipeline.\n\t */\n\tprivate async rebuildHookPipeline(): Promise<void> {\n\t\tconst enabledList = this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id));\n\t\tconst newPipeline = createHookPipeline(enabledList, this.pipelineFactoryOptions);\n\n\t\t// Re-resolve exclusive hooks against the new pipeline\n\t\tawait EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);\n\n\t\t// Carry over context factory options from the old pipeline so that\n\t\t// email, cron reschedule, and other wired-in options are preserved.\n\t\t// The old pipeline's contextFactoryOptions were built up incrementally\n\t\t// via setContextFactory calls during create(). We replay them here.\n\t\tif (this.email) {\n\t\t\tnewPipeline.setContextFactory({ db: this.db, emailPipeline: this.email });\n\t\t}\n\t\tif (this.cronScheduler) {\n\t\t\tconst scheduler = this.cronScheduler;\n\t\t\tnewPipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => scheduler.reschedule(),\n\t\t\t});\n\t\t}\n\n\t\t// Update the email pipeline to use the new hook pipeline\n\t\tif (this.email) {\n\t\t\tthis.email.setPipeline(newPipeline);\n\t\t}\n\n\t\t// Update the mutable ref so the cron closure dispatches through\n\t\t// the new pipeline without needing to reconstruct the CronExecutor.\n\t\tthis.pipelineRef.current = newPipeline;\n\n\t\tthis._hooks = newPipeline;\n\t}\n\n\t/**\n\t * Synchronize marketplace plugin runtime state with DB + storage.\n\t *\n\t * Ensures install/update/uninstall changes take effect immediately in the\n\t * current worker: loads newly active plugins and removes uninstalled ones.\n\t */\n\tasync syncMarketplacePlugins(): Promise<void> {\n\t\tif (!this.config.marketplace || !this.storage) return;\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) return;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst marketplaceStates = await stateRepo.getMarketplacePlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of marketplaceStates) {\n\t\t\t\tthis.pluginStates.set(state.pluginId, state.status);\n\t\t\t\tif (state.status === \"active\") {\n\t\t\t\t\tthis.enabledPlugins.add(state.pluginId);\n\t\t\t\t} else {\n\t\t\t\t\tthis.enabledPlugins.delete(state.pluginId);\n\t\t\t\t}\n\t\t\t\tif (state.status !== \"active\") continue;\n\t\t\t\tdesired.set(state.pluginId, state.marketplaceVersion ?? state.version);\n\t\t\t}\n\n\t\t\t// Remove uninstalled or no-longer-active marketplace plugins from memory.\n\t\t\tconst keysToRemove: string[] = [];\n\t\t\tfor (const key of marketplacePluginKeys) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;\n\t\t\t\tkeysToRemove.push(key);\n\t\t\t}\n\n\t\t\tfor (const key of keysToRemove) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (!desiredVersion) {\n\t\t\t\t\tthis.pluginStates.delete(pluginId);\n\t\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t\t}\n\n\t\t\t\tconst existing = sandboxedPluginCache.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait existing.terminate();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsandboxedPluginCache.delete(key);\n\t\t\t\tthis.sandboxedPlugins.delete(key);\n\t\t\t\tmarketplacePluginKeys.delete(key);\n\t\t\t\tif (pluginId) {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Load newly active marketplace plugins.\n\t\t\tfor (const [pluginId, version] of desired) {\n\t\t\t\tconst key = `${pluginId}:${version}`;\n\t\t\t\tif (sandboxedPluginCache.has(key)) {\n\t\t\t\t\tmarketplacePluginKeys.add(key);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst bundle = await loadBundleFromR2(this.storage, pluginId, version);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\tsandboxedPluginCache.set(key, loaded);\n\t\t\t\tthis.sandboxedPlugins.set(key, loaded);\n\t\t\t\tmarketplacePluginKeys.add(key);\n\n\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\tmarketplaceManifestCache.set(pluginId, {\n\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t});\n\n\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\tconst routeMetaMap = new Map<string, RouteMeta>();\n\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\trouteMetaMap.set(normalized.name, { public: normalized.public === true });\n\t\t\t\t\t}\n\t\t\t\t\tsandboxedRouteMetaCache.set(pluginId, routeMetaMap);\n\t\t\t\t} else {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash: Failed to sync marketplace plugins:\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create and initialize the runtime\n\t */\n\tstatic async create(deps: RuntimeDependencies): Promise<EmDashRuntime> {\n\t\t// Initialize database\n\t\tconst db = await EmDashRuntime.getDatabase(deps);\n\n\t\t// Verify and repair FTS indexes (auto-heal crash corruption)\n\t\t// FTS5 is SQLite-only; on other dialects, search is a no-op until\n\t\t// the pluggable SearchProvider work lands.\n\t\tif (isSqlite(db)) {\n\t\t\ttry {\n\t\t\t\tconst ftsManager = new FTSManager(db);\n\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\tif (repaired > 0) {\n\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal.\n\t\t\t}\n\t\t}\n\n\t\t// Initialize storage\n\t\tconst storage = EmDashRuntime.getStorage(deps);\n\n\t\t// Fetch plugin states from database\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\ttry {\n\t\t\tconst states = await db.selectFrom(\"_plugin_state\").select([\"plugin_id\", \"status\"]).execute();\n\t\t\tpluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));\n\t\t} catch {\n\t\t\t// Plugin state table may not exist yet\n\t\t}\n\n\t\t// Build set of enabled plugins\n\t\tconst enabledPlugins = new Set<string>();\n\t\tfor (const plugin of deps.plugins) {\n\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t}\n\t\t}\n\n\t\t// Load site info for plugin context extensions\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\ttry {\n\t\t\tconst optionsRepo = new OptionsRepository(db);\n\t\t\tconst siteName = await optionsRepo.get<string>(\"emdash:site_title\");\n\t\t\tconst siteUrl = await optionsRepo.get<string>(\"emdash:site_url\");\n\t\t\tconst locale = await optionsRepo.get<string>(\"emdash:locale\");\n\t\t\tsiteInfo = {\n\t\t\t\tsiteName: siteName ?? undefined,\n\t\t\t\tsiteUrl: siteUrl ?? undefined,\n\t\t\t\tlocale: locale ?? undefined,\n\t\t\t};\n\t\t} catch {\n\t\t\t// Options table may not exist yet (pre-setup)\n\t\t}\n\n\t\t// Build the full list of pipeline-eligible plugins: all configured\n\t\t// plugins (regardless of current enabled status) plus built-in plugins.\n\t\t// rebuildHookPipeline() filters this to only enabled plugins.\n\t\tconst allPipelinePlugins: ResolvedPlugin[] = [...deps.plugins];\n\n\t\t// In dev mode, register a built-in console email provider.\n\t\t// It participates in exclusive hook resolution like any other plugin —\n\t\t// auto-selected when it's the sole provider, overridden when a real one is configured.\n\t\t// Gated by import.meta.env.DEV to prevent silent email loss in production.\n\t\tif (import.meta.env.DEV) {\n\t\t\ttry {\n\t\t\t\tconst devConsolePlugin = definePlugin({\n\t\t\t\t\tid: DEV_CONSOLE_EMAIL_PLUGIN_ID,\n\t\t\t\t\tversion: \"0.0.0\",\n\t\t\t\t\tcapabilities: [\"email:provide\"],\n\t\t\t\t\thooks: {\n\t\t\t\t\t\t\"email:deliver\": {\n\t\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\t\thandler: devConsoleEmailDeliver,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tallPipelinePlugins.push(devConsolePlugin);\n\t\t\t\t// Built-in plugins are always enabled\n\t\t\t\tenabledPlugins.add(devConsolePlugin.id);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[email] Failed to register dev console email provider:\", error);\n\t\t\t}\n\t\t}\n\n\t\t// Register built-in default comment moderator.\n\t\t// Always present — auto-selected as the sole comment:moderate provider\n\t\t// unless a plugin (e.g. AI moderation) provides its own.\n\t\ttry {\n\t\t\tconst defaultModeratorPlugin = definePlugin({\n\t\t\t\tid: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\t\t\t\tversion: \"0.0.0\",\n\t\t\t\tcapabilities: [\"read:users\"],\n\t\t\t\thooks: {\n\t\t\t\t\t\"comment:moderate\": {\n\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\thandler: defaultCommentModerate,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t\tallPipelinePlugins.push(defaultModeratorPlugin);\n\t\t\t// Built-in plugins are always enabled\n\t\t\tenabledPlugins.add(defaultModeratorPlugin.id);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[comments] Failed to register default moderator:\", error);\n\t\t}\n\n\t\t// Filter to currently enabled plugins for the initial pipeline\n\t\tconst enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));\n\n\t\t// Create hook pipeline\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tstorage: storage ?? undefined,\n\t\t\tsiteInfo,\n\t\t};\n\t\tconst pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);\n\n\t\t// Load sandboxed plugins (build-time)\n\t\tconst sandboxedPlugins = await EmDashRuntime.loadSandboxedPlugins(deps, db);\n\n\t\t// Cold-start: load marketplace-installed plugins from site R2\n\t\tif (deps.config.marketplace && storage) {\n\t\t\tawait EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins);\n\t\t}\n\n\t\t// Initialize media providers\n\t\tconst mediaProviders = new Map<string, MediaProvider>();\n\t\tconst mediaProviderEntries = deps.mediaProviderEntries ?? [];\n\t\tconst providerContext: MediaProviderContext = { db, storage };\n\n\t\tfor (const entry of mediaProviderEntries) {\n\t\t\ttry {\n\t\t\t\tconst provider = entry.createProvider(providerContext);\n\t\t\t\tmediaProviders.set(entry.id, provider);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Failed to initialize media provider \"${entry.id}\":`, error);\n\t\t\t}\n\t\t}\n\n\t\t// Resolve exclusive hooks — auto-select providers and sync with DB\n\t\tawait EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps);\n\n\t\t// ── Email pipeline ───────────────────────────────────────────────\n\t\t// The email pipeline orchestrates beforeSend → deliver → afterSend.\n\t\t// The dev console provider was registered above and will be auto-selected\n\t\t// by resolveExclusiveHooks if it's the sole email:deliver provider.\n\t\tconst emailPipeline = new EmailPipeline(pipeline);\n\n\t\t// Wire email send into sandbox runner (created earlier but without\n\t\t// email pipeline since it didn't exist yet)\n\t\tif (sandboxRunner) {\n\t\t\tsandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));\n\t\t}\n\n\t\t// ── Cron system ──────────────────────────────────────────────────\n\t\t// Create executor with a hook dispatch function that uses the pipeline.\n\t\t// The callback reads from a mutable ref so that rebuildHookPipeline()\n\t\t// can swap the pipeline without reconstructing the CronExecutor.\n\t\tconst pipelineRef = { current: pipeline };\n\t\tconst invokeCronHook: InvokeCronHookFn = async (pluginId, event) => {\n\t\t\tconst result = await pipelineRef.current.invokeCronHook(pluginId, event);\n\t\t\tif (!result.success && result.error) {\n\t\t\t\tthrow result.error;\n\t\t\t}\n\t\t};\n\n\t\t// Wire email pipeline into context factory (independent of cron —\n\t\t// must not be inside the cron try/catch or ctx.email breaks when cron fails)\n\t\tpipeline.setContextFactory({ db, emailPipeline });\n\n\t\tlet cronExecutor: CronExecutor | null = null;\n\t\tlet cronScheduler: CronScheduler | null = null;\n\n\t\ttry {\n\t\t\tcronExecutor = new CronExecutor(db, invokeCronHook);\n\n\t\t\t// Recover stale locks from previous crashes\n\t\t\tconst recovered = await cronExecutor.recoverStaleLocks();\n\t\t\tif (recovered > 0) {\n\t\t\t\tconsole.log(`[cron] Recovered ${recovered} stale task lock(s)`);\n\t\t\t}\n\n\t\t\t// Detect platform and create appropriate scheduler.\n\t\t\t// On Cloudflare Workers, setTimeout is available but unreliable for\n\t\t\t// long durations — use PiggybackScheduler as default.\n\t\t\t// In Node/Bun, use NodeCronScheduler with real timers.\n\t\t\tconst isWorkersRuntime =\n\t\t\t\ttypeof globalThis.navigator !== \"undefined\" &&\n\t\t\t\tglobalThis.navigator.userAgent === \"Cloudflare-Workers\";\n\n\t\t\tif (isWorkersRuntime) {\n\t\t\t\tcronScheduler = new PiggybackScheduler(cronExecutor);\n\t\t\t} else {\n\t\t\t\tcronScheduler = new NodeCronScheduler(cronExecutor);\n\t\t\t}\n\n\t\t\t// Register system cleanup to run alongside each scheduler tick.\n\t\t\t// Pass storage so cleanupPendingUploads can delete orphaned files.\n\t\t\tcronScheduler.setSystemCleanup(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runSystemCleanup(db, storage ?? undefined);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Non-fatal -- individual cleanup failures are already logged\n\t\t\t\t\t// by runSystemCleanup. This catches unexpected errors.\n\t\t\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Add cron reschedule callback (merges with existing factory options)\n\t\t\tpipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => cronScheduler?.reschedule(),\n\t\t\t});\n\n\t\t\t// Start the scheduler\n\t\t\tawait cronScheduler.start();\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[cron] Failed to initialize cron system:\", error);\n\t\t\t// Non-fatal — CMS works without cron\n\t\t}\n\n\t\treturn new EmDashRuntime(\n\t\t\tdb,\n\t\t\tstorage,\n\t\t\tdeps.plugins,\n\t\t\tsandboxedPlugins,\n\t\t\tdeps.sandboxedPluginEntries,\n\t\t\tpipeline,\n\t\t\tenabledPlugins,\n\t\t\tpluginStates,\n\t\t\tdeps.config,\n\t\t\tmediaProviders,\n\t\t\tmediaProviderEntries,\n\t\t\tcronExecutor,\n\t\t\tcronScheduler,\n\t\t\temailPipeline,\n\t\t\tallPipelinePlugins,\n\t\t\tpipelineFactoryOptions,\n\t\t\tdeps,\n\t\t\tpipelineRef,\n\t\t);\n\t}\n\n\t/**\n\t * Get a media provider by ID\n\t */\n\tgetMediaProvider(providerId: string): MediaProvider | undefined {\n\t\treturn this.mediaProviders.get(providerId);\n\t}\n\n\t/**\n\t * Get all media provider entries (for admin UI)\n\t */\n\tgetMediaProviderList(): Array<{\n\t\tid: string;\n\t\tname: string;\n\t\ticon?: string;\n\t\tcapabilities: MediaProviderCapabilities;\n\t}> {\n\t\treturn this.mediaProviderEntries.map((e) => ({\n\t\t\tid: e.id,\n\t\t\tname: e.name,\n\t\t\ticon: e.icon,\n\t\t\tcapabilities: e.capabilities,\n\t\t}));\n\t}\n\n\t/**\n\t * Get or create database instance\n\t */\n\tprivate static async getDatabase(deps: RuntimeDependencies): Promise<Kysely<Database>> {\n\t\t// If a per-request DB override is set (e.g. by the playground middleware\n\t\t// which runs before the runtime init), use that directly. This allows\n\t\t// the runtime to initialize against the real DO database instead of\n\t\t// the dummy singleton dialect.\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is typed as unknown to avoid circular deps\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\n\t\tconst dbConfig = deps.config.database;\n\n\t\t// If no database configured in integration, try to get from loader\n\t\tif (!dbConfig) {\n\t\t\ttry {\n\t\t\t\treturn await getDb();\n\t\t\t} catch {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst cacheKey = dbConfig.entrypoint;\n\n\t\t// Return cached instance if available\n\t\tconst cached = dbCache.get(cacheKey);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Use initialization lock to prevent race conditions.\n\t\t// Sharing this promise across requests is safe because the Kysely instance\n\t\t// doesn't hold a request-scoped resource — the DO dialect uses a getStub()\n\t\t// factory that creates a fresh stub per query execution.\n\t\tif (dbInitPromise) {\n\t\t\treturn dbInitPromise;\n\t\t}\n\n\t\tdbInitPromise = (async () => {\n\t\t\tconst dialect = deps.createDialect(dbConfig.config);\n\t\t\tconst db = new Kysely<Database>({ dialect });\n\n\t\t\tawait runMigrations(db);\n\n\t\t\t// Auto-seed schema if no collections exist and setup hasn't run.\n\t\t\t// This covers first-load on sites that skip the setup wizard.\n\t\t\t// Dev-bypass and the wizard apply seeds explicitly.\n\t\t\ttry {\n\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\tdb\n\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\tdb\n\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t]);\n\n\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t})();\n\n\t\t\t\tif (collectionCount.count === 0 && !setupDone) {\n\t\t\t\t\tconst { applySeed } = await import(\"./seed/apply.js\");\n\t\t\t\t\tconst { loadSeed } = await import(\"./seed/load.js\");\n\t\t\t\t\tconst { validateSeed } = await import(\"./seed/validate.js\");\n\n\t\t\t\t\tconst seed = await loadSeed();\n\t\t\t\t\tconst validation = validateSeed(seed);\n\t\t\t\t\tif (validation.valid) {\n\t\t\t\t\t\tawait applySeed(db, seed, { onConflict: \"skip\" });\n\t\t\t\t\t\tconsole.log(\"Auto-seeded default collections\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Tables may not exist yet. Non-fatal.\n\t\t\t}\n\n\t\t\tdbCache.set(cacheKey, db);\n\t\t\treturn db;\n\t\t})();\n\n\t\ttry {\n\t\t\treturn await dbInitPromise;\n\t\t} finally {\n\t\t\tdbInitPromise = null;\n\t\t}\n\t}\n\n\t/**\n\t * Get or create storage instance\n\t */\n\tprivate static getStorage(deps: RuntimeDependencies): Storage | null {\n\t\tconst storageConfig = deps.config.storage;\n\t\tif (!storageConfig || !deps.createStorage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst cacheKey = storageConfig.entrypoint;\n\t\tconst cached = storageCache.get(cacheKey);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\tconst storage = deps.createStorage(storageConfig.config);\n\t\tstorageCache.set(cacheKey, storage);\n\t\treturn storage;\n\t}\n\n\t/**\n\t * Load sandboxed plugins using SandboxRunner\n\t */\n\tprivate static async loadSandboxedPlugins(\n\t\tdeps: RuntimeDependencies,\n\t\tdb: Kysely<Database>,\n\t): Promise<Map<string, SandboxedPlugin>> {\n\t\t// Return cached plugins if already loaded\n\t\tif (sandboxedPluginCache.size > 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Check if sandboxing is enabled\n\t\tif (!deps.sandboxEnabled || deps.sandboxedPluginEntries.length === 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Create sandbox runner if not exists\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({ db });\n\t\t}\n\n\t\tif (!sandboxRunner) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Check if the runner is actually available (has required bindings)\n\t\tif (!sandboxRunner.isAvailable()) {\n\t\t\tconsole.debug(\"EmDash: Sandbox runner not available (missing bindings), skipping sandbox\");\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Load each sandboxed plugin\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tconst pluginKey = `${entry.id}:${entry.version}`;\n\t\t\tif (sandboxedPluginCache.has(pluginKey)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Build manifest from entry's declared config\n\t\t\t\tconst manifest: PluginManifest = {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tcapabilities: entry.capabilities ?? [],\n\t\t\t\t\tallowedHosts: entry.allowedHosts ?? [],\n\t\t\t\t\tstorage: entry.storage ?? {},\n\t\t\t\t\thooks: [],\n\t\t\t\t\troutes: [],\n\t\t\t\t\tadmin: {},\n\t\t\t\t};\n\n\t\t\t\tconst plugin = await sandboxRunner.load(manifest, entry.code);\n\t\t\t\tsandboxedPluginCache.set(pluginKey, plugin);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(\", \")}]`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn sandboxedPluginCache;\n\t}\n\n\t/**\n\t * Cold-start: load marketplace-installed plugins from site-local R2 storage\n\t *\n\t * Queries _plugin_state for source='marketplace' rows, fetches each bundle\n\t * from R2, and loads via SandboxRunner.\n\t */\n\tprivate static async loadMarketplacePlugins(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t\tdeps: RuntimeDependencies,\n\t\tcache: Map<string, SandboxedPlugin>,\n\t): Promise<void> {\n\t\t// Ensure sandbox runner exists\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({ db });\n\t\t}\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\n\t\t\tfor (const plugin of marketplacePlugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\n\t\t\t\tconst version = plugin.marketplaceVersion ?? plugin.version;\n\t\t\t\tconst pluginKey = `${plugin.pluginId}:${version}`;\n\n\t\t\t\t// Skip if already loaded (shouldn't happen, but guard)\n\t\t\t\tif (cache.has(pluginKey)) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst bundle = await loadBundleFromR2(storage, plugin.pluginId, version);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\t\tcache.set(pluginKey, loaded);\n\t\t\t\t\tmarketplacePluginKeys.add(pluginKey);\n\n\t\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\t\tmarketplaceManifestCache.set(plugin.pluginId, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\t\tconst routeMeta = new Map<string, RouteMeta>();\n\t\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\t\trouteMeta.set(normalized.name, { public: normalized.public === true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);\n\t\t\t\t\t}\n\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded marketplace plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(\", \")}]`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet (pre-migration)\n\t\t}\n\t}\n\n\t/**\n\t * Resolve exclusive hook selections on startup.\n\t *\n\t * Delegates to the shared resolveExclusiveHooks() in hooks.ts.\n\t * The runtime version considers all pipeline providers as \"active\" since\n\t * the pipeline was already built from only active/enabled plugins.\n\t */\n\tprivate static async resolveExclusiveHooks(\n\t\tpipeline: HookPipeline,\n\t\tdb: Kysely<Database>,\n\t\tdeps: RuntimeDependencies,\n\t): Promise<void> {\n\t\tconst exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();\n\t\tif (exclusiveHookNames.length === 0) return;\n\n\t\tlet optionsRepo: OptionsRepository;\n\t\ttry {\n\t\t\toptionsRepo = new OptionsRepository(db);\n\t\t} catch {\n\t\t\treturn; // Options table may not exist yet\n\t\t}\n\n\t\t// Build preferred hints from sandboxed plugin entries\n\t\tconst preferredHints = new Map<string, string[]>();\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tif (entry.preferred && entry.preferred.length > 0) {\n\t\t\t\tpreferredHints.set(entry.id, entry.preferred);\n\t\t\t}\n\t\t}\n\n\t\t// The pipeline was created from only enabled plugins, so all providers\n\t\t// in it are active. The isActive check always returns true.\n\t\tawait resolveExclusiveHooksShared({\n\t\t\tpipeline,\n\t\t\tisActive: () => true,\n\t\t\tgetOption: (key) => optionsRepo.get<string>(key),\n\t\t\tsetOption: (key, value) => optionsRepo.set(key, value),\n\t\t\tdeleteOption: async (key) => {\n\t\t\t\tawait optionsRepo.delete(key);\n\t\t\t},\n\t\t\tpreferredHints,\n\t\t});\n\t}\n\n\t// =========================================================================\n\t// Manifest\n\t// =========================================================================\n\n\t/**\n\t * Build the manifest (rebuilt on each request for freshness)\n\t */\n\tasync getManifest(): Promise<EmDashManifest> {\n\t\t// Build collections from database.\n\t\t// Use this.db (ALS-aware getter) so playground mode picks up the\n\t\t// per-session DO database instead of the hardcoded singleton.\n\t\tconst manifestCollections: Record<string, ManifestCollection> = {};\n\t\ttry {\n\t\t\tconst registry = new SchemaRegistry(this.db);\n\t\t\tconst dbCollections = await registry.listCollections();\n\t\t\tfor (const collection of dbCollections) {\n\t\t\t\tconst collectionWithFields = await registry.getCollectionWithFields(collection.slug);\n\t\t\t\tconst fields: Record<\n\t\t\t\t\tstring,\n\t\t\t\t\t{\n\t\t\t\t\t\tkind: string;\n\t\t\t\t\t\tlabel?: string;\n\t\t\t\t\t\trequired?: boolean;\n\t\t\t\t\t\twidget?: string;\n\t\t\t\t\t\toptions?: Array<{ value: string; label: string }>;\n\t\t\t\t\t}\n\t\t\t\t> = {};\n\n\t\t\t\tif (collectionWithFields?.fields) {\n\t\t\t\t\tfor (const field of collectionWithFields.fields) {\n\t\t\t\t\t\tconst entry: (typeof fields)[string] = {\n\t\t\t\t\t\t\tkind: FIELD_TYPE_TO_KIND[field.type] ?? \"string\",\n\t\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\t\trequired: field.required,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (field.widget) entry.widget = field.widget;\n\t\t\t\t\t\t// Include select/multiSelect options from validation\n\t\t\t\t\t\tif (field.validation?.options) {\n\t\t\t\t\t\t\tentry.options = field.validation.options.map((v) => ({\n\t\t\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfields[field.slug] = entry;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tmanifestCollections[collection.slug] = {\n\t\t\t\t\tlabel: collection.label,\n\t\t\t\t\tlabelSingular: collection.labelSingular || collection.label,\n\t\t\t\t\tsupports: collection.supports || [],\n\t\t\t\t\thasSeo: collection.hasSeo,\n\t\t\t\t\tfields,\n\t\t\t\t};\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.debug(\"EmDash: Could not load database collections:\", error);\n\t\t}\n\n\t\t// Build plugins manifest\n\t\tconst manifestPlugins: Record<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tversion?: string;\n\t\t\t\tenabled?: boolean;\n\t\t\t\tsandboxed?: boolean;\n\t\t\t\tadminMode?: \"react\" | \"blocks\" | \"none\";\n\t\t\t\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t\t\t\tdashboardWidgets?: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\ttitle?: string;\n\t\t\t\t\tsize?: string;\n\t\t\t\t}>;\n\t\t\t\tportableTextBlocks?: Array<{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\ticon?: string;\n\t\t\t\t\tdescription?: string;\n\t\t\t\t\tplaceholder?: string;\n\t\t\t\t\tfields?: Element[];\n\t\t\t\t}>;\n\t\t\t\tfieldWidgets?: Array<{\n\t\t\t\t\tname: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\tfieldTypes: string[];\n\t\t\t\t\telements?: Element[];\n\t\t\t\t}>;\n\t\t\t}\n\t\t> = {};\n\n\t\tfor (const plugin of this.configuredPlugins) {\n\t\t\tconst status = this.pluginStates.get(plugin.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\t// Determine admin mode: has admin entry → react, has pages/widgets → blocks, else none\n\t\t\tconst hasAdminEntry = !!plugin.admin?.entry;\n\t\t\tconst hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;\n\t\t\tlet adminMode: \"react\" | \"blocks\" | \"none\" = \"none\";\n\t\t\tif (hasAdminEntry) {\n\t\t\t\tadminMode = \"react\";\n\t\t\t} else if (hasAdminPages || hasWidgets) {\n\t\t\t\tadminMode = \"blocks\";\n\t\t\t}\n\n\t\t\tmanifestPlugins[plugin.id] = {\n\t\t\t\tversion: plugin.version,\n\t\t\t\tenabled,\n\t\t\t\tadminMode,\n\t\t\t\tadminPages: plugin.admin?.pages,\n\t\t\t\tdashboardWidgets: plugin.admin?.widgets,\n\t\t\t\tportableTextBlocks: plugin.admin?.portableTextBlocks,\n\t\t\t\tfieldWidgets: plugin.admin?.fieldWidgets,\n\t\t\t};\n\t\t}\n\n\t\t// Add sandboxed plugins (use entries for admin config)\n\t\t// TODO: sandboxed plugins need fieldWidgets extracted from their manifest\n\t\t// to support Block Kit field widgets. Currently only trusted plugins carry\n\t\t// fieldWidgets through the admin.fieldWidgets path.\n\t\tfor (const entry of this.sandboxedPluginEntries) {\n\t\t\tconst status = this.pluginStates.get(entry.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\tconst hasAdminPages = (entry.adminPages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[entry.id] = {\n\t\t\t\tversion: entry.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: entry.adminPages,\n\t\t\t\tdashboardWidgets: entry.adminWidgets,\n\t\t\t};\n\t\t}\n\n\t\t// Add marketplace-installed plugins (dynamically loaded from R2)\n\t\tfor (const [pluginId, meta] of marketplaceManifestCache) {\n\t\t\t// Skip if already included from build-time config\n\t\t\tif (manifestPlugins[pluginId]) continue;\n\n\t\t\tconst status = this.pluginStates.get(pluginId);\n\t\t\tconst enabled = status === \"active\";\n\n\t\t\tconst pages = meta.admin?.pages;\n\t\t\tconst widgets = meta.admin?.widgets;\n\t\t\tconst hasAdminPages = (pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (widgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[pluginId] = {\n\t\t\t\tversion: meta.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: pages,\n\t\t\t\tdashboardWidgets: widgets,\n\t\t\t};\n\t\t}\n\n\t\t// Generate hash from both collections and plugins so cache invalidates\n\t\t// when plugins are enabled/disabled or their config changes\n\t\tconst manifestHash = await hashString(\n\t\t\tJSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins),\n\t\t);\n\n\t\t// Determine auth mode\n\t\tconst authMode = getAuthMode(this.config);\n\t\tconst authModeValue = authMode.type === \"external\" ? authMode.providerType : \"passkey\";\n\n\t\t// Include i18n config if enabled\n\t\tconst { getI18nConfig, isI18nEnabled } = await import(\"./i18n/config.js\");\n\t\tconst i18nConfig = getI18nConfig();\n\t\tconst i18n =\n\t\t\tisI18nEnabled() && i18nConfig\n\t\t\t\t? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }\n\t\t\t\t: undefined;\n\n\t\treturn {\n\t\t\tversion: \"0.1.0\",\n\t\t\thash: manifestHash,\n\t\t\tcollections: manifestCollections,\n\t\t\tplugins: manifestPlugins,\n\t\t\tauthMode: authModeValue,\n\t\t\ti18n,\n\t\t\tmarketplace: !!this.config.marketplace,\n\t\t};\n\t}\n\n\t/**\n\t * Invalidate the cached manifest (no-op now that we don't cache).\n\t * Kept for API compatibility.\n\t */\n\tinvalidateManifest(): void {\n\t\t// No-op - manifest is rebuilt on each request\n\t}\n\n\t// =========================================================================\n\t// Content Handlers\n\t// =========================================================================\n\n\tasync handleContentList(\n\t\tcollection: string,\n\t\tparams: {\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t\tstatus?: string;\n\t\t\torderBy?: string;\n\t\t\torder?: \"asc\" | \"desc\";\n\t\t\tlocale?: string;\n\t\t},\n\t) {\n\t\treturn handleContentList(this.db, collection, params);\n\t}\n\n\tasync handleContentGet(collection: string, id: string, locale?: string) {\n\t\treturn handleContentGet(this.db, collection, id, locale);\n\t}\n\n\tasync handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {\n\t\treturn handleContentGetIncludingTrashed(this.db, collection, id, locale);\n\t}\n\n\tasync handleContentCreate(\n\t\tcollection: string,\n\t\tbody: {\n\t\t\tdata: Record<string, unknown>;\n\t\t\tslug?: string;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\tlocale?: string;\n\t\t\ttranslationOf?: string;\n\t\t},\n\t) {\n\t\t// Run beforeSave hooks (trusted plugins)\n\t\tlet processedData = body.data;\n\t\tif (this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(body.data, collection, true);\n\t\t\tprocessedData = hookResult.content;\n\t\t}\n\n\t\t// Run beforeSave hooks (sandboxed plugins)\n\t\tprocessedData = await this.runSandboxedBeforeSave(processedData, collection, true);\n\n\t\t// Normalize media fields (fill dimensions, storageKey, etc.)\n\t\tprocessedData = await this.normalizeMediaFields(collection, processedData);\n\n\t\t// Create the content\n\t\tconst result = await handleContentCreate(this.db, collection, {\n\t\t\t...body,\n\t\t\tdata: processedData,\n\t\t\tauthorId: body.authorId,\n\t\t\tbylines: body.bylines,\n\t\t});\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUpdate(\n\t\tcollection: string,\n\t\tid: string,\n\t\tbody: {\n\t\t\tdata?: Record<string, unknown>;\n\t\t\tslug?: string;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string | null;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\t/** Skip revision creation (used by autosave) */\n\t\t\tskipRevision?: boolean;\n\t\t\t_rev?: string;\n\t\t},\n\t) {\n\t\t// Resolve slug → ID if needed (before any lookups)\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(this.db);\n\t\tconst resolvedItem = await repo.findByIdOrSlug(collection, id);\n\t\tconst resolvedId = resolvedItem?.id ?? id;\n\n\t\t// Validate _rev early — before draft revision writes which modify updated_at.\n\t\t// After validation, strip _rev so the handler doesn't double-check against\n\t\t// the now-modified timestamp.\n\t\tif (body._rev) {\n\t\t\tif (!resolvedItem) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: `Content item not found: ${id}` },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst revCheck = validateRev(body._rev, resolvedItem);\n\t\t\tif (!revCheck.valid) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"CONFLICT\", message: revCheck.message },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tconst { _rev: _discardedRev, ...bodyWithoutRev } = body;\n\n\t\t// Run beforeSave hooks if data is provided\n\t\tlet processedData = bodyWithoutRev.data;\n\t\tif (bodyWithoutRev.data) {\n\t\t\tif (this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(\n\t\t\t\t\tbodyWithoutRev.data,\n\t\t\t\t\tcollection,\n\t\t\t\t\tfalse,\n\t\t\t\t);\n\t\t\t\tprocessedData = hookResult.content;\n\t\t\t}\n\n\t\t\t// Run sandboxed beforeSave hooks\n\t\t\tprocessedData = await this.runSandboxedBeforeSave(processedData!, collection, false);\n\n\t\t\t// Normalize media fields (fill dimensions, storageKey, etc.)\n\t\t\tprocessedData = await this.normalizeMediaFields(collection, processedData);\n\t\t}\n\n\t\t// Draft-aware revision handling (if collection supports revisions)\n\t\t// Content table columns = published data (never written by saves).\n\t\t// Draft data lives only in the revisions table.\n\t\tlet usesDraftRevisions = false;\n\t\tif (processedData) {\n\t\t\ttry {\n\t\t\t\tconst collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);\n\t\t\t\tif (collectionInfo?.supports?.includes(\"revisions\")) {\n\t\t\t\t\tusesDraftRevisions = true;\n\t\t\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\t\t\t// Re-fetch to get latest state (resolvedItem may be stale after _rev check)\n\t\t\t\t\tconst existing = await repo.findById(collection, resolvedId);\n\n\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t// Build the draft data: merge with existing draft revision if one exists,\n\t\t\t\t\t\t// otherwise merge with the published data from the content table\n\t\t\t\t\t\tlet baseData: Record<string, unknown>;\n\t\t\t\t\t\tif (existing.draftRevisionId) {\n\t\t\t\t\t\t\tconst draftRevision = await revisionRepo.findById(existing.draftRevisionId);\n\t\t\t\t\t\t\tbaseData = draftRevision?.data ?? existing.data;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbaseData = existing.data;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Include slug in the revision data if it changed\n\t\t\t\t\t\tconst mergedData = { ...baseData, ...processedData };\n\t\t\t\t\t\tif (bodyWithoutRev.slug !== undefined) {\n\t\t\t\t\t\t\tmergedData._slug = bodyWithoutRev.slug;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (bodyWithoutRev.skipRevision && existing.draftRevisionId) {\n\t\t\t\t\t\t\t// Autosave: update existing draft revision in place\n\t\t\t\t\t\t\tawait revisionRepo.updateData(existing.draftRevisionId, mergedData);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create new draft revision\n\t\t\t\t\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\tentryId: resolvedId,\n\t\t\t\t\t\t\t\tdata: mergedData,\n\t\t\t\t\t\t\t\tauthorId: bodyWithoutRev.authorId ?? undefined,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Update entry to point to new draft (metadata only, not data columns)\n\t\t\t\t\t\t\tconst tableName = `ec_${collection}`;\n\t\t\t\t\t\t\tawait sql`\n\t\t\t\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\t\t\t\tSET draft_revision_id = ${revision.id},\n\t\t\t\t\t\t\t\t\tupdated_at = ${new Date().toISOString()}\n\t\t\t\t\t\t\t\tWHERE id = ${resolvedId}\n\t\t\t\t\t\t\t`.execute(this.db);\n\n\t\t\t\t\t\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\t\t\t\t\t\tvoid revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Don't fail the update if revision creation fails\n\t\t\t}\n\t\t}\n\n\t\t// Update the content table:\n\t\t// - If collection uses draft revisions: only update metadata (no data fields, no slug)\n\t\t// - Otherwise: update everything as before\n\t\tconst result = await handleContentUpdate(this.db, collection, resolvedId, {\n\t\t\t...bodyWithoutRev,\n\t\t\tdata: usesDraftRevisions ? undefined : processedData,\n\t\t\tslug: usesDraftRevisions ? undefined : bodyWithoutRev.slug,\n\t\t\tauthorId: bodyWithoutRev.authorId,\n\t\t\tbylines: bodyWithoutRev.bylines,\n\t\t});\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, false);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentDelete(collection: string, id: string) {\n\t\t// Run beforeDelete hooks (trusted plugins)\n\t\tif (this.hooks.hasHooks(\"content:beforeDelete\")) {\n\t\t\tconst { allowed } = await this.hooks.runContentBeforeDelete(id, collection);\n\t\t\tif (!allowed) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"DELETE_BLOCKED\",\n\t\t\t\t\t\tmessage: \"Delete blocked by plugin hook\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Run sandboxed beforeDelete hooks\n\t\tconst sandboxAllowed = await this.runSandboxedBeforeDelete(id, collection);\n\t\tif (!sandboxAllowed) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"DELETE_BLOCKED\",\n\t\t\t\t\tmessage: \"Delete blocked by sandboxed plugin hook\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Delete the content\n\t\tconst result = await handleContentDelete(this.db, collection, id);\n\n\t\t// Run afterDelete hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Trash Handlers\n\t// =========================================================================\n\n\tasync handleContentListTrashed(\n\t\tcollection: string,\n\t\tparams: { cursor?: string; limit?: number } = {},\n\t) {\n\t\treturn handleContentListTrashed(this.db, collection, params);\n\t}\n\n\tasync handleContentRestore(collection: string, id: string) {\n\t\treturn handleContentRestore(this.db, collection, id);\n\t}\n\n\tasync handleContentPermanentDelete(collection: string, id: string) {\n\t\treturn handleContentPermanentDelete(this.db, collection, id);\n\t}\n\n\tasync handleContentCountTrashed(collection: string) {\n\t\treturn handleContentCountTrashed(this.db, collection);\n\t}\n\n\tasync handleContentDuplicate(collection: string, id: string, authorId?: string) {\n\t\treturn handleContentDuplicate(this.db, collection, id, authorId);\n\t}\n\n\t// =========================================================================\n\t// Publishing & Scheduling Handlers\n\t// =========================================================================\n\n\tasync handleContentPublish(collection: string, id: string) {\n\t\treturn handleContentPublish(this.db, collection, id);\n\t}\n\n\tasync handleContentUnpublish(collection: string, id: string) {\n\t\treturn handleContentUnpublish(this.db, collection, id);\n\t}\n\n\tasync handleContentSchedule(collection: string, id: string, scheduledAt: string) {\n\t\treturn handleContentSchedule(this.db, collection, id, scheduledAt);\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\treturn handleContentUnschedule(this.db, collection, id);\n\t}\n\n\tasync handleContentCountScheduled(collection: string) {\n\t\treturn handleContentCountScheduled(this.db, collection);\n\t}\n\n\tasync handleContentDiscardDraft(collection: string, id: string) {\n\t\treturn handleContentDiscardDraft(this.db, collection, id);\n\t}\n\n\tasync handleContentCompare(collection: string, id: string) {\n\t\treturn handleContentCompare(this.db, collection, id);\n\t}\n\n\tasync handleContentTranslations(collection: string, id: string) {\n\t\treturn handleContentTranslations(this.db, collection, id);\n\t}\n\n\t// =========================================================================\n\t// Media Handlers\n\t// =========================================================================\n\n\tasync handleMediaList(params: { cursor?: string; limit?: number; mimeType?: string }) {\n\t\treturn handleMediaList(this.db, params);\n\t}\n\n\tasync handleMediaGet(id: string) {\n\t\treturn handleMediaGet(this.db, id);\n\t}\n\n\tasync handleMediaCreate(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t}) {\n\t\t// Run beforeUpload hooks\n\t\tlet processedInput = input;\n\t\tif (this.hooks.hasHooks(\"media:beforeUpload\")) {\n\t\t\tconst hookResult = await this.hooks.runMediaBeforeUpload({\n\t\t\t\tname: input.filename,\n\t\t\t\ttype: input.mimeType,\n\t\t\t\tsize: input.size || 0,\n\t\t\t});\n\t\t\tprocessedInput = {\n\t\t\t\t...input,\n\t\t\t\tfilename: hookResult.file.name,\n\t\t\t\tmimeType: hookResult.file.type,\n\t\t\t\tsize: hookResult.file.size,\n\t\t\t};\n\t\t}\n\n\t\t// Create the media record\n\t\tconst result = await handleMediaCreate(this.db, processedInput);\n\n\t\t// Run afterUpload hooks (fire-and-forget)\n\t\tif (result.success && this.hooks.hasHooks(\"media:afterUpload\")) {\n\t\t\tconst item = result.data.item;\n\t\t\tconst mediaItem: MediaItem = {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t\tthis.hooks\n\t\t\t\t.runMediaAfterUpload(mediaItem)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUpload hook error:\", err));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleMediaUpdate(\n\t\tid: string,\n\t\tinput: { alt?: string; caption?: string; width?: number; height?: number },\n\t) {\n\t\treturn handleMediaUpdate(this.db, id, input);\n\t}\n\n\tasync handleMediaDelete(id: string) {\n\t\treturn handleMediaDelete(this.db, id);\n\t}\n\n\t// =========================================================================\n\t// Revision Handlers\n\t// =========================================================================\n\n\tasync handleRevisionList(collection: string, entryId: string, params: { limit?: number } = {}) {\n\t\treturn handleRevisionList(this.db, collection, entryId, params);\n\t}\n\n\tasync handleRevisionGet(revisionId: string) {\n\t\treturn handleRevisionGet(this.db, revisionId);\n\t}\n\n\tasync handleRevisionRestore(revisionId: string, callerUserId: string) {\n\t\treturn handleRevisionRestore(this.db, revisionId, callerUserId);\n\t}\n\n\t// =========================================================================\n\t// Plugin Routes\n\t// =========================================================================\n\n\t/**\n\t * Get route metadata for a plugin route without invoking the handler.\n\t * Used by the catch-all route to decide auth before dispatch.\n\t * Returns null if the plugin or route doesn't exist.\n\t */\n\tgetPluginRouteMeta(pluginId: string, path: string): RouteMeta | null {\n\t\tif (!this.isPluginEnabled(pluginId)) return null;\n\n\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\t// Check trusted plugins first\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin) {\n\t\t\tconst route = trustedPlugin.routes[routeKey];\n\t\t\tif (!route) return null;\n\t\t\treturn { public: route.public === true };\n\t\t}\n\n\t\t// Check sandboxed plugin route metadata cache\n\t\tconst meta = sandboxedRouteMetaCache.get(pluginId);\n\t\tif (meta) {\n\t\t\tconst routeMeta = meta.get(routeKey);\n\t\t\tif (routeMeta) return routeMeta;\n\t\t}\n\n\t\t// The \"admin\" route is implicitly available for any sandboxed plugin\n\t\t// that declares admin pages or widgets. This handles plugins installed\n\t\t// from bundles that predate the explicit admin route requirement.\n\t\tif (routeKey === \"admin\") {\n\t\t\tconst manifestMeta = marketplaceManifestCache.get(pluginId);\n\t\t\tif (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t\t// Also check build-time sandboxed entries\n\t\t\tconst entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\t\tif (entry?.adminPages?.length || entry?.adminWidgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: if the plugin exists in the sandbox cache, allow the route.\n\t\t// The sandbox runner will return an error if the route doesn't actually exist.\n\t\tif (this.findSandboxedPlugin(pluginId)) {\n\t\t\treturn { public: false };\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tasync handlePluginApiRoute(pluginId: string, _method: string, path: string, request: Request) {\n\t\tif (!this.isPluginEnabled(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not enabled: ${pluginId}` },\n\t\t\t};\n\t\t}\n\n\t\t// Check trusted (configured) plugins first — this must match the\n\t\t// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {\n\t\t\tconst routeRegistry = new PluginRouteRegistry({ db: this.db });\n\t\t\trouteRegistry.register(trustedPlugin);\n\n\t\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\t\tlet body: unknown = undefined;\n\t\t\ttry {\n\t\t\t\tbody = await request.json();\n\t\t\t} catch {\n\t\t\t\t// No body or not JSON\n\t\t\t}\n\n\t\t\treturn routeRegistry.invoke(pluginId, routeKey, { request, body });\n\t\t}\n\n\t\t// Check sandboxed (marketplace) plugins second\n\t\tconst sandboxedPlugin = this.findSandboxedPlugin(pluginId);\n\t\tif (sandboxedPlugin) {\n\t\t\treturn this.handleSandboxedRoute(sandboxedPlugin, path, request);\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Sandboxed Plugin Helpers\n\t// =========================================================================\n\n\tprivate findSandboxedPlugin(pluginId: string): SandboxedPlugin | undefined {\n\t\tfor (const [key, plugin] of this.sandboxedPlugins) {\n\t\t\tif (key.startsWith(pluginId + \":\")) {\n\t\t\t\treturn plugin;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Normalize image/file fields in content data.\n\t * Fills missing dimensions, storageKey, mimeType, and filename from providers.\n\t */\n\tprivate async normalizeMediaFields(\n\t\tcollection: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<Record<string, unknown>> {\n\t\tlet collectionInfo;\n\t\ttry {\n\t\t\tcollectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);\n\t\t} catch {\n\t\t\treturn data;\n\t\t}\n\t\tif (!collectionInfo?.fields) return data;\n\n\t\tconst imageFields = collectionInfo.fields.filter(\n\t\t\t(f) => f.type === \"image\" || f.type === \"file\",\n\t\t);\n\t\tif (imageFields.length === 0) return data;\n\n\t\tconst getProvider = (id: string) => this.getMediaProvider(id);\n\t\tconst result = { ...data };\n\n\t\tfor (const field of imageFields) {\n\t\t\tconst value = result[field.slug];\n\t\t\tif (value == null) continue;\n\n\t\t\ttry {\n\t\t\t\tconst normalized = await normalizeMediaValue(value, getProvider);\n\t\t\t\tif (normalized) {\n\t\t\t\t\tresult[field.slug] = normalized;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Don't fail the save if normalization fails for a single field\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate async runSandboxedBeforeSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<Record<string, unknown>> {\n\t\tlet result = content;\n\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst hookResult = await plugin.invokeHook(\"content:beforeSave\", {\n\t\t\t\t\tcontent: result,\n\t\t\t\t\tcollection,\n\t\t\t\t\tisNew,\n\t\t\t\t});\n\t\t\t\tif (hookResult && typeof hookResult === \"object\" && !Array.isArray(hookResult)) {\n\t\t\t\t\t// Sandbox returns unknown; convert to record by iterating own properties\n\t\t\t\t\tconst record: Record<string, unknown> = {};\n\t\t\t\t\tfor (const [k, v] of Object.entries(hookResult)) {\n\t\t\t\t\t\trecord[k] = v;\n\t\t\t\t\t}\n\t\t\t\t\tresult = record;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} beforeSave hook error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate async runSandboxedBeforeDelete(id: string, collection: string): Promise<boolean> {\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst result = await plugin.invokeHook(\"content:beforeDelete\", {\n\t\t\t\t\tid,\n\t\t\t\t\tcollection,\n\t\t\t\t});\n\t\t\t\tif (result === false) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate runAfterSaveHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterSave\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterSave(content, collection, isNew)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterSave hook error:\", err));\n\t\t}\n\n\t\t// Sandboxed plugins\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\tplugin\n\t\t\t\t.invokeHook(\"content:afterSave\", { content, collection, isNew })\n\t\t\t\t.catch((err) => console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err));\n\t\t}\n\t}\n\n\tprivate runAfterDeleteHooks(id: string, collection: string): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterDelete\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterDelete(id, collection)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterDelete hook error:\", err));\n\t\t}\n\n\t\t// Sandboxed plugins\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\tplugin\n\t\t\t\t.invokeHook(\"content:afterDelete\", { id, collection })\n\t\t\t\t.catch((err) =>\n\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err),\n\t\t\t\t);\n\t\t}\n\t}\n\n\tprivate async handleSandboxedRoute(\n\t\tplugin: SandboxedPlugin,\n\t\tpath: string,\n\t\trequest: Request,\n\t): Promise<{\n\t\tsuccess: boolean;\n\t\tdata?: unknown;\n\t\terror?: { code: string; message: string };\n\t}> {\n\t\tconst routeName = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\tlet body: unknown = undefined;\n\t\ttry {\n\t\t\tbody = await request.json();\n\t\t} catch {\n\t\t\t// No body or not JSON\n\t\t}\n\n\t\ttry {\n\t\t\tconst headers = sanitizeHeadersForSandbox(request.headers);\n\t\t\tconst meta = extractRequestMeta(request);\n\t\t\tconst result = await plugin.invokeRoute(routeName, body, {\n\t\t\t\turl: request.url,\n\t\t\t\tmethod: request.method,\n\t\t\t\theaders,\n\t\t\t\tmeta,\n\t\t\t});\n\t\t\treturn { success: true, data: result };\n\t\t} catch (error) {\n\t\t\tconsole.error(`EmDash: Sandboxed plugin route error:`, error);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_ERROR\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Public Page Contributions\n\t// =========================================================================\n\n\t/**\n\t * Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext\n\t * object so results are collected once per page context per request, even when\n\t * multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)\n\t * request contributions from the same page.\n\t */\n\tprivate pageContributionCache = new WeakMap<PublicPageContext, Promise<PageContributions>>();\n\n\t/**\n\t * Collect all page contributions (metadata + fragments) in a single pass.\n\t * Results are cached by page context object identity.\n\t */\n\tasync collectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst cached = this.pageContributionCache.get(page);\n\t\tif (cached) return cached;\n\n\t\tconst promise = this.doCollectPageContributions(page);\n\t\tthis.pageContributionCache.set(page, promise);\n\t\treturn promise;\n\t}\n\n\tprivate async doCollectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst metadata: PageMetadataContribution[] = [];\n\t\tconst fragments: PageFragmentContribution[] = [];\n\n\t\t// Trusted plugins via HookPipeline — both metadata and fragments\n\t\tif (this.hooks.hasHooks(\"page:metadata\")) {\n\t\t\tconst results = await this.hooks.runPageMetadata({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tmetadata.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\tif (this.hooks.hasHooks(\"page:fragments\")) {\n\t\t\tconst results = await this.hooks.runPageFragments({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tfragments.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\t// Sandboxed plugins — metadata only, never fragments\n\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst result = await plugin.invokeHook(\"page:metadata\", { page });\n\t\t\t\tif (result != null) {\n\t\t\t\t\tconst items = Array.isArray(result) ? result : [result];\n\t\t\t\t\tfor (const item of items) {\n\t\t\t\t\t\tif (isValidMetadataContribution(item)) {\n\t\t\t\t\t\t\tmetadata.push(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn { metadata, fragments };\n\t}\n\n\t/**\n\t * Collect page metadata contributions from trusted and sandboxed plugins.\n\t * Delegates to the single-pass collector and returns the metadata portion.\n\t */\n\tasync collectPageMetadata(page: PublicPageContext): Promise<PageMetadataContribution[]> {\n\t\tconst { metadata } = await this.collectPageContributions(page);\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Collect page fragment contributions from trusted plugins only.\n\t * Delegates to the single-pass collector and returns the fragments portion.\n\t */\n\tasync collectPageFragments(page: PublicPageContext): Promise<PageFragmentContribution[]> {\n\t\tconst { fragments } = await this.collectPageContributions(page);\n\t\treturn fragments;\n\t}\n\n\tprivate isPluginEnabled(pluginId: string): boolean {\n\t\tconst status = this.pluginStates.get(pluginId);\n\t\treturn status === undefined || status === \"active\";\n\t}\n}\n","/**\n * EmDash middleware\n *\n * Thin wrapper that initializes EmDashRuntime and attaches it to locals.\n * All heavy lifting happens in EmDashRuntime.\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\nimport { Kysely } from \"kysely\";\n// Import from virtual modules (populated by integration at build time)\n// @ts-ignore - virtual module\nimport virtualConfig from \"virtual:emdash/config\";\n// @ts-ignore - virtual module\nimport {\n\tcreateDialect as virtualCreateDialect,\n\tisSessionEnabled as virtualIsSessionEnabled,\n\tgetD1Binding as virtualGetD1Binding,\n\tgetDefaultConstraint as virtualGetDefaultConstraint,\n\tgetBookmarkCookieName as virtualGetBookmarkCookieName,\n\tcreateSessionDialect as virtualCreateSessionDialect,\n} from \"virtual:emdash/dialect\";\n// @ts-ignore - virtual module\nimport { mediaProviders as virtualMediaProviders } from \"virtual:emdash/media-providers\";\n// @ts-ignore - virtual module\nimport { plugins as virtualPlugins } from \"virtual:emdash/plugins\";\nimport {\n\tcreateSandboxRunner as virtualCreateSandboxRunner,\n\tsandboxEnabled as virtualSandboxEnabled,\n\t// @ts-ignore - virtual module\n} from \"virtual:emdash/sandbox-runner\";\n// @ts-ignore - virtual module\nimport { sandboxedPlugins as virtualSandboxedPlugins } from \"virtual:emdash/sandboxed-plugins\";\n// @ts-ignore - virtual module\nimport { createStorage as virtualCreateStorage } from \"virtual:emdash/storage\";\n\nimport {\n\tEmDashRuntime,\n\ttype RuntimeDependencies,\n\ttype SandboxedPluginEntry,\n\ttype MediaProviderEntry,\n} from \"../emdash-runtime.js\";\nimport { setI18nConfig } from \"../i18n/config.js\";\nimport type { Database, Storage } from \"../index.js\";\nimport type { SandboxRunner } from \"../plugins/sandbox/types.js\";\nimport type { ResolvedPlugin } from \"../plugins/types.js\";\nimport { runWithContext } from \"../request-context.js\";\nimport type { EmDashConfig } from \"./integration/runtime.js\";\n\n// Cached runtime instance (persists across requests within worker)\nlet runtimeInstance: EmDashRuntime | null = null;\n// Whether initialization is in progress (prevents concurrent init attempts)\nlet runtimeInitializing = false;\n\n/** Whether i18n config has been initialized from the virtual module */\nlet i18nInitialized = false;\n\n/**\n * Whether we've verified the database has been set up.\n * On a fresh deployment the first request may hit a public page, bypassing\n * runtime init. Without this check, template helpers like getSiteSettings()\n * would query an empty database and crash. Once verified (or once the runtime\n * has initialized via an admin/API request), this stays true for the worker's\n * lifetime.\n */\nlet setupVerified = false;\n\n/**\n * Get EmDash configuration from virtual module\n */\nfunction getConfig(): EmDashConfig | null {\n\tif (virtualConfig && typeof virtualConfig === \"object\") {\n\t\t// Initialize i18n config on first access (once per worker lifetime)\n\t\tif (!i18nInitialized) {\n\t\t\ti18nInitialized = true;\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module checked as object above\n\t\t\tconst config = virtualConfig as Record<string, unknown>;\n\t\t\tif (config.i18n && typeof config.i18n === \"object\") {\n\t\t\t\tsetI18nConfig(\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- runtime-checked above\n\t\t\t\t\tconfig.i18n as {\n\t\t\t\t\t\tdefaultLocale: string;\n\t\t\t\t\t\tlocales: string[];\n\t\t\t\t\t\tfallback?: Record<string, string>;\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tsetI18nConfig(null);\n\t\t\t}\n\t\t}\n\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\treturn virtualConfig as EmDashConfig;\n\t}\n\treturn null;\n}\n\n/**\n * Get plugins from virtual module\n */\nfunction getPlugins(): ResolvedPlugin[] {\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\treturn (virtualPlugins as ResolvedPlugin[]) || [];\n}\n\n/**\n * Build runtime dependencies from virtual modules\n */\nfunction buildDependencies(config: EmDashConfig): RuntimeDependencies {\n\treturn {\n\t\tconfig,\n\t\tplugins: getPlugins(),\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateDialect: virtualCreateDialect as (config: Record<string, unknown>) => unknown,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateStorage: virtualCreateStorage as ((config: Record<string, unknown>) => Storage) | null,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tsandboxEnabled: virtualSandboxEnabled as boolean,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tsandboxedPluginEntries: (virtualSandboxedPlugins as SandboxedPluginEntry[]) || [],\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tcreateSandboxRunner: virtualCreateSandboxRunner as\n\t\t\t| ((opts: { db: Kysely<Database> }) => SandboxRunner)\n\t\t\t| null,\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above)\n\t\tmediaProviderEntries: (virtualMediaProviders as MediaProviderEntry[]) || [],\n\t};\n}\n\n/**\n * Get or create the runtime instance\n */\nasync function getRuntime(config: EmDashConfig): Promise<EmDashRuntime> {\n\t// Return cached instance if available\n\tif (runtimeInstance) {\n\t\treturn runtimeInstance;\n\t}\n\n\t// If another request is already initializing, wait and retry.\n\t// We don't share the promise across requests because workerd flags\n\t// cross-request promise resolution (causes warnings + potential hangs).\n\tif (runtimeInitializing) {\n\t\t// Poll until the initializing request finishes\n\t\tawait new Promise((resolve) => setTimeout(resolve, 50));\n\t\treturn getRuntime(config);\n\t}\n\n\truntimeInitializing = true;\n\ttry {\n\t\tconst deps = buildDependencies(config);\n\t\tconst runtime = await EmDashRuntime.create(deps);\n\t\truntimeInstance = runtime;\n\t\treturn runtime;\n\t} finally {\n\t\truntimeInitializing = false;\n\t}\n}\n\n/**\n * Baseline security headers applied to all responses.\n * Admin routes get additional headers (strict CSP) from auth middleware.\n */\nfunction setBaselineSecurityHeaders(response: Response): void {\n\t// Prevent MIME type sniffing\n\tresponse.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\t// Control referrer information\n\tresponse.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\t// Restrict access to sensitive browser APIs\n\tresponse.headers.set(\n\t\t\"Permissions-Policy\",\n\t\t\"camera=(), microphone=(), geolocation=(), payment=()\",\n\t);\n\t// Prevent clickjacking (non-admin routes; admin CSP uses frame-ancestors)\n\tif (!response.headers.has(\"Content-Security-Policy\")) {\n\t\tresponse.headers.set(\"X-Frame-Options\", \"SAMEORIGIN\");\n\t}\n}\n\n/** Public routes that require the runtime (sitemap, robots.txt, etc.) */\nconst PUBLIC_RUNTIME_ROUTES = new Set([\"/sitemap.xml\", \"/robots.txt\"]);\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { request, locals, cookies } = context;\n\tconst url = context.url;\n\n\t// Process /_emdash routes and public routes with an active session\n\t// (logged-in editors need the runtime for toolbar/visual editing on public pages)\n\tconst isEmDashRoute = url.pathname.startsWith(\"/_emdash\");\n\tconst isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname);\n\n\t// Check for edit mode cookie - editors viewing public pages need the runtime\n\t// so auth middleware can verify their session for visual editing\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// Playground mode: the playground middleware stashes the per-session DO database\n\t// on locals.__playgroundDb. When present, use runWithContext() to make it\n\t// available to getDb() and the runtime's db getter via the correct ALS instance.\n\tconst playgroundDb = locals.__playgroundDb;\n\n\tif (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {\n\t\tconst sessionUser = await context.session?.get(\"user\");\n\t\tif (!sessionUser && !playgroundDb) {\n\t\t\t// On a fresh deployment the database may be completely empty.\n\t\t\t// Public pages call getSiteSettings() / getMenu() via getDb(), which\n\t\t\t// bypasses runtime init and would crash with \"no such table: options\".\n\t\t\t// Do a one-time lightweight probe using the same getDb() instance the\n\t\t\t// page will use: if the migrations table doesn't exist, no migrations\n\t\t\t// have ever run -- redirect to the setup wizard.\n\t\t\tif (!setupVerified) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { getDb } = await import(\"../loader.js\");\n\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\tawait db\n\t\t\t\t\t\t.selectFrom(\"_emdash_migrations\" as keyof Database)\n\t\t\t\t\t\t.selectAll()\n\t\t\t\t\t\t.limit(1)\n\t\t\t\t\t\t.execute();\n\t\t\t\t\tsetupVerified = true;\n\t\t\t\t} catch {\n\t\t\t\t\t// Table doesn't exist -> fresh database, redirect to setup\n\t\t\t\t\treturn context.redirect(\"/_emdash/admin/setup\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst response = await next();\n\t\t\tsetBaselineSecurityHeaders(response);\n\t\t\treturn response;\n\t\t}\n\t}\n\n\tconst config = getConfig();\n\tif (!config) {\n\t\tconsole.error(\"EmDash: No configuration found\");\n\t\treturn next();\n\t}\n\n\t// In playground mode, wrap the entire runtime init + request handling in\n\t// runWithContext so that getDatabase() and all init queries use the real\n\t// DO database via the same AsyncLocalStorage instance as the loader.\n\tconst doInit = async () => {\n\t\ttry {\n\t\t\t// Get or create runtime\n\t\t\tconst runtime = await getRuntime(config);\n\n\t\t\t// Runtime init runs migrations, so the DB is guaranteed set up\n\t\t\tsetupVerified = true;\n\n\t\t\t// Get manifest (cached after first call)\n\t\t\tconst manifest = await runtime.getManifest();\n\n\t\t\t// Attach to locals for route handlers\n\t\t\tlocals.emdashManifest = manifest;\n\t\t\tlocals.emdash = {\n\t\t\t\t// Content handlers\n\t\t\t\thandleContentList: runtime.handleContentList.bind(runtime),\n\t\t\t\thandleContentGet: runtime.handleContentGet.bind(runtime),\n\t\t\t\thandleContentCreate: runtime.handleContentCreate.bind(runtime),\n\t\t\t\thandleContentUpdate: runtime.handleContentUpdate.bind(runtime),\n\t\t\t\thandleContentDelete: runtime.handleContentDelete.bind(runtime),\n\n\t\t\t\t// Trash handlers\n\t\t\t\thandleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),\n\t\t\t\thandleContentRestore: runtime.handleContentRestore.bind(runtime),\n\t\t\t\thandleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),\n\t\t\t\thandleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),\n\t\t\t\thandleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),\n\n\t\t\t\t// Duplicate handler\n\t\t\t\thandleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),\n\n\t\t\t\t// Publishing & Scheduling handlers\n\t\t\t\thandleContentPublish: runtime.handleContentPublish.bind(runtime),\n\t\t\t\thandleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),\n\t\t\t\thandleContentSchedule: runtime.handleContentSchedule.bind(runtime),\n\t\t\t\thandleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),\n\t\t\t\thandleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),\n\t\t\t\thandleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),\n\t\t\t\thandleContentCompare: runtime.handleContentCompare.bind(runtime),\n\t\t\t\thandleContentTranslations: runtime.handleContentTranslations.bind(runtime),\n\n\t\t\t\t// Media handlers\n\t\t\t\thandleMediaList: runtime.handleMediaList.bind(runtime),\n\t\t\t\thandleMediaGet: runtime.handleMediaGet.bind(runtime),\n\t\t\t\thandleMediaCreate: runtime.handleMediaCreate.bind(runtime),\n\t\t\t\thandleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),\n\t\t\t\thandleMediaDelete: runtime.handleMediaDelete.bind(runtime),\n\n\t\t\t\t// Revision handlers\n\t\t\t\thandleRevisionList: runtime.handleRevisionList.bind(runtime),\n\t\t\t\thandleRevisionGet: runtime.handleRevisionGet.bind(runtime),\n\t\t\t\thandleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),\n\n\t\t\t\t// Plugin routes\n\t\t\t\thandlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),\n\t\t\t\tgetPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),\n\n\t\t\t\t// Media provider methods\n\t\t\t\tgetMediaProvider: runtime.getMediaProvider.bind(runtime),\n\t\t\t\tgetMediaProviderList: runtime.getMediaProviderList.bind(runtime),\n\n\t\t\t\t// Direct access (for advanced use cases)\n\t\t\t\tstorage: runtime.storage,\n\t\t\t\tdb: runtime.db,\n\t\t\t\thooks: runtime.hooks,\n\t\t\t\temail: runtime.email,\n\t\t\t\tconfiguredPlugins: runtime.configuredPlugins,\n\n\t\t\t\t// Configuration (for checking database type, auth mode, etc.)\n\t\t\t\tconfig,\n\n\t\t\t\t// Manifest invalidation (call after schema changes)\n\t\t\t\tinvalidateManifest: runtime.invalidateManifest.bind(runtime),\n\n\t\t\t\t// Sandbox runner (for marketplace plugin install/update)\n\t\t\t\tgetSandboxRunner: runtime.getSandboxRunner.bind(runtime),\n\n\t\t\t\t// Sync marketplace plugin states (after install/update/uninstall)\n\t\t\t\tsyncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),\n\n\t\t\t\t// Update plugin enabled/disabled status and rebuild hook pipeline\n\t\t\t\tsetPluginStatus: runtime.setPluginStatus.bind(runtime),\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash middleware error:\", error);\n\t\t}\n\n\t\t// =========================================================================\n\t\t// D1 Read Replica Session Management\n\t\t//\n\t\t// When D1 sessions are enabled, we create a per-request D1 session and\n\t\t// Kysely instance. The session is wrapped in ALS so `runtime.db` (a getter)\n\t\t// picks up the per-request instance instead of the singleton.\n\t\t//\n\t\t// After the response, we extract the bookmark from the session and set\n\t\t// it as a cookie for authenticated users (read-your-writes consistency).\n\t\t// =========================================================================\n\t\tconst dbConfig = config?.database?.config;\n\t\tconst sessionEnabled =\n\t\t\tdbConfig &&\n\t\t\ttypeof virtualIsSessionEnabled === \"function\" &&\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t(virtualIsSessionEnabled as (config: unknown) => boolean)(dbConfig);\n\n\t\tif (\n\t\t\tsessionEnabled &&\n\t\t\ttypeof virtualGetD1Binding === \"function\" &&\n\t\t\tvirtualCreateSessionDialect\n\t\t) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\tconst d1Binding = (virtualGetD1Binding as (config: unknown) => unknown)(dbConfig);\n\n\t\t\tif (d1Binding && typeof d1Binding === \"object\" && \"withSession\" in d1Binding) {\n\t\t\t\tconst isAuthenticated = !!(await context.session?.get(\"user\"));\n\t\t\t\tconst isWrite = request.method !== \"GET\" && request.method !== \"HEAD\";\n\n\t\t\t\t// Determine session constraint:\n\t\t\t\t// - Config says \"primary-first\" → always \"first-primary\"\n\t\t\t\t// - Authenticated writes → \"first-primary\" (need to hit primary)\n\t\t\t\t// - Authenticated reads with bookmark → resume from bookmark\n\t\t\t\t// - Otherwise → \"first-unconstrained\" (nearest replica)\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\tconst configConstraint = (virtualGetDefaultConstraint as (config: unknown) => string)(\n\t\t\t\t\tdbConfig,\n\t\t\t\t);\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\tconst cookieName = (virtualGetBookmarkCookieName as (config: unknown) => string)(dbConfig);\n\n\t\t\t\tlet constraint: string = configConstraint;\n\t\t\t\tif (isAuthenticated && isWrite) {\n\t\t\t\t\tconstraint = \"first-primary\";\n\t\t\t\t} else if (isAuthenticated) {\n\t\t\t\t\tconst bookmarkCookie = context.cookies.get(cookieName);\n\t\t\t\t\tif (bookmarkCookie?.value) {\n\t\t\t\t\t\tconstraint = bookmarkCookie.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Create the D1 session and per-request Kysely instance.\n\t\t\t\t// D1DatabaseSession has the same prepare()/batch() interface as D1Database,\n\t\t\t\t// so createSessionDialect passes it straight to D1Dialect.\n\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- D1 binding with Sessions API, checked via \"withSession\" in d1Binding above\n\t\t\t\tconst withSession = (d1Binding as { withSession: (c: string) => unknown }).withSession;\n\t\t\t\tconst session = withSession.call(d1Binding, constraint);\n\t\t\t\tconst sessionDialect =\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module functions are untyped\n\t\t\t\t\t(virtualCreateSessionDialect as (db: unknown) => import(\"kysely\").Dialect)(session);\n\t\t\t\tconst sessionDb = new Kysely<Database>({ dialect: sessionDialect });\n\n\t\t\t\t// Wrap the request in ALS with the per-request db\n\t\t\t\treturn runWithContext({ editMode: false, db: sessionDb }, async () => {\n\t\t\t\t\tconst response = await next();\n\t\t\t\t\tsetBaselineSecurityHeaders(response);\n\n\t\t\t\t\t// Set bookmark cookie for authenticated users only — they need\n\t\t\t\t\t// read-your-writes consistency across requests. Anonymous visitors\n\t\t\t\t\t// don't write, so they get \"first-unconstrained\" every time.\n\t\t\t\t\tif (\n\t\t\t\t\t\tisAuthenticated &&\n\t\t\t\t\t\tsession &&\n\t\t\t\t\t\ttypeof session === \"object\" &&\n\t\t\t\t\t\t\"getBookmark\" in session\n\t\t\t\t\t) {\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- D1DatabaseSession with getBookmark()\n\t\t\t\t\t\tconst getBookmark = (session as { getBookmark: () => string | null }).getBookmark;\n\t\t\t\t\t\tconst newBookmark = getBookmark.call(session);\n\t\t\t\t\t\tif (newBookmark) {\n\t\t\t\t\t\t\tresponse.headers.append(\n\t\t\t\t\t\t\t\t\"Set-Cookie\",\n\t\t\t\t\t\t\t\t`${cookieName}=${newBookmark}; Path=/; HttpOnly; SameSite=Lax; Secure`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn response;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst response = await next();\n\t\tsetBaselineSecurityHeaders(response);\n\t\treturn response;\n\t}; // end doInit\n\n\tif (playgroundDb) {\n\t\t// Read the edit-mode cookie to determine if visual editing is active.\n\t\t// Default to false -- editing is opt-in via the playground toolbar toggle.\n\t\tconst editMode = context.cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode, db: playgroundDb }, doInit);\n\t}\n\treturn doInit();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Astro-native CMS with WordPress migration support",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -178,9 +178,9 @@
178
178
  "ulidx": "^2.4.1",
179
179
  "upng-js": "^2.1.0",
180
180
  "zod": "^4.3.5",
181
- "@emdash-cms/admin": "0.0.1",
182
- "@emdash-cms/auth": "0.0.1",
183
- "@emdash-cms/gutenberg-to-portable-text": "0.0.1"
181
+ "@emdash-cms/gutenberg-to-portable-text": "0.0.1",
182
+ "@emdash-cms/admin": "0.0.2",
183
+ "@emdash-cms/auth": "0.0.1"
184
184
  },
185
185
  "optionalDependencies": {
186
186
  "@libsql/kysely-libsql": "^0.4.0",
@@ -54,6 +54,16 @@ let runtimeInitializing = false;
54
54
  /** Whether i18n config has been initialized from the virtual module */
55
55
  let i18nInitialized = false;
56
56
 
57
+ /**
58
+ * Whether we've verified the database has been set up.
59
+ * On a fresh deployment the first request may hit a public page, bypassing
60
+ * runtime init. Without this check, template helpers like getSiteSettings()
61
+ * would query an empty database and crash. Once verified (or once the runtime
62
+ * has initialized via an admin/API request), this stays true for the worker's
63
+ * lifetime.
64
+ */
65
+ let setupVerified = false;
66
+
57
67
  /**
58
68
  * Get EmDash configuration from virtual module
59
69
  */
@@ -190,6 +200,28 @@ export const onRequest = defineMiddleware(async (context, next) => {
190
200
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
191
201
  const sessionUser = await context.session?.get("user");
192
202
  if (!sessionUser && !playgroundDb) {
203
+ // On a fresh deployment the database may be completely empty.
204
+ // Public pages call getSiteSettings() / getMenu() via getDb(), which
205
+ // bypasses runtime init and would crash with "no such table: options".
206
+ // Do a one-time lightweight probe using the same getDb() instance the
207
+ // page will use: if the migrations table doesn't exist, no migrations
208
+ // have ever run -- redirect to the setup wizard.
209
+ if (!setupVerified) {
210
+ try {
211
+ const { getDb } = await import("../loader.js");
212
+ const db = await getDb();
213
+ await db
214
+ .selectFrom("_emdash_migrations" as keyof Database)
215
+ .selectAll()
216
+ .limit(1)
217
+ .execute();
218
+ setupVerified = true;
219
+ } catch {
220
+ // Table doesn't exist -> fresh database, redirect to setup
221
+ return context.redirect("/_emdash/admin/setup");
222
+ }
223
+ }
224
+
193
225
  const response = await next();
194
226
  setBaselineSecurityHeaders(response);
195
227
  return response;
@@ -210,6 +242,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
210
242
  // Get or create runtime
211
243
  const runtime = await getRuntime(config);
212
244
 
245
+ // Runtime init runs migrations, so the DB is guaranteed set up
246
+ setupVerified = true;
247
+
213
248
  // Get manifest (cached after first call)
214
249
  const manifest = await runtime.getManifest();
215
250