emdash 0.27.0 → 0.28.1

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.
Files changed (537) hide show
  1. package/dist/{adapters-u037EnTR.d.mts → adapters-hUy8vOB-.d.mts} +1 -1
  2. package/dist/{adapters-u037EnTR.d.mts.map → adapters-hUy8vOB-.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-D5FxMUo8.mjs → allowed-origins-CRsJ6BPM.mjs} +2 -2
  4. package/dist/{allowed-origins-D5FxMUo8.mjs.map → allowed-origins-CRsJ6BPM.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +17 -16
  7. package/dist/api/route-utils.mjs.map +1 -1
  8. package/dist/api/schemas/index.d.mts +1 -1
  9. package/dist/api/schemas/index.mjs +3 -3
  10. package/dist/{api-80kycR9o.mjs → api-DxFSorgk.mjs} +33 -23
  11. package/dist/api-DxFSorgk.mjs.map +1 -0
  12. package/dist/{api-tokens-D6ppjIVi.mjs → api-tokens-DvqeZmVq.mjs} +2 -2
  13. package/dist/{api-tokens-D6ppjIVi.mjs.map → api-tokens-DvqeZmVq.mjs.map} +1 -1
  14. package/dist/{apply-DSypk6Iu.mjs → apply-CDJqIjPU.mjs} +111 -74
  15. package/dist/apply-CDJqIjPU.mjs.map +1 -0
  16. package/dist/astro/index.d.mts +12 -12
  17. package/dist/astro/index.mjs +3 -3
  18. package/dist/astro/index.mjs.map +1 -1
  19. package/dist/astro/middleware/auth.d.mts +10 -10
  20. package/dist/astro/middleware/auth.d.mts.map +1 -1
  21. package/dist/astro/middleware/auth.mjs +50 -11
  22. package/dist/astro/middleware/auth.mjs.map +1 -1
  23. package/dist/astro/middleware/redirect.mjs +5 -5
  24. package/dist/astro/middleware/request-context.mjs +2 -2
  25. package/dist/astro/middleware/setup.mjs +1 -1
  26. package/dist/astro/middleware.d.mts +2 -2
  27. package/dist/astro/middleware.mjs +124 -71
  28. package/dist/astro/middleware.mjs.map +1 -1
  29. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  30. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  31. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
  32. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  33. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
  34. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
  35. package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
  36. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
  37. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -14
  38. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +14 -14
  39. package/dist/astro/routes/api/admin/bylines/index.mjs +14 -14
  40. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
  41. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  42. package/dist/astro/routes/api/admin/comments/bulk.mjs +10 -10
  43. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  44. package/dist/astro/routes/api/admin/comments/index.mjs +10 -10
  45. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  46. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  47. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  48. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  49. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +37 -36
  50. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  51. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +37 -36
  52. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +36 -35
  54. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +36 -35
  56. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +36 -35
  58. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/plugins/index.mjs +36 -35
  60. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  61. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  62. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +36 -35
  63. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  64. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +36 -35
  65. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  66. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +36 -35
  67. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  68. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +36 -35
  69. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +37 -36
  71. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  72. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +36 -35
  73. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  74. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +37 -36
  75. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  76. package/dist/astro/routes/api/admin/plugins/updates.mjs +36 -35
  77. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  78. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +36 -35
  79. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  81. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +36 -35
  82. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  84. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  85. package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
  86. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  87. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  88. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
  89. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  90. package/dist/astro/routes/api/auth/invite/complete.mjs +10 -10
  91. package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
  92. package/dist/astro/routes/api/auth/invite/register-options.mjs +9 -9
  93. package/dist/astro/routes/api/auth/logout.mjs +3 -3
  94. package/dist/astro/routes/api/auth/magic-link/send.mjs +9 -9
  95. package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
  96. package/dist/astro/routes/api/auth/me.mjs +7 -7
  97. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  98. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  99. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  100. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  101. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  102. package/dist/astro/routes/api/auth/passkey/options.mjs +11 -11
  103. package/dist/astro/routes/api/auth/passkey/register/options.mjs +9 -9
  104. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +10 -10
  105. package/dist/astro/routes/api/auth/passkey/verify.mjs +10 -10
  106. package/dist/astro/routes/api/auth/signup/complete.mjs +10 -10
  107. package/dist/astro/routes/api/auth/signup/request.mjs +9 -9
  108. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  109. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
  110. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
  111. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  112. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  113. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  114. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  115. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +10 -10
  116. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +7 -7
  117. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  118. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  119. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +7 -7
  120. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
  121. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  122. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  123. package/dist/astro/routes/api/content/_collection_/_id_.mjs +7 -7
  124. package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
  125. package/dist/astro/routes/api/content/_collection_/index.mjs +7 -7
  126. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  127. package/dist/astro/routes/api/dashboard.mjs +8 -8
  128. package/dist/astro/routes/api/dev/emails.mjs +3 -3
  129. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  130. package/dist/astro/routes/api/import/probe.mjs +19 -11
  131. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  132. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  133. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -41
  134. package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
  135. package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -407
  136. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  137. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  138. package/dist/astro/routes/api/import/wordpress/media.mjs +9 -9
  139. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  140. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +19 -1
  141. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
  142. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +97 -79
  143. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  144. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  145. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +19 -11
  146. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  147. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +40 -2
  148. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -1
  149. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +519 -39
  150. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  151. package/dist/astro/routes/api/manifest.mjs +4 -4
  152. package/dist/astro/routes/api/mcp.mjs +39 -39
  153. package/dist/astro/routes/api/media/_id_/confirm.mjs +7 -7
  154. package/dist/astro/routes/api/media/_id_.mjs +7 -7
  155. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  156. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  157. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  158. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  159. package/dist/astro/routes/api/media/upload-url.mjs +8 -8
  160. package/dist/astro/routes/api/media.mjs +9 -9
  161. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +9 -9
  162. package/dist/astro/routes/api/menus/_name_/items.mjs +9 -9
  163. package/dist/astro/routes/api/menus/_name_/reorder.mjs +9 -9
  164. package/dist/astro/routes/api/menus/_name_/translations.mjs +9 -9
  165. package/dist/astro/routes/api/menus/_name_.mjs +9 -9
  166. package/dist/astro/routes/api/menus/index.mjs +9 -9
  167. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  168. package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
  169. package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
  170. package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
  171. package/dist/astro/routes/api/oauth/register.mjs +3 -3
  172. package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
  173. package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
  174. package/dist/astro/routes/api/oauth/token.mjs +6 -6
  175. package/dist/astro/routes/api/openapi.json.mjs +4 -4
  176. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  177. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +6 -14
  178. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  179. package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
  180. package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
  181. package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
  182. package/dist/astro/routes/api/redirects/index.mjs +10 -10
  183. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  184. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  185. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +36 -35
  186. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  187. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +36 -35
  188. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  189. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +36 -35
  190. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  191. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +36 -35
  192. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  193. package/dist/astro/routes/api/schema/collections/index.mjs +36 -35
  194. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  195. package/dist/astro/routes/api/schema/index.mjs +7 -6
  196. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  197. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +36 -35
  198. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  199. package/dist/astro/routes/api/schema/orphans/index.mjs +36 -35
  200. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  201. package/dist/astro/routes/api/search/enable.mjs +10 -10
  202. package/dist/astro/routes/api/search/index.mjs +9 -9
  203. package/dist/astro/routes/api/search/rebuild.mjs +10 -10
  204. package/dist/astro/routes/api/search/stats.mjs +6 -6
  205. package/dist/astro/routes/api/search/suggest.mjs +9 -9
  206. package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
  207. package/dist/astro/routes/api/sections/index.mjs +9 -9
  208. package/dist/astro/routes/api/settings/email.mjs +4 -4
  209. package/dist/astro/routes/api/settings.mjs +12 -12
  210. package/dist/astro/routes/api/setup/admin-verify.mjs +11 -11
  211. package/dist/astro/routes/api/setup/admin.mjs +10 -10
  212. package/dist/astro/routes/api/setup/dev-bypass.mjs +25 -24
  213. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  214. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  215. package/dist/astro/routes/api/setup/index.mjs +26 -25
  216. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  217. package/dist/astro/routes/api/setup/status.mjs +4 -4
  218. package/dist/astro/routes/api/snapshot.mjs +6 -6
  219. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
  220. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
  221. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
  222. package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
  223. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  224. package/dist/astro/routes/api/typegen.mjs +5 -5
  225. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  226. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  227. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  228. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
  229. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +9 -9
  230. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +9 -9
  231. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  232. package/dist/astro/routes/api/widget-areas/index.mjs +9 -9
  233. package/dist/astro/routes/api/widget-components.mjs +3 -3
  234. package/dist/astro/routes/robots.txt.mjs +6 -6
  235. package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
  236. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  237. package/dist/astro/types.d.mts +13 -13
  238. package/dist/auth/providers/github.d.mts +1 -1
  239. package/dist/auth/providers/google.d.mts +1 -1
  240. package/dist/{authorize-C3a_zGsM.mjs → authorize-Dz88F_Fw.mjs} +2 -2
  241. package/dist/{authorize-C3a_zGsM.mjs.map → authorize-Dz88F_Fw.mjs.map} +1 -1
  242. package/dist/{byline-DEKSVPTy.mjs → byline-Bm8ZgUP9.mjs} +6 -6
  243. package/dist/{byline-DEKSVPTy.mjs.map → byline-Bm8ZgUP9.mjs.map} +1 -1
  244. package/dist/{byline-fields-eyJMblSr.mjs → byline-fields-2Lp5Hbgf.mjs} +18 -3
  245. package/dist/byline-fields-2Lp5Hbgf.mjs.map +1 -0
  246. package/dist/{byline-fields-CtQ6blXd.d.mts → byline-fields-CHE8UStY.d.mts} +47 -32
  247. package/dist/{byline-fields-CtQ6blXd.d.mts.map → byline-fields-CHE8UStY.d.mts.map} +1 -1
  248. package/dist/{byline-fields-DaMKzkhO.mjs → byline-fields-CIx_GuN4.mjs} +1 -1
  249. package/dist/{byline-fields-DaMKzkhO.mjs.map → byline-fields-CIx_GuN4.mjs.map} +1 -1
  250. package/dist/{bylines-Ds4-otXl.mjs → bylines-6tm9mgQb.mjs} +4 -4
  251. package/dist/{bylines-Ds4-otXl.mjs.map → bylines-6tm9mgQb.mjs.map} +1 -1
  252. package/dist/{bylines-CiqgTHWF.mjs → bylines-CAZxGSrm.mjs} +6 -6
  253. package/dist/{bylines-CiqgTHWF.mjs.map → bylines-CAZxGSrm.mjs.map} +1 -1
  254. package/dist/{cache-Ciu3e3AF.mjs → cache-CEBAZjir.mjs} +2 -2
  255. package/dist/{cache-Ciu3e3AF.mjs.map → cache-CEBAZjir.mjs.map} +1 -1
  256. package/dist/{challenge-store-LhiqMccz.mjs → challenge-store-anhwVER0.mjs} +1 -1
  257. package/dist/{challenge-store-LhiqMccz.mjs.map → challenge-store-anhwVER0.mjs.map} +1 -1
  258. package/dist/{chunks-CZFiauig.mjs → chunks-B2isWQhr.mjs} +2 -2
  259. package/dist/{chunks-CZFiauig.mjs.map → chunks-B2isWQhr.mjs.map} +1 -1
  260. package/dist/cli/index.mjs +22 -21
  261. package/dist/cli/index.mjs.map +1 -1
  262. package/dist/client/cf-access.d.mts +1 -1
  263. package/dist/client/index.d.mts +1 -1
  264. package/dist/client/index.mjs +2 -2
  265. package/dist/{comment-CH1IrFFs.mjs → comment-CzeatwBV.mjs} +3 -3
  266. package/dist/{comment-CH1IrFFs.mjs.map → comment-CzeatwBV.mjs.map} +1 -1
  267. package/dist/{comment-reaction-kruNY-kP.mjs → comment-reaction-Cj9QJY7I.mjs} +2 -2
  268. package/dist/{comment-reaction-kruNY-kP.mjs.map → comment-reaction-Cj9QJY7I.mjs.map} +1 -1
  269. package/dist/{comments-BUJyXbgq.mjs → comments-BLE-jv5k.mjs} +3 -3
  270. package/dist/{comments-BUJyXbgq.mjs.map → comments-BLE-jv5k.mjs.map} +1 -1
  271. package/dist/{components-CYt4uVK9.mjs → components-B_sATNUc.mjs} +1 -1
  272. package/dist/{components-CYt4uVK9.mjs.map → components-B_sATNUc.mjs.map} +1 -1
  273. package/dist/{content-RmfHoWqK.mjs → content-DhgEth4k.mjs} +4 -4
  274. package/dist/{content-RmfHoWqK.mjs.map → content-DhgEth4k.mjs.map} +1 -1
  275. package/dist/content-refresh-Br4gMzh8.mjs +1537 -0
  276. package/dist/content-refresh-Br4gMzh8.mjs.map +1 -0
  277. package/dist/{context-CzUU_WzL.mjs → context-BRIpHARC.mjs} +76 -54
  278. package/dist/context-BRIpHARC.mjs.map +1 -0
  279. package/dist/{cron-C5LVoNmP.mjs → cron-v4FTEFR_.mjs} +1 -1
  280. package/dist/{cron-C5LVoNmP.mjs.map → cron-v4FTEFR_.mjs.map} +1 -1
  281. package/dist/{dashboard-CVyGhift.mjs → dashboard-EvEKF5en.mjs} +4 -4
  282. package/dist/{dashboard-CVyGhift.mjs.map → dashboard-EvEKF5en.mjs.map} +1 -1
  283. package/dist/db/index.d.mts +3 -3
  284. package/dist/db/index.mjs +1 -1
  285. package/dist/db/libsql.d.mts +1 -1
  286. package/dist/db/postgres.d.mts +1 -1
  287. package/dist/db/sqlite.d.mts +1 -1
  288. package/dist/{default-CeRG-Ot4.mjs → default-CM_D83Mg.mjs} +1 -1
  289. package/dist/{default-CeRG-Ot4.mjs.map → default-CM_D83Mg.mjs.map} +1 -1
  290. package/dist/{device-flow-DENDCQ9F.mjs → device-flow-CfGP18Uw.mjs} +4 -4
  291. package/dist/{device-flow-DENDCQ9F.mjs.map → device-flow-CfGP18Uw.mjs.map} +1 -1
  292. package/dist/{email-console-DJP32ucW.mjs → email-console-CIf4Lfvv.mjs} +1 -1
  293. package/dist/{email-console-DJP32ucW.mjs.map → email-console-CIf4Lfvv.mjs.map} +1 -1
  294. package/dist/{error-BHkLZk-w.mjs → error-CkAseSqZ.mjs} +2 -2
  295. package/dist/{error-BHkLZk-w.mjs.map → error-CkAseSqZ.mjs.map} +1 -1
  296. package/dist/{escape-Bjio4ZsM.mjs → escape-DqD0aCKu.mjs} +1 -1
  297. package/dist/{escape-Bjio4ZsM.mjs.map → escape-DqD0aCKu.mjs.map} +1 -1
  298. package/dist/{field-defs-cache-BA-8ZPiT.mjs → field-defs-cache-WUI88LX5.mjs} +2 -2
  299. package/dist/{field-defs-cache-BA-8ZPiT.mjs.map → field-defs-cache-WUI88LX5.mjs.map} +1 -1
  300. package/dist/{fts-manager-Db9jGavf.mjs → fts-manager-BbFaF_V8.mjs} +2 -2
  301. package/dist/{fts-manager-Db9jGavf.mjs.map → fts-manager-BbFaF_V8.mjs.map} +1 -1
  302. package/dist/{import-Bb1T9WJS.mjs → import-fYLf14Co.mjs} +514 -25
  303. package/dist/import-fYLf14Co.mjs.map +1 -0
  304. package/dist/{index-CDZZjrLT.d.mts → index-B9sIxwe4.d.mts} +21 -22
  305. package/dist/{index-CDZZjrLT.d.mts.map → index-B9sIxwe4.d.mts.map} +1 -1
  306. package/dist/{index-DGIjmUXQ.d.mts → index-Bm-Gy8Mh.d.mts} +25 -4
  307. package/dist/index-Bm-Gy8Mh.d.mts.map +1 -0
  308. package/dist/index.d.mts +18 -18
  309. package/dist/index.mjs +54 -53
  310. package/dist/{load-DpHICVPh.mjs → load-Djn3HWQS.mjs} +2 -2
  311. package/dist/{load-DpHICVPh.mjs.map → load-Djn3HWQS.mjs.map} +1 -1
  312. package/dist/{loader-4K37qA-y.mjs → loader-CB6YHeul.mjs} +6 -5
  313. package/dist/loader-CB6YHeul.mjs.map +1 -0
  314. package/dist/{manifest-schema-kPGX7VS-.mjs → manifest-schema-BHlgZM1W.mjs} +1 -1
  315. package/dist/{manifest-schema-kPGX7VS-.mjs.map → manifest-schema-BHlgZM1W.mjs.map} +1 -1
  316. package/dist/media/index.d.mts +1 -1
  317. package/dist/media/local-runtime.d.mts +12 -12
  318. package/dist/media/local-runtime.mjs +5 -5
  319. package/dist/{media-C0nRRfs1.mjs → media-BrXIaMFN.mjs} +2 -2
  320. package/dist/{media-C0nRRfs1.mjs.map → media-BrXIaMFN.mjs.map} +1 -1
  321. package/dist/{media-allowlist-Du8t1u6a.mjs → media-allowlist-CRHEeJ_i.mjs} +1 -1
  322. package/dist/{media-allowlist-Du8t1u6a.mjs.map → media-allowlist-CRHEeJ_i.mjs.map} +1 -1
  323. package/dist/{media-url-O4rm9-SQ.mjs → media-url-CKi5XSw4.mjs} +1 -1
  324. package/dist/{media-url-O4rm9-SQ.mjs.map → media-url-CKi5XSw4.mjs.map} +1 -1
  325. package/dist/{menus-SL2K0xq2.mjs → menus-Cfivc2Ad.mjs} +3 -3
  326. package/dist/{menus-SL2K0xq2.mjs.map → menus-Cfivc2Ad.mjs.map} +1 -1
  327. package/dist/{menus-DzRyC1ID.mjs → menus-b2T4clbO.mjs} +16 -12
  328. package/dist/{menus-DzRyC1ID.mjs.map → menus-b2T4clbO.mjs.map} +1 -1
  329. package/dist/{mode-BB0F8xTC.mjs → mode-BM9QgEmt.mjs} +1 -1
  330. package/dist/{mode-BB0F8xTC.mjs.map → mode-BM9QgEmt.mjs.map} +1 -1
  331. package/dist/{oauth-authorization-CsvzIp_F.mjs → oauth-authorization-CXWkIGT4.mjs} +4 -4
  332. package/dist/{oauth-authorization-CsvzIp_F.mjs.map → oauth-authorization-CXWkIGT4.mjs.map} +1 -1
  333. package/dist/{oauth-clients-C9SYwEbZ.mjs → oauth-clients-BmTI5rsl.mjs} +1 -1
  334. package/dist/{oauth-clients-C9SYwEbZ.mjs.map → oauth-clients-BmTI5rsl.mjs.map} +1 -1
  335. package/dist/{oauth-state-store---zrApfB.mjs → oauth-state-store-YLFLcH5F.mjs} +1 -1
  336. package/dist/{oauth-state-store---zrApfB.mjs.map → oauth-state-store-YLFLcH5F.mjs.map} +1 -1
  337. package/dist/{oauth-user-lookup-SHsWRlG9.mjs → oauth-user-lookup-Bsc_GsX-.mjs} +1 -1
  338. package/dist/{oauth-user-lookup-SHsWRlG9.mjs.map → oauth-user-lookup-Bsc_GsX-.mjs.map} +1 -1
  339. package/dist/object-cache/memory.d.mts +1 -1
  340. package/dist/{object-cache-CHbHv83-.mjs → object-cache-Bok5j2ae.mjs} +43 -3
  341. package/dist/object-cache-Bok5j2ae.mjs.map +1 -0
  342. package/dist/{options-41nCWqi9.d.mts → options-iMpRRxIe.d.mts} +3 -3
  343. package/dist/{options-41nCWqi9.d.mts.map → options-iMpRRxIe.d.mts.map} +1 -1
  344. package/dist/page/index.d.mts +2 -2
  345. package/dist/{parse-CqyZj3Hp.mjs → parse-GmjecGct.mjs} +2 -2
  346. package/dist/{parse-CqyZj3Hp.mjs.map → parse-GmjecGct.mjs.map} +1 -1
  347. package/dist/{passkey-config-C0YfSBko.mjs → passkey-config-De0ahnRg.mjs} +1 -1
  348. package/dist/{passkey-config-C0YfSBko.mjs.map → passkey-config-De0ahnRg.mjs.map} +1 -1
  349. package/dist/{placeholder-Cuce9U-m.d.mts → placeholder-BSKwzRe4.d.mts} +1 -1
  350. package/dist/{placeholder-Cuce9U-m.d.mts.map → placeholder-BSKwzRe4.d.mts.map} +1 -1
  351. package/dist/plugin-types.d.mts +1 -1
  352. package/dist/plugin-utils.d.mts +10 -10
  353. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  354. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  355. package/dist/{portable-text-BICg8bTk.mjs → portable-text-cCa0cGV6.mjs} +1 -1
  356. package/dist/{portable-text-BICg8bTk.mjs.map → portable-text-cCa0cGV6.mjs.map} +1 -1
  357. package/dist/{preview-DKGCt2_p.mjs → preview-SSXNVPAC.mjs} +2 -2
  358. package/dist/{preview-DKGCt2_p.mjs.map → preview-SSXNVPAC.mjs.map} +1 -1
  359. package/dist/{public-url-CTVqgMmg.mjs → public-url-Danv47cS.mjs} +1 -1
  360. package/dist/{public-url-CTVqgMmg.mjs.map → public-url-Danv47cS.mjs.map} +1 -1
  361. package/dist/{query-B0M-9LYR.mjs → query-1Cp6U9oo.mjs} +14 -14
  362. package/dist/{query-B0M-9LYR.mjs.map → query-1Cp6U9oo.mjs.map} +1 -1
  363. package/dist/{rate-limit-BwoUdIpC.mjs → rate-limit-C_Lf63Dd.mjs} +2 -2
  364. package/dist/{rate-limit-BwoUdIpC.mjs.map → rate-limit-C_Lf63Dd.mjs.map} +1 -1
  365. package/dist/{redirect-CS-PHtNh.mjs → redirect-CoW3G-0U.mjs} +1 -1
  366. package/dist/{redirect-CS-PHtNh.mjs.map → redirect-CoW3G-0U.mjs.map} +1 -1
  367. package/dist/{redirect-lMOzG04S.mjs → redirect-IyfNirgS.mjs} +2 -2
  368. package/dist/{redirect-lMOzG04S.mjs.map → redirect-IyfNirgS.mjs.map} +1 -1
  369. package/dist/{redirects-9218MmYl.mjs → redirects-Bs1g42Ri.mjs} +4 -4
  370. package/dist/{redirects-9218MmYl.mjs.map → redirects-Bs1g42Ri.mjs.map} +1 -1
  371. package/dist/{redirects-Dcbuisoj.mjs → redirects-DXEXBAII.mjs} +2 -2
  372. package/dist/{redirects-Dcbuisoj.mjs.map → redirects-DXEXBAII.mjs.map} +1 -1
  373. package/dist/{registry-DP-iEtdR.mjs → registry-B57vp_X9.mjs} +122 -79
  374. package/dist/registry-B57vp_X9.mjs.map +1 -0
  375. package/dist/{request-meta-CmS1tDFf.mjs → request-meta-DbvXaAvU.mjs} +2 -2
  376. package/dist/{request-meta-CmS1tDFf.mjs.map → request-meta-DbvXaAvU.mjs.map} +1 -1
  377. package/dist/{resolve-C7I0qiR0.mjs → resolve-DR3wqMTy.mjs} +1 -1
  378. package/dist/{resolve-C7I0qiR0.mjs.map → resolve-DR3wqMTy.mjs.map} +1 -1
  379. package/dist/{runner-BbR3DfrL.d.mts → runner-DvFWoz8-.d.mts} +2 -2
  380. package/dist/{runner-BbR3DfrL.d.mts.map → runner-DvFWoz8-.d.mts.map} +1 -1
  381. package/dist/{runner-CFngB4WZ.mjs → runner-K8XK101B.mjs} +309 -190
  382. package/dist/runner-K8XK101B.mjs.map +1 -0
  383. package/dist/runtime.d.mts +11 -11
  384. package/dist/runtime.mjs +2 -2
  385. package/dist/{scheduled-publish-BMljtY5G.d.mts → scheduled-publish-Drffrf2a.d.mts} +1 -1
  386. package/dist/scheduled-publish-Drffrf2a.d.mts.map +1 -0
  387. package/dist/{schema-BfJ7bIXH.mjs → schema-CX9cfNkZ.mjs} +5 -5
  388. package/dist/{schema-BfJ7bIXH.mjs.map → schema-CX9cfNkZ.mjs.map} +1 -1
  389. package/dist/{search-C7t3PH1J.mjs → search-DZN2HZ-V.mjs} +4 -4
  390. package/dist/{search-C7t3PH1J.mjs.map → search-DZN2HZ-V.mjs.map} +1 -1
  391. package/dist/{secrets-BSf9pRRY.mjs → secrets-kp760LlD.mjs} +2 -2
  392. package/dist/{secrets-BSf9pRRY.mjs.map → secrets-kp760LlD.mjs.map} +1 -1
  393. package/dist/{sections-Dm0CVX23.mjs → sections-CpF5SaGr.mjs} +3 -3
  394. package/dist/{sections-Dm0CVX23.mjs.map → sections-CpF5SaGr.mjs.map} +1 -1
  395. package/dist/seed/index.d.mts +2 -2
  396. package/dist/seed/index.mjs +19 -18
  397. package/dist/seo/index.d.mts +1 -1
  398. package/dist/seo/index.mjs +1 -1
  399. package/dist/{seo-BkhuuaaE.mjs → seo-89-53DDs.mjs} +1 -1
  400. package/dist/{seo-BkhuuaaE.mjs.map → seo-89-53DDs.mjs.map} +1 -1
  401. package/dist/{seo-Dd3tbY_k.mjs → seo-DXu2UQnC.mjs} +3 -3
  402. package/dist/{seo-Dd3tbY_k.mjs.map → seo-DXu2UQnC.mjs.map} +1 -1
  403. package/dist/{service-CTrqp_Jy.mjs → service-DQmiBMzt.mjs} +3 -3
  404. package/dist/{service-CTrqp_Jy.mjs.map → service-DQmiBMzt.mjs.map} +1 -1
  405. package/dist/{session-user-B8aLtKAH.mjs → session-user-Cascbax7.mjs} +1 -1
  406. package/dist/{session-user-B8aLtKAH.mjs.map → session-user-Cascbax7.mjs.map} +1 -1
  407. package/dist/{settings-CZ7ZP9cW.mjs → settings-rZyHEcNT.mjs} +3 -3
  408. package/dist/{settings-CZ7ZP9cW.mjs.map → settings-rZyHEcNT.mjs.map} +1 -1
  409. package/dist/{settings-BX8cWBus.mjs → settings-zhmdKyJQ.mjs} +5 -5
  410. package/dist/{settings-BX8cWBus.mjs.map → settings-zhmdKyJQ.mjs.map} +1 -1
  411. package/dist/{setup-complete-gEiySUc-.mjs → setup-complete-Cb3LRyzy.mjs} +1 -1
  412. package/dist/{setup-complete-gEiySUc-.mjs.map → setup-complete-Cb3LRyzy.mjs.map} +1 -1
  413. package/dist/{setup-nonce-DzS50zme.mjs → setup-nonce-Z8x0tHf8.mjs} +1 -1
  414. package/dist/{setup-nonce-DzS50zme.mjs.map → setup-nonce-Z8x0tHf8.mjs.map} +1 -1
  415. package/dist/{single-flight-cache-Cdfkic3t.mjs → single-flight-cache-BG3B_GhN.mjs} +1 -1
  416. package/dist/{single-flight-cache-Cdfkic3t.mjs.map → single-flight-cache-BG3B_GhN.mjs.map} +1 -1
  417. package/dist/{site-url-BLebyON8.mjs → site-url-BBfDTgZB.mjs} +1 -1
  418. package/dist/{site-url-BLebyON8.mjs.map → site-url-BBfDTgZB.mjs.map} +1 -1
  419. package/dist/{ssrf-BRKb343l.mjs → ssrf-1g7usGBg.mjs} +1 -1
  420. package/dist/{ssrf-BRKb343l.mjs.map → ssrf-1g7usGBg.mjs.map} +1 -1
  421. package/dist/{status-vUK0SA17.mjs → status-CqJQX1QN.mjs} +1 -1
  422. package/dist/{status-vUK0SA17.mjs.map → status-CqJQX1QN.mjs.map} +1 -1
  423. package/dist/storage/local.d.mts +1 -1
  424. package/dist/storage/local.mjs +1 -1
  425. package/dist/storage/s3.d.mts +1 -1
  426. package/dist/storage/s3.mjs +1 -1
  427. package/dist/{taxonomies-DChKcVxf.mjs → taxonomies-Bs13SN-F.mjs} +4 -4
  428. package/dist/{taxonomies-DChKcVxf.mjs.map → taxonomies-Bs13SN-F.mjs.map} +1 -1
  429. package/dist/{taxonomies-ckPjz-6d.mjs → taxonomies-kH9ABDUF.mjs} +7 -7
  430. package/dist/{taxonomies-ckPjz-6d.mjs.map → taxonomies-kH9ABDUF.mjs.map} +1 -1
  431. package/dist/{taxonomy-Cru-he6s.mjs → taxonomy-BxGxZZdu.mjs} +4 -4
  432. package/dist/{taxonomy-Cru-he6s.mjs.map → taxonomy-BxGxZZdu.mjs.map} +1 -1
  433. package/dist/{tokens-LeuXF9gG.mjs → tokens-BGT0TUUo.mjs} +1 -1
  434. package/dist/{tokens-LeuXF9gG.mjs.map → tokens-BGT0TUUo.mjs.map} +1 -1
  435. package/dist/{transport-Blrl2k_o.d.mts → transport-BMDWHSgZ.d.mts} +1 -1
  436. package/dist/{transport-Blrl2k_o.d.mts.map → transport-BMDWHSgZ.d.mts.map} +1 -1
  437. package/dist/{transport-_2nBz7e9.mjs → transport-DPefPMcz.mjs} +1 -1
  438. package/dist/{transport-_2nBz7e9.mjs.map → transport-DPefPMcz.mjs.map} +1 -1
  439. package/dist/{trusted-proxy-DZY5WCn2.mjs → trusted-proxy-VTPRiX2s.mjs} +1 -1
  440. package/dist/{trusted-proxy-DZY5WCn2.mjs.map → trusted-proxy-VTPRiX2s.mjs.map} +1 -1
  441. package/dist/{types-ETmO_jQr.d.mts → types-BcLUUbbR.d.mts} +2 -2
  442. package/dist/{types-ETmO_jQr.d.mts.map → types-BcLUUbbR.d.mts.map} +1 -1
  443. package/dist/{types-BYnDBVwe.d.mts → types-CGbYowGQ.d.mts} +2 -2
  444. package/dist/{types-BYnDBVwe.d.mts.map → types-CGbYowGQ.d.mts.map} +1 -1
  445. package/dist/{types-CvuKO5Pn.d.mts → types-CgvhU-hF.d.mts} +1 -1
  446. package/dist/{types-CvuKO5Pn.d.mts.map → types-CgvhU-hF.d.mts.map} +1 -1
  447. package/dist/{types-Y09-wtyU.d.mts → types-ClUkdbWb.d.mts} +1 -1
  448. package/dist/{types-Y09-wtyU.d.mts.map → types-ClUkdbWb.d.mts.map} +1 -1
  449. package/dist/{types-DU4TSFS5.mjs → types-CuW0zWDA.mjs} +2 -2
  450. package/dist/{types-DU4TSFS5.mjs.map → types-CuW0zWDA.mjs.map} +1 -1
  451. package/dist/{types-BkZ8DUEI.d.mts → types-DmHk7vAt.d.mts} +1 -1
  452. package/dist/{types-BkZ8DUEI.d.mts.map → types-DmHk7vAt.d.mts.map} +1 -1
  453. package/dist/{types-CQAugunJ.mjs → types-Jo3UYd6b.mjs} +1 -1
  454. package/dist/{types-CQAugunJ.mjs.map → types-Jo3UYd6b.mjs.map} +1 -1
  455. package/dist/{types-xIfVRNLp.d.mts → types-_HFLRzTn.d.mts} +22 -1
  456. package/dist/types-_HFLRzTn.d.mts.map +1 -0
  457. package/dist/{types-ByChcBgE.d.mts → types-ea9sAPck.d.mts} +1 -1
  458. package/dist/{types-ByChcBgE.d.mts.map → types-ea9sAPck.d.mts.map} +1 -1
  459. package/dist/{types-CNlaBFzx.d.mts → types-g_aFIdW0.d.mts} +1 -1
  460. package/dist/{types-CNlaBFzx.d.mts.map → types-g_aFIdW0.d.mts.map} +1 -1
  461. package/dist/{types-Dbqff978.d.mts → types-vgEWF18A.d.mts} +19 -1
  462. package/dist/{types-Dbqff978.d.mts.map → types-vgEWF18A.d.mts.map} +1 -1
  463. package/dist/{user-DgGbKL9s.mjs → user-BjAOvU3Z.mjs} +3 -3
  464. package/dist/{user-DgGbKL9s.mjs.map → user-BjAOvU3Z.mjs.map} +1 -1
  465. package/dist/{utils-B7A57fm9.mjs → utils-BJXElxdw.mjs} +156 -7
  466. package/dist/utils-BJXElxdw.mjs.map +1 -0
  467. package/dist/{validate-CRkcYZAR.mjs → validate-CWSnKVIJ.mjs} +2 -2
  468. package/dist/{validate-CRkcYZAR.mjs.map → validate-CWSnKVIJ.mjs.map} +1 -1
  469. package/dist/{validate-9ECmtEpJ.d.mts → validate-Z1b86_3B.d.mts} +5 -5
  470. package/dist/{validate-9ECmtEpJ.d.mts.map → validate-Z1b86_3B.d.mts.map} +1 -1
  471. package/dist/{validation-sR26SOwD.mjs → validation-0l8-RYj4.mjs} +8 -5
  472. package/dist/{validation-sR26SOwD.mjs.map → validation-0l8-RYj4.mjs.map} +1 -1
  473. package/dist/version-qmeO1XZY.mjs +7 -0
  474. package/dist/{version-CrqhKGWn.mjs.map → version-qmeO1XZY.mjs.map} +1 -1
  475. package/dist/{widgets-DRgtn7Cl.mjs → widgets-hWcIicX8.mjs} +3 -3
  476. package/dist/{widgets-DRgtn7Cl.mjs.map → widgets-hWcIicX8.mjs.map} +1 -1
  477. package/dist/wxr-taxonomies-0_uxSrmf.d.mts +34 -0
  478. package/dist/wxr-taxonomies-0_uxSrmf.d.mts.map +1 -0
  479. package/dist/wxr-taxonomies-CNFcsNCr.mjs +425 -0
  480. package/dist/wxr-taxonomies-CNFcsNCr.mjs.map +1 -0
  481. package/dist/{zod-generator-C5JaV--i.mjs → zod-generator-CacrR-i8.mjs} +2 -2
  482. package/dist/{zod-generator-C5JaV--i.mjs.map → zod-generator-CacrR-i8.mjs.map} +1 -1
  483. package/package.json +7 -7
  484. package/src/api/handlers/content.ts +10 -7
  485. package/src/api/schemas/import.ts +19 -0
  486. package/src/astro/integration/runtime.ts +3 -6
  487. package/src/astro/middleware/auth.ts +9 -3
  488. package/src/astro/middleware/csp.ts +54 -3
  489. package/src/astro/routes/admin.astro +9 -0
  490. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +122 -98
  491. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +794 -44
  492. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +17 -15
  493. package/src/components/EmDashImage.astro +59 -23
  494. package/src/components/Image.astro +36 -35
  495. package/src/database/migrations/049_taxonomies_name_locale_index.ts +42 -0
  496. package/src/database/migrations/050_media_usage_index_status.ts +142 -0
  497. package/src/database/migrations/runner.ts +4 -0
  498. package/src/database/repositories/media-usage.ts +955 -59
  499. package/src/database/types.ts +22 -0
  500. package/src/emdash-runtime.ts +123 -3
  501. package/src/import/comments.ts +159 -0
  502. package/src/import/index.ts +8 -1
  503. package/src/import/settings.ts +65 -189
  504. package/src/import/sources/wordpress-plugin.ts +338 -42
  505. package/src/import/sources/wxr.ts +4 -1
  506. package/src/import/types.ts +12 -0
  507. package/src/import/utils.ts +207 -0
  508. package/src/import/wxr-taxonomies.ts +39 -0
  509. package/src/loader.ts +13 -2
  510. package/src/media/usage/content-fields.ts +119 -0
  511. package/src/media/usage/content-refresh.ts +524 -0
  512. package/src/media/usage/content-repair.ts +675 -0
  513. package/src/media/usage/content-snapshots.ts +439 -0
  514. package/src/media/usage/extractor.ts +4 -4
  515. package/src/media/usage/source-key.ts +22 -0
  516. package/src/menus/index.ts +17 -1
  517. package/src/object-cache/index.ts +82 -7
  518. package/src/plugins/context.ts +100 -71
  519. package/src/schema/registry.ts +240 -154
  520. package/src/seed/apply.ts +156 -99
  521. package/dist/api-80kycR9o.mjs.map +0 -1
  522. package/dist/apply-DSypk6Iu.mjs.map +0 -1
  523. package/dist/byline-fields-eyJMblSr.mjs.map +0 -1
  524. package/dist/context-CzUU_WzL.mjs.map +0 -1
  525. package/dist/import-Bb1T9WJS.mjs.map +0 -1
  526. package/dist/index-DGIjmUXQ.d.mts.map +0 -1
  527. package/dist/loader-4K37qA-y.mjs.map +0 -1
  528. package/dist/object-cache-CHbHv83-.mjs.map +0 -1
  529. package/dist/registry-DP-iEtdR.mjs.map +0 -1
  530. package/dist/runner-CFngB4WZ.mjs.map +0 -1
  531. package/dist/scheduled-publish-BMljtY5G.d.mts.map +0 -1
  532. package/dist/types-xIfVRNLp.d.mts.map +0 -1
  533. package/dist/utils-B7A57fm9.mjs.map +0 -1
  534. package/dist/version-CrqhKGWn.mjs +0 -7
  535. /package/dist/{api-tokens-B4BQybOp.mjs → api-tokens-DsvvCtdh.mjs} +0 -0
  536. /package/dist/{ssrf-CcX9zvMK.mjs → ssrf-CmuGgCq3.mjs} +0 -0
  537. /package/dist/{types-BvbeGEtr.mjs → types-DROWRxYV.mjs} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","virtualPlugins","virtualCreateDialect","virtualCreateCoalescingDialect","virtualCreateStorage","virtualCreateScheduler","virtualSandboxedPlugins","virtualMediaProviders","createRequestScopedDb","virtualCreateRequestScopedDb"],"sources":["../../src/cleanup.ts","../../src/comments/moderator.ts","../../src/scheduled-publish.ts","../../src/emdash-runtime.ts","../../src/media/url.ts","../../src/astro/middleware/scoped-db.ts","../../src/astro/middleware/stream-end-metrics.ts","../../src/astro/prefetch.ts","../../src/astro/public-plugin-api-routes.ts","../../src/astro/middleware.ts"],"sourcesContent":["/**\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 }>`\n\t\tSELECT collection, entry_id\n\t\tFROM revisions\n\t\tGROUP BY collection, entry_id\n\t\tHAVING COUNT(*) > ${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 * Scheduled publishing sweep\n *\n * Promotes content whose scheduled publish time has passed. Driven by the\n * platform scheduler alongside cron ticks and system cleanup — never by a\n * request. On Node the cron scheduler's maintenance pass calls it; on\n * Cloudflare the Worker's `scheduled()` handler does.\n *\n * Like `runSystemCleanup`, each collection sweep is independent and non-fatal:\n * one collection failing must not stop the rest.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { handleContentPublish } from \"./api/handlers/content.js\";\nimport { ContentRepository } from \"./database/repositories/content.js\";\nimport type { Database } from \"./database/types.js\";\nimport { SchemaRegistry } from \"./schema/registry.js\";\n\n/** A content item that was promoted to published by a sweep. */\nexport interface PublishedRef {\n\tcollection: string;\n\tid: string;\n}\n\n/**\n * Default cap on items promoted per collection in a single sweep. Bounds the\n * publish/webhook fan-out of one tick so a large backlog can't exhaust a Worker\n * invocation's CPU/subrequest budget; the remainder drains on later ticks.\n */\nexport const SCHEDULED_PUBLISH_BATCH_LIMIT = 100;\n\n/**\n * Publishes a single content item. Mirrors the relevant subset of\n * `handleContentPublish`'s return shape. Production callers pass\n * `EmDashRuntime.handleContentPublish` so `content:afterPublish` hooks fire\n * (search indexing, webhooks, syndication); the default falls back to the raw\n * handler (no hooks) for callers that have only a `db`.\n */\nexport type ScheduledPublishFn = (\n\tcollection: string,\n\tid: string,\n\toptions: { publishedAt?: string; requireScheduledDue?: boolean },\n) => Promise<{ success: boolean; error?: { code?: string } }>;\n\nexport interface PublishDueContentOptions {\n\t/**\n\t * Publish callback. Production callers pass the runtime's\n\t * `handleContentPublish` so `content:afterPublish` hooks fire (search\n\t * indexing, webhooks, syndication). Defaults to the raw DB handler (no hooks).\n\t */\n\tpublish?: ScheduledPublishFn;\n\t/**\n\t * Invoked after each collection's batch with the items promoted in that\n\t * batch. Lets request-less callers (the Cloudflare `scheduled()` handler)\n\t * purge edge-cache tags incrementally instead of only after the whole sweep,\n\t * so a runtime killed mid-sweep strands at most one batch behind stale cache\n\t * rather than everything published so far. Failures are logged, never fatal.\n\t */\n\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t/**\n\t * Maximum items promoted per collection per sweep. Defaults to\n\t * `SCHEDULED_PUBLISH_BATCH_LIMIT`. Pass `0` (or a negative) for unbounded.\n\t */\n\tlimit?: number;\n}\n\n/**\n * Publish every content item whose `scheduled_at` is in the past.\n *\n * Iterates all collections, finds due items (`findReadyToPublish` returns both\n * scheduled drafts and published entries with pending scheduled changes), and\n * publishes each. `publish()` clears `scheduled_at`, so a second sweep is a\n * no-op — safe to run on every tick.\n *\n * Bounded per collection by `limit` (default `SCHEDULED_PUBLISH_BATCH_LIMIT`):\n * a large backlog drains across successive ticks rather than in one unbounded\n * pass. After each collection's batch, `onPublished` (if given) is awaited so\n * cache-tag invalidation happens incrementally, not just at the very end.\n *\n * Returns every item it promoted so request-less callers (the Cloudflare\n * `scheduled()` handler) can also act on the full set.\n */\nexport async function publishDueContent(\n\tdb: Kysely<Database>,\n\toptions: PublishDueContentOptions = {},\n): Promise<PublishedRef[]> {\n\tconst { publish, onPublished, limit = SCHEDULED_PUBLISH_BATCH_LIMIT } = options;\n\tconst published: PublishedRef[] = [];\n\n\tlet collections;\n\ttry {\n\t\tcollections = await new SchemaRegistry(db).listCollections();\n\t} catch (error) {\n\t\tconsole.error(\"[scheduled-publish] Failed to list collections:\", error);\n\t\treturn published;\n\t}\n\n\tconst repo = new ContentRepository(db);\n\tconst doPublish: ScheduledPublishFn =\n\t\tpublish ?? ((collection, id, opts) => handleContentPublish(db, collection, id, opts));\n\t// 0 / negative means unbounded; findReadyToPublish treats that as \"no LIMIT\".\n\tconst batchLimit = limit > 0 ? limit : undefined;\n\n\tfor (const collection of collections) {\n\t\ttry {\n\t\t\tconst due = await repo.findReadyToPublish(collection.slug, batchLimit);\n\t\t\tconst batch: PublishedRef[] = [];\n\t\t\tfor (const item of due) {\n\t\t\t\t// First publication of a scheduled draft should record the intended\n\t\t\t\t// scheduled time, not the (later) sweep time. Items already published\n\t\t\t\t// with pending draft changes keep their original published_at.\n\t\t\t\tconst publishedAt = item.publishedAt == null ? (item.scheduledAt ?? undefined) : undefined;\n\t\t\t\tconst result = await doPublish(collection.slug, item.id, {\n\t\t\t\t\tpublishedAt,\n\t\t\t\t\trequireScheduledDue: true,\n\t\t\t\t});\n\t\t\t\tif (result.success) {\n\t\t\t\t\tbatch.push({ collection: collection.slug, id: item.id });\n\t\t\t\t} else if (result.error?.code === \"NOT_DUE\") {\n\t\t\t\t\t// Unscheduled or rescheduled between selection and publish — the\n\t\t\t\t\t// editor changed their mind; skip quietly, not a failure.\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[scheduled-publish] Failed to publish ${collection.slug}/${item.id}:`,\n\t\t\t\t\t\tresult.error,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (batch.length > 0) {\n\t\t\t\tpublished.push(...batch);\n\t\t\t\tif (onPublished) {\n\t\t\t\t\t// Purge this batch's cache tags before moving to the next\n\t\t\t\t\t// collection, so a mid-sweep kill can't strand already-published\n\t\t\t\t\t// content behind stale cache.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait onPublished(batch);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[scheduled-publish] onPublished failed after \"${collection.slug}\" batch:`,\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(`[scheduled-publish] Sweep failed for \"${collection.slug}\":`, error);\n\t\t}\n\t}\n\n\treturn published;\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\";\nimport virtualConfig from \"virtual:emdash/config\";\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 { getTrustedProxyHeaders } from \"./auth/trusted-proxy.js\";\nimport { isSqlite } from \"./database/dialect-helpers.js\";\nimport { kyselyLogOption } from \"./database/instrumentation.js\";\nimport { MIGRATION_RACE_WAIT_MS, runMigrations } from \"./database/migrations/runner.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type {\n\tContentItem as ContentItemInternal,\n\tContentDateField,\n} from \"./database/repositories/types.js\";\nimport { validateIdentifier } from \"./database/validate.js\";\nimport { normalizeMediaValue } from \"./media/normalize.js\";\nimport type { MediaProvider, MediaProviderCapabilities } from \"./media/types.js\";\nimport type { SandboxedPluginInstance, 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\tPortableTextBlockConfig,\n\tFieldWidgetConfig,\n} from \"./plugins/types.js\";\nimport type { FieldType } from \"./schema/types.js\";\nimport { hashString } from \"./utils/hash.js\";\nimport { createInitLock, type InitLock, initWithLock } from \"./utils/init-lock.js\";\nimport { createSingleFlightCache, singleFlightCached } from \"./utils/single-flight-cache.js\";\nimport { COMMIT, VERSION } from \"./version.js\";\n\nconst LEADING_SLASH_PATTERN = /^\\//;\n\n/**\n * Parse a JSON column expected to contain an array of strings.\n *\n * Throws on malformed JSON rather than returning []; callers are responsible\n * for deciding how to handle/log the error. Empty string / null inputs return\n * [] (they represent \"no value\"). Non-string array entries are filtered out.\n */\nfunction parseStringArray(raw: string | null | undefined): string[] {\n\tif (!raw) return [];\n\tconst parsed: unknown = JSON.parse(raw);\n\tif (!Array.isArray(parsed)) return [];\n\treturn parsed.filter((v): v is string => typeof v === \"string\");\n}\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\"nlweb\",\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 { after } from \"./after.js\";\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 { validateEncryptionKeyAtStartup } from \"./config/secrets.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport {\n\thandleContentList,\n\thandleContentAuthors,\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 type { CronScheduler } from \"./plugins/scheduler/types.js\";\nimport { PluginStateRepository } from \"./plugins/state.js\";\nimport { normalizeRegistryConfig } from \"./registry/config.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { publishDueContent, type PublishedRef } from \"./scheduled-publish.js\";\nimport { FTSManager } from \"./search/fts-manager.js\";\nimport { invalidateSiteSettingsCache } from \"./settings/index.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\turl: \"url\",\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\trepeater: \"repeater\",\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/** Portable Text block types contributed to the editor (declarative Block Kit) */\n\tportableTextBlocks?: PortableTextBlockConfig[];\n\t/** Field widget types contributed for schema-field editing UIs */\n\tfieldWidgets?: FieldWidgetConfig[];\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\t/**\n\t * Resolver for the live connection, preferred over `db` by providers that\n\t * query EmDash's database. Resolves the current request/event-scoped\n\t * connection from ALS so connection-backed adapters (Postgres over\n\t * Hyperdrive) don't reuse the per-isolate singleton's socket across events.\n\t * Providers should resolve per operation rather than capturing `db` once.\n\t * Omitted-safe: falls back to `db` for stateless adapters (D1, Node SQLite).\n\t */\n\tgetDb?: () => Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * Builds the timer-based scheduler that drives cron ticks and maintenance.\n * Injected via `virtual:emdash/scheduler` so the platform — not core — decides\n * whether a long-lived heartbeat exists.\n */\nexport type CreateSchedulerFn = (executor: CronExecutor) => CronScheduler;\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/**\n\t * Factory for a dialect that batches same-turn reads into one round trip\n\t * ({@link EmDashRuntime.create} uses it for the cold-start read phase).\n\t * Present only on batching backends (D1, DO); absent backends fall back to\n\t * the singleton. Returns a fresh connection each call — it must never be the\n\t * long-lived singleton, whose coalescing buffer would be shared across\n\t * requests.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateCoalescingDialect?: (config: any) => Dialect | null;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateStorage: ((config: any) => Storage) | null;\n\tsandboxEnabled: boolean;\n\t/** sandbox: false escape hatch - load sandboxed plugins in-process */\n\tsandboxBypassed?: boolean;\n\t/**\n\t * Factory for the timer-based cron/maintenance heartbeat. Supplied by the\n\t * generated `virtual:emdash/scheduler` module: a `NodeCronScheduler` factory\n\t * on long-lived runtimes (Node/Bun), or `null` on serverless adapters where\n\t * an external driver (e.g. the Cloudflare Worker's `scheduled()` Cron\n\t * Trigger) calls `runScheduledTasks()` instead. When absent or null, the\n\t * runtime starts no scheduler. Keeping the platform decision in the\n\t * integration means core has no adapter-specific runtime checks.\n\t */\n\tcreateScheduler?: CreateSchedulerFn | null;\n\t/** Media provider entries from virtual module */\n\tmediaProviderEntries?: MediaProviderEntry[];\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\t/** Factory function matching SandboxRunnerFactory signature */\n\tcreateSandboxRunner:\n\t\t| ((opts: {\n\t\t\t\tdb: Kysely<Database>;\n\t\t\t\tmediaStorage?: {\n\t\t\t\t\tupload(options: { key: string; body: Uint8Array; contentType: string }): Promise<unknown>;\n\t\t\t\t\tdelete(key: string): Promise<unknown>;\n\t\t\t\t};\n\t\t }) => SandboxRunner)\n\t\t| null;\n}\n\n/**\n * Constructor parameters for `EmDashRuntime`.\n *\n * Production code should use `EmDashRuntime.create()` which discovers and\n * loads all parts (database, plugins, hooks, cron, etc.) and then calls the\n * constructor. Direct construction is supported for callers that already\n * have all the dependencies in hand — for example, integration tests that\n * supply a pre-migrated database and an empty plugin set.\n *\n * Every field corresponds 1:1 to internal state set on the runtime — none of\n * these are derived. If you don't have a value for one, see what `create()`\n * passes for that field as the canonical default.\n */\nexport interface EmDashRuntimeParts {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n\tconfiguredPlugins: ResolvedPlugin[];\n\tsandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\thooks: HookPipeline;\n\tenabledPlugins: Set<string>;\n\tpluginStates: Map<string, string>;\n\tconfig: EmDashConfig;\n\tmediaProviders: Map<string, MediaProvider>;\n\tmediaProviderEntries: MediaProviderEntry[];\n\tcronExecutor: CronExecutor | null;\n\tcronScheduler: CronScheduler | null;\n\temailPipeline: EmailPipeline | null;\n\tallPipelinePlugins: ResolvedPlugin[];\n\tpipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tgetDb?: () => Kysely<Database>;\n\t\tstorage?: Storage;\n\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t};\n\truntimeDeps: RuntimeDependencies;\n\tpipelineRef: { current: HookPipeline };\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/**\n * Db init lock reclaim deadline. Derived from the migration race wait so\n * they can't drift apart: a healthy init can legitimately block for the\n * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold\n * connect and migrator work, before it should be presumed dead. The outer\n * runtime init lock (middleware.ts) must use a strictly larger deadline —\n * it wraps create() → getDatabase() → this lock, and equal deadlines would\n * let the outer reclaim while the inner is legitimately still working.\n */\nexport const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;\n\n/**\n * Db cache + its init lock live on globalThis behind a Symbol: the bundler\n * can duplicate this module across SSR chunks (same reasoning as\n * request-cache.ts), and a duplicated cache/lock would mean concurrent\n * independent db inits — and duplicate migrators — per isolate.\n */\nconst DB_HOLDER_KEY = Symbol.for(\"emdash:db-cache\");\ninterface DbHolder {\n\tcache: Map<string, Kysely<Database>>;\n\tlock: InitLock;\n}\nconst globalSymbolStore = globalThis as Record<symbol, unknown>;\nfunction getDbHolder(): DbHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;\n\tif (!holder) {\n\t\tholder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };\n\t\tglobalSymbolStore[DB_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\n\n/**\n * Auto-seed runs at most once per isolate per database. Its lock + \"done\" set\n * live on globalThis (same bundler-duplication reasoning as the db cache) so a\n * reclaimed-and-rerun `create()` can't seed a second time concurrently. The\n * lock polls rather than sharing a promise, so it is safe to await across a\n * cancelled owner in workerd.\n */\nconst SEED_HOLDER_KEY = Symbol.for(\"emdash:seed-state\");\ninterface SeedHolder {\n\tdone: Set<string>;\n\tlock: InitLock;\n}\nfunction getSeedHolder(): SeedHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[SEED_HOLDER_KEY] as SeedHolder | undefined;\n\tif (!holder) {\n\t\tholder = { done: new Set<string>(), lock: createInitLock() };\n\t\tglobalSymbolStore[SEED_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\nconst storageCache = new Map<string, Storage>();\nconst sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();\n/**\n * Per-tier sets of `${pluginId}:${version}` keys present in\n * `sandboxedPluginCache`. Used during sync to know which entries belong\n * to which install source so we can invalidate only what belongs to the\n * tier currently being synced.\n */\nconst marketplacePluginKeys = new Set<string>();\nconst registryPluginKeys = new Set<string>();\n/**\n * Manifest metadata for runtime-installed sandboxed plugins (marketplace\n * and registry both). Keyed by `pluginId`; readers don't care which\n * source the plugin came from. Named `marketplace*` for legacy reasons.\n */\nconst marketplaceManifestCache = new Map<\n\tstring,\n\t{\n\t\tid: string;\n\t\tversion: string;\n\t\tadmin?: {\n\t\t\tpages?: PluginAdminPage[];\n\t\t\twidgets?: PluginDashboardWidget[];\n\t\t};\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, SandboxedPluginInstance>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\t/**\n\t * Schema registry bound to the current request/event-scoped connection.\n\t * Built per access (SchemaRegistry just wraps a db) against `this.db`, the\n\t * ALS-aware getter — never a captured snapshot of the singleton. On a\n\t * connection-backed adapter (Postgres over Hyperdrive) a captured singleton\n\t * would query a socket opened by an earlier event and trip workerd's\n\t * cross-request I/O guard; the catch in handlers like handleContentUpdate\n\t * would then silently treat a revision-enabled collection as non-revisioned\n\t * and write draft edits to live columns. Same reasoning as the per-call\n\t * registry in _buildManifest().\n\t */\n\tget schemaRegistry(): SchemaRegistry {\n\t\treturn new SchemaRegistry(this.db);\n\t}\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/**\n\t * Isolate-lifetime guard so FTS indexes are verified at most once per\n\t * worker rather than on every admin request. See ensureSearchHealthy().\n\t * Uses the poison-immune single-flight cache (never a shared awaitable\n\t * promise) so a cancelled first caller can't wedge later ones.\n\t */\n\tprivate readonly _searchHealthCache = createSingleFlightCache<void>();\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\tgetDb?: () => 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/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\tconstructor(parts: EmDashRuntimeParts) {\n\t\tthis._db = parts.db;\n\t\tthis.storage = parts.storage;\n\t\tthis.configuredPlugins = parts.configuredPlugins;\n\t\tthis.sandboxedPlugins = parts.sandboxedPlugins;\n\t\tthis.sandboxedPluginEntries = parts.sandboxedPluginEntries;\n\t\tthis._hooks = parts.hooks;\n\t\tthis.enabledPlugins = parts.enabledPlugins;\n\t\tthis.pluginStates = parts.pluginStates;\n\t\tthis.config = parts.config;\n\t\tthis.mediaProviders = parts.mediaProviders;\n\t\tthis.mediaProviderEntries = parts.mediaProviderEntries;\n\t\tthis.cronExecutor = parts.cronExecutor;\n\t\tthis.cronScheduler = parts.cronScheduler;\n\t\tthis.email = parts.emailPipeline;\n\t\tthis.allPipelinePlugins = parts.allPipelinePlugins;\n\t\tthis.pipelineFactoryOptions = parts.pipelineFactoryOptions;\n\t\tthis.runtimeDeps = parts.runtimeDeps;\n\t\tthis.pipelineRef = parts.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 * Whether the sandbox bypass mode (sandbox: false) is active.\n\t * Marketplace install/update handlers use this to skip the\n\t * SANDBOX_NOT_AVAILABLE gate, since the bypass path loads\n\t * marketplace plugins in-process via syncMarketplacePlugins().\n\t */\n\tisSandboxBypassed(): boolean {\n\t\treturn this.runtimeDeps.sandboxBypassed === true;\n\t}\n\n\t/**\n\t * Publish any content whose scheduled time has passed.\n\t * Returns the items promoted so callers can invalidate their cache tags.\n\t */\n\tasync publishScheduled(): Promise<PublishedRef[]> {\n\t\treturn publishDueContent(this.db, {\n\t\t\tpublish: (collection, id, options) => this.handleContentPublish(collection, id, options),\n\t\t});\n\t}\n\n\t/**\n\t * Run the full scheduled-maintenance batch: cron tasks, scheduled\n\t * publishing, and system cleanup. For request-less drivers — the\n\t * Cloudflare `scheduled()` handler invokes this from a Cron Trigger.\n\t * (On Node the timer-based scheduler drives the same work itself.)\n\t *\n\t * Each step is independent and non-fatal. Returns the content promoted\n\t * by the publishing sweep so the caller can purge edge-cache tags.\n\t *\n\t * `onPublished` (optional) is awaited after each collection's batch so a\n\t * request-less driver can invalidate edge-cache tags incrementally rather\n\t * than only after the whole sweep — bounding stale-cache exposure if the\n\t * runtime is killed mid-sweep.\n\t */\n\tasync runScheduledTasks(\n\t\toptions: {\n\t\t\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t\t} = {},\n\t): Promise<{ published: PublishedRef[] }> {\n\t\tif (this.cronExecutor) {\n\t\t\ttry {\n\t\t\t\tawait this.cronExecutor.tick();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Tick failed:\", error);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.cronExecutor.recoverStaleLocks();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Stale lock recovery failed:\", error);\n\t\t\t}\n\t\t}\n\n\t\tlet published: PublishedRef[] = [];\n\t\ttry {\n\t\t\t// Route through the runtime wrapper so content:afterPublish hooks fire.\n\t\t\tpublished = await publishDueContent(this.db, {\n\t\t\t\tpublish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),\n\t\t\t\tonPublished: options.onPublished,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t}\n\n\t\ttry {\n\t\t\tawait runSystemCleanup(this.db, this.storage ?? undefined);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t}\n\n\t\treturn { published };\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\tawait this.rebuildHookPipeline();\n\t\t\tawait this._hooks.runPluginActivate(pluginId);\n\t\t} else {\n\t\t\t// Fire deactivate on the current pipeline while the plugin is still in it\n\t\t\tawait this._hooks.runPluginDeactivate(pluginId);\n\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\tawait this.rebuildHookPipeline();\n\t\t}\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\t// db/getDb are already wired by createHookPipeline above (they live in\n\t\t\t// pipelineFactoryOptions), so the merge only adds emailPipeline.\n\t\t\tnewPipeline.setContextFactory({ 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) return;\n\n\t\t// In sandbox bypass mode (sandbox: false), the noop runner reports\n\t\t// unavailable but we still want admin metadata for newly installed\n\t\t// marketplace plugins to refresh in-process. Hooks/routes still won't\n\t\t// execute (matches the cold-start bypass behavior), but Configure\n\t\t// links and admin pages appear immediately.\n\t\tif (this.runtimeDeps.sandboxBypassed) {\n\t\t\tawait this.syncMarketplacePluginsBypassed();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.syncSandboxedSourcePlugins(\"marketplace\");\n\t}\n\n\t/**\n\t * Synchronize registry plugin runtime state with DB + storage.\n\t *\n\t * Mirrors {@link syncMarketplacePlugins} for plugins installed via the\n\t * experimental decentralized plugin registry. Called after install,\n\t * update, and uninstall handlers complete.\n\t */\n\tasync syncRegistryPlugins(): Promise<void> {\n\t\tif (!this.config.experimental?.registry) return;\n\t\tawait this.syncSandboxedSourcePlugins(\"registry\");\n\t}\n\n\t/**\n\t * Internal: reconcile in-memory sandboxed-plugin state with the\n\t * `_plugin_state` table for the given source tier. Shared\n\t * implementation behind {@link syncMarketplacePlugins} and\n\t * {@link syncRegistryPlugins}.\n\t *\n\t * Each source tier has its own key set in `${source}PluginKeys` so a\n\t * sync for one tier doesn't invalidate the other.\n\t */\n\tprivate async syncSandboxedSourcePlugins(source: \"marketplace\" | \"registry\"): Promise<void> {\n\t\tif (!this.storage) return;\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) return;\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst states =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of states) {\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\t// Marketplace plugins use `marketplaceVersion` when present;\n\t\t\t\t// registry plugins always use `version`.\n\t\t\t\tconst desiredVersion =\n\t\t\t\t\tsource === \"marketplace\" ? (state.marketplaceVersion ?? state.version) : state.version;\n\t\t\t\tdesired.set(state.pluginId, desiredVersion);\n\t\t\t}\n\n\t\t\t// Remove uninstalled or no-longer-active plugins from memory.\n\t\t\tconst keysToRemove: string[] = [];\n\t\t\tfor (const key of keySet) {\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\tkeySet.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 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\tkeySet.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, source);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: ${source} 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\tkeySet.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 ${source} plugins:`, error);\n\t\t}\n\t}\n\n\t/**\n\t * Remove a plugin from the in-memory pipeline lists by ID.\n\t * Mutates allPipelinePlugins and configuredPlugins in place.\n\t */\n\tprivate removePluginFromLists(pluginId: string): void {\n\t\tconst allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId);\n\t\tif (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1);\n\t\tconst configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId);\n\t\tif (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1);\n\t}\n\n\t/**\n\t * Sync marketplace plugin metadata in sandbox: false bypass mode.\n\t *\n\t * In bypass mode the noop runner can't load plugins, but admin pages,\n\t * widgets, and route metadata still need to refresh in-process when an\n\t * admin installs/updates/uninstalls a marketplace plugin. Otherwise the\n\t * admin UI shows stale data until the server restarts.\n\t *\n\t * Hooks and routes still won't execute under bypass (matches the\n\t * cold-start bypass behavior in loadMarketplacePluginsBypassed).\n\t *\n\t * Known limitation: bypass plugins are loaded via `import(dataUrl)`,\n\t * which Node's ESM cache keys on the full URL. Updates create fresh\n\t * module objects, but old ones remain cached for the worker's lifetime.\n\t * In practice this is a few KB per update — only matters for sites with\n\t * very frequent marketplace updates running long-lived processes. The\n\t * fix would be vm.SourceTextModule for explicit lifecycle management.\n\t */\n\tprivate async syncMarketplacePluginsBypassed(): Promise<void> {\n\t\tif (!this.storage) return;\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// Drop metadata for plugins no longer active.\n\t\t\tconst toRemove: string[] = [];\n\t\t\tfor (const pluginId of marketplaceManifestCache.keys()) {\n\t\t\t\tif (!desired.has(pluginId)) toRemove.push(pluginId);\n\t\t\t}\n\t\t\tfor (const pluginId of toRemove) {\n\t\t\t\t// Fire plugin:deactivate hook before removal\n\t\t\t\tconst resolved = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (resolved) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst deactivateHook = resolved.hooks?.[\"plugin:deactivate\"];\n\t\t\t\t\t\tif (deactivateHook) {\n\t\t\t\t\t\t\tconst handler =\n\t\t\t\t\t\t\t\ttypeof deactivateHook === \"function\" ? deactivateHook : deactivateHook.handler;\n\t\t\t\t\t\t\tif (typeof handler === \"function\") {\n\t\t\t\t\t\t\t\t// Sandbox-bypass cleanup: the plugin context isn't constructable\n\t\t\t\t\t\t\t\t// here (no DB binding, no media, etc.), but well-behaved\n\t\t\t\t\t\t\t\t// deactivate hooks should be no-op safe. If a hook does require\n\t\t\t\t\t\t\t\t// ctx, it throws and the surrounding catch logs it.\n\t\t\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- best-effort cleanup; see comment above\n\t\t\t\t\t\t\t\tawait handler({ pluginId }, {} as never);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.warn(`[emdash] plugin:deactivate hook failed for ${pluginId}:`, err);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t// Remove from pipeline lists too (mutate in place since the\n\t\t\t\t// arrays are readonly references but mutable contents)\n\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t}\n\n\t\t\t// Load plugin code, adapt as trusted plugins, and add to pipeline lists\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\t\tconst newPlugins: ResolvedPlugin[] = [];\n\t\t\tfor (const [pluginId, version] of desired) {\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\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\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\n\t\t\t\t// Skip if already in the pipeline at this version\n\t\t\t\tconst existing = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (existing && existing.version === bundle.manifest.version) continue;\n\n\t\t\t\t// Remove any older version\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// Dynamic data: import returns `any` from a base64-encoded module.\n\t\t\t\t\t// We trust the bundle to be shaped like a plugin (built by plugin-cli);\n\t\t\t\t\t// adaptSandboxEntry then validates fields it cares about.\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\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\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tnewPlugins.push(adapted);\n\t\t\t\t\tthis.allPipelinePlugins.push(adapted);\n\t\t\t\t\tthis.configuredPlugins.push(adapted);\n\t\t\t\t\tthis.enabledPlugins.add(adapted.id);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${pluginId}@${version} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If anything changed, rebuild the hook pipeline so new/removed\n\t\t\t// plugins take effect immediately without a server restart.\n\t\t\tif (toRemove.length > 0 || newPlugins.length > 0) {\n\t\t\t\tawait this.rebuildHookPipeline();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash: Failed to sync marketplace plugins (bypass):\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create and initialize the runtime\n\t */\n\tstatic async create(\n\t\tdeps: RuntimeDependencies,\n\t\ttimings?: Array<{ name: string; dur: number; desc?: string }>,\n\t): Promise<EmDashRuntime> {\n\t\t// Helper: time a phase and push into the shared timings array when\n\t\t// provided. Uses performance.now() — monotonic across async boundaries.\n\t\t// No-op when `timings` wasn't passed (preserves backwards compatibility\n\t\t// with callers that don't care about per-phase breakdown).\n\t\tconst phase = async <T>(name: string, desc: string, fn: () => Promise<T>): Promise<T> => {\n\t\t\tif (!timings) return fn();\n\t\t\tconst t0 = performance.now();\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\ttimings.push({ name, dur: performance.now() - t0, desc });\n\t\t\t}\n\t\t};\n\n\t\t// Initialize database (connects, runs migrations if needed)\n\t\tconst db = await phase(\"rt.db\", \"DB init + migrations\", () => EmDashRuntime.getDatabase(deps));\n\n\t\t// Resolver for the live connection, mirroring the `get db()` getter\n\t\t// below (which can't be used here — the runtime instance doesn't exist\n\t\t// yet). Long-lived subsystems built during create() (cron executor,\n\t\t// plugin context factory, media providers) capture this resolver rather\n\t\t// than the `db` snapshot, so a connection-backed adapter (Postgres over\n\t\t// Hyperdrive) serves their queries from the current request/event-scoped\n\t\t// connection in ALS instead of the per-isolate singleton — whose socket\n\t\t// belongs to an earlier request and would trip workerd's cross-request\n\t\t// I/O guard. Stateless adapters (D1, Node SQLite) set no ALS db on most\n\t\t// paths, so this falls back to the singleton: unchanged behavior.\n\t\tconst resolveDb = (): Kysely<Database> => {\n\t\t\tconst ctx = getRequestContext();\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- ALS db is typed unknown to avoid a circular import; middleware always sets a Kysely<Database>\n\t\t\treturn (ctx?.db as Kysely<Database> | undefined) ?? db;\n\t\t};\n\n\t\t// Validate EMDASH_ENCRYPTION_KEY once here so a malformed value\n\t\t// surfaces in startup logs instead of as request-time 500s. The key\n\t\t// itself is not yet consumed (a follow-up PR adds plugin-secret\n\t\t// encryption); validating early just guards against silent\n\t\t// misconfiguration.\n\t\tawait phase(\"rt.secrets\", \"Validate encryption key\", () => validateEncryptionKeyAtStartup());\n\n\t\t// FTS verify/repair is deferred off the cold-start hot path.\n\t\t// See EmDashRuntime.ensureSearchHealthy().\n\n\t\t// Initialize storage (sync)\n\t\tconst storage = EmDashRuntime.getStorage(deps);\n\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\t// \"Already set up\" by default so a read failure (e.g. tables absent on a\n\t\t// pre-migration db) skips seeding rather than seeding a half-built db.\n\t\tlet seedGate = { collectionCount: 1, setupDone: true };\n\n\t\t// Seeding must only touch the configured singleton, never a borrowed\n\t\t// per-request db (playground / DO preview) or the loader-fallback db.\n\t\tconst reqCtx = getRequestContext();\n\t\tconst ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);\n\n\t\t// Run the init reads on a coalescing connection so the concurrent\n\t\t// same-turn reads flush as one batch() round trip. The singleton can't:\n\t\t// its plain SqliteAdapter reports supportsMultipleConnections=false, so\n\t\t// Kysely's connection mutex serializes concurrent reads into N round\n\t\t// trips. The connection is per-init and discarded — the long-lived\n\t\t// singleton must never coalesce or one request's reads could land in\n\t\t// another's batch. Backends without a coalescing dialect (Node/SQLite)\n\t\t// fall back to the singleton, where serialization costs nothing.\n\t\tlet readDb = db;\n\t\tlet readDbDisposable: Kysely<Database> | undefined;\n\t\tif (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) {\n\t\t\ttry {\n\t\t\t\tconst dialect = deps.createCoalescingDialect(deps.config.database.config);\n\t\t\t\tif (dialect) {\n\t\t\t\t\treadDb = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\t\t\t\t\treadDbDisposable = readDb;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treadDb = db;\n\t\t\t}\n\t\t}\n\t\tconst optionsRepo = new OptionsRepository(readDb);\n\n\t\tconst readSiteInfo = async () => {\n\t\t\tconst siteOpts = await optionsRepo.getMany<string>([\n\t\t\t\t\"emdash:site_title\",\n\t\t\t\t\"emdash:site_url\",\n\t\t\t\t\"emdash:locale\",\n\t\t\t]);\n\t\t\treturn {\n\t\t\t\tsiteName: siteOpts.get(\"emdash:site_title\") ?? undefined,\n\t\t\t\tsiteUrl: siteOpts.get(\"emdash:site_url\") ?? undefined,\n\t\t\t\tlocale: siteOpts.get(\"emdash:locale\") ?? undefined,\n\t\t\t};\n\t\t};\n\n\t\tconst coldStartReads: Array<Promise<void>> = [\n\t\t\tphase(\"rt.plugins\", \"Plugin states\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst states = await readDb\n\t\t\t\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t\t\t\t.select([\"plugin_id\", \"status\"])\n\t\t\t\t\t\t.execute();\n\t\t\t\t\tpluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));\n\t\t\t\t} catch {\n\t\t\t\t\t// _plugin_state may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t\tphase(\"rt.site\", \"Site info options\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch {\n\t\t\t\t\t// options may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t];\n\n\t\tif (ownsConfiguredDb) {\n\t\t\tcoldStartReads.push(\n\t\t\t\tphase(\"rt.seedcheck\", \"Auto-seed gate\", async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\t\t\treadDb\n\t\t\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\t\t\treadDb\n\t\t\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn !!setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})();\n\t\t\t\t\t\tseedGate = { collectionCount: collectionCount.count, setupDone };\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Leave the \"already set up\" default so a read failure never\n\t\t\t\t\t\t// triggers a seed onto a half-built db.\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tawait Promise.all(coldStartReads);\n\n\t\t// Auto-seed the default schema for a first load that skipped the setup\n\t\t// wizard (the wizard and dev-bypass apply seeds explicitly). Run under a\n\t\t// per-isolate lock keyed by the configured db so a reclaimed-and-rerun\n\t\t// create() can't apply the seed a second time concurrently.\n\t\tif (seedGate.collectionCount === 0 && !seedGate.setupDone) {\n\t\t\tconst seedKey = deps.config.database?.entrypoint ?? \"default\";\n\t\t\tconst seedHolder = getSeedHolder();\n\t\t\ttry {\n\t\t\t\tawait initWithLock(\n\t\t\t\t\tseedHolder.lock,\n\t\t\t\t\t() => (seedHolder.done.has(seedKey) ? true : undefined),\n\t\t\t\t\tasync () => {\n\t\t\t\t\t\tconst { applySeed } = await import(\"./seed/apply.js\");\n\t\t\t\t\t\tconst { loadSeed } = await import(\"./seed/load.js\");\n\t\t\t\t\t\tconst { validateSeed } = await import(\"./seed/validate.js\");\n\n\t\t\t\t\t\tconst seed = await loadSeed();\n\t\t\t\t\t\tconst validation = validateSeed(seed);\n\t\t\t\t\t\tif (validation.valid) {\n\t\t\t\t\t\t\tawait applySeed(db, seed, { onConflict: \"skip\" });\n\t\t\t\t\t\t\tconsole.log(\"Auto-seeded default collections\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tseedHolder.done.add(seedKey);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\t\t\t\t\t{ deadlineMs: DB_INIT_DEADLINE_MS, anchor: (promise) => after(() => promise) },\n\t\t\t\t);\n\t\t\t\t// The site-info read ran before the seed wrote its defaults, so\n\t\t\t\t// refresh the snapshot the plugin context sees.\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch {\n\t\t\t\t\t// Non-fatal — plugin context falls back to undefined fields.\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — a failed seed (e.g. missing seed module) leaves the\n\t\t\t\t// site un-seeded; a later request retries.\n\t\t\t}\n\t\t}\n\n\t\t// The read connection is single-use; everything below uses the singleton.\n\t\tif (readDbDisposable) {\n\t\t\ttry {\n\t\t\t\tawait readDbDisposable.destroy();\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — the underlying binding is shared and needs no teardown.\n\t\t\t}\n\t\t}\n\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// 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// Collected bypassed plugins (sandbox: false escape hatch).\n\t\t// These need to be added to BOTH the pipeline (for hooks) AND the\n\t\t// configuredPlugins list (for route dispatch).\n\t\tconst bypassedPluginsList: ResolvedPlugin[] = [];\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: [\"hooks.email-transport:register\"],\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: [\"users:read\"],\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// sandbox: false escape hatch - load sandboxed plugin entries in-process\n\t\t// as trusted plugins (no isolation) so they participate in the hook pipeline.\n\t\t// Block this on Cloudflare Workers where dynamic import(dataUrl) is not\n\t\t// available and running untrusted code in-process is a security risk.\n\t\tif (deps.sandboxBypassed && deps.sandboxedPluginEntries.length > 0) {\n\t\t\tconst isCfWorkers =\n\t\t\t\ttypeof navigator !== \"undefined\" &&\n\t\t\t\ttypeof navigator.userAgent === \"string\" &&\n\t\t\t\tnavigator.userAgent.includes(\"Cloudflare-Workers\");\n\t\t\tif (isCfWorkers) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"sandbox: false is not supported in Cloudflare Workers. \" +\n\t\t\t\t\t\t\"Remove the sandbox: false option or use the Cloudflare sandbox runner.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Sandboxed plugins will run in-process without isolation.\",\n\t\t\t);\n\t\t\tconst bypassedPlugins = await EmDashRuntime.loadBypassedPlugins(deps.sandboxedPluginEntries);\n\t\t\tfor (const plugin of bypassedPlugins) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\t// Respect plugin state: only enable if active or no record exists.\n\t\t\t\t// Plugins an admin previously disabled should stay disabled.\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// In bypass mode, also load marketplace plugins from R2 as trusted\n\t\t// in-process plugins BEFORE pipeline creation. They need to be in the\n\t\t// pipeline to participate in hook dispatch.\n\t\tif (deps.sandboxBypassed && deps.config.marketplace && storage) {\n\t\t\tconst marketplaceBypassed = await EmDashRuntime.loadMarketplacePluginsBypassed(db, storage);\n\t\t\tfor (const plugin of marketplaceBypassed) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\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. getDb travels here (not just via the email\n\t\t// setContextFactory call below) so it survives rebuildHookPipeline(),\n\t\t// which reconstructs the factory from pipelineFactoryOptions. Without it,\n\t\t// toggling a plugin on an email-less deployment would silently revert\n\t\t// plugin contexts to the singleton db — re-breaking connection-backed\n\t\t// adapters. See #1622.\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tgetDb: resolveDb,\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, sandbox runner path)\n\t\tconst sandboxedPlugins = await phase(\"rt.sandbox\", \"Sandboxed plugins\", () =>\n\t\t\tEmDashRuntime.loadSandboxedPlugins(deps, db, storage),\n\t\t);\n\n\t\t// Cold-start: load marketplace- and registry-installed plugins from\n\t\t// site R2 via the sandbox runner. The two tiers only depend on the\n\t\t// sandbox phase above, not on each other, so when both are enabled\n\t\t// they run concurrently instead of paying two sequential loads.\n\t\t// In bypass mode marketplace plugins were already handled above.\n\t\tconst installedTierPhases: Promise<void>[] = [];\n\t\tif (deps.config.marketplace && storage && !deps.sandboxBypassed) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.market\", \"Marketplace plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"marketplace\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPlugins,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Cold-start: load registry-installed plugins from site R2\n\t\tif (deps.config.experimental?.registry && storage) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.registry\", \"Registry plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"registry\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPlugins,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (installedTierPhases.length > 0) {\n\t\t\tawait Promise.all(installedTierPhases);\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, getDb: resolveDb };\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 phase(\"rt.hooks\", \"Exclusive hook resolution\", () =>\n\t\t\tEmDashRuntime.resolveExclusiveHooks(pipeline, db, deps),\n\t\t);\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\t// db/getDb were already set via pipelineFactoryOptions above; merge only\n\t\t// adds emailPipeline.\n\t\tpipeline.setContextFactory({ emailPipeline });\n\n\t\tlet cronExecutor: CronExecutor | null = null;\n\t\tlet cronScheduler: CronScheduler | null = null;\n\t\t// Populated with the constructed runtime just before this method returns,\n\t\t// so the timer scheduler's cleanup can route scheduled publishing through\n\t\t// the runtime wrapper (firing content:afterPublish hooks). The first tick\n\t\t// is ≥1s out, well after the synchronous assignment below.\n\t\tconst runtimeRef: { current: EmDashRuntime | null } = { current: null };\n\n\t\tawait phase(\"rt.cron\", \"Cron init (recovery deferred post-response)\", async () => {\n\t\t\ttry {\n\t\t\t\tcronExecutor = new CronExecutor(resolveDb, invokeCronHook);\n\n\t\t\t\t// Recover stale locks from previous crashes. Pure bookkeeping\n\t\t\t\t// against the _emdash_cron_tasks table — no request needs the\n\t\t\t\t// result — so we defer it past the response via after(). On\n\t\t\t\t// Cloudflare this goes into waitUntil (extending the worker\n\t\t\t\t// lifetime); on Node it's fire-and-forget (the process stays\n\t\t\t\t// up anyway). Saves one cold-start write per D1 isolate.\n\t\t\t\tconst executorForRecovery = cronExecutor;\n\t\t\t\tafter(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst recovered = await executorForRecovery.recoverStaleLocks();\n\t\t\t\t\t\tif (recovered > 0) {\n\t\t\t\t\t\t\tconsole.log(`[cron] Recovered ${recovered} stale task lock(s)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Keep the `[cron]` prefix so a failure is easy to trace back\n\t\t\t\t\t\t// rather than surfacing as a generic deferred-task error.\n\t\t\t\t\t\tconsole.error(\"[cron] Failed to recover stale task locks:\", error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\t// The platform decides whether a long-lived timer heartbeat exists.\n\t\t\t\t// `createScheduler` is injected by the generated virtual:emdash/scheduler\n\t\t\t\t// module: a NodeCronScheduler factory on Node/Bun, or null on serverless\n\t\t\t\t// adapters (e.g. Cloudflare) where the Worker's `scheduled()` handler\n\t\t\t\t// drives runScheduledTasks() instead. No adapter check lives here.\n\t\t\t\tif (deps.createScheduler) {\n\t\t\t\t\tconst scheduler = deps.createScheduler(cronExecutor);\n\t\t\t\t\tcronScheduler = scheduler;\n\n\t\t\t\t\t// Run scheduled publishing and system cleanup alongside each tick.\n\t\t\t\t\t// Pass storage so cleanupPendingUploads can delete orphaned files.\n\t\t\t\t\tscheduler.setSystemCleanup(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Route through the runtime so content:afterPublish hooks fire.\n\t\t\t\t\t\t\t// Falls back to the raw handler if (improbably) the tick beats\n\t\t\t\t\t\t\t// the post-construction ref assignment.\n\t\t\t\t\t\t\tconst runtime = runtimeRef.current;\n\t\t\t\t\t\t\tawait publishDueContent(db, {\n\t\t\t\t\t\t\t\tpublish: runtime\n\t\t\t\t\t\t\t\t\t? (collection, id, options) =>\n\t\t\t\t\t\t\t\t\t\t\truntime.handleContentPublish(collection, id, options)\n\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait runSystemCleanup(db, storage ?? undefined);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t// Non-fatal -- individual cleanup failures are already logged\n\t\t\t\t\t\t\t// by runSystemCleanup. This catches unexpected errors.\n\t\t\t\t\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\t// Add cron reschedule callback (merges with existing factory options)\n\t\t\t\t\tpipeline.setContextFactory({\n\t\t\t\t\t\tcronReschedule: () => cronScheduler?.reschedule(),\n\t\t\t\t\t});\n\n\t\t\t\t\t// start() is void on the timer scheduler but the interface\n\t\t\t\t\t// allows a promise (alarm-backed schedulers); we don't block on it.\n\t\t\t\t\tvoid scheduler.start();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[cron] Failed to initialize cron system:\", error);\n\t\t\t\t// Non-fatal — CMS works without cron\n\t\t\t}\n\t\t});\n\n\t\tconst runtime = new EmDashRuntime({\n\t\t\tdb,\n\t\t\tstorage,\n\t\t\t// Include bypassed sandboxed plugins in configuredPlugins so route\n\t\t\t// dispatch can find them under sandbox: false (they're treated as\n\t\t\t// trusted plugins for the duration of the bypass).\n\t\t\tconfiguredPlugins: [...deps.plugins, ...bypassedPluginsList],\n\t\t\tsandboxedPlugins,\n\t\t\tsandboxedPluginEntries: deps.sandboxedPluginEntries,\n\t\t\thooks: pipeline,\n\t\t\tenabledPlugins,\n\t\t\tpluginStates,\n\t\t\tconfig: deps.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\truntimeDeps: deps,\n\t\t\tpipelineRef,\n\t\t});\n\t\t// Hand the constructed instance to the scheduler-cleanup closure so the\n\t\t// timer-driven sweep can fire publish hooks (see runtimeRef above).\n\t\truntimeRef.current = runtime;\n\t\treturn runtime;\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// Only use the per-request `ctx.db` when it's an isolated instance\n\t\t// (playground / DO preview). Plain D1 Sessions set `ctx.db` on every\n\t\t// anonymous request — if we captured one of those session-bound\n\t\t// Kyselys into the cached runtime, every request would accidentally\n\t\t// share one request's session. The configured `deps.createDialect`\n\t\t// path gives us a fresh singleton instead.\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.dbIsIsolated && ctx.db) {\n\t\t\t// eslint-disable-next-line typescript/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// Waiters poll the cache rather than sharing the initializing request's\n\t\t// promise: if the request that owns the init is cancelled mid-await\n\t\t// (e.g. client disconnect during cold migrations), a shared promise\n\t\t// never settles — and the owner's `finally` that would clear it never\n\t\t// runs — deadlocking every later request in the isolate. Prevention:\n\t\t// the in-flight init is anchored via after()/waitUntil so a cancelled\n\t\t// owner's init still completes and populates the cache. Net: a stale\n\t\t// lock is reclaimed after a deadline.\n\t\tconst holder = getDbHolder();\n\t\treturn initWithLock(\n\t\t\tholder.lock,\n\t\t\t() => holder.cache.get(cacheKey),\n\t\t\tasync (isCurrentClaim) => {\n\t\t\t\tconst dialect = deps.createDialect(dbConfig.config);\n\t\t\t\tconst db = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\n\t\t\t\tawait runMigrations(db);\n\n\t\t\t\t// Note: legacy installs may carry a stray `emdash:manifest_cache`\n\t\t\t\t// row in the options table from versions that persisted a JSON\n\t\t\t\t// manifest. The runtime no longer reads or writes it. We do not\n\t\t\t\t// proactively delete it: the row is a few hundred bytes of dead\n\t\t\t\t// weight and is never on the read path, whereas a one-shot\n\t\t\t\t// cleanup-flag check costs an extra `options.get()` on every\n\t\t\t\t// isolate cold boot forever. Cheaper to leave it.\n\n\t\t\t\t// This returns a migrated but possibly unseeded db; create() runs\n\t\t\t\t// the seed gate and applies the seed, batched with its other init\n\t\t\t\t// reads.\n\n\t\t\t\t// Publish only while still the current owner: a reclaimed slow\n\t\t\t\t// init must not flip the cached Kysely identity back after the\n\t\t\t\t// reclaimer has published its own. The unpublished instance is\n\t\t\t\t// still returned and fully valid for the request that built it.\n\t\t\t\tif (isCurrentClaim()) {\n\t\t\t\t\tholder.cache.set(cacheKey, db);\n\t\t\t\t}\n\t\t\t\treturn db;\n\t\t\t},\n\t\t\t{\n\t\t\t\tdeadlineMs: DB_INIT_DEADLINE_MS,\n\t\t\t\tanchor: (promise) => after(() => promise),\n\t\t\t},\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 plugin entries as trusted in-process plugins.\n\t * Used by the sandbox: false debugging escape hatch.\n\t *\n\t * Imports each plugin's bundled ESM code via a data URL, adapts it\n\t * with adaptSandboxEntry, and returns ResolvedPlugin objects ready\n\t * to be merged into the pipeline plugin list.\n\t */\n\tprivate static async loadBypassedPlugins(\n\t\tentries: SandboxedPluginEntry[],\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\tconst plugins: ResolvedPlugin[] = [];\n\t\tfor (const entry of entries) {\n\t\t\ttry {\n\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(entry.code).toString(\"base64\")}`;\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<string, unknown>;\n\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t>[0];\n\t\t\t\t// PluginDescriptor.storage's TypeScript type is narrower than what\n\t\t\t\t// adaptSandboxEntry actually accepts at runtime — it copies indexes\n\t\t\t\t// through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// (string[][]). Pass the raw entry.storage with a structural cast\n\t\t\t\t// to preserve composite index declarations.\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// Preserve admin metadata so plugin-management APIs can derive\n\t\t\t\t// hasAdminPages / hasDashboardWidgets correctly. Without this,\n\t\t\t\t// the admin UI hides Configure links and dashboard widgets for\n\t\t\t\t// bypassed plugins even though they declared them.\n\t\t\t\t// SandboxedPluginEntry uses looser types than PluginDescriptor\n\t\t\t\t// (label?, size: string), so coerce to the descriptor shape.\n\t\t\t\tconst adminPages = entry.adminPages?.map((p) => ({\n\t\t\t\t\tpath: p.path,\n\t\t\t\t\tlabel: p.label ?? p.path,\n\t\t\t\t\ticon: p.icon,\n\t\t\t\t}));\n\t\t\t\tconst adminWidgets:\n\t\t\t\t\t| Array<{\n\t\t\t\t\t\t\tid: string;\n\t\t\t\t\t\t\ttitle?: string;\n\t\t\t\t\t\t\tsize?: \"full\" | \"half\" | \"third\";\n\t\t\t\t\t }>\n\t\t\t\t\t| undefined = entry.adminWidgets?.map((w) => {\n\t\t\t\t\tconst size: \"full\" | \"half\" | \"third\" | undefined =\n\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined;\n\t\t\t\t\treturn { id: w.id, title: w.title, size };\n\t\t\t\t});\n\t\t\t\tconst resolved = adaptSandboxEntry(pluginDef, {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tentrypoint: \"\",\n\t\t\t\t\tcapabilities: entry.capabilities,\n\t\t\t\t\tallowedHosts: entry.allowedHosts,\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\tstorage: entry.storage as never,\n\t\t\t\t\tadminPages,\n\t\t\t\t\tadminWidgets,\n\t\t\t\t\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\t\tfieldWidgets: entry.fieldWidgets,\n\t\t\t\t});\n\t\t\t\tplugins.push(resolved);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded plugin ${entry.id}:${entry.version} in-process (sandbox bypassed)`,\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} in-process:`, error);\n\t\t\t}\n\t\t}\n\t\treturn plugins;\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\tmediaStorage?: Storage | null,\n\t): Promise<Map<string, SandboxedPluginInstance>> {\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) {\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({\n\t\t\t\tdb,\n\t\t\t\tmediaStorage: mediaStorage\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\t\t\tmediaStorage.upload({\n\t\t\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tdelete: (key) => mediaStorage.delete(key),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t});\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\t// Warn regardless of whether there are plugins to load, so operators\n\t\t// see the issue even if no marketplace plugins are installed yet.\n\t\tif (!sandboxRunner.isAvailable()) {\n\t\t\tconsole.warn(\n\t\t\t\t\"EmDash: Plugin sandbox is configured but not available on this platform. \" +\n\t\t\t\t\t\"Sandboxed plugins will not be loaded. \" +\n\t\t\t\t\t\"If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.\",\n\t\t\t);\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\tif (deps.sandboxedPluginEntries.length === 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// sandbox: false escape hatch is handled separately (before pipeline\n\t\t// creation) via loadBypassedPlugins. If we somehow reach here with the\n\t\t// flag set, just return — the plugins are already in the trusted pipeline.\n\t\tif (deps.sandboxBypassed) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Load each sandboxed plugin via sandbox runner\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\t/**\n\t * Cold-start load of all active sandboxed plugins for one install\n\t * tier (marketplace or registry) from site-local R2.\n\t *\n\t * Mirrors {@link syncSandboxedSourcePlugins} but runs once at runtime\n\t * creation, before request traffic arrives; the sync method runs on\n\t * demand after install / update / uninstall handlers.\n\t */\n\tprivate static async loadInstalledSandboxedPlugins(\n\t\tsource: \"marketplace\" | \"registry\",\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t\tdeps: RuntimeDependencies,\n\t\tcache: Map<string, SandboxedPluginInstance>,\n\t): Promise<void> {\n\t\t// Ensure sandbox runner exists with media storage wired up.\n\t\t// (storage here is the media Storage adapter from the runtime.)\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({\n\t\t\t\tdb,\n\t\t\t\tmediaStorage: {\n\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\tstorage.upload({\n\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t}),\n\t\t\t\t\tdelete: (key) => storage.delete(key),\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t\t// In sandbox bypass mode, marketplace plugins are loaded in-process\n\t\t// BEFORE pipeline creation by EmDashRuntime.create(). Skip here.\n\t\tif (deps.sandboxBypassed) return;\n\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst plugins =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tfor (const plugin of plugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\n\t\t\t\t// Marketplace plugins record the live version in\n\t\t\t\t// `marketplaceVersion`; registry plugins use `version` directly.\n\t\t\t\tconst version =\n\t\t\t\t\tsource === \"marketplace\" ? (plugin.marketplaceVersion ?? plugin.version) : 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, source);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(`EmDash: ${source} plugin ${plugin.pluginId}@${version} not found in R2`);\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\tkeySet.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 ${source} 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 ${source} 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 * Cold-start: load marketplace plugins in bypass mode (sandbox: false).\n\t *\n\t * Each active marketplace bundle is read, evaluated via data URL, adapted\n\t * with adaptSandboxEntry, and returned as a ResolvedPlugin. The caller is\n\t * responsible for merging these into allPipelinePlugins / configuredPlugins\n\t * BEFORE the hook pipeline is created, so hooks and routes register in\n\t * the trusted pipeline.\n\t *\n\t * Also caches manifest and route metadata so admin UI / getManifest() work.\n\t *\n\t * Returns ResolvedPlugins to be merged into the pipeline.\n\t */\n\tprivate static async loadMarketplacePluginsBypassed(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst resolved: ResolvedPlugin[] = [];\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\t\t\tif (marketplacePlugins.length === 0) return resolved;\n\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Marketplace plugins will run in-process without isolation.\",\n\t\t\t);\n\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\n\t\t\tfor (const plugin of marketplacePlugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\t\t\t\tconst version = plugin.marketplaceVersion ?? plugin.version;\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\t// Cache manifest and route metadata for admin UI and route auth\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\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\t// Evaluate the bundled ESM and adapt it as a trusted plugin\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\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\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tresolved.push(adapted);\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded marketplace plugin ${plugin.pluginId}@${version} in-process (sandbox bypassed)`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${plugin.pluginId} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet\n\t\t}\n\t\treturn resolved;\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\tgetOptions: (keys) => optionsRepo.getMany<string>(keys),\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 admin manifest from the live database.\n\t *\n\t * Used by the admin UI (sidebar collections, content editor field\n\t * dispatch, manifest endpoint) and by WordPress import — it's never\n\t * read on a public request, so this isn't on any anonymous hot path.\n\t *\n\t * No cross-request cache. The previous worker-isolate cache produced\n\t * a class of cross-isolate staleness bugs (#776, #873, #876, #877)\n\t * because Cloudflare Workers keeps multiple warm isolates per region\n\t * and there's no fan-out primitive to invalidate them in step. The\n\t * cache existed to amortize an N+1 schema query pattern; now that\n\t * `listCollectionsWithFields()` does the same work in two queries,\n\t * the rebuild is fast enough to pay on every admin request.\n\t *\n\t * Within a single request, `requestCached` deduplicates concurrent\n\t * callers (the manifest endpoint and an admin SSR template, say).\n\t */\n\tgetManifest(): Promise<EmDashManifest> {\n\t\treturn requestCached(\"emdash:manifest\", () => this._buildManifest());\n\t}\n\n\t/**\n\t * Build the manifest from the database.\n\t *\n\t * Constant query shapes via `listCollectionsWithFields()` — one query\n\t * for collections, one batched query for fields (chunked at\n\t * `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter\n\t * limit). Typical sites stay well under the chunk threshold, so this\n\t * is two queries in practice; never N+1.\n\t */\n\tprivate async _buildManifest(): 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.listCollectionsWithFields();\n\t\t\tfor (const collection of dbCollections) {\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\t// Two shapes: legacy enum-style `[{ value, label }]` for select widgets,\n\t\t\t\t\t\t// or arbitrary `Record<string, unknown>` for plugin field widgets that\n\t\t\t\t\t\t// need per-field config (e.g. a checkbox grid receiving its column defs).\n\t\t\t\t\t\toptions?: Array<{ value: string; label: string }> | Record<string, unknown>;\n\t\t\t\t\t\tid?: string;\n\t\t\t\t\t\tvalidation?: Record<string, unknown>;\n\t\t\t\t\t}\n\t\t\t\t> = {};\n\n\t\t\t\tfor (const field of collection.fields) {\n\t\t\t\t\tconst entry: (typeof fields)[string] = {\n\t\t\t\t\t\tkind: FIELD_TYPE_TO_KIND[field.type] ?? \"string\",\n\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\trequired: field.required,\n\t\t\t\t\t};\n\t\t\t\t\t// Always include the field's database ID so the admin can forward it\n\t\t\t\t\t// to upload/media-list API calls for MIME allowlist widening.\n\t\t\t\t\tentry.id = field.id;\n\t\t\t\t\tif (field.widget) entry.widget = field.widget;\n\t\t\t\t\t// Plugin field widgets read their per-field config from `field.options`,\n\t\t\t\t\t// which the seed schema types as `Record<string, unknown>`. Pass it\n\t\t\t\t\t// through to the manifest so plugin widgets in the admin SPA receive it.\n\t\t\t\t\tif (field.options) {\n\t\t\t\t\t\tentry.options = field.options;\n\t\t\t\t\t}\n\t\t\t\t\t// Legacy: select/multiSelect enum options live on `field.validation.options`.\n\t\t\t\t\t// Wins over `field.options` to preserve existing behavior for enum widgets.\n\t\t\t\t\tif (field.validation?.options) {\n\t\t\t\t\t\tentry.options = field.validation.options.map((v) => ({\n\t\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\t\t\t\t\t// Include full validation for repeater fields (subFields, minItems, maxItems)\n\t\t\t\t\t// and for file/image fields (allowedMimeTypes).\n\t\t\t\t\tif (\n\t\t\t\t\t\t(field.type === \"repeater\" || field.type === \"file\" || field.type === \"image\") &&\n\t\t\t\t\t\tfield.validation\n\t\t\t\t\t) {\n\t\t\t\t\t\tentry.validation = { ...field.validation };\n\t\t\t\t\t}\n\t\t\t\t\tfields[field.slug] = entry;\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\turlPattern: collection.urlPattern,\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\tcategory?: string;\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\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\t// `adminMode` reflects only admin pages/widgets. A plugin can\n\t\t\t\t// contribute portableTextBlocks/fieldWidgets with adminMode \"none\" —\n\t\t\t\t// the admin reads those from the manifest regardless, so don't gate\n\t\t\t\t// admin contributions on `adminMode`.\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\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\tfieldWidgets: entry.fieldWidgets,\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// Build taxonomies from database\n\t\tlet manifestTaxonomies: Array<{\n\t\t\tname: string;\n\t\t\tlabel: string;\n\t\t\tlabelSingular?: string;\n\t\t\thierarchical: boolean;\n\t\t\tcollections: string[];\n\t\t}> = [];\n\t\ttry {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.orderBy(\"name\")\n\t\t\t\t.execute();\n\t\t\tmanifestTaxonomies = rows.map((row) => ({\n\t\t\t\tname: row.name,\n\t\t\t\tlabel: row.label,\n\t\t\t\tlabelSingular: row.label_singular ?? undefined,\n\t\t\t\thierarchical: row.hierarchical === 1,\n\t\t\t\tcollections: parseStringArray(row.collections).toSorted(),\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tconsole.debug(\"EmDash: Could not load taxonomy definitions:\", error);\n\t\t}\n\n\t\t// Build manifest hash\n\t\tconst manifestHash = await hashString(\n\t\t\tJSON.stringify(manifestCollections) +\n\t\t\t\tJSON.stringify(manifestPlugins) +\n\t\t\t\tJSON.stringify(manifestTaxonomies),\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 (read from virtual module to avoid SSR module singleton mismatch)\n\t\tconst i18nConfig = virtualConfig?.i18n;\n\t\tconst i18n =\n\t\t\ti18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1\n\t\t\t\t? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }\n\t\t\t\t: undefined;\n\n\t\t// Normalize the experimental registry config for browser consumption.\n\t\t// Validation errors here surface as 500s from the manifest endpoint\n\t\t// rather than being silently dropped -- a misconfigured registry\n\t\t// should be loud, not invisible.\n\t\tconst registry = normalizeRegistryConfig(this.config.experimental?.registry) ?? undefined;\n\n\t\treturn {\n\t\t\tversion: VERSION,\n\t\t\tcommit: COMMIT,\n\t\t\tastroVersion: this.config.astroVersion,\n\t\t\thash: manifestHash,\n\t\t\tcollections: manifestCollections,\n\t\t\tplugins: manifestPlugins,\n\t\t\ttaxonomies: manifestTaxonomies,\n\t\t\tauthMode: authModeValue,\n\t\t\ti18n,\n\t\t\tmarketplace: !!this.config.marketplace,\n\t\t\tregistry,\n\t\t};\n\t}\n\n\t/**\n\t * Verify and repair FTS indexes on demand. Runs at most once per worker\n\t * lifetime.\n\t *\n\t * Originally called from `EmDashRuntime.create()`, but on a busy D1 link\n\t * (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold\n\t * start for a modest-sized site — more than every other init phase\n\t * combined. Anonymous public reads never touch the search write path,\n\t * so the cost isn't paid back for the vast majority of requests.\n\t *\n\t * Instead, search endpoints call this lazily: the first request that\n\t * actually needs the index pays the verify cost (usually fast — no\n\t * rebuild needed), everyone else runs cold-free.\n\t *\n\t * Uses the runtime's singleton database (`this._db`) rather than the\n\t * request-scoped DB. Verify reads only, but `rebuildIndex` writes, and\n\t * a GET search request on D1 carries a `first-unconstrained` session\n\t * that's free to route at a read replica — unsafe for writes. The\n\t * singleton always goes through the default binding, which the D1\n\t * adapter will promote to `first-primary` for write statements.\n\t *\n\t * Safe to call concurrently: repeated callers share the same in-flight\n\t * promise. Errors are swallowed internally so callers don't need to\n\t * defend against FTS not existing yet (pre-setup).\n\t */\n\tasync ensureSearchHealthy(): Promise<void> {\n\t\t// Non-SQLite has no FTS to verify; the check is a cheap synchronous\n\t\t// branch, no need to cache it.\n\t\tif (!isSqlite(this._db)) return;\n\t\ttry {\n\t\t\tawait singleFlightCached(\n\t\t\t\tthis._searchHealthCache,\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst ftsManager = new FTSManager(this._db);\n\t\t\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\t\t\tif (repaired > 0) {\n\t\t\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal — cache\n\t\t\t\t\t\t// the \"checked\" state regardless so we don't re-scan.\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t\t);\n\t\t} catch {\n\t\t\t// This check is best-effort and must never fail the calling request.\n\t\t\t// The inner body already swallows verify errors; this guards the\n\t\t\t// outer failure modes (owner timeout, waiter give-up) so a slow FTS\n\t\t\t// scan degrades to \"unverified\", not a 500 on admin/search routes.\n\t\t}\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\tq?: string;\n\t\t\tauthorId?: string;\n\t\t\tdateField?: ContentDateField;\n\t\t\tdateFrom?: string;\n\t\t\tdateTo?: string;\n\t\t},\n\t) {\n\t\treturn handleContentList(this.db, collection, params);\n\t}\n\n\tasync handleContentAuthors(collection: string) {\n\t\treturn handleContentAuthors(this.db, collection);\n\t}\n\n\tasync handleContentGet(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGet(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\tasync handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\t/**\n\t * If the response item has a `draftRevisionId`, replace `item.data` with\n\t * the draft revision's data and expose the original published values as\n\t * `liveData`. This makes the content_get / content_update round-trip\n\t * intuitive — read returns the latest content the caller has saved\n\t * (their pending draft), with the previously-published values still\n\t * accessible for compare-style flows.\n\t *\n\t * No-op when no draft exists or the response is an error.\n\t */\n\tprivate async hydrateDraftData<T>(result: T): Promise<T> {\n\t\tif (!result || typeof result !== \"object\") return result;\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below\n\t\tconst r = result as {\n\t\t\tsuccess?: boolean;\n\t\t\tdata?: { item?: Record<string, unknown> };\n\t\t};\n\t\tif (!r.success || !r.data?.item) return result;\n\t\tconst item = r.data.item;\n\t\tconst draftRevisionId = typeof item.draftRevisionId === \"string\" ? item.draftRevisionId : null;\n\t\tif (!draftRevisionId) return result;\n\t\ttry {\n\t\t\tconst revision = await new RevisionRepository(this.db).findById(draftRevisionId);\n\t\t\tif (!revision) return result;\n\t\t\tconst liveData =\n\t\t\t\titem.data && typeof item.data === \"object\"\n\t\t\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above\n\t\t\t\t\t\t(item.data as Record<string, unknown>)\n\t\t\t\t\t: {};\n\t\t\t// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the\n\t\t\t// revision data — those are handler-internal markers and don't\n\t\t\t// belong in the surfaced `data` field. Match syncDataColumns at\n\t\t\t// content.ts:~1119.\n\t\t\tconst revisionData: Record<string, unknown> = {};\n\t\t\tfor (const [key, value] of Object.entries(revision.data)) {\n\t\t\t\tif (!key.startsWith(\"_\")) revisionData[key] = value;\n\t\t\t}\n\t\t\tconst mergedData = { ...liveData, ...revisionData };\n\t\t\t// Return a clone rather than mutating in place. The response\n\t\t\t// object isn't retained by the runtime today, but a future\n\t\t\t// request-cache layer would observe stale-after-mutation bugs;\n\t\t\t// cloning closes that footgun.\n\t\t\t// `r.data` was narrowed to `{ item?: ... }` at the top of this\n\t\t\t// method; spread its other keys (e.g. `_rev`) alongside the\n\t\t\t// hydrated item without going back through `unknown`.\n\t\t\treturn {\n\t\t\t\t...result,\n\t\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope\n\t\t\t\tdata: {\n\t\t\t\t\t...r.data,\n\t\t\t\t\titem: { ...item, data: mergedData, liveData },\n\t\t\t\t},\n\t\t\t} as T;\n\t\t} catch (error) {\n\t\t\t// Non-fatal — fall back to the unhydrated response. Log so the\n\t\t\t// failure isn't completely silent (the response will look stale\n\t\t\t// to the caller but no error is raised).\n\t\t\tconsole.error(\"[emdash] draft hydration failed:\", error);\n\t\t\treturn result;\n\t\t}\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// Validate against the collection schema. Hook output is validated\n\t\t// rather than `body.data` so plugins that mutate field values can't\n\t\t// sneak invalid data past.\n\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\tpartial: false,\n\t\t});\n\t\tif (!validation.ok) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: validation.error,\n\t\t\t};\n\t\t}\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\tseo?: {\n\t\t\t\ttitle?: string | null;\n\t\t\t\tdescription?: string | null;\n\t\t\t\timage?: string | null;\n\t\t\t\tcanonical?: string | null;\n\t\t\t\tnoIndex?: boolean;\n\t\t\t};\n\t\t\tpublishedAt?: string | null;\n\t\t\tlocale?: string;\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, body.locale);\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\n\t\t\t// Validate field-level shape BEFORE the draft-revision write so\n\t\t\t// invalid updates can't silently land in revision history.\n\t\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\t\tpartial: true,\n\t\t\t});\n\t\t\tif (!validation.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: validation.error,\n\t\t\t\t};\n\t\t\t}\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\tvalidateIdentifier(collection, \"collection\");\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// Hydrate draft data BEFORE firing afterSave hooks so the hook sees\n\t\t// the same effective data the response surfaces — for revision-\n\t\t// supporting collections, that's the just-saved draft, not the live\n\t\t// columns.\n\t\tconst hydrated = await this.hydrateDraftData(result);\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (hydrated.success && hydrated.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);\n\t\t}\n\n\t\treturn hydrated;\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, false);\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\tconst result = await handleContentRestore(this.db, collection, id);\n\n\t\t// Run afterRestore hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentPermanentDelete(collection: string, id: string) {\n\t\tconst result = await handleContentPermanentDelete(this.db, collection, id);\n\n\t\t// Run afterDelete hooks so plugins (e.g. AI Search) can clean up\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection, true);\n\t\t}\n\n\t\treturn result;\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(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { publishedAt?: string; requireScheduledDue?: boolean } = {},\n\t) {\n\t\tconst result = await handleContentPublish(this.db, collection, id, options);\n\n\t\t// Run afterPublish hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnpublish(collection: string, id: string) {\n\t\tconst result = await handleContentUnpublish(this.db, collection, id);\n\n\t\t// Run afterUnpublish hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentSchedule(collection: string, id: string, scheduledAt: string) {\n\t\tconst result = await handleContentSchedule(this.db, collection, id, scheduledAt);\n\n\t\t// Run afterSchedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\tconst result = await handleContentUnschedule(this.db, collection, id);\n\n\t\t// Run afterUnschedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\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: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tmimeType?: string | readonly string[];\n\t\tq?: string;\n\t}) {\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\tauthorId?: 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\tconst result = await handleMediaUpdate(this.db, id, input);\n\t\t// Resolved media references in site settings (`logo`, `favicon`,\n\t\t// `seo.defaultOgImage`) bake in the media row's `contentType`,\n\t\t// `width`, and `height`. A metadata edit invalidates that snapshot\n\t\t// for every entry point: REST routes, MCP tools, plugin code, and\n\t\t// any future caller of `handleMediaUpdate`. Cross-isolate staleness\n\t\t// remains bounded by isolate lifetime.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handleMediaDelete(id: string) {\n\t\tconst result = await handleMediaDelete(this.db, id);\n\t\t// Same reasoning as `handleMediaUpdate`: if the deleted media row\n\t\t// was referenced by a setting, the cached resolved URL now points\n\t\t// at a 404. Invalidation is unconditional on success — cheaper than\n\t\t// querying which settings reference the id.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\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\t// Discover the parent entry up front so we can branch on whether\n\t\t// the collection uses draft revisions.\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);\n\t\tconst usesDraftRevisions = collectionInfo?.supports?.includes(\"revisions\") ?? false;\n\n\t\t// Non-revision collections: keep the legacy behavior of writing the\n\t\t// revision's data straight onto the live row. This preserves\n\t\t// behavior for collections that opt out of the draft model.\n\t\tif (!usesDraftRevisions) {\n\t\t\tconst result = await handleRevisionRestore(this.db, revisionId, callerUserId);\n\t\t\treturn this.hydrateDraftData(result);\n\t\t}\n\n\t\t// Revision-capable collections: restore is \"make this revision the\n\t\t// current draft\". The live row's data columns are left untouched\n\t\t// (only `draft_revision_id` and `updated_at` change). The caller\n\t\t// must then `content_publish` to promote the restored draft to\n\t\t// live, matching the documented tool contract.\n\t\ttry {\n\t\t\tconst newDraft = await revisionRepo.create({\n\t\t\t\tcollection: revision.collection,\n\t\t\t\tentryId: revision.entryId,\n\t\t\t\tdata: revision.data,\n\t\t\t\tauthorId: callerUserId,\n\t\t\t});\n\n\t\t\tvalidateIdentifier(revision.collection, \"collection\");\n\t\t\tconst tableName = `ec_${revision.collection}`;\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET draft_revision_id = ${newDraft.id},\n\t\t\t\t\tupdated_at = ${new Date().toISOString()}\n\t\t\t\tWHERE id = ${revision.entryId}\n\t\t\t`.execute(this.db);\n\n\t\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\t\tvoid revisionRepo\n\t\t\t\t.pruneOldRevisions(revision.collection, revision.entryId, 50)\n\t\t\t\t.catch(() => {});\n\n\t\t\t// Return the freshly-fetched item with the new draft hydrated\n\t\t\t// onto `data`. Without this the response would echo the live\n\t\t\t// columns and the next `content_get` would surface different\n\t\t\t// values (the bug that motivated this rewrite).\n\t\t\tconst refetched = await handleContentGet(this.db, revision.collection, revision.entryId);\n\t\t\treturn this.hydrateDraftData(refetched);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[emdash] revision restore failed:\", error);\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\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({\n\t\t\t\tdb: this.db,\n\t\t\t\tstorage: this.storage ?? undefined,\n\t\t\t\temailPipeline: this.email ?? undefined,\n\t\t\t\ttrustedProxyHeaders: getTrustedProxyHeaders(this.config),\n\t\t\t});\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): SandboxedPluginInstance | 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\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterSave\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterSave(content, collection, isNew);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterSave hook error:\", err);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sandboxed plugins\n\t\t\tconst tasks: Promise<void>[] = [];\n\t\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\t\ttasks.push(\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait plugin.invokeHook(\"content:afterSave\", { content, collection, isNew });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait Promise.allSettled(tasks);\n\t\t});\n\t}\n\n\tprivate runAfterDeleteHooks(id: string, collection: string, permanent: boolean): 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, permanent)\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, permanent })\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 runDeferredContentHook(\n\t\tname:\n\t\t\t| \"content:afterPublish\"\n\t\t\t| \"content:afterUnpublish\"\n\t\t\t| \"content:afterRestore\"\n\t\t\t| \"content:afterSchedule\"\n\t\t\t| \"content:afterUnschedule\",\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): void {\n\t\tconst label = name.slice(\"content:\".length);\n\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(name)) {\n\t\t\t\ttry {\n\t\t\t\t\tswitch (name) {\n\t\t\t\t\t\tcase \"content:afterPublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterPublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnpublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnpublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterRestore\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterRestore(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterSchedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterSchedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnschedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnschedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(`EmDash ${label} hook error:`, err);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sandboxed plugins\n\t\t\tconst tasks: Promise<void>[] = [];\n\t\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\t\ttasks.push(\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait plugin.invokeHook(name, { content, collection });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} ${label} error:`, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait Promise.allSettled(tasks);\n\t\t});\n\t}\n\n\tprivate runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterPublish\", content, collection);\n\t}\n\n\tprivate runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterUnpublish\", content, collection);\n\t}\n\n\tprivate runAfterRestoreHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterRestore\", content, collection);\n\t}\n\n\tprivate runAfterScheduleHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterSchedule\", content, collection);\n\t}\n\n\tprivate runAfterUnscheduleHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterUnschedule\", content, collection);\n\t}\n\n\tprivate async handleSandboxedRoute(\n\t\tplugin: SandboxedPluginInstance,\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, this.config);\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 * Public media URL resolution.\n *\n * Used at render time by the Image components to decide whether a storage\n * key should be served from the configured `publicUrl` (R2 custom domain,\n * S3 CDN) or through the internal `/_emdash/api/media/file/{key}` route.\n */\nimport type { Storage } from \"../storage/types.js\";\nimport { INTERNAL_MEDIA_PREFIX } from \"./normalize.js\";\n\n// Keys accepted by the public-URL rewrite: the `{ulid}{ext}` shape produced by\n// the upload pipeline, with letters, digits, dots, dashes, and underscores.\n// Slashes, `?`, `#`, and `%` are rejected so attacker-controlled content in a\n// portable-text `asset.url` cannot traverse or reroute on the CDN origin.\nconst SAFE_STORAGE_KEY = /^[A-Za-z0-9._-]+$/;\n\n/**\n * Resolve the public URL for a locally stored media key. Returns an empty\n * string when no key is given. When a storage adapter is supplied, defers to\n * `storage.getPublicUrl()`; otherwise returns the internal proxy route.\n */\nexport function resolvePublicMediaUrl(\n\tstorage: Storage | null | undefined,\n\tstorageKey: string,\n): string {\n\tif (!storageKey) return \"\";\n\tif (storage) return storage.getPublicUrl(storageKey);\n\treturn `/_emdash/api/media/file/${storageKey}`;\n}\n\n/**\n * Build the `getPublicMediaUrl` closure attached to `Astro.locals.emdash`.\n * Shared by the anonymous fast path and the full-runtime path in middleware.\n *\n * @internal\n */\nexport function createPublicMediaUrlResolver(\n\tstorage: Storage | null | undefined,\n): (key: string) => string {\n\treturn (key) => resolvePublicMediaUrl(storage, key);\n}\n\n/** Input shape for {@link buildRenderMediaUrl}. */\nexport interface RenderMediaRef {\n\t/** Storage key with extension (the canonical shape from the upload pipeline). */\n\tstorageKey?: string;\n\t/** Pre-baked URL (either an internal proxy URL or an external URL). */\n\turl?: string;\n\t/** Bare media id (ULID without extension); only the internal proxy can look this up. */\n\tid?: string;\n}\n\n/**\n * Build a render-time media URL. Prefers `storageKey`, then rewrites an\n * internal `url` via `resolve`, then falls back to the internal proxy for a\n * bare `id`. External URLs and non-matching internal-looking URLs pass\n * through untouched. Returns `\"\"` when nothing usable is present.\n *\n * @internal\n */\nexport function buildRenderMediaUrl(\n\tresolve: ((key: string) => string) | undefined,\n\tref: RenderMediaRef,\n): string {\n\tconst { storageKey, url, id } = ref;\n\tif (storageKey) {\n\t\treturn resolve ? resolve(storageKey) : `${INTERNAL_MEDIA_PREFIX}${storageKey}`;\n\t}\n\tif (url) {\n\t\tif (resolve && url.startsWith(INTERNAL_MEDIA_PREFIX)) {\n\t\t\tconst key = url.slice(INTERNAL_MEDIA_PREFIX.length);\n\t\t\tif (SAFE_STORAGE_KEY.test(key)) return resolve(key);\n\t\t}\n\t\treturn url;\n\t}\n\tif (id) return `${INTERNAL_MEDIA_PREFIX}${id}`;\n\treturn \"\";\n}\n","/**\n * Request-scoped database lifecycle helpers.\n *\n * Extracted from middleware.ts so they can be unit-tested without pulling in\n * the virtual:emdash/* module graph. The middleware imports these to settle a\n * request-scoped db adapter's lifecycle around the response.\n */\n\n/**\n * Astro attaches AstroCookies to outgoing responses via a well-known global\n * symbol. Cloning a Response (`new Response(body, init)`) drops non-header\n * metadata, so any helper that wraps the response must explicitly forward this\n * symbol or `cookies.set()` calls will be silently dropped. `Symbol.for`\n * returns the same registry symbol everywhere, so this matches the copy in\n * middleware.ts.\n */\nexport const ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\n\n/**\n * Run a request-scoped db's `close()` once the response body has finished\n * streaming. Astro streams HTML and components issue DB queries during that\n * stream, so a connection-backed adapter (e.g. Postgres over Hyperdrive) must\n * not be torn down until the body is flushed. Bodyless responses (redirects,\n * 304s, errors) close immediately. A guard makes close idempotent and a stream\n * `cancel` (client disconnect) still triggers it so connections never leak.\n *\n * No-op for adapters without a `close` (D1): the response passes through.\n */\nexport function wrapResponseForScopedClose(response: Response, close: () => void): Response {\n\tlet closed = false;\n\tconst runClose = () => {\n\t\tif (closed) return;\n\t\tclosed = true;\n\t\ttry {\n\t\t\tclose();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[emdash] request-scoped db close failed:\", error);\n\t\t}\n\t};\n\n\tif (!response.body) {\n\t\trunClose();\n\t\treturn response;\n\t}\n\n\tconst transform = new TransformStream<Uint8Array, Uint8Array>({\n\t\tflush: runClose,\n\t\tcancel: runClose,\n\t});\n\tconst wrapped = new Response(response.body.pipeThrough(transform), response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(wrapped, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// Byte counts are preserved by the identity transform, but a stale\n\t// Content-Length on a reconstructed streaming Response risks truncation.\n\twrapped.headers.delete(\"Content-Length\");\n\treturn wrapped;\n}\n\n/**\n * Run the request body under a request-scoped db, then settle its lifecycle:\n * `commit()` runs before the response is returned (so per-request state like a\n * D1 bookmark cookie is persisted in the headers, even if render throws), while\n * `close()` (if any) is deferred to stream-end so a connection-backed adapter\n * isn't torn down mid-render. On error the connection is closed immediately\n * before rethrowing so it never leaks.\n *\n * On the error path both `commit()` and `close()` are defended: a throw from\n * either is logged and swallowed so it can't replace the propagating render\n * error (which is the one the caller needs to see). On the success path\n * `commit()` is guarded too — if it throws, the connection is closed before the\n * failure is surfaced, so it never leaks. For the current adapters `commit()`\n * is a no-op (Hyperdrive) or a cookie write (D1, no `close`) and `close()` is\n * fire-and-forget, so these guards only matter for a future connection-backed\n * adapter with throwing teardown, but the helper is generic and must not leak\n * or mask.\n */\nexport async function finishScoped(\n\tscoped: { commit: () => void; close?: () => void },\n\trun: () => Promise<Response>,\n): Promise<Response> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await run();\n\t} catch (error) {\n\t\t// A render error is already propagating; neither commit nor close may\n\t\t// mask it, and close must still run so the connection doesn't leak.\n\t\tcommitSafely(scoped.commit);\n\t\tcloseSafely(scoped.close);\n\t\tthrow error;\n\t}\n\ttry {\n\t\tscoped.commit();\n\t} catch (error) {\n\t\t// commit() failed on the success path: close the connection now (the\n\t\t// response won't be wrapped, so stream-end close would never run) and\n\t\t// surface the failure. close is swallowed so it can't mask the commit\n\t\t// error that the caller needs to see.\n\t\tcloseSafely(scoped.close);\n\t\tthrow error;\n\t}\n\treturn scoped.close ? wrapResponseForScopedClose(response, scoped.close) : response;\n}\n\n/**\n * Run commit() swallowing any error. Used where an exception is already\n * propagating (or about to be thrown) and a commit failure must neither mask it\n * nor skip the subsequent close().\n */\nfunction commitSafely(commit: () => void): void {\n\ttry {\n\t\tcommit();\n\t} catch (error) {\n\t\tconsole.error(\"[emdash] request-scoped db commit failed during error handling:\", error);\n\t}\n}\n\n/**\n * Run close() swallowing any error. Used on the error/commit-failure paths\n * where another exception is the one the caller must see; a throwing teardown\n * must not replace it.\n */\nfunction closeSafely(close: (() => void) | undefined): void {\n\tif (!close) return;\n\ttry {\n\t\tclose();\n\t} catch (error) {\n\t\tconsole.error(\"[emdash] request-scoped db close failed during error handling:\", error);\n\t}\n}\n","/**\n * Stream-end metrics\n *\n * Server-Timing db.* counters are snapshotted when middleware's next()\n * returns — but at that point only the response *headers* are final.\n * Astro streams the body afterwards, and components rendered during\n * streaming issue further DB queries that the headers can never report.\n *\n * This module wraps the response body in an identity TransformStream and\n * snapshots the request metrics in flush(), i.e. when the body actually\n * finishes streaming. The metrics object lives on the request context\n * (AsyncLocalStorage) and is mutated in-place by the Kysely log hook, so\n * a reference captured before wrapping observes every post-header query.\n * The snapshot is emitted as prefixed NDJSON on stdout (same transport as\n * [emdash-query-log] — console.log works in both Node and workerd).\n *\n * Gated on isInstrumentationEnabled() (EMDASH_QUERY_LOG=1): zero overhead\n * in normal production traffic.\n */\n\nimport { flushRecorder, isInstrumentationEnabled } from \"../../database/instrumentation.js\";\nimport { getRequestContext } from \"../../request-context.js\";\n// Reuse the single source of truth for Astro's well-known cookies symbol\n// rather than redefining `Symbol.for(\"astro.cookies\")` here — it must stay in\n// lockstep with the copy the rest of the middleware forwards.\nimport { ASTRO_COOKIES_SYMBOL } from \"./scoped-db.js\";\n\nexport const STREAM_END_PREFIX = \"[emdash-stream-end]\";\n\n/** Shape of the NDJSON snapshot emitted when the body finishes streaming. */\nexport interface StreamEndSnapshot {\n\troute?: string;\n\tmethod?: string;\n\tphase?: string;\n\t/** Total elapsed ms from middleware entry to end of body streaming. */\n\ttotalMs: number;\n\tdbCount: number;\n\tdbTotalMs: number;\n\tdbFirstOffset: number | null;\n\tdbLastOffset: number | null;\n\tcacheHits: number;\n\tcacheMisses: number;\n}\n\n/**\n * Wrap a response body so the FINAL request metrics are emitted when the\n * body finishes streaming. Returns the response unchanged when\n * instrumentation is disabled, the body is null, or no request metrics\n * are attached (e.g. outside the middleware's ALS context).\n */\nexport function wrapBodyForStreamMetrics(response: Response): Response {\n\tif (!isInstrumentationEnabled()) return response;\n\tif (!response.body) return response;\n\n\t// Capture the context's metrics object BEFORE wrapping: flush() runs\n\t// after the middleware's ALS frame may have exited, but the object\n\t// reference stays live and is mutated in-place by the Kysely log hook.\n\tconst ctx = getRequestContext();\n\tconst metrics = ctx?.metrics;\n\tif (!metrics) return response;\n\tconst recorder = ctx?.queryRecorder;\n\n\t// Claim the per-query flush: the recorder is mutated in-place by the\n\t// Kysely log hook for the whole request, including queries issued by\n\t// components while the body streams. Flushing here (rather than when\n\t// middleware returns) is what captures those streaming queries. The\n\t// flag tells the middleware's fallback flush to leave this recorder\n\t// to us.\n\tif (recorder) recorder.deferredFlush = true;\n\n\tconst transform = new TransformStream<Uint8Array, Uint8Array>({\n\t\tflush() {\n\t\t\tconst snapshot: StreamEndSnapshot = {\n\t\t\t\troute: recorder?.route,\n\t\t\t\tmethod: recorder?.method,\n\t\t\t\tphase: recorder?.phase,\n\t\t\t\ttotalMs: performance.now() - metrics.start,\n\t\t\t\tdbCount: metrics.dbCount,\n\t\t\t\tdbTotalMs: metrics.dbTotalMs,\n\t\t\t\tdbFirstOffset: metrics.dbFirstOffset,\n\t\t\t\tdbLastOffset: metrics.dbLastOffset,\n\t\t\t\tcacheHits: metrics.cacheHits,\n\t\t\t\tcacheMisses: metrics.cacheMisses,\n\t\t\t};\n\t\t\tconsole.log(`${STREAM_END_PREFIX} ${JSON.stringify(snapshot)}`);\n\t\t\t// Emit the full per-query log now that streaming is complete.\n\t\t\tif (recorder) flushRecorder(recorder);\n\t\t},\n\t});\n\n\tconst wrapped = new Response(response.body.pipeThrough(transform), response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(wrapped, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// The identity transform preserves byte counts today, but a stale\n\t// Content-Length on a re-constructed streaming Response risks\n\t// truncation if anything upstream changes; the header is recomputed\n\t// (or chunked encoding used) by the server layer anyway.\n\twrapped.headers.delete(\"Content-Length\");\n\treturn wrapped;\n}\n","/**\n * Eager, transparent prefetch of site-global \"chrome\" data.\n *\n * On a public page render, the shared layout pulls the same site-global data on\n * every request -- menus, widget areas, taxonomy term lists, site settings --\n * but each is awaited inside a separately-rendered Astro component, so they\n * execute as serial DB round trips. This fires them all CONCURRENTLY at the\n * very start of the request, before `next()`:\n *\n * - On remote backends (D1, Durable Objects) the round trips overlap instead\n * of serializing, collapsing ~N sequential RTTs into ~1 wall-clock RTT. On\n * a coalescing backend they additionally batch into a single round trip.\n * - The results land in the per-request `requestCached` store under the exact\n * keys the layout helpers use, so when the components render they hit a warm\n * (in-flight or resolved) cache entry instead of issuing their own query.\n *\n * Nothing here changes what templates call -- it warms the real helpers, so the\n * cache keys and value shapes are guaranteed identical. The caller gates this to\n * the public-page path on a request-scoped (remote) backend; it is a no-op-ish\n * waste on synchronous local SQLite, so don't call it there.\n *\n * Fire-and-forget: never awaited by middleware, never throws (a prefetch failure\n * must not affect the request -- the helpers will simply run on demand).\n */\n\nimport { getDb } from \"../loader.js\";\nimport { getMenu } from \"../menus/index.js\";\nimport { setRequestCacheEntry } from \"../request-cache.js\";\nimport { getSiteSettings } from \"../settings/index.js\";\nimport { getTaxonomyDefs, getTaxonomyTerms } from \"../taxonomies/index.js\";\nimport { getWidgetAreas } from \"../widgets/index.js\";\n\n/** Warm widget areas: one bulk load, primed under each per-area cache key. */\nasync function prefetchWidgetAreas(): Promise<void> {\n\tconst areas = await getWidgetAreas();\n\t// getWidgetArea(name) caches under `widget-area:${name}` and returns the same\n\t// WidgetArea shape getWidgetAreas yields, so priming here makes those calls hit.\n\tfor (const area of areas) {\n\t\tsetRequestCacheEntry(`widget-area:${area.name}`, area);\n\t}\n}\n\n/** Warm every taxonomy's term list via the real helper (primes per-name keys). */\nasync function prefetchTaxonomyTerms(): Promise<void> {\n\tconst defs = await getTaxonomyDefs();\n\tawait Promise.allSettled(defs.map((def) => getTaxonomyTerms(def.name)));\n}\n\n/** Warm every menu via the real helper (primes `menu:${name}:${locale}`). */\nasync function prefetchMenus(): Promise<void> {\n\tconst db = await getDb();\n\t// The layout calls getMenu(name) with hardcoded names; we can't know them, so\n\t// discover every menu name and warm them all (small, bounded chrome table).\n\tconst rows = await db.selectFrom(\"_emdash_menus\").select(\"name\").distinct().execute();\n\tconst names = [...new Set(rows.map((r) => r.name))];\n\tawait Promise.allSettled(names.map((name) => getMenu(name)));\n}\n\n/**\n * Concurrently warm the site-global layout data for the current request.\n * Safe to call only inside the request ALS frame that owns the (remote)\n * request-scoped db. Never throws.\n */\nexport async function prefetchLayoutData(): Promise<void> {\n\ttry {\n\t\tawait Promise.allSettled([\n\t\t\tgetSiteSettings(),\n\t\t\tprefetchMenus(),\n\t\t\tprefetchWidgetAreas(),\n\t\t\tprefetchTaxonomyTerms(),\n\t\t]);\n\t} catch (error) {\n\t\t// Defensive: Promise.allSettled shouldn't reject, but never let a prefetch\n\t\t// failure surface to the request.\n\t\tconsole.error(\"[emdash] layout prefetch failed (non-fatal):\", error);\n\t}\n}\n","import type { HandlerResponse } from \"./types.js\";\n\nexport type PublicPluginApiRouteHandler = (\n\tpluginId: string,\n\tmethod: string,\n\tpath: string,\n\trequest: Request,\n) => Promise<HandlerResponse>;\n\ninterface PublicPluginApiRouteRuntime {\n\tgetPluginRouteMeta(pluginId: string, path: string): { public: boolean } | null;\n\thandlePluginApiRoute(\n\t\tpluginId: string,\n\t\tmethod: string,\n\t\tpath: string,\n\t\trequest: Request,\n\t): Promise<HandlerResponse>;\n}\n\nfunction pluginRouteNotFound(): HandlerResponse {\n\treturn {\n\t\tsuccess: false,\n\t\terror: {\n\t\t\tcode: \"NOT_FOUND\",\n\t\t\tmessage: \"Plugin route not found\",\n\t\t},\n\t};\n}\n\nexport function createPublicPluginApiRouteHandler(\n\truntime: PublicPluginApiRouteRuntime,\n): PublicPluginApiRouteHandler {\n\treturn async (pluginId, method, path, request) => {\n\t\tconst meta = runtime.getPluginRouteMeta(pluginId, path);\n\t\tif (meta?.public !== true) {\n\t\t\treturn pluginRouteNotFound();\n\t\t}\n\n\t\treturn runtime.handlePluginApiRoute(pluginId, method, path, request);\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 type { 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\tcreateCoalescingDialect as virtualCreateCoalescingDialect,\n\tcreateDialect as virtualCreateDialect,\n\tcreateRequestScopedDb as virtualCreateRequestScopedDb,\n} from \"virtual:emdash/dialect\";\nimport type { RequestScopedDbOpts } 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\";\n// @ts-ignore - virtual module\nimport * as virtualSandboxRunnerModule 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 { createScheduler as virtualCreateScheduler } from \"virtual:emdash/scheduler\";\n// @ts-ignore - virtual module\nimport { createStorage as virtualCreateStorage } from \"virtual:emdash/storage\";\n\nimport { after } from \"../after.js\";\nimport {\n\tcreateRecorder,\n\tflushRecorder,\n\tisInstrumentationEnabled,\n} from \"../database/instrumentation.js\";\nimport {\n\tDB_INIT_DEADLINE_MS,\n\tEmDashRuntime,\n\ttype RuntimeDependencies,\n\ttype SandboxedPluginEntry,\n\ttype MediaProviderEntry,\n\ttype CreateSchedulerFn,\n} from \"../emdash-runtime.js\";\nimport { setI18nConfig } from \"../i18n/config.js\";\nimport type { Database, Storage } from \"../index.js\";\nimport { createPublicMediaUrlResolver } from \"../media/url.js\";\nimport type { SandboxRunner } from \"../plugins/sandbox/types.js\";\nimport type { ResolvedPlugin } from \"../plugins/types.js\";\nimport { invalidateUrlPatternCache } from \"../query.js\";\nimport {\n\tcreateRequestMetrics,\n\tgetRequestContext,\n\ttype RequestMetrics,\n\trunWithContext,\n} from \"../request-context.js\";\nimport type { PublishedRef } from \"../scheduled-publish.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport { createInitLock, type InitLock, initWithLock } from \"../utils/init-lock.js\";\nimport type { EmDashConfig } from \"./integration/runtime.js\";\nimport { ASTRO_COOKIES_SYMBOL, finishScoped } from \"./middleware/scoped-db.js\";\nimport { wrapBodyForStreamMetrics } from \"./middleware/stream-end-metrics.js\";\nimport { prefetchLayoutData } from \"./prefetch.js\";\nimport { createPublicPluginApiRouteHandler } from \"./public-plugin-api-routes.js\";\nimport { resolveSessionUser } from \"./session-user.js\";\nimport type { EmDashHandlers } from \"./types.js\";\n\n/**\n * Runtime init lock reclaim deadline. Must be strictly larger than the db\n * init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →\n * the db init lock, and equal deadlines would let this outer lock reclaim\n * (spawning a second cron scheduler and sandbox runner) while the inner db\n * init is legitimately still working through a contended migration.\n */\nconst RUNTIME_INIT_DEADLINE_MS = DB_INIT_DEADLINE_MS + 15_000;\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 *\n * Stored on globalThis behind a Symbol key so the flag is a true singleton\n * even when the bundler duplicates this module across SSR chunks (same\n * pattern as request-cache.ts). A plain module-scoped `let` becomes multiple\n * independent variables, which would make the setup probe re-run far more\n * often than intended — and every re-run is another chance for a transient\n * DB error to be misread as \"fresh install\" and bounce visitors to setup.\n */\nconst SETUP_VERIFIED_KEY = Symbol.for(\"emdash:setup-verified\");\nconst setupFlagStore = globalThis as Record<symbol, unknown>;\n\nfunction isSetupVerified(): boolean {\n\treturn setupFlagStore[SETUP_VERIFIED_KEY] === true;\n}\n\nfunction markSetupVerified(): void {\n\tsetupFlagStore[SETUP_VERIFIED_KEY] = true;\n}\n\n/**\n * The runtime singleton and its init lock live on globalThis behind a\n * Symbol — same reasoning as SETUP_VERIFIED_KEY above: the bundler can\n * duplicate this module across SSR chunks, and a duplicated instance/lock\n * would mean multiple runtimes (each with its own cron scheduler) per\n * isolate, initializing and reclaiming independently.\n */\nconst RUNTIME_HOLDER_KEY = Symbol.for(\"emdash:runtime-holder\");\ninterface RuntimeHolder {\n\tinstance: EmDashRuntime | null;\n\tlock: InitLock;\n}\n\nfunction getRuntimeHolder(): RuntimeHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = setupFlagStore[RUNTIME_HOLDER_KEY] as RuntimeHolder | undefined;\n\tif (!holder) {\n\t\tholder = { instance: null, lock: createInitLock() };\n\t\tsetupFlagStore[RUNTIME_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\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/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/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/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/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\t/* eslint-disable typescript-eslint/no-unsafe-type-assertion --\n\t The virtual:emdash/* imports above use @ts-ignore because tsgo/IDE\n\t resolution can't see virtual-modules.d.ts in every consumer setup,\n\t so they arrive as `any`. The casts here line each entry up with\n\t RuntimeDependencies's expected shape. The contract is enforced by\n\t the integration that populates these virtual modules. */\n\tconst sandboxModule = virtualSandboxRunnerModule as Record<string, unknown>;\n\treturn {\n\t\tconfig,\n\t\tplugins: getPlugins(),\n\t\tcreateDialect: virtualCreateDialect as (config: Record<string, unknown>) => unknown,\n\t\t// Optional: only batching backends (D1, DO) export this; undefined otherwise.\n\t\tcreateCoalescingDialect: virtualCreateCoalescingDialect as\n\t\t\t| ((config: Record<string, unknown>) => unknown)\n\t\t\t| undefined,\n\t\tcreateStorage: virtualCreateStorage as ((config: Record<string, unknown>) => Storage) | null,\n\t\tcreateScheduler: virtualCreateScheduler as CreateSchedulerFn | null,\n\t\tsandboxEnabled: sandboxModule.sandboxEnabled as boolean,\n\t\tsandboxBypassed: (sandboxModule.sandboxBypassed as boolean) ?? false,\n\t\tsandboxedPluginEntries: (virtualSandboxedPlugins as SandboxedPluginEntry[]) || [],\n\t\tcreateSandboxRunner: sandboxModule.createSandboxRunner as\n\t\t\t| ((opts: {\n\t\t\t\t\tdb: Kysely<Database>;\n\t\t\t\t\tmediaStorage?: {\n\t\t\t\t\t\tupload(options: {\n\t\t\t\t\t\t\tkey: string;\n\t\t\t\t\t\t\tbody: Uint8Array;\n\t\t\t\t\t\t\tcontentType: string;\n\t\t\t\t\t\t}): Promise<unknown>;\n\t\t\t\t\t\tdelete(key: string): Promise<unknown>;\n\t\t\t\t\t};\n\t\t\t }) => SandboxRunner)\n\t\t\t| null,\n\t\tmediaProviderEntries: (virtualMediaProviders as MediaProviderEntry[]) || [],\n\t};\n\t/* eslint-enable typescript-eslint/no-unsafe-type-assertion */\n}\n\n/**\n * Get or create the runtime instance.\n *\n * When `initTimings` is provided, any timing samples recorded during a\n * genuine cold init are appended. Subsequent warm calls (hitting the\n * cached instance) push nothing — callers should treat an empty array\n * as \"warm, nothing to report\".\n */\nasync function getRuntime(\n\tconfig: EmDashConfig,\n\tinitTimings?: Array<{ name: string; dur: number; desc?: string }>,\n): Promise<EmDashRuntime> {\n\t// Waiters poll rather than awaiting the initializing request's promise —\n\t// workerd flags cross-request promise resolution (warnings + potential\n\t// hangs). If the initializing request is cancelled mid-create (client\n\t// disconnect tears down its continuation, skipping any `finally`), the\n\t// anchored init keeps running under waitUntil and populates the cache;\n\t// failing that, the stale lock is reclaimed after a deadline instead of\n\t// hanging every subsequent request in the isolate until eviction.\n\tconst holder = getRuntimeHolder();\n\treturn initWithLock(\n\t\tholder.lock,\n\t\t() => holder.instance,\n\t\tasync (isCurrentClaim) => {\n\t\t\tconst deps = buildDependencies(config);\n\t\t\tconst runtime = await EmDashRuntime.create(deps, initTimings);\n\t\t\tif (isCurrentClaim()) {\n\t\t\t\tholder.instance = runtime;\n\t\t\t} else {\n\t\t\t\t// This init was reclaimed mid-flight (it ran past the deadline\n\t\t\t\t// and a waiter started its own). Don't overwrite the\n\t\t\t\t// reclaimer's published runtime, and stop this one's cron\n\t\t\t\t// scheduler so it doesn't keep firing unreferenced. The\n\t\t\t\t// runtime is still returned — it's fully functional for the\n\t\t\t\t// request that built it.\n\t\t\t\truntime.stopCron().catch((error: unknown) => {\n\t\t\t\t\tconsole.error(\"[emdash] failed to stop superseded runtime's cron:\", error);\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn runtime;\n\t\t},\n\t\t{\n\t\t\tdeadlineMs: RUNTIME_INIT_DEADLINE_MS,\n\t\t\tanchor: (promise) => after(() => promise),\n\t\t},\n\t);\n}\n\n/**\n * Run scheduled maintenance (cron tasks, scheduled publishing, system cleanup)\n * outside any request. Resolves the runtime from the build-time virtual config\n * and the cached singleton — the same instance request handlers use.\n *\n * Wired into a platform heartbeat that is not a request: the Cloudflare Worker's\n * `scheduled()` handler (Cron Trigger) calls this. On Node the runtime's own\n * timer-based scheduler already drives the same work, so this isn't needed there.\n *\n * Returns the content promoted by the publishing sweep so the caller can purge\n * edge-cache tags for it. `onPublished` (optional) is awaited after each\n * collection's batch so the caller can invalidate edge-cache tags incrementally\n * rather than only after the whole sweep.\n */\nexport async function runScheduledTasks(\n\toptions: { onPublished?: (refs: PublishedRef[]) => Promise<void> } = {},\n): Promise<{ published: PublishedRef[] }> {\n\tconst config = getConfig();\n\tif (!config) return { published: [] };\n\tconst runtime = await getRuntime(config);\n\n\t// Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse\n\t// the per-isolate singleton from a Cron Trigger: its socket belongs to the\n\t// request that opened it, and workerd rejects cross-event I/O. Open an\n\t// event-scoped connection for the sweep and run the batch under it in ALS —\n\t// the runtime's db getter, the cron executor, and plugin cron contexts all\n\t// resolve the connection from ALS — then close it. Gated on the adapter\n\t// being connection-backed (it exposes `close()`); stateless adapters (D1,\n\t// Node SQLite) return null or a close-less scope and keep using the\n\t// singleton, so their cron path is unchanged.\n\tconst scoped = createRequestScopedDb({\n\t\tconfig: config.database?.config,\n\t\tisAuthenticated: false,\n\t\t// The sweep publishes and cleans up — a write workload — so a\n\t\t// connection-backed adapter routes it to the primary.\n\t\tisWrite: true,\n\t\tcookies: NOOP_COOKIE_JAR,\n\t\turl: CRON_EVENT_URL,\n\t});\n\tif (!scoped?.close) {\n\t\t// Stateless adapter (or no per-request scoping): the singleton is safe\n\t\t// outside a request. Any close-less scope created above is discarded.\n\t\treturn runtime.runScheduledTasks(options);\n\t}\n\n\tconst parent = getRequestContext();\n\tconst ctx = parent\n\t\t? { ...parent, db: scoped.db }\n\t\t: { editMode: false, db: scoped.db, metrics: createRequestMetrics(performance.now()) };\n\ttry {\n\t\treturn await runWithContext(ctx, () => runtime.runScheduledTasks(options));\n\t} finally {\n\t\t// Guard both so a throw in teardown can't mask the sweep result or skip\n\t\t// close() and leak the connection. Mirrors closeSafely() in scoped-db.ts.\n\t\ttry {\n\t\t\tscoped.commit();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled] request-scoped db commit failed:\", error);\n\t\t}\n\t\ttry {\n\t\t\tscoped.close();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled] request-scoped db close failed:\", error);\n\t\t}\n\t}\n}\n\n/**\n * A cookie jar that reads nothing and writes nothing, for request-scoped db\n * adapters invoked outside an HTTP request (the Cron Trigger sweep). Connection\n * adapters like Hyperdrive ignore cookies entirely; the D1 session adapter\n * reads/writes a bookmark cookie, but cron never reaches that path (it has no\n * `close()`), so the no-ops are never observed.\n */\nconst NOOP_COOKIE_JAR = {\n\tget: () => undefined,\n\tset: () => {},\n};\n\n/**\n * Synthetic URL for the cron sweep's request-scoped db opts. Only the D1\n * session adapter inspects `url` (for cookie `secure`), and cron doesn't take\n * that path, so the value is never used — it exists to satisfy the contract.\n */\nconst CRON_EVENT_URL = new URL(\"https://cron.emdash.internal/\");\n\n/**\n * Baseline security headers applied to all responses.\n * Admin routes get additional headers (strict CSP) from auth middleware.\n */\nfunction finalizeResponse(\n\tresponse: Response,\n\tserverTimings?: Array<{ name: string; dur: number; desc?: string }>,\n): Response {\n\tconst res = new Response(response.body, response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(res, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// Set-if-absent so a host app that sets stricter values on its own routes\n\t// wins. The middleware registers `order: 'pre'` (#1282), so on the response\n\t// path it runs *after* host middleware; unconditional `set()` would clobber\n\t// the host's headers on every public route (#1393). Mirrors the CSP guard.\n\tif (!res.headers.has(\"X-Content-Type-Options\")) {\n\t\tres.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\t}\n\tif (!res.headers.has(\"Referrer-Policy\")) {\n\t\tres.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\t}\n\tif (!res.headers.has(\"Permissions-Policy\")) {\n\t\tres.headers.set(\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=(), payment=()\");\n\t}\n\tif (!res.headers.has(\"Content-Security-Policy\")) {\n\t\tres.headers.set(\"X-Frame-Options\", \"SAMEORIGIN\");\n\t}\n\tif (serverTimings && serverTimings.length > 0) {\n\t\tres.headers.set(\n\t\t\t\"Server-Timing\",\n\t\t\tserverTimings\n\t\t\t\t.map((t) => {\n\t\t\t\t\tconst dur = Math.round(t.dur);\n\t\t\t\t\treturn t.desc ? `${t.name};dur=${dur};desc=\"${t.desc}\"` : `${t.name};dur=${dur}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \"),\n\t\t);\n\t}\n\treturn res;\n}\n\n/**\n * Append always-on counters (db.*, cache.*) to the Server-Timing list.\n *\n * dur values for `count`, `hit`, `miss` are integer counts — Server-Timing\n * spec only models milliseconds, but browsers show whatever number is given,\n * which is the convention most projects use for non-time samples.\n */\nfunction pushMetricsTimings(\n\ttimings: Array<{ name: string; dur: number; desc?: string }>,\n\tmetrics: RequestMetrics,\n): void {\n\tif (metrics.dbCount > 0) {\n\t\ttimings.push({ name: \"db.total\", dur: metrics.dbTotalMs, desc: \"DB total\" });\n\t\ttimings.push({ name: \"db.count\", dur: metrics.dbCount, desc: \"Query count\" });\n\t\tif (metrics.dbFirstOffset !== null) {\n\t\t\ttimings.push({ name: \"db.first\", dur: metrics.dbFirstOffset, desc: \"First query at\" });\n\t\t}\n\t\tif (metrics.dbLastOffset !== null) {\n\t\t\ttimings.push({ name: \"db.last\", dur: metrics.dbLastOffset, desc: \"Last query at\" });\n\t\t}\n\t}\n\tif (metrics.rpcCount > 0) {\n\t\ttimings.push({ name: \"rpc.count\", dur: metrics.rpcCount, desc: \"DB round trips\" });\n\t}\n\tif (metrics.cacheHits + metrics.cacheMisses > 0) {\n\t\ttimings.push({ name: \"cache.hit\", dur: metrics.cacheHits, desc: \"Cache hits\" });\n\t\ttimings.push({ name: \"cache.miss\", dur: metrics.cacheMisses, desc: \"Cache misses\" });\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\"]);\nconst SITEMAP_COLLECTION_RE = /^\\/sitemap-[a-z][a-z0-9_]*\\.xml$/;\n\n/**\n * Ask the configured database adapter for a per-request scoped Kysely. The\n * adapter encapsulates any per-request semantics (D1 sessions, read-replica\n * routing, bookmark cookies, etc.); core just forwards the cookie jar and\n * request flags and wraps next() in ALS if a scope was returned.\n */\nfunction createRequestScopedDb(\n\topts: RequestScopedDbOpts,\n): { db: Kysely<Database>; commit: () => void; close?: () => void } | null {\n\tif (typeof virtualCreateRequestScopedDb !== \"function\") return null;\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- adapter returns Kysely<unknown>; cast to Database since core owns that type\n\tconst fn = virtualCreateRequestScopedDb as (\n\t\to: RequestScopedDbOpts,\n\t) => { db: Kysely<Database>; commit: () => void; close?: () => void } | null;\n\treturn fn(opts);\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { request, locals, cookies } = context;\n\tconst url = context.url;\n\n\t// Fast path: routes outside /_emdash/ that plugins inject (e.g.,\n\t// /.well-known/atproto-client-metadata.json) skip the entire runtime\n\t// init + middleware chain. External servers fetch these with tight\n\t// timeouts (~1-2s) so they must respond quickly even on cold starts.\n\tif (!url.pathname.startsWith(\"/_emdash\") && virtualConfig?.authProviders) {\n\t\tconst isPluginFastRoute = virtualConfig.authProviders.some(\n\t\t\t(p: { routes?: { pattern?: string }[] }) =>\n\t\t\t\tp.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),\n\t\t);\n\t\tif (isPluginFastRoute) {\n\t\t\treturn finalizeResponse(await next());\n\t\t}\n\t}\n\n\tconst queryRecorder = isInstrumentationEnabled()\n\t\t? createRecorder(url.pathname, request.method, request.headers.get(\"x-perf-phase\") ?? \"default\")\n\t\t: undefined;\n\n\tconst metrics = createRequestMetrics(performance.now());\n\n\tconst run = async (): Promise<Response> => {\n\t\t// Process /_emdash routes and public routes with an active session\n\t\t// (logged-in editors need the runtime for toolbar/visual editing on public pages)\n\t\tconst isEmDashRoute = url.pathname.startsWith(\"/_emdash\");\n\t\tconst isPublicRuntimeRoute =\n\t\t\tPUBLIC_RUNTIME_ROUTES.has(url.pathname) || SITEMAP_COLLECTION_RE.test(url.pathname);\n\n\t\t// Check for edit mode cookie - editors viewing public pages need the runtime\n\t\t// so auth middleware can verify their session for visual editing\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t\t// Playground mode: the playground middleware stashes the per-session DO database\n\t\t// on locals.__playgroundDb. When present, use runWithContext() to make it\n\t\t// available to getDb() and the runtime's db getter via the correct ALS instance.\n\t\tconst playgroundDb = locals.__playgroundDb;\n\n\t\t// Read the Astro session user once up-front. Both the anonymous fast path\n\t\t// and the full doInit path need this, and the session store is network-backed\n\t\t// (KV / Durable Object) so we want to avoid re-fetching on the hot path.\n\t\t// Skipped entirely for:\n\t\t// - prerendered requests (no session at build time)\n\t\t// - requests without an `astro-session` cookie (no session to look up)\n\t\t// The cookie check matters on Cloudflare Workers, where Astro's session\n\t\t// backend is KV: calling session.get() on every anonymous public request\n\t\t// turns normal traffic into a flood of KV read misses. See #733.\n\t\tconst hasSessionCookie = cookies.get(\"astro-session\") !== undefined;\n\t\tconst sessionUser =\n\t\t\tcontext.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);\n\n\t\t// Credentialed API requests (API tokens `ec_pat_*`, OAuth tokens\n\t\t// `ec_oat_*`, and other Bearer credentials) carry no `astro-session`\n\t\t// cookie, so `sessionUser` is null for them -- yet they still expect\n\t\t// read-your-writes. The auth middleware that resolves the token runs\n\t\t// *after* this one, so `locals.user` isn't populated here; detect the\n\t\t// credential directly on the request. Request-scoped adapters use this to\n\t\t// keep such requests on the primary/uncached connection (not a lagging\n\t\t// read replica or the Hyperdrive query cache).\n\t\tconst hasBearerAuth = (request.headers.get(\"authorization\") ?? \"\")\n\t\t\t.toLowerCase()\n\t\t\t.startsWith(\"bearer \");\n\n\t\tif (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {\n\t\t\tif (!sessionUser && !playgroundDb) {\n\t\t\t\tconst timings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\tconst mwStart = performance.now();\n\n\t\t\t\t// On a fresh deployment the database may be completely empty.\n\t\t\t\t// Public pages call getSiteSettings() / getMenu() via getDb(), which\n\t\t\t\t// bypasses runtime init and would crash with \"no such table: options\".\n\t\t\t\t// Do a one-time lightweight probe using the same getDb() instance the\n\t\t\t\t// page will use: if the migrations table doesn't exist, no migrations\n\t\t\t\t// have ever run -- redirect to the setup wizard.\n\t\t\t\t// Skip the probe when prerendering: a prerendered route is built to\n\t\t\t\t// static HTML, so returning context.redirect(\"/_emdash/admin/setup\")\n\t\t\t\t// below would bake that redirect into the page and ship it to\n\t\t\t\t// production. The build database is legitimately empty in CI and there\n\t\t\t\t// is no live visitor to send to the wizard at build time (session reads\n\t\t\t\t// are already skipped for prerender above for the same reason).\n\t\t\t\tif (!isSetupVerified() && !context.isPrerendered) {\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst { getDb } = await import(\"../loader.js\");\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\tawait db\n\t\t\t\t\t\t\t.selectFrom(\"_emdash_migrations\" as keyof Database)\n\t\t\t\t\t\t\t.selectAll()\n\t\t\t\t\t\t\t.limit(1)\n\t\t\t\t\t\t\t.execute();\n\t\t\t\t\t\tmarkSetupVerified();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Only a genuinely-missing migrations table means a fresh,\n\t\t\t\t\t\t// un-set-up database — redirect to the setup wizard.\n\t\t\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\t\t\treturn context.redirect(\"/_emdash/admin/setup\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Any other failure (transient D1/replica error, timeout, cold-start\n\t\t\t\t\t\t// race, locked SQLite) must NOT be read as \"fresh install\" — doing so\n\t\t\t\t\t\t// bounces real visitors on a set-up site to /_emdash/admin/setup.\n\t\t\t\t\t\t// Leave the flag unset so a later request can re-verify, and fall\n\t\t\t\t\t\t// through to render the page normally.\n\t\t\t\t\t\tconsole.error(\"Setup probe failed (non-fatal):\", error);\n\t\t\t\t\t}\n\t\t\t\t\ttimings.push({ name: \"setup\", dur: performance.now() - t0, desc: \"Setup probe\" });\n\t\t\t\t}\n\n\t\t\t\t// Initialize the runtime for page:metadata and page:fragments hooks.\n\t\t\t\t// The runtime is a cached singleton — after the first request,\n\t\t\t\t// getRuntime() is just a null-check. This enables SEO plugins to\n\t\t\t\t// contribute meta tags for all visitors, not just logged-in editors.\n\t\t\t\tconst config = getConfig();\n\t\t\t\tif (config) {\n\t\t\t\t\t// Sub-phase timings are populated only on the cold init. Warm\n\t\t\t\t\t// requests hit the cached runtime and leave this empty.\n\t\t\t\t\tconst initSubTimings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst runtime = await getRuntime(config, initSubTimings);\n\t\t\t\t\t\tmarkSetupVerified();\n\t\t\t\t\t\tconst handlePublicPluginApiRoute = createPublicPluginApiRouteHandler(runtime);\n\t\t\t\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- partial object; getPageRuntime() only checks for the page-contribution methods\n\t\t\t\t\t\tlocals.emdash = {\n\t\t\t\t\t\t\thandlePublicPluginApiRoute,\n\t\t\t\t\t\t\tcollectPageMetadata: runtime.collectPageMetadata.bind(runtime),\n\t\t\t\t\t\t\tcollectPageFragments: runtime.collectPageFragments.bind(runtime),\n\t\t\t\t\t\t\tgetPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),\n\t\t\t\t\t\t\t// Exposed so the wrapped image endpoint (`/_image`) can read media\n\t\t\t\t\t\t\t// bytes from storage on the anonymous fast path -- public `<img>`\n\t\t\t\t\t\t\t// requests carry no session.\n\t\t\t\t\t\t\tstorage: runtime.storage,\n\t\t\t\t\t\t} as EmDashHandlers;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Non-fatal — EmDashHead will fall back to base SEO contributions\n\t\t\t\t\t}\n\t\t\t\t\ttimings.push({ name: \"rt\", dur: performance.now() - t0, desc: \"Runtime init\" });\n\t\t\t\t\t// Append cold-only sub-phase timings so the breakdown is visible\n\t\t\t\t\t// in Server-Timing (rt.db, rt.fts, rt.plugins, rt.site,\n\t\t\t\t\t// rt.sandbox, rt.market, rt.hooks, rt.cron).\n\t\t\t\t\tfor (const sub of initSubTimings) timings.push(sub);\n\t\t\t\t}\n\n\t\t\t\t// Even on the anonymous fast path we ask the adapter for a per-request\n\t\t\t\t// scoped db. For D1 with read replication this routes anonymous reads\n\t\t\t\t// to the nearest replica; for other adapters it's a no-op.\n\t\t\t\tconst anonScoped = createRequestScopedDb({\n\t\t\t\t\tconfig: config?.database?.config,\n\t\t\t\t\tisAuthenticated: false,\n\t\t\t\t\tisWrite: request.method !== \"GET\" && request.method !== \"HEAD\",\n\t\t\t\t\tcookies,\n\t\t\t\t\turl,\n\t\t\t\t});\n\t\t\t\tconst runAnon = async () => {\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\tconst response = await next();\n\t\t\t\t\ttimings.push({ name: \"render\", dur: performance.now() - t0, desc: \"Page render\" });\n\t\t\t\t\ttimings.push({ name: \"mw\", dur: performance.now() - mwStart, desc: \"Total middleware\" });\n\t\t\t\t\tpushMetricsTimings(timings, metrics);\n\t\t\t\t\t// Server-Timing only sees pre-stream queries; the stream-end\n\t\t\t\t\t// wrapper (instrumentation-gated, no-op otherwise) emits the\n\t\t\t\t\t// final counters once the body finishes streaming.\n\t\t\t\t\treturn wrapBodyForStreamMetrics(finalizeResponse(response, timings));\n\t\t\t\t};\n\t\t\t\tif (anonScoped) {\n\t\t\t\t\tconst parent = getRequestContext();\n\t\t\t\t\tconst ctx = parent\n\t\t\t\t\t\t? { ...parent, db: anonScoped.db }\n\t\t\t\t\t\t: { editMode: false, db: anonScoped.db, metrics };\n\t\t\t\t\t// Eagerly warm site-global layout data (menus, widget areas,\n\t\t\t\t\t// taxonomy terms, settings) concurrently so the layout's\n\t\t\t\t\t// per-component reads overlap into ~one wall-clock round trip and\n\t\t\t\t\t// hit a warm cache instead of serializing. Three guards:\n\t\t\t\t\t// - request-scoped (remote) backend only -- this branch implies it;\n\t\t\t\t\t// pointless on synchronous local SQLite.\n\t\t\t\t\t// - HTML navigations only -- feeds/sitemaps/JSON don't render the\n\t\t\t\t\t// layout, so prefetching their chrome is pure waste.\n\t\t\t\t\t// - via after(): it runs immediately (still warms the render) but\n\t\t\t\t\t// hands the promise to waitUntil, so the surplus warm-up (chrome a\n\t\t\t\t\t// given page doesn't render) is kept alive past the response rather\n\t\t\t\t\t// than erroring on workerd as orphaned request I/O.\n\t\t\t\t\t// Gate on the CLIENT'S PREFERRED type (leading media range), not a\n\t\t\t\t\t// substring -- browser navigations lead with `text/html`, while feed\n\t\t\t\t\t// readers lead with `application/rss+xml` etc. and only list\n\t\t\t\t\t// `text/html;q=0.8` later, so a substring match would leak onto feeds.\n\t\t\t\t\tconst acceptsHtml = (request.headers.get(\"accept\") ?? \"\")\n\t\t\t\t\t\t.split(\",\", 1)[0]!\n\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t.startsWith(\"text/html\");\n\t\t\t\t\treturn runWithContext(ctx, async () => {\n\t\t\t\t\t\tif (acceptsHtml) after(() => prefetchLayoutData());\n\t\t\t\t\t\t// commit() persists per-request state (e.g. the D1 bookmark cookie)\n\t\t\t\t\t\t// before the response is returned, even if render throws; close()\n\t\t\t\t\t\t// (connection teardown) is deferred to stream-end. See finishScoped.\n\t\t\t\t\t\treturn finishScoped(anonScoped, runAnon);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn runAnon();\n\t\t\t}\n\t\t}\n\n\t\tconst config = getConfig();\n\t\tif (!config) {\n\t\t\tconsole.error(\"EmDash: No configuration found\");\n\t\t\treturn finalizeResponse(await next());\n\t\t}\n\n\t\t// In playground mode, wrap the entire runtime init + request handling in\n\t\t// runWithContext so that getDatabase() and all init queries use the real\n\t\t// DO database via the same AsyncLocalStorage instance as the loader.\n\t\tconst doInit = async () => {\n\t\t\tconst timings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\tconst mwStart = performance.now();\n\n\t\t\ttry {\n\t\t\t\t// Get or create runtime. Sub-phase timings (rt.db, rt.fts, rt.plugins,\n\t\t\t\t// rt.site, rt.sandbox, rt.market, rt.hooks, rt.cron) are populated\n\t\t\t\t// only on the cold init — subsequent warm calls find the cached\n\t\t\t\t// instance and `initSubTimings` stays empty.\n\t\t\t\tconst initSubTimings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\tlet t0 = performance.now();\n\t\t\t\tconst runtime = await getRuntime(config, initSubTimings);\n\t\t\t\ttimings.push({ name: \"rt\", dur: performance.now() - t0, desc: \"Runtime init\" });\n\t\t\t\t// Forward any sub-phase samples so cold-start breakdown is visible\n\t\t\t\t// in Server-Timing. Each phase appears prefixed \"rt.\" to distinguish\n\t\t\t\t// from the aggregate \"rt\" timing above.\n\t\t\t\tfor (const sub of initSubTimings) timings.push(sub);\n\n\t\t\t\t// Runtime init runs migrations, so the DB is guaranteed set up\n\t\t\t\tmarkSetupVerified();\n\n\t\t\t\t// The manifest is no longer pre-loaded here. It's admin-only\n\t\t\t\t// content that public/anonymous requests never read, and\n\t\t\t\t// loading it on every request put logged-out hot paths on\n\t\t\t\t// the same staleness budget as admin operations. Admin\n\t\t\t\t// routes call `emdash.getManifest()` directly.\n\n\t\t\t\t// Attach to locals for route handlers\n\t\t\t\tlocals.emdash = {\n\t\t\t\t\t// Content handlers\n\t\t\t\t\thandleContentList: runtime.handleContentList.bind(runtime),\n\t\t\t\t\thandleContentGet: runtime.handleContentGet.bind(runtime),\n\t\t\t\t\thandleContentAuthors: runtime.handleContentAuthors.bind(runtime),\n\t\t\t\t\thandleContentCreate: runtime.handleContentCreate.bind(runtime),\n\t\t\t\t\thandleContentUpdate: runtime.handleContentUpdate.bind(runtime),\n\t\t\t\t\thandleContentDelete: runtime.handleContentDelete.bind(runtime),\n\n\t\t\t\t\t// Trash handlers\n\t\t\t\t\thandleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),\n\t\t\t\t\thandleContentRestore: runtime.handleContentRestore.bind(runtime),\n\t\t\t\t\thandleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),\n\t\t\t\t\thandleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),\n\t\t\t\t\thandleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),\n\n\t\t\t\t\t// Duplicate handler\n\t\t\t\t\thandleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),\n\n\t\t\t\t\t// Publishing & Scheduling handlers\n\t\t\t\t\thandleContentPublish: runtime.handleContentPublish.bind(runtime),\n\t\t\t\t\thandleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),\n\t\t\t\t\thandleContentSchedule: runtime.handleContentSchedule.bind(runtime),\n\t\t\t\t\thandleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),\n\t\t\t\t\thandleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),\n\t\t\t\t\thandleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),\n\t\t\t\t\thandleContentCompare: runtime.handleContentCompare.bind(runtime),\n\t\t\t\t\thandleContentTranslations: runtime.handleContentTranslations.bind(runtime),\n\n\t\t\t\t\t// Media handlers\n\t\t\t\t\thandleMediaList: runtime.handleMediaList.bind(runtime),\n\t\t\t\t\thandleMediaGet: runtime.handleMediaGet.bind(runtime),\n\t\t\t\t\thandleMediaCreate: runtime.handleMediaCreate.bind(runtime),\n\t\t\t\t\thandleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),\n\t\t\t\t\thandleMediaDelete: runtime.handleMediaDelete.bind(runtime),\n\n\t\t\t\t\t// Revision handlers\n\t\t\t\t\thandleRevisionList: runtime.handleRevisionList.bind(runtime),\n\t\t\t\t\thandleRevisionGet: runtime.handleRevisionGet.bind(runtime),\n\t\t\t\t\thandleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),\n\n\t\t\t\t\t// Plugin routes\n\t\t\t\t\thandlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),\n\t\t\t\t\thandlePublicPluginApiRoute: createPublicPluginApiRouteHandler(runtime),\n\t\t\t\t\tgetPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),\n\n\t\t\t\t\t// Media provider methods\n\t\t\t\t\tgetMediaProvider: runtime.getMediaProvider.bind(runtime),\n\t\t\t\t\tgetMediaProviderList: runtime.getMediaProviderList.bind(runtime),\n\n\t\t\t\t\t// Page contribution methods (for EmDashHead/EmDashBodyStart/EmDashBodyEnd)\n\t\t\t\t\tcollectPageMetadata: runtime.collectPageMetadata.bind(runtime),\n\t\t\t\t\tcollectPageFragments: runtime.collectPageFragments.bind(runtime),\n\n\t\t\t\t\t// Lazy search index health check — search endpoints call this\n\t\t\t\t\t// before querying so a crash-corrupted index gets repaired on\n\t\t\t\t\t// first use rather than stalling every cold start.\n\t\t\t\t\tensureSearchHealthy: runtime.ensureSearchHealthy.bind(runtime),\n\n\t\t\t\t\t// Direct access (for advanced use cases)\n\t\t\t\t\tstorage: runtime.storage,\n\t\t\t\t\t// Lazy getter, not an eager snapshot: `locals.emdash` is built\n\t\t\t\t\t// before the per-request scoped db is installed in ALS, so reading\n\t\t\t\t\t// `runtime.db` here would capture the per-isolate singleton. Routes\n\t\t\t\t\t// access `emdash.db` later, during the request, when the scoped db\n\t\t\t\t\t// is active. For a stateless binding (D1) the two are equivalent,\n\t\t\t\t\t// but for a request-bound connection (pg/Hyperdrive) the singleton\n\t\t\t\t\t// belongs to the cold-start request and reusing it from a warm\n\t\t\t\t\t// request hangs on workerd's cross-request I/O guard.\n\t\t\t\t\tget db() {\n\t\t\t\t\t\treturn runtime.db;\n\t\t\t\t\t},\n\t\t\t\t\tgetPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),\n\t\t\t\t\thooks: runtime.hooks,\n\t\t\t\t\temail: runtime.email,\n\t\t\t\t\tconfiguredPlugins: runtime.configuredPlugins,\n\t\t\t\t\tsandboxedPluginEntries: runtime.sandboxedPluginEntries,\n\n\t\t\t\t\t// Configuration (for checking database type, auth mode, etc.)\n\t\t\t\t\tconfig,\n\n\t\t\t\t\t// Lazy manifest accessor — admin-only consumers call this on\n\t\t\t\t\t// demand. `requestCached` inside `getManifest` dedupes within\n\t\t\t\t\t// a single request.\n\t\t\t\t\tgetManifest: runtime.getManifest.bind(runtime),\n\n\t\t\t\t\t// Clear the URL pattern cache after schema mutations that\n\t\t\t\t\t// affect collection URL patterns.\n\t\t\t\t\tinvalidateUrlPatternCache,\n\n\t\t\t\t\t// Sandbox runner (for marketplace plugin install/update)\n\t\t\t\t\tgetSandboxRunner: runtime.getSandboxRunner.bind(runtime),\n\t\t\t\t\tisSandboxBypassed: runtime.isSandboxBypassed.bind(runtime),\n\n\t\t\t\t\t// Sync marketplace plugin states (after install/update/uninstall)\n\t\t\t\t\tsyncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),\n\n\t\t\t\t\t// Sync registry plugin states (after install/update/uninstall)\n\t\t\t\t\tsyncRegistryPlugins: runtime.syncRegistryPlugins.bind(runtime),\n\n\t\t\t\t\t// Update plugin enabled/disabled status and rebuild hook pipeline\n\t\t\t\t\tsetPluginStatus: runtime.setPluginStatus.bind(runtime),\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"EmDash middleware error:\", error);\n\t\t\t}\n\n\t\t\t// Ask the adapter for a request-scoped db. When it returns one, we stash\n\t\t\t// it in ALS so the runtime's db getter and loader's getDb() pick it up,\n\t\t\t// then call commit() after next() so the adapter can persist any\n\t\t\t// per-request state (e.g. a D1 bookmark cookie for read-your-writes).\n\t\t\tconst scoped = createRequestScopedDb({\n\t\t\t\tconfig: config?.database?.config,\n\t\t\t\tisAuthenticated: !!sessionUser || hasBearerAuth,\n\t\t\t\tisWrite: request.method !== \"GET\" && request.method !== \"HEAD\",\n\t\t\t\tcookies: context.cookies,\n\t\t\t\turl,\n\t\t\t});\n\n\t\t\tconst renderAndFinalize = async () => {\n\t\t\t\tconst t0 = performance.now();\n\t\t\t\tconst response = await next();\n\t\t\t\ttimings.push({ name: \"render\", dur: performance.now() - t0, desc: \"Page render\" });\n\t\t\t\ttimings.push({ name: \"mw\", dur: performance.now() - mwStart, desc: \"Total middleware\" });\n\t\t\t\tpushMetricsTimings(timings, metrics);\n\t\t\t\t// Server-Timing only sees pre-stream queries; the stream-end\n\t\t\t\t// wrapper (instrumentation-gated, no-op otherwise) emits the\n\t\t\t\t// final counters once the body finishes streaming.\n\t\t\t\treturn wrapBodyForStreamMetrics(finalizeResponse(response, timings));\n\t\t\t};\n\n\t\t\tif (scoped) {\n\t\t\t\tconst parent = getRequestContext();\n\t\t\t\tconst ctx = parent\n\t\t\t\t\t? { ...parent, db: scoped.db }\n\t\t\t\t\t: { editMode: false, db: scoped.db, metrics };\n\t\t\t\treturn runWithContext(ctx, () =>\n\t\t\t\t\t// commit() persists per-request state (e.g. the D1 bookmark cookie)\n\t\t\t\t\t// before the response returns, even if render throws; close()\n\t\t\t\t\t// (connection teardown) is deferred to stream-end. See finishScoped.\n\t\t\t\t\tfinishScoped(scoped, renderAndFinalize),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn renderAndFinalize();\n\t\t}; // end doInit\n\n\t\tif (playgroundDb) {\n\t\t\t// Read the edit-mode cookie to determine if visual editing is active.\n\t\t\t// Default to false -- editing is opt-in via the playground toolbar toggle.\n\t\t\tconst editMode = context.cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\t\t// Playground DBs are per-session isolated instances whose schema is\n\t\t\t// independent of the configured one — flag as isolated so schema-\n\t\t\t// derived caches (manifest, taxonomy defs) rebuild against it.\n\t\t\tconst parent = getRequestContext();\n\t\t\tconst ctx = parent\n\t\t\t\t? { ...parent, editMode, db: playgroundDb, dbIsIsolated: true }\n\t\t\t\t: { editMode, db: playgroundDb, dbIsIsolated: true, metrics };\n\t\t\treturn runWithContext(ctx, doInit);\n\t\t}\n\t\treturn doInit();\n\t};\n\n\ttry {\n\t\treturn await runWithContext({ editMode: false, queryRecorder, metrics }, run);\n\t} finally {\n\t\t// Streamed responses defer the flush to stream end (see\n\t\t// wrapBodyForStreamMetrics) so the log captures queries issued while\n\t\t// the body renders. Only flush here for responses that were not\n\t\t// wrapped (no body: redirects, 304s, bodyless errors), where all\n\t\t// queries have already run by the time middleware returns.\n\t\tif (queryRecorder && !queryRecorder.deferredFlush) flushRecorder(queryRecorder);\n\t}\n});\n\nexport default onRequest;\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","virtualPlugins","virtualCreateDialect","virtualCreateCoalescingDialect","virtualCreateStorage","virtualCreateScheduler","virtualSandboxedPlugins","virtualMediaProviders","createRequestScopedDb","virtualCreateRequestScopedDb"],"sources":["../../src/cleanup.ts","../../src/comments/moderator.ts","../../src/scheduled-publish.ts","../../src/emdash-runtime.ts","../../src/media/url.ts","../../src/astro/middleware/scoped-db.ts","../../src/astro/middleware/stream-end-metrics.ts","../../src/astro/prefetch.ts","../../src/astro/public-plugin-api-routes.ts","../../src/astro/middleware.ts"],"sourcesContent":["/**\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 }>`\n\t\tSELECT collection, entry_id\n\t\tFROM revisions\n\t\tGROUP BY collection, entry_id\n\t\tHAVING COUNT(*) > ${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 * Scheduled publishing sweep\n *\n * Promotes content whose scheduled publish time has passed. Driven by the\n * platform scheduler alongside cron ticks and system cleanup — never by a\n * request. On Node the cron scheduler's maintenance pass calls it; on\n * Cloudflare the Worker's `scheduled()` handler does.\n *\n * Like `runSystemCleanup`, each collection sweep is independent and non-fatal:\n * one collection failing must not stop the rest.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { handleContentPublish } from \"./api/handlers/content.js\";\nimport { ContentRepository } from \"./database/repositories/content.js\";\nimport type { Database } from \"./database/types.js\";\nimport { SchemaRegistry } from \"./schema/registry.js\";\n\n/** A content item that was promoted to published by a sweep. */\nexport interface PublishedRef {\n\tcollection: string;\n\tid: string;\n}\n\n/**\n * Default cap on items promoted per collection in a single sweep. Bounds the\n * publish/webhook fan-out of one tick so a large backlog can't exhaust a Worker\n * invocation's CPU/subrequest budget; the remainder drains on later ticks.\n */\nexport const SCHEDULED_PUBLISH_BATCH_LIMIT = 100;\n\n/**\n * Publishes a single content item. Mirrors the relevant subset of\n * `handleContentPublish`'s return shape. Production callers pass\n * `EmDashRuntime.handleContentPublish` so `content:afterPublish` hooks fire\n * (search indexing, webhooks, syndication); the default falls back to the raw\n * handler (no hooks) for callers that have only a `db`.\n */\nexport type ScheduledPublishFn = (\n\tcollection: string,\n\tid: string,\n\toptions: { publishedAt?: string; requireScheduledDue?: boolean },\n) => Promise<{ success: boolean; error?: { code?: string } }>;\n\nexport interface PublishDueContentOptions {\n\t/**\n\t * Publish callback. Production callers pass the runtime's\n\t * `handleContentPublish` so `content:afterPublish` hooks fire (search\n\t * indexing, webhooks, syndication). Defaults to the raw DB handler (no hooks).\n\t */\n\tpublish?: ScheduledPublishFn;\n\t/**\n\t * Invoked after each collection's batch with the items promoted in that\n\t * batch. Lets request-less callers (the Cloudflare `scheduled()` handler)\n\t * purge edge-cache tags incrementally instead of only after the whole sweep,\n\t * so a runtime killed mid-sweep strands at most one batch behind stale cache\n\t * rather than everything published so far. Failures are logged, never fatal.\n\t */\n\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t/**\n\t * Maximum items promoted per collection per sweep. Defaults to\n\t * `SCHEDULED_PUBLISH_BATCH_LIMIT`. Pass `0` (or a negative) for unbounded.\n\t */\n\tlimit?: number;\n}\n\n/**\n * Publish every content item whose `scheduled_at` is in the past.\n *\n * Iterates all collections, finds due items (`findReadyToPublish` returns both\n * scheduled drafts and published entries with pending scheduled changes), and\n * publishes each. `publish()` clears `scheduled_at`, so a second sweep is a\n * no-op — safe to run on every tick.\n *\n * Bounded per collection by `limit` (default `SCHEDULED_PUBLISH_BATCH_LIMIT`):\n * a large backlog drains across successive ticks rather than in one unbounded\n * pass. After each collection's batch, `onPublished` (if given) is awaited so\n * cache-tag invalidation happens incrementally, not just at the very end.\n *\n * Returns every item it promoted so request-less callers (the Cloudflare\n * `scheduled()` handler) can also act on the full set.\n */\nexport async function publishDueContent(\n\tdb: Kysely<Database>,\n\toptions: PublishDueContentOptions = {},\n): Promise<PublishedRef[]> {\n\tconst { publish, onPublished, limit = SCHEDULED_PUBLISH_BATCH_LIMIT } = options;\n\tconst published: PublishedRef[] = [];\n\n\tlet collections;\n\ttry {\n\t\tcollections = await new SchemaRegistry(db).listCollections();\n\t} catch (error) {\n\t\tconsole.error(\"[scheduled-publish] Failed to list collections:\", error);\n\t\treturn published;\n\t}\n\n\tconst repo = new ContentRepository(db);\n\tconst doPublish: ScheduledPublishFn =\n\t\tpublish ?? ((collection, id, opts) => handleContentPublish(db, collection, id, opts));\n\t// 0 / negative means unbounded; findReadyToPublish treats that as \"no LIMIT\".\n\tconst batchLimit = limit > 0 ? limit : undefined;\n\n\tfor (const collection of collections) {\n\t\ttry {\n\t\t\tconst due = await repo.findReadyToPublish(collection.slug, batchLimit);\n\t\t\tconst batch: PublishedRef[] = [];\n\t\t\tfor (const item of due) {\n\t\t\t\t// First publication of a scheduled draft should record the intended\n\t\t\t\t// scheduled time, not the (later) sweep time. Items already published\n\t\t\t\t// with pending draft changes keep their original published_at.\n\t\t\t\tconst publishedAt = item.publishedAt == null ? (item.scheduledAt ?? undefined) : undefined;\n\t\t\t\tconst result = await doPublish(collection.slug, item.id, {\n\t\t\t\t\tpublishedAt,\n\t\t\t\t\trequireScheduledDue: true,\n\t\t\t\t});\n\t\t\t\tif (result.success) {\n\t\t\t\t\tbatch.push({ collection: collection.slug, id: item.id });\n\t\t\t\t} else if (result.error?.code === \"NOT_DUE\") {\n\t\t\t\t\t// Unscheduled or rescheduled between selection and publish — the\n\t\t\t\t\t// editor changed their mind; skip quietly, not a failure.\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[scheduled-publish] Failed to publish ${collection.slug}/${item.id}:`,\n\t\t\t\t\t\tresult.error,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (batch.length > 0) {\n\t\t\t\tpublished.push(...batch);\n\t\t\t\tif (onPublished) {\n\t\t\t\t\t// Purge this batch's cache tags before moving to the next\n\t\t\t\t\t// collection, so a mid-sweep kill can't strand already-published\n\t\t\t\t\t// content behind stale cache.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait onPublished(batch);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[scheduled-publish] onPublished failed after \"${collection.slug}\" batch:`,\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(`[scheduled-publish] Sweep failed for \"${collection.slug}\":`, error);\n\t\t}\n\t}\n\n\treturn published;\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\";\nimport virtualConfig from \"virtual:emdash/config\";\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 { getTrustedProxyHeaders } from \"./auth/trusted-proxy.js\";\nimport { isSqlite } from \"./database/dialect-helpers.js\";\nimport { kyselyLogOption } from \"./database/instrumentation.js\";\nimport { MIGRATION_RACE_WAIT_MS, runMigrations } from \"./database/migrations/runner.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type {\n\tContentItem as ContentItemInternal,\n\tContentDateField,\n} from \"./database/repositories/types.js\";\nimport { validateIdentifier } from \"./database/validate.js\";\nimport { normalizeMediaValue } from \"./media/normalize.js\";\nimport type { MediaProvider, MediaProviderCapabilities } from \"./media/types.js\";\nimport {\n\tdeleteContentMediaUsage,\n\tfindNonTranslatableSiblingContentIds,\n\tmarkContentMediaUsageCollectionStale,\n\trefreshContentMediaUsageAfterWrite,\n} from \"./media/usage/content-refresh.js\";\nimport type { SandboxedPluginInstance, 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\tPortableTextBlockConfig,\n\tFieldWidgetConfig,\n} from \"./plugins/types.js\";\nimport type { FieldType } from \"./schema/types.js\";\nimport { hashString } from \"./utils/hash.js\";\nimport { createInitLock, type InitLock, initWithLock } from \"./utils/init-lock.js\";\nimport { createSingleFlightCache, singleFlightCached } from \"./utils/single-flight-cache.js\";\nimport { COMMIT, VERSION } from \"./version.js\";\n\nconst LEADING_SLASH_PATTERN = /^\\//;\n\n/**\n * Parse a JSON column expected to contain an array of strings.\n *\n * Throws on malformed JSON rather than returning []; callers are responsible\n * for deciding how to handle/log the error. Empty string / null inputs return\n * [] (they represent \"no value\"). Non-string array entries are filtered out.\n */\nfunction parseStringArray(raw: string | null | undefined): string[] {\n\tif (!raw) return [];\n\tconst parsed: unknown = JSON.parse(raw);\n\tif (!Array.isArray(parsed)) return [];\n\treturn parsed.filter((v): v is string => typeof v === \"string\");\n}\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\"nlweb\",\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 { after } from \"./after.js\";\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 { validateEncryptionKeyAtStartup } from \"./config/secrets.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport {\n\thandleContentList,\n\thandleContentAuthors,\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 type { CronScheduler } from \"./plugins/scheduler/types.js\";\nimport { PluginStateRepository } from \"./plugins/state.js\";\nimport { normalizeRegistryConfig } from \"./registry/config.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { publishDueContent, type PublishedRef } from \"./scheduled-publish.js\";\nimport { FTSManager } from \"./search/fts-manager.js\";\nimport { invalidateSiteSettingsCache } from \"./settings/index.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\turl: \"url\",\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\trepeater: \"repeater\",\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/** Portable Text block types contributed to the editor (declarative Block Kit) */\n\tportableTextBlocks?: PortableTextBlockConfig[];\n\t/** Field widget types contributed for schema-field editing UIs */\n\tfieldWidgets?: FieldWidgetConfig[];\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\t/**\n\t * Resolver for the live connection, preferred over `db` by providers that\n\t * query EmDash's database. Resolves the current request/event-scoped\n\t * connection from ALS so connection-backed adapters (Postgres over\n\t * Hyperdrive) don't reuse the per-isolate singleton's socket across events.\n\t * Providers should resolve per operation rather than capturing `db` once.\n\t * Omitted-safe: falls back to `db` for stateless adapters (D1, Node SQLite).\n\t */\n\tgetDb?: () => Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * Builds the timer-based scheduler that drives cron ticks and maintenance.\n * Injected via `virtual:emdash/scheduler` so the platform — not core — decides\n * whether a long-lived heartbeat exists.\n */\nexport type CreateSchedulerFn = (executor: CronExecutor) => CronScheduler;\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/**\n\t * Factory for a dialect that batches same-turn reads into one round trip\n\t * ({@link EmDashRuntime.create} uses it for the cold-start read phase).\n\t * Present only on batching backends (D1, DO); absent backends fall back to\n\t * the singleton. Returns a fresh connection each call — it must never be the\n\t * long-lived singleton, whose coalescing buffer would be shared across\n\t * requests.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateCoalescingDialect?: (config: any) => Dialect | null;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateStorage: ((config: any) => Storage) | null;\n\tsandboxEnabled: boolean;\n\t/** sandbox: false escape hatch - load sandboxed plugins in-process */\n\tsandboxBypassed?: boolean;\n\t/**\n\t * Factory for the timer-based cron/maintenance heartbeat. Supplied by the\n\t * generated `virtual:emdash/scheduler` module: a `NodeCronScheduler` factory\n\t * on long-lived runtimes (Node/Bun), or `null` on serverless adapters where\n\t * an external driver (e.g. the Cloudflare Worker's `scheduled()` Cron\n\t * Trigger) calls `runScheduledTasks()` instead. When absent or null, the\n\t * runtime starts no scheduler. Keeping the platform decision in the\n\t * integration means core has no adapter-specific runtime checks.\n\t */\n\tcreateScheduler?: CreateSchedulerFn | null;\n\t/** Media provider entries from virtual module */\n\tmediaProviderEntries?: MediaProviderEntry[];\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\t/** Factory function matching SandboxRunnerFactory signature */\n\tcreateSandboxRunner:\n\t\t| ((opts: {\n\t\t\t\tdb: Kysely<Database>;\n\t\t\t\tmediaStorage?: {\n\t\t\t\t\tupload(options: { key: string; body: Uint8Array; contentType: string }): Promise<unknown>;\n\t\t\t\t\tdelete(key: string): Promise<unknown>;\n\t\t\t\t};\n\t\t }) => SandboxRunner)\n\t\t| null;\n}\n\n/**\n * Constructor parameters for `EmDashRuntime`.\n *\n * Production code should use `EmDashRuntime.create()` which discovers and\n * loads all parts (database, plugins, hooks, cron, etc.) and then calls the\n * constructor. Direct construction is supported for callers that already\n * have all the dependencies in hand — for example, integration tests that\n * supply a pre-migrated database and an empty plugin set.\n *\n * Every field corresponds 1:1 to internal state set on the runtime — none of\n * these are derived. If you don't have a value for one, see what `create()`\n * passes for that field as the canonical default.\n */\nexport interface EmDashRuntimeParts {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n\tconfiguredPlugins: ResolvedPlugin[];\n\tsandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\thooks: HookPipeline;\n\tenabledPlugins: Set<string>;\n\tpluginStates: Map<string, string>;\n\tconfig: EmDashConfig;\n\tmediaProviders: Map<string, MediaProvider>;\n\tmediaProviderEntries: MediaProviderEntry[];\n\tcronExecutor: CronExecutor | null;\n\tcronScheduler: CronScheduler | null;\n\temailPipeline: EmailPipeline | null;\n\tallPipelinePlugins: ResolvedPlugin[];\n\tpipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tgetDb?: () => Kysely<Database>;\n\t\tstorage?: Storage;\n\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t};\n\truntimeDeps: RuntimeDependencies;\n\tpipelineRef: { current: HookPipeline };\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/**\n * Db init lock reclaim deadline. Derived from the migration race wait so\n * they can't drift apart: a healthy init can legitimately block for the\n * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold\n * connect and migrator work, before it should be presumed dead. The outer\n * runtime init lock (middleware.ts) must use a strictly larger deadline —\n * it wraps create() → getDatabase() → this lock, and equal deadlines would\n * let the outer reclaim while the inner is legitimately still working.\n */\nexport const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;\n\n/**\n * Db cache + its init lock live on globalThis behind a Symbol: the bundler\n * can duplicate this module across SSR chunks (same reasoning as\n * request-cache.ts), and a duplicated cache/lock would mean concurrent\n * independent db inits — and duplicate migrators — per isolate.\n */\nconst DB_HOLDER_KEY = Symbol.for(\"emdash:db-cache\");\ninterface DbHolder {\n\tcache: Map<string, Kysely<Database>>;\n\tlock: InitLock;\n}\nconst globalSymbolStore = globalThis as Record<symbol, unknown>;\nfunction getDbHolder(): DbHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;\n\tif (!holder) {\n\t\tholder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };\n\t\tglobalSymbolStore[DB_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\n\n/**\n * Auto-seed runs at most once per isolate per database. Its lock + \"done\" set\n * live on globalThis (same bundler-duplication reasoning as the db cache) so a\n * reclaimed-and-rerun `create()` can't seed a second time concurrently. The\n * lock polls rather than sharing a promise, so it is safe to await across a\n * cancelled owner in workerd.\n */\nconst SEED_HOLDER_KEY = Symbol.for(\"emdash:seed-state\");\ninterface SeedHolder {\n\tdone: Set<string>;\n\tlock: InitLock;\n}\nfunction getSeedHolder(): SeedHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[SEED_HOLDER_KEY] as SeedHolder | undefined;\n\tif (!holder) {\n\t\tholder = { done: new Set<string>(), lock: createInitLock() };\n\t\tglobalSymbolStore[SEED_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\nconst storageCache = new Map<string, Storage>();\nconst sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();\n/**\n * Per-tier sets of `${pluginId}:${version}` keys present in\n * `sandboxedPluginCache`. Used during sync to know which entries belong\n * to which install source so we can invalidate only what belongs to the\n * tier currently being synced.\n */\nconst marketplacePluginKeys = new Set<string>();\nconst registryPluginKeys = new Set<string>();\n/**\n * Manifest metadata for runtime-installed sandboxed plugins (marketplace\n * and registry both). Keyed by `pluginId`; readers don't care which\n * source the plugin came from. Named `marketplace*` for legacy reasons.\n */\nconst marketplaceManifestCache = new Map<\n\tstring,\n\t{\n\t\tid: string;\n\t\tversion: string;\n\t\tadmin?: {\n\t\t\tpages?: PluginAdminPage[];\n\t\t\twidgets?: PluginDashboardWidget[];\n\t\t};\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, SandboxedPluginInstance>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\t/**\n\t * Schema registry bound to the current request/event-scoped connection.\n\t * Built per access (SchemaRegistry just wraps a db) against `this.db`, the\n\t * ALS-aware getter — never a captured snapshot of the singleton. On a\n\t * connection-backed adapter (Postgres over Hyperdrive) a captured singleton\n\t * would query a socket opened by an earlier event and trip workerd's\n\t * cross-request I/O guard; the catch in handlers like handleContentUpdate\n\t * would then silently treat a revision-enabled collection as non-revisioned\n\t * and write draft edits to live columns. Same reasoning as the per-call\n\t * registry in _buildManifest().\n\t */\n\tget schemaRegistry(): SchemaRegistry {\n\t\treturn new SchemaRegistry(this.db);\n\t}\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/**\n\t * Isolate-lifetime guard so FTS indexes are verified at most once per\n\t * worker rather than on every admin request. See ensureSearchHealthy().\n\t * Uses the poison-immune single-flight cache (never a shared awaitable\n\t * promise) so a cancelled first caller can't wedge later ones.\n\t */\n\tprivate readonly _searchHealthCache = createSingleFlightCache<void>();\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\tgetDb?: () => 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/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\tconstructor(parts: EmDashRuntimeParts) {\n\t\tthis._db = parts.db;\n\t\tthis.storage = parts.storage;\n\t\tthis.configuredPlugins = parts.configuredPlugins;\n\t\tthis.sandboxedPlugins = parts.sandboxedPlugins;\n\t\tthis.sandboxedPluginEntries = parts.sandboxedPluginEntries;\n\t\tthis._hooks = parts.hooks;\n\t\tthis.enabledPlugins = parts.enabledPlugins;\n\t\tthis.pluginStates = parts.pluginStates;\n\t\tthis.config = parts.config;\n\t\tthis.mediaProviders = parts.mediaProviders;\n\t\tthis.mediaProviderEntries = parts.mediaProviderEntries;\n\t\tthis.cronExecutor = parts.cronExecutor;\n\t\tthis.cronScheduler = parts.cronScheduler;\n\t\tthis.email = parts.emailPipeline;\n\t\tthis.allPipelinePlugins = parts.allPipelinePlugins;\n\t\tthis.pipelineFactoryOptions = parts.pipelineFactoryOptions;\n\t\tthis.runtimeDeps = parts.runtimeDeps;\n\t\tthis.pipelineRef = parts.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 * Whether the sandbox bypass mode (sandbox: false) is active.\n\t * Marketplace install/update handlers use this to skip the\n\t * SANDBOX_NOT_AVAILABLE gate, since the bypass path loads\n\t * marketplace plugins in-process via syncMarketplacePlugins().\n\t */\n\tisSandboxBypassed(): boolean {\n\t\treturn this.runtimeDeps.sandboxBypassed === true;\n\t}\n\n\t/**\n\t * Publish any content whose scheduled time has passed.\n\t * Returns the items promoted so callers can invalidate their cache tags.\n\t */\n\tasync publishScheduled(): Promise<PublishedRef[]> {\n\t\treturn publishDueContent(this.db, {\n\t\t\tpublish: (collection, id, options) => this.handleContentPublish(collection, id, options),\n\t\t});\n\t}\n\n\t/**\n\t * Run the full scheduled-maintenance batch: cron tasks, scheduled\n\t * publishing, and system cleanup. For request-less drivers — the\n\t * Cloudflare `scheduled()` handler invokes this from a Cron Trigger.\n\t * (On Node the timer-based scheduler drives the same work itself.)\n\t *\n\t * Each step is independent and non-fatal. Returns the content promoted\n\t * by the publishing sweep so the caller can purge edge-cache tags.\n\t *\n\t * `onPublished` (optional) is awaited after each collection's batch so a\n\t * request-less driver can invalidate edge-cache tags incrementally rather\n\t * than only after the whole sweep — bounding stale-cache exposure if the\n\t * runtime is killed mid-sweep.\n\t */\n\tasync runScheduledTasks(\n\t\toptions: {\n\t\t\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t\t} = {},\n\t): Promise<{ published: PublishedRef[] }> {\n\t\tif (this.cronExecutor) {\n\t\t\ttry {\n\t\t\t\tawait this.cronExecutor.tick();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Tick failed:\", error);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.cronExecutor.recoverStaleLocks();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Stale lock recovery failed:\", error);\n\t\t\t}\n\t\t}\n\n\t\tlet published: PublishedRef[] = [];\n\t\ttry {\n\t\t\t// Route through the runtime wrapper so content:afterPublish hooks fire.\n\t\t\tpublished = await publishDueContent(this.db, {\n\t\t\t\tpublish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),\n\t\t\t\tonPublished: options.onPublished,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t}\n\n\t\ttry {\n\t\t\tawait runSystemCleanup(this.db, this.storage ?? undefined);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t}\n\n\t\treturn { published };\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\tawait this.rebuildHookPipeline();\n\t\t\tawait this._hooks.runPluginActivate(pluginId);\n\t\t} else {\n\t\t\t// Fire deactivate on the current pipeline while the plugin is still in it\n\t\t\tawait this._hooks.runPluginDeactivate(pluginId);\n\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\tawait this.rebuildHookPipeline();\n\t\t}\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\t// db/getDb are already wired by createHookPipeline above (they live in\n\t\t\t// pipelineFactoryOptions), so the merge only adds emailPipeline.\n\t\t\tnewPipeline.setContextFactory({ 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) return;\n\n\t\t// In sandbox bypass mode (sandbox: false), the noop runner reports\n\t\t// unavailable but we still want admin metadata for newly installed\n\t\t// marketplace plugins to refresh in-process. Hooks/routes still won't\n\t\t// execute (matches the cold-start bypass behavior), but Configure\n\t\t// links and admin pages appear immediately.\n\t\tif (this.runtimeDeps.sandboxBypassed) {\n\t\t\tawait this.syncMarketplacePluginsBypassed();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.syncSandboxedSourcePlugins(\"marketplace\");\n\t}\n\n\t/**\n\t * Synchronize registry plugin runtime state with DB + storage.\n\t *\n\t * Mirrors {@link syncMarketplacePlugins} for plugins installed via the\n\t * experimental decentralized plugin registry. Called after install,\n\t * update, and uninstall handlers complete.\n\t */\n\tasync syncRegistryPlugins(): Promise<void> {\n\t\tif (!this.config.experimental?.registry) return;\n\t\tawait this.syncSandboxedSourcePlugins(\"registry\");\n\t}\n\n\t/**\n\t * Internal: reconcile in-memory sandboxed-plugin state with the\n\t * `_plugin_state` table for the given source tier. Shared\n\t * implementation behind {@link syncMarketplacePlugins} and\n\t * {@link syncRegistryPlugins}.\n\t *\n\t * Each source tier has its own key set in `${source}PluginKeys` so a\n\t * sync for one tier doesn't invalidate the other.\n\t */\n\tprivate async syncSandboxedSourcePlugins(source: \"marketplace\" | \"registry\"): Promise<void> {\n\t\tif (!this.storage) return;\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) return;\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst states =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of states) {\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\t// Marketplace plugins use `marketplaceVersion` when present;\n\t\t\t\t// registry plugins always use `version`.\n\t\t\t\tconst desiredVersion =\n\t\t\t\t\tsource === \"marketplace\" ? (state.marketplaceVersion ?? state.version) : state.version;\n\t\t\t\tdesired.set(state.pluginId, desiredVersion);\n\t\t\t}\n\n\t\t\t// Remove uninstalled or no-longer-active plugins from memory.\n\t\t\tconst keysToRemove: string[] = [];\n\t\t\tfor (const key of keySet) {\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\tkeySet.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 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\tkeySet.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, source);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: ${source} 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\tkeySet.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 ${source} plugins:`, error);\n\t\t}\n\t}\n\n\t/**\n\t * Remove a plugin from the in-memory pipeline lists by ID.\n\t * Mutates allPipelinePlugins and configuredPlugins in place.\n\t */\n\tprivate removePluginFromLists(pluginId: string): void {\n\t\tconst allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId);\n\t\tif (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1);\n\t\tconst configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId);\n\t\tif (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1);\n\t}\n\n\t/**\n\t * Sync marketplace plugin metadata in sandbox: false bypass mode.\n\t *\n\t * In bypass mode the noop runner can't load plugins, but admin pages,\n\t * widgets, and route metadata still need to refresh in-process when an\n\t * admin installs/updates/uninstalls a marketplace plugin. Otherwise the\n\t * admin UI shows stale data until the server restarts.\n\t *\n\t * Hooks and routes still won't execute under bypass (matches the\n\t * cold-start bypass behavior in loadMarketplacePluginsBypassed).\n\t *\n\t * Known limitation: bypass plugins are loaded via `import(dataUrl)`,\n\t * which Node's ESM cache keys on the full URL. Updates create fresh\n\t * module objects, but old ones remain cached for the worker's lifetime.\n\t * In practice this is a few KB per update — only matters for sites with\n\t * very frequent marketplace updates running long-lived processes. The\n\t * fix would be vm.SourceTextModule for explicit lifecycle management.\n\t */\n\tprivate async syncMarketplacePluginsBypassed(): Promise<void> {\n\t\tif (!this.storage) return;\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// Drop metadata for plugins no longer active.\n\t\t\tconst toRemove: string[] = [];\n\t\t\tfor (const pluginId of marketplaceManifestCache.keys()) {\n\t\t\t\tif (!desired.has(pluginId)) toRemove.push(pluginId);\n\t\t\t}\n\t\t\tfor (const pluginId of toRemove) {\n\t\t\t\t// Fire plugin:deactivate hook before removal\n\t\t\t\tconst resolved = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (resolved) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst deactivateHook = resolved.hooks?.[\"plugin:deactivate\"];\n\t\t\t\t\t\tif (deactivateHook) {\n\t\t\t\t\t\t\tconst handler =\n\t\t\t\t\t\t\t\ttypeof deactivateHook === \"function\" ? deactivateHook : deactivateHook.handler;\n\t\t\t\t\t\t\tif (typeof handler === \"function\") {\n\t\t\t\t\t\t\t\t// Sandbox-bypass cleanup: the plugin context isn't constructable\n\t\t\t\t\t\t\t\t// here (no DB binding, no media, etc.), but well-behaved\n\t\t\t\t\t\t\t\t// deactivate hooks should be no-op safe. If a hook does require\n\t\t\t\t\t\t\t\t// ctx, it throws and the surrounding catch logs it.\n\t\t\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- best-effort cleanup; see comment above\n\t\t\t\t\t\t\t\tawait handler({ pluginId }, {} as never);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.warn(`[emdash] plugin:deactivate hook failed for ${pluginId}:`, err);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t// Remove from pipeline lists too (mutate in place since the\n\t\t\t\t// arrays are readonly references but mutable contents)\n\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t}\n\n\t\t\t// Load plugin code, adapt as trusted plugins, and add to pipeline lists\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\t\tconst newPlugins: ResolvedPlugin[] = [];\n\t\t\tfor (const [pluginId, version] of desired) {\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\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\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\n\t\t\t\t// Skip if already in the pipeline at this version\n\t\t\t\tconst existing = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (existing && existing.version === bundle.manifest.version) continue;\n\n\t\t\t\t// Remove any older version\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// Dynamic data: import returns `any` from a base64-encoded module.\n\t\t\t\t\t// We trust the bundle to be shaped like a plugin (built by plugin-cli);\n\t\t\t\t\t// adaptSandboxEntry then validates fields it cares about.\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\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\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tnewPlugins.push(adapted);\n\t\t\t\t\tthis.allPipelinePlugins.push(adapted);\n\t\t\t\t\tthis.configuredPlugins.push(adapted);\n\t\t\t\t\tthis.enabledPlugins.add(adapted.id);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${pluginId}@${version} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If anything changed, rebuild the hook pipeline so new/removed\n\t\t\t// plugins take effect immediately without a server restart.\n\t\t\tif (toRemove.length > 0 || newPlugins.length > 0) {\n\t\t\t\tawait this.rebuildHookPipeline();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash: Failed to sync marketplace plugins (bypass):\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create and initialize the runtime\n\t */\n\tstatic async create(\n\t\tdeps: RuntimeDependencies,\n\t\ttimings?: Array<{ name: string; dur: number; desc?: string }>,\n\t): Promise<EmDashRuntime> {\n\t\t// Helper: time a phase and push into the shared timings array when\n\t\t// provided. Uses performance.now() — monotonic across async boundaries.\n\t\t// No-op when `timings` wasn't passed (preserves backwards compatibility\n\t\t// with callers that don't care about per-phase breakdown).\n\t\tconst phase = async <T>(name: string, desc: string, fn: () => Promise<T>): Promise<T> => {\n\t\t\tif (!timings) return fn();\n\t\t\tconst t0 = performance.now();\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\ttimings.push({ name, dur: performance.now() - t0, desc });\n\t\t\t}\n\t\t};\n\n\t\t// Initialize database (connects, runs migrations if needed)\n\t\tconst db = await phase(\"rt.db\", \"DB init + migrations\", () => EmDashRuntime.getDatabase(deps));\n\n\t\t// Resolver for the live connection, mirroring the `get db()` getter\n\t\t// below (which can't be used here — the runtime instance doesn't exist\n\t\t// yet). Long-lived subsystems built during create() (cron executor,\n\t\t// plugin context factory, media providers) capture this resolver rather\n\t\t// than the `db` snapshot, so a connection-backed adapter (Postgres over\n\t\t// Hyperdrive) serves their queries from the current request/event-scoped\n\t\t// connection in ALS instead of the per-isolate singleton — whose socket\n\t\t// belongs to an earlier request and would trip workerd's cross-request\n\t\t// I/O guard. Stateless adapters (D1, Node SQLite) set no ALS db on most\n\t\t// paths, so this falls back to the singleton: unchanged behavior.\n\t\tconst resolveDb = (): Kysely<Database> => {\n\t\t\tconst ctx = getRequestContext();\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- ALS db is typed unknown to avoid a circular import; middleware always sets a Kysely<Database>\n\t\t\treturn (ctx?.db as Kysely<Database> | undefined) ?? db;\n\t\t};\n\n\t\t// Validate EMDASH_ENCRYPTION_KEY once here so a malformed value\n\t\t// surfaces in startup logs instead of as request-time 500s. The key\n\t\t// itself is not yet consumed (a follow-up PR adds plugin-secret\n\t\t// encryption); validating early just guards against silent\n\t\t// misconfiguration.\n\t\tawait phase(\"rt.secrets\", \"Validate encryption key\", () => validateEncryptionKeyAtStartup());\n\n\t\t// FTS verify/repair is deferred off the cold-start hot path.\n\t\t// See EmDashRuntime.ensureSearchHealthy().\n\n\t\t// Initialize storage (sync)\n\t\tconst storage = EmDashRuntime.getStorage(deps);\n\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\t// \"Already set up\" by default so a read failure (e.g. tables absent on a\n\t\t// pre-migration db) skips seeding rather than seeding a half-built db.\n\t\tlet seedGate = { collectionCount: 1, setupDone: true };\n\n\t\t// Seeding must only touch the configured singleton, never a borrowed\n\t\t// per-request db (playground / DO preview) or the loader-fallback db.\n\t\tconst reqCtx = getRequestContext();\n\t\tconst ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);\n\n\t\t// Run the init reads on a coalescing connection so the concurrent\n\t\t// same-turn reads flush as one batch() round trip. The singleton can't:\n\t\t// its plain SqliteAdapter reports supportsMultipleConnections=false, so\n\t\t// Kysely's connection mutex serializes concurrent reads into N round\n\t\t// trips. The connection is per-init and discarded — the long-lived\n\t\t// singleton must never coalesce or one request's reads could land in\n\t\t// another's batch. Backends without a coalescing dialect (Node/SQLite)\n\t\t// fall back to the singleton, where serialization costs nothing.\n\t\tlet readDb = db;\n\t\tlet readDbDisposable: Kysely<Database> | undefined;\n\t\tif (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) {\n\t\t\ttry {\n\t\t\t\tconst dialect = deps.createCoalescingDialect(deps.config.database.config);\n\t\t\t\tif (dialect) {\n\t\t\t\t\treadDb = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\t\t\t\t\treadDbDisposable = readDb;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treadDb = db;\n\t\t\t}\n\t\t}\n\t\tconst optionsRepo = new OptionsRepository(readDb);\n\n\t\tconst readSiteInfo = async () => {\n\t\t\tconst siteOpts = await optionsRepo.getMany<string>([\n\t\t\t\t\"emdash:site_title\",\n\t\t\t\t\"emdash:site_url\",\n\t\t\t\t\"emdash:locale\",\n\t\t\t]);\n\t\t\treturn {\n\t\t\t\tsiteName: siteOpts.get(\"emdash:site_title\") ?? undefined,\n\t\t\t\tsiteUrl: siteOpts.get(\"emdash:site_url\") ?? undefined,\n\t\t\t\tlocale: siteOpts.get(\"emdash:locale\") ?? undefined,\n\t\t\t};\n\t\t};\n\n\t\tconst coldStartReads: Array<Promise<void>> = [\n\t\t\tphase(\"rt.plugins\", \"Plugin states\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst states = await readDb\n\t\t\t\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t\t\t\t.select([\"plugin_id\", \"status\"])\n\t\t\t\t\t\t.execute();\n\t\t\t\t\tpluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));\n\t\t\t\t} catch {\n\t\t\t\t\t// _plugin_state may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t\tphase(\"rt.site\", \"Site info options\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch {\n\t\t\t\t\t// options may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t];\n\n\t\tif (ownsConfiguredDb) {\n\t\t\tcoldStartReads.push(\n\t\t\t\tphase(\"rt.seedcheck\", \"Auto-seed gate\", async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\t\t\treadDb\n\t\t\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\t\t\treadDb\n\t\t\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn !!setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})();\n\t\t\t\t\t\tseedGate = { collectionCount: collectionCount.count, setupDone };\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Leave the \"already set up\" default so a read failure never\n\t\t\t\t\t\t// triggers a seed onto a half-built db.\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tawait Promise.all(coldStartReads);\n\n\t\t// Auto-seed the default schema for a first load that skipped the setup\n\t\t// wizard (the wizard and dev-bypass apply seeds explicitly). Run under a\n\t\t// per-isolate lock keyed by the configured db so a reclaimed-and-rerun\n\t\t// create() can't apply the seed a second time concurrently.\n\t\tif (seedGate.collectionCount === 0 && !seedGate.setupDone) {\n\t\t\tconst seedKey = deps.config.database?.entrypoint ?? \"default\";\n\t\t\tconst seedHolder = getSeedHolder();\n\t\t\ttry {\n\t\t\t\tawait initWithLock(\n\t\t\t\t\tseedHolder.lock,\n\t\t\t\t\t() => (seedHolder.done.has(seedKey) ? true : undefined),\n\t\t\t\t\tasync () => {\n\t\t\t\t\t\tconst { applySeed } = await import(\"./seed/apply.js\");\n\t\t\t\t\t\tconst { loadSeed } = await import(\"./seed/load.js\");\n\t\t\t\t\t\tconst { validateSeed } = await import(\"./seed/validate.js\");\n\n\t\t\t\t\t\tconst seed = await loadSeed();\n\t\t\t\t\t\tconst validation = validateSeed(seed);\n\t\t\t\t\t\tif (validation.valid) {\n\t\t\t\t\t\t\tawait applySeed(db, seed, { onConflict: \"skip\" });\n\t\t\t\t\t\t\tconsole.log(\"Auto-seeded default collections\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tseedHolder.done.add(seedKey);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\t\t\t\t\t{ deadlineMs: DB_INIT_DEADLINE_MS, anchor: (promise) => after(() => promise) },\n\t\t\t\t);\n\t\t\t\t// The site-info read ran before the seed wrote its defaults, so\n\t\t\t\t// refresh the snapshot the plugin context sees.\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch {\n\t\t\t\t\t// Non-fatal — plugin context falls back to undefined fields.\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — a failed seed (e.g. missing seed module) leaves the\n\t\t\t\t// site un-seeded; a later request retries.\n\t\t\t}\n\t\t}\n\n\t\t// The read connection is single-use; everything below uses the singleton.\n\t\tif (readDbDisposable) {\n\t\t\ttry {\n\t\t\t\tawait readDbDisposable.destroy();\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — the underlying binding is shared and needs no teardown.\n\t\t\t}\n\t\t}\n\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// 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// Collected bypassed plugins (sandbox: false escape hatch).\n\t\t// These need to be added to BOTH the pipeline (for hooks) AND the\n\t\t// configuredPlugins list (for route dispatch).\n\t\tconst bypassedPluginsList: ResolvedPlugin[] = [];\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: [\"hooks.email-transport:register\"],\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: [\"users:read\"],\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// sandbox: false escape hatch - load sandboxed plugin entries in-process\n\t\t// as trusted plugins (no isolation) so they participate in the hook pipeline.\n\t\t// Block this on Cloudflare Workers where dynamic import(dataUrl) is not\n\t\t// available and running untrusted code in-process is a security risk.\n\t\tif (deps.sandboxBypassed && deps.sandboxedPluginEntries.length > 0) {\n\t\t\tconst isCfWorkers =\n\t\t\t\ttypeof navigator !== \"undefined\" &&\n\t\t\t\ttypeof navigator.userAgent === \"string\" &&\n\t\t\t\tnavigator.userAgent.includes(\"Cloudflare-Workers\");\n\t\t\tif (isCfWorkers) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"sandbox: false is not supported in Cloudflare Workers. \" +\n\t\t\t\t\t\t\"Remove the sandbox: false option or use the Cloudflare sandbox runner.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Sandboxed plugins will run in-process without isolation.\",\n\t\t\t);\n\t\t\tconst bypassedPlugins = await EmDashRuntime.loadBypassedPlugins(deps.sandboxedPluginEntries);\n\t\t\tfor (const plugin of bypassedPlugins) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\t// Respect plugin state: only enable if active or no record exists.\n\t\t\t\t// Plugins an admin previously disabled should stay disabled.\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// In bypass mode, also load marketplace plugins from R2 as trusted\n\t\t// in-process plugins BEFORE pipeline creation. They need to be in the\n\t\t// pipeline to participate in hook dispatch.\n\t\tif (deps.sandboxBypassed && deps.config.marketplace && storage) {\n\t\t\tconst marketplaceBypassed = await EmDashRuntime.loadMarketplacePluginsBypassed(db, storage);\n\t\t\tfor (const plugin of marketplaceBypassed) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\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. getDb travels here (not just via the email\n\t\t// setContextFactory call below) so it survives rebuildHookPipeline(),\n\t\t// which reconstructs the factory from pipelineFactoryOptions. Without it,\n\t\t// toggling a plugin on an email-less deployment would silently revert\n\t\t// plugin contexts to the singleton db — re-breaking connection-backed\n\t\t// adapters. See #1622.\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tgetDb: resolveDb,\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, sandbox runner path)\n\t\tconst sandboxedPlugins = await phase(\"rt.sandbox\", \"Sandboxed plugins\", () =>\n\t\t\tEmDashRuntime.loadSandboxedPlugins(deps, db, storage),\n\t\t);\n\n\t\t// Cold-start: load marketplace- and registry-installed plugins from\n\t\t// site R2 via the sandbox runner. The two tiers only depend on the\n\t\t// sandbox phase above, not on each other, so when both are enabled\n\t\t// they run concurrently instead of paying two sequential loads.\n\t\t// In bypass mode marketplace plugins were already handled above.\n\t\tconst installedTierPhases: Promise<void>[] = [];\n\t\tif (deps.config.marketplace && storage && !deps.sandboxBypassed) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.market\", \"Marketplace plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"marketplace\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPlugins,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Cold-start: load registry-installed plugins from site R2\n\t\tif (deps.config.experimental?.registry && storage) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.registry\", \"Registry plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"registry\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPlugins,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (installedTierPhases.length > 0) {\n\t\t\tawait Promise.all(installedTierPhases);\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, getDb: resolveDb };\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 phase(\"rt.hooks\", \"Exclusive hook resolution\", () =>\n\t\t\tEmDashRuntime.resolveExclusiveHooks(pipeline, db, deps),\n\t\t);\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\t// db/getDb were already set via pipelineFactoryOptions above; merge only\n\t\t// adds emailPipeline.\n\t\tpipeline.setContextFactory({ emailPipeline });\n\n\t\tlet cronExecutor: CronExecutor | null = null;\n\t\tlet cronScheduler: CronScheduler | null = null;\n\t\t// Populated with the constructed runtime just before this method returns,\n\t\t// so the timer scheduler's cleanup can route scheduled publishing through\n\t\t// the runtime wrapper (firing content:afterPublish hooks). The first tick\n\t\t// is ≥1s out, well after the synchronous assignment below.\n\t\tconst runtimeRef: { current: EmDashRuntime | null } = { current: null };\n\n\t\tawait phase(\"rt.cron\", \"Cron init (recovery deferred post-response)\", async () => {\n\t\t\ttry {\n\t\t\t\tcronExecutor = new CronExecutor(resolveDb, invokeCronHook);\n\n\t\t\t\t// Recover stale locks from previous crashes. Pure bookkeeping\n\t\t\t\t// against the _emdash_cron_tasks table — no request needs the\n\t\t\t\t// result — so we defer it past the response via after(). On\n\t\t\t\t// Cloudflare this goes into waitUntil (extending the worker\n\t\t\t\t// lifetime); on Node it's fire-and-forget (the process stays\n\t\t\t\t// up anyway). Saves one cold-start write per D1 isolate.\n\t\t\t\tconst executorForRecovery = cronExecutor;\n\t\t\t\tafter(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst recovered = await executorForRecovery.recoverStaleLocks();\n\t\t\t\t\t\tif (recovered > 0) {\n\t\t\t\t\t\t\tconsole.log(`[cron] Recovered ${recovered} stale task lock(s)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Keep the `[cron]` prefix so a failure is easy to trace back\n\t\t\t\t\t\t// rather than surfacing as a generic deferred-task error.\n\t\t\t\t\t\tconsole.error(\"[cron] Failed to recover stale task locks:\", error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\t// The platform decides whether a long-lived timer heartbeat exists.\n\t\t\t\t// `createScheduler` is injected by the generated virtual:emdash/scheduler\n\t\t\t\t// module: a NodeCronScheduler factory on Node/Bun, or null on serverless\n\t\t\t\t// adapters (e.g. Cloudflare) where the Worker's `scheduled()` handler\n\t\t\t\t// drives runScheduledTasks() instead. No adapter check lives here.\n\t\t\t\tif (deps.createScheduler) {\n\t\t\t\t\tconst scheduler = deps.createScheduler(cronExecutor);\n\t\t\t\t\tcronScheduler = scheduler;\n\n\t\t\t\t\t// Run scheduled publishing and system cleanup alongside each tick.\n\t\t\t\t\t// Pass storage so cleanupPendingUploads can delete orphaned files.\n\t\t\t\t\tscheduler.setSystemCleanup(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Route through the runtime so content:afterPublish hooks fire.\n\t\t\t\t\t\t\t// Falls back to the raw handler if (improbably) the tick beats\n\t\t\t\t\t\t\t// the post-construction ref assignment.\n\t\t\t\t\t\t\tconst runtime = runtimeRef.current;\n\t\t\t\t\t\t\tawait publishDueContent(db, {\n\t\t\t\t\t\t\t\tpublish: runtime\n\t\t\t\t\t\t\t\t\t? (collection, id, options) =>\n\t\t\t\t\t\t\t\t\t\t\truntime.handleContentPublish(collection, id, options)\n\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait runSystemCleanup(db, storage ?? undefined);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t// Non-fatal -- individual cleanup failures are already logged\n\t\t\t\t\t\t\t// by runSystemCleanup. This catches unexpected errors.\n\t\t\t\t\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\t// Add cron reschedule callback (merges with existing factory options)\n\t\t\t\t\tpipeline.setContextFactory({\n\t\t\t\t\t\tcronReschedule: () => cronScheduler?.reschedule(),\n\t\t\t\t\t});\n\n\t\t\t\t\t// start() is void on the timer scheduler but the interface\n\t\t\t\t\t// allows a promise (alarm-backed schedulers); we don't block on it.\n\t\t\t\t\tvoid scheduler.start();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[cron] Failed to initialize cron system:\", error);\n\t\t\t\t// Non-fatal — CMS works without cron\n\t\t\t}\n\t\t});\n\n\t\tconst runtime = new EmDashRuntime({\n\t\t\tdb,\n\t\t\tstorage,\n\t\t\t// Include bypassed sandboxed plugins in configuredPlugins so route\n\t\t\t// dispatch can find them under sandbox: false (they're treated as\n\t\t\t// trusted plugins for the duration of the bypass).\n\t\t\tconfiguredPlugins: [...deps.plugins, ...bypassedPluginsList],\n\t\t\tsandboxedPlugins,\n\t\t\tsandboxedPluginEntries: deps.sandboxedPluginEntries,\n\t\t\thooks: pipeline,\n\t\t\tenabledPlugins,\n\t\t\tpluginStates,\n\t\t\tconfig: deps.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\truntimeDeps: deps,\n\t\t\tpipelineRef,\n\t\t});\n\t\t// Hand the constructed instance to the scheduler-cleanup closure so the\n\t\t// timer-driven sweep can fire publish hooks (see runtimeRef above).\n\t\truntimeRef.current = runtime;\n\t\treturn runtime;\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// Only use the per-request `ctx.db` when it's an isolated instance\n\t\t// (playground / DO preview). Plain D1 Sessions set `ctx.db` on every\n\t\t// anonymous request — if we captured one of those session-bound\n\t\t// Kyselys into the cached runtime, every request would accidentally\n\t\t// share one request's session. The configured `deps.createDialect`\n\t\t// path gives us a fresh singleton instead.\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.dbIsIsolated && ctx.db) {\n\t\t\t// eslint-disable-next-line typescript/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// Waiters poll the cache rather than sharing the initializing request's\n\t\t// promise: if the request that owns the init is cancelled mid-await\n\t\t// (e.g. client disconnect during cold migrations), a shared promise\n\t\t// never settles — and the owner's `finally` that would clear it never\n\t\t// runs — deadlocking every later request in the isolate. Prevention:\n\t\t// the in-flight init is anchored via after()/waitUntil so a cancelled\n\t\t// owner's init still completes and populates the cache. Net: a stale\n\t\t// lock is reclaimed after a deadline.\n\t\tconst holder = getDbHolder();\n\t\treturn initWithLock(\n\t\t\tholder.lock,\n\t\t\t() => holder.cache.get(cacheKey),\n\t\t\tasync (isCurrentClaim) => {\n\t\t\t\tconst dialect = deps.createDialect(dbConfig.config);\n\t\t\t\tconst db = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\n\t\t\t\tawait runMigrations(db);\n\n\t\t\t\t// Note: legacy installs may carry a stray `emdash:manifest_cache`\n\t\t\t\t// row in the options table from versions that persisted a JSON\n\t\t\t\t// manifest. The runtime no longer reads or writes it. We do not\n\t\t\t\t// proactively delete it: the row is a few hundred bytes of dead\n\t\t\t\t// weight and is never on the read path, whereas a one-shot\n\t\t\t\t// cleanup-flag check costs an extra `options.get()` on every\n\t\t\t\t// isolate cold boot forever. Cheaper to leave it.\n\n\t\t\t\t// This returns a migrated but possibly unseeded db; create() runs\n\t\t\t\t// the seed gate and applies the seed, batched with its other init\n\t\t\t\t// reads.\n\n\t\t\t\t// Publish only while still the current owner: a reclaimed slow\n\t\t\t\t// init must not flip the cached Kysely identity back after the\n\t\t\t\t// reclaimer has published its own. The unpublished instance is\n\t\t\t\t// still returned and fully valid for the request that built it.\n\t\t\t\tif (isCurrentClaim()) {\n\t\t\t\t\tholder.cache.set(cacheKey, db);\n\t\t\t\t}\n\t\t\t\treturn db;\n\t\t\t},\n\t\t\t{\n\t\t\t\tdeadlineMs: DB_INIT_DEADLINE_MS,\n\t\t\t\tanchor: (promise) => after(() => promise),\n\t\t\t},\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 plugin entries as trusted in-process plugins.\n\t * Used by the sandbox: false debugging escape hatch.\n\t *\n\t * Imports each plugin's bundled ESM code via a data URL, adapts it\n\t * with adaptSandboxEntry, and returns ResolvedPlugin objects ready\n\t * to be merged into the pipeline plugin list.\n\t */\n\tprivate static async loadBypassedPlugins(\n\t\tentries: SandboxedPluginEntry[],\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\tconst plugins: ResolvedPlugin[] = [];\n\t\tfor (const entry of entries) {\n\t\t\ttry {\n\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(entry.code).toString(\"base64\")}`;\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<string, unknown>;\n\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t>[0];\n\t\t\t\t// PluginDescriptor.storage's TypeScript type is narrower than what\n\t\t\t\t// adaptSandboxEntry actually accepts at runtime — it copies indexes\n\t\t\t\t// through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// (string[][]). Pass the raw entry.storage with a structural cast\n\t\t\t\t// to preserve composite index declarations.\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// Preserve admin metadata so plugin-management APIs can derive\n\t\t\t\t// hasAdminPages / hasDashboardWidgets correctly. Without this,\n\t\t\t\t// the admin UI hides Configure links and dashboard widgets for\n\t\t\t\t// bypassed plugins even though they declared them.\n\t\t\t\t// SandboxedPluginEntry uses looser types than PluginDescriptor\n\t\t\t\t// (label?, size: string), so coerce to the descriptor shape.\n\t\t\t\tconst adminPages = entry.adminPages?.map((p) => ({\n\t\t\t\t\tpath: p.path,\n\t\t\t\t\tlabel: p.label ?? p.path,\n\t\t\t\t\ticon: p.icon,\n\t\t\t\t}));\n\t\t\t\tconst adminWidgets:\n\t\t\t\t\t| Array<{\n\t\t\t\t\t\t\tid: string;\n\t\t\t\t\t\t\ttitle?: string;\n\t\t\t\t\t\t\tsize?: \"full\" | \"half\" | \"third\";\n\t\t\t\t\t }>\n\t\t\t\t\t| undefined = entry.adminWidgets?.map((w) => {\n\t\t\t\t\tconst size: \"full\" | \"half\" | \"third\" | undefined =\n\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined;\n\t\t\t\t\treturn { id: w.id, title: w.title, size };\n\t\t\t\t});\n\t\t\t\tconst resolved = adaptSandboxEntry(pluginDef, {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tentrypoint: \"\",\n\t\t\t\t\tcapabilities: entry.capabilities,\n\t\t\t\t\tallowedHosts: entry.allowedHosts,\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\tstorage: entry.storage as never,\n\t\t\t\t\tadminPages,\n\t\t\t\t\tadminWidgets,\n\t\t\t\t\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\t\tfieldWidgets: entry.fieldWidgets,\n\t\t\t\t});\n\t\t\t\tplugins.push(resolved);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded plugin ${entry.id}:${entry.version} in-process (sandbox bypassed)`,\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} in-process:`, error);\n\t\t\t}\n\t\t}\n\t\treturn plugins;\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\tmediaStorage?: Storage | null,\n\t): Promise<Map<string, SandboxedPluginInstance>> {\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) {\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({\n\t\t\t\tdb,\n\t\t\t\tmediaStorage: mediaStorage\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\t\t\tmediaStorage.upload({\n\t\t\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tdelete: (key) => mediaStorage.delete(key),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t});\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\t// Warn regardless of whether there are plugins to load, so operators\n\t\t// see the issue even if no marketplace plugins are installed yet.\n\t\tif (!sandboxRunner.isAvailable()) {\n\t\t\tconsole.warn(\n\t\t\t\t\"EmDash: Plugin sandbox is configured but not available on this platform. \" +\n\t\t\t\t\t\"Sandboxed plugins will not be loaded. \" +\n\t\t\t\t\t\"If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.\",\n\t\t\t);\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\tif (deps.sandboxedPluginEntries.length === 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// sandbox: false escape hatch is handled separately (before pipeline\n\t\t// creation) via loadBypassedPlugins. If we somehow reach here with the\n\t\t// flag set, just return — the plugins are already in the trusted pipeline.\n\t\tif (deps.sandboxBypassed) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Load each sandboxed plugin via sandbox runner\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\t/**\n\t * Cold-start load of all active sandboxed plugins for one install\n\t * tier (marketplace or registry) from site-local R2.\n\t *\n\t * Mirrors {@link syncSandboxedSourcePlugins} but runs once at runtime\n\t * creation, before request traffic arrives; the sync method runs on\n\t * demand after install / update / uninstall handlers.\n\t */\n\tprivate static async loadInstalledSandboxedPlugins(\n\t\tsource: \"marketplace\" | \"registry\",\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t\tdeps: RuntimeDependencies,\n\t\tcache: Map<string, SandboxedPluginInstance>,\n\t): Promise<void> {\n\t\t// Ensure sandbox runner exists with media storage wired up.\n\t\t// (storage here is the media Storage adapter from the runtime.)\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner({\n\t\t\t\tdb,\n\t\t\t\tmediaStorage: {\n\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\tstorage.upload({\n\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t}),\n\t\t\t\t\tdelete: (key) => storage.delete(key),\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t\t// In sandbox bypass mode, marketplace plugins are loaded in-process\n\t\t// BEFORE pipeline creation by EmDashRuntime.create(). Skip here.\n\t\tif (deps.sandboxBypassed) return;\n\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst plugins =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tfor (const plugin of plugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\n\t\t\t\t// Marketplace plugins record the live version in\n\t\t\t\t// `marketplaceVersion`; registry plugins use `version` directly.\n\t\t\t\tconst version =\n\t\t\t\t\tsource === \"marketplace\" ? (plugin.marketplaceVersion ?? plugin.version) : 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, source);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(`EmDash: ${source} plugin ${plugin.pluginId}@${version} not found in R2`);\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\tkeySet.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 ${source} 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 ${source} 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 * Cold-start: load marketplace plugins in bypass mode (sandbox: false).\n\t *\n\t * Each active marketplace bundle is read, evaluated via data URL, adapted\n\t * with adaptSandboxEntry, and returned as a ResolvedPlugin. The caller is\n\t * responsible for merging these into allPipelinePlugins / configuredPlugins\n\t * BEFORE the hook pipeline is created, so hooks and routes register in\n\t * the trusted pipeline.\n\t *\n\t * Also caches manifest and route metadata so admin UI / getManifest() work.\n\t *\n\t * Returns ResolvedPlugins to be merged into the pipeline.\n\t */\n\tprivate static async loadMarketplacePluginsBypassed(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst resolved: ResolvedPlugin[] = [];\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\t\t\tif (marketplacePlugins.length === 0) return resolved;\n\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Marketplace plugins will run in-process without isolation.\",\n\t\t\t);\n\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\n\t\t\tfor (const plugin of marketplacePlugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\t\t\t\tconst version = plugin.marketplaceVersion ?? plugin.version;\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\t// Cache manifest and route metadata for admin UI and route auth\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\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\t// Evaluate the bundled ESM and adapt it as a trusted plugin\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\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\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tresolved.push(adapted);\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded marketplace plugin ${plugin.pluginId}@${version} in-process (sandbox bypassed)`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${plugin.pluginId} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet\n\t\t}\n\t\treturn resolved;\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\tgetOptions: (keys) => optionsRepo.getMany<string>(keys),\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 admin manifest from the live database.\n\t *\n\t * Used by the admin UI (sidebar collections, content editor field\n\t * dispatch, manifest endpoint) and by WordPress import — it's never\n\t * read on a public request, so this isn't on any anonymous hot path.\n\t *\n\t * No cross-request cache. The previous worker-isolate cache produced\n\t * a class of cross-isolate staleness bugs (#776, #873, #876, #877)\n\t * because Cloudflare Workers keeps multiple warm isolates per region\n\t * and there's no fan-out primitive to invalidate them in step. The\n\t * cache existed to amortize an N+1 schema query pattern; now that\n\t * `listCollectionsWithFields()` does the same work in two queries,\n\t * the rebuild is fast enough to pay on every admin request.\n\t *\n\t * Within a single request, `requestCached` deduplicates concurrent\n\t * callers (the manifest endpoint and an admin SSR template, say).\n\t */\n\tgetManifest(): Promise<EmDashManifest> {\n\t\treturn requestCached(\"emdash:manifest\", () => this._buildManifest());\n\t}\n\n\t/**\n\t * Build the manifest from the database.\n\t *\n\t * Constant query shapes via `listCollectionsWithFields()` — one query\n\t * for collections, one batched query for fields (chunked at\n\t * `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter\n\t * limit). Typical sites stay well under the chunk threshold, so this\n\t * is two queries in practice; never N+1.\n\t */\n\tprivate async _buildManifest(): 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.listCollectionsWithFields();\n\t\t\tfor (const collection of dbCollections) {\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\t// Two shapes: legacy enum-style `[{ value, label }]` for select widgets,\n\t\t\t\t\t\t// or arbitrary `Record<string, unknown>` for plugin field widgets that\n\t\t\t\t\t\t// need per-field config (e.g. a checkbox grid receiving its column defs).\n\t\t\t\t\t\toptions?: Array<{ value: string; label: string }> | Record<string, unknown>;\n\t\t\t\t\t\tid?: string;\n\t\t\t\t\t\tvalidation?: Record<string, unknown>;\n\t\t\t\t\t}\n\t\t\t\t> = {};\n\n\t\t\t\tfor (const field of collection.fields) {\n\t\t\t\t\tconst entry: (typeof fields)[string] = {\n\t\t\t\t\t\tkind: FIELD_TYPE_TO_KIND[field.type] ?? \"string\",\n\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\trequired: field.required,\n\t\t\t\t\t};\n\t\t\t\t\t// Always include the field's database ID so the admin can forward it\n\t\t\t\t\t// to upload/media-list API calls for MIME allowlist widening.\n\t\t\t\t\tentry.id = field.id;\n\t\t\t\t\tif (field.widget) entry.widget = field.widget;\n\t\t\t\t\t// Plugin field widgets read their per-field config from `field.options`,\n\t\t\t\t\t// which the seed schema types as `Record<string, unknown>`. Pass it\n\t\t\t\t\t// through to the manifest so plugin widgets in the admin SPA receive it.\n\t\t\t\t\tif (field.options) {\n\t\t\t\t\t\tentry.options = field.options;\n\t\t\t\t\t}\n\t\t\t\t\t// Legacy: select/multiSelect enum options live on `field.validation.options`.\n\t\t\t\t\t// Wins over `field.options` to preserve existing behavior for enum widgets.\n\t\t\t\t\tif (field.validation?.options) {\n\t\t\t\t\t\tentry.options = field.validation.options.map((v) => ({\n\t\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\t\t\t\t\t// Include full validation for repeater fields (subFields, minItems, maxItems)\n\t\t\t\t\t// and for file/image fields (allowedMimeTypes).\n\t\t\t\t\tif (\n\t\t\t\t\t\t(field.type === \"repeater\" || field.type === \"file\" || field.type === \"image\") &&\n\t\t\t\t\t\tfield.validation\n\t\t\t\t\t) {\n\t\t\t\t\t\tentry.validation = { ...field.validation };\n\t\t\t\t\t}\n\t\t\t\t\tfields[field.slug] = entry;\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\turlPattern: collection.urlPattern,\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\tcategory?: string;\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\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\t// `adminMode` reflects only admin pages/widgets. A plugin can\n\t\t\t\t// contribute portableTextBlocks/fieldWidgets with adminMode \"none\" —\n\t\t\t\t// the admin reads those from the manifest regardless, so don't gate\n\t\t\t\t// admin contributions on `adminMode`.\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\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\tfieldWidgets: entry.fieldWidgets,\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// Build taxonomies from database\n\t\tlet manifestTaxonomies: Array<{\n\t\t\tname: string;\n\t\t\tlabel: string;\n\t\t\tlabelSingular?: string;\n\t\t\thierarchical: boolean;\n\t\t\tcollections: string[];\n\t\t}> = [];\n\t\ttry {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.orderBy(\"name\")\n\t\t\t\t.execute();\n\t\t\tmanifestTaxonomies = rows.map((row) => ({\n\t\t\t\tname: row.name,\n\t\t\t\tlabel: row.label,\n\t\t\t\tlabelSingular: row.label_singular ?? undefined,\n\t\t\t\thierarchical: row.hierarchical === 1,\n\t\t\t\tcollections: parseStringArray(row.collections).toSorted(),\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tconsole.debug(\"EmDash: Could not load taxonomy definitions:\", error);\n\t\t}\n\n\t\t// Build manifest hash\n\t\tconst manifestHash = await hashString(\n\t\t\tJSON.stringify(manifestCollections) +\n\t\t\t\tJSON.stringify(manifestPlugins) +\n\t\t\t\tJSON.stringify(manifestTaxonomies),\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 (read from virtual module to avoid SSR module singleton mismatch)\n\t\tconst i18nConfig = virtualConfig?.i18n;\n\t\tconst i18n =\n\t\t\ti18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1\n\t\t\t\t? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }\n\t\t\t\t: undefined;\n\n\t\t// Normalize the experimental registry config for browser consumption.\n\t\t// Validation errors here surface as 500s from the manifest endpoint\n\t\t// rather than being silently dropped -- a misconfigured registry\n\t\t// should be loud, not invisible.\n\t\tconst registry = normalizeRegistryConfig(this.config.experimental?.registry) ?? undefined;\n\n\t\treturn {\n\t\t\tversion: VERSION,\n\t\t\tcommit: COMMIT,\n\t\t\tastroVersion: this.config.astroVersion,\n\t\t\thash: manifestHash,\n\t\t\tcollections: manifestCollections,\n\t\t\tplugins: manifestPlugins,\n\t\t\ttaxonomies: manifestTaxonomies,\n\t\t\tauthMode: authModeValue,\n\t\t\ti18n,\n\t\t\tmarketplace: !!this.config.marketplace,\n\t\t\tregistry,\n\t\t};\n\t}\n\n\t/**\n\t * Verify and repair FTS indexes on demand. Runs at most once per worker\n\t * lifetime.\n\t *\n\t * Originally called from `EmDashRuntime.create()`, but on a busy D1 link\n\t * (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold\n\t * start for a modest-sized site — more than every other init phase\n\t * combined. Anonymous public reads never touch the search write path,\n\t * so the cost isn't paid back for the vast majority of requests.\n\t *\n\t * Instead, search endpoints call this lazily: the first request that\n\t * actually needs the index pays the verify cost (usually fast — no\n\t * rebuild needed), everyone else runs cold-free.\n\t *\n\t * Uses the runtime's singleton database (`this._db`) rather than the\n\t * request-scoped DB. Verify reads only, but `rebuildIndex` writes, and\n\t * a GET search request on D1 carries a `first-unconstrained` session\n\t * that's free to route at a read replica — unsafe for writes. The\n\t * singleton always goes through the default binding, which the D1\n\t * adapter will promote to `first-primary` for write statements.\n\t *\n\t * Safe to call concurrently: repeated callers share the same in-flight\n\t * promise. Errors are swallowed internally so callers don't need to\n\t * defend against FTS not existing yet (pre-setup).\n\t */\n\tasync ensureSearchHealthy(): Promise<void> {\n\t\t// Non-SQLite has no FTS to verify; the check is a cheap synchronous\n\t\t// branch, no need to cache it.\n\t\tif (!isSqlite(this._db)) return;\n\t\ttry {\n\t\t\tawait singleFlightCached(\n\t\t\t\tthis._searchHealthCache,\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst ftsManager = new FTSManager(this._db);\n\t\t\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\t\t\tif (repaired > 0) {\n\t\t\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal — cache\n\t\t\t\t\t\t// the \"checked\" state regardless so we don't re-scan.\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t\t);\n\t\t} catch {\n\t\t\t// This check is best-effort and must never fail the calling request.\n\t\t\t// The inner body already swallows verify errors; this guards the\n\t\t\t// outer failure modes (owner timeout, waiter give-up) so a slow FTS\n\t\t\t// scan degrades to \"unverified\", not a 500 on admin/search routes.\n\t\t}\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\tq?: string;\n\t\t\tauthorId?: string;\n\t\t\tdateField?: ContentDateField;\n\t\t\tdateFrom?: string;\n\t\t\tdateTo?: string;\n\t\t},\n\t) {\n\t\treturn handleContentList(this.db, collection, params);\n\t}\n\n\tasync handleContentAuthors(collection: string) {\n\t\treturn handleContentAuthors(this.db, collection);\n\t}\n\n\tasync handleContentGet(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGet(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\tasync handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\t/**\n\t * If the response item has a `draftRevisionId`, replace `item.data` with\n\t * the draft revision's data and expose the original published values as\n\t * `liveData`. This makes the content_get / content_update round-trip\n\t * intuitive — read returns the latest content the caller has saved\n\t * (their pending draft), with the previously-published values still\n\t * accessible for compare-style flows.\n\t *\n\t * No-op when no draft exists or the response is an error.\n\t */\n\tprivate async hydrateDraftData<T>(result: T): Promise<T> {\n\t\tif (!result || typeof result !== \"object\") return result;\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below\n\t\tconst r = result as {\n\t\t\tsuccess?: boolean;\n\t\t\tdata?: { item?: Record<string, unknown> };\n\t\t};\n\t\tif (!r.success || !r.data?.item) return result;\n\t\tconst item = r.data.item;\n\t\tconst draftRevisionId = typeof item.draftRevisionId === \"string\" ? item.draftRevisionId : null;\n\t\tif (!draftRevisionId) return result;\n\t\ttry {\n\t\t\tconst revision = await new RevisionRepository(this.db).findById(draftRevisionId);\n\t\t\tif (!revision) return result;\n\t\t\tconst liveData =\n\t\t\t\titem.data && typeof item.data === \"object\"\n\t\t\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above\n\t\t\t\t\t\t(item.data as Record<string, unknown>)\n\t\t\t\t\t: {};\n\t\t\t// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the\n\t\t\t// revision data — those are handler-internal markers and don't\n\t\t\t// belong in the surfaced `data` field. Match syncDataColumns at\n\t\t\t// content.ts:~1119.\n\t\t\tconst revisionData: Record<string, unknown> = {};\n\t\t\tfor (const [key, value] of Object.entries(revision.data)) {\n\t\t\t\tif (!key.startsWith(\"_\")) revisionData[key] = value;\n\t\t\t}\n\t\t\tconst mergedData = { ...liveData, ...revisionData };\n\t\t\t// Return a clone rather than mutating in place. The response\n\t\t\t// object isn't retained by the runtime today, but a future\n\t\t\t// request-cache layer would observe stale-after-mutation bugs;\n\t\t\t// cloning closes that footgun.\n\t\t\t// `r.data` was narrowed to `{ item?: ... }` at the top of this\n\t\t\t// method; spread its other keys (e.g. `_rev`) alongside the\n\t\t\t// hydrated item without going back through `unknown`.\n\t\t\treturn {\n\t\t\t\t...result,\n\t\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope\n\t\t\t\tdata: {\n\t\t\t\t\t...r.data,\n\t\t\t\t\titem: { ...item, data: mergedData, liveData },\n\t\t\t\t},\n\t\t\t} as T;\n\t\t} catch (error) {\n\t\t\t// Non-fatal — fall back to the unhydrated response. Log so the\n\t\t\t// failure isn't completely silent (the response will look stale\n\t\t\t// to the caller but no error is raised).\n\t\t\tconsole.error(\"[emdash] draft hydration failed:\", error);\n\t\t\treturn result;\n\t\t}\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// Validate against the collection schema. Hook output is validated\n\t\t// rather than `body.data` so plugins that mutate field values can't\n\t\t// sneak invalid data past.\n\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\tpartial: false,\n\t\t});\n\t\tif (!validation.ok) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: validation.error,\n\t\t\t};\n\t\t}\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\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\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\tseo?: {\n\t\t\t\ttitle?: string | null;\n\t\t\t\tdescription?: string | null;\n\t\t\t\timage?: string | null;\n\t\t\t\tcanonical?: string | null;\n\t\t\t\tnoIndex?: boolean;\n\t\t\t};\n\t\t\tpublishedAt?: string | null;\n\t\t\tlocale?: string;\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, body.locale);\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\n\t\t\t// Validate field-level shape BEFORE the draft-revision write so\n\t\t\t// invalid updates can't silently land in revision history.\n\t\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\t\tpartial: true,\n\t\t\t});\n\t\t\tif (!validation.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: validation.error,\n\t\t\t\t};\n\t\t\t}\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\tlet draftStorageChanged = 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\tdraftStorageChanged = true;\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\tvalidateIdentifier(collection, \"collection\");\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\t\t\t\t\t\t\tdraftStorageChanged = true;\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// Hydrate draft data BEFORE firing afterSave hooks so the hook sees\n\t\t// the same effective data the response surfaces — for revision-\n\t\t// supporting collections, that's the just-saved draft, not the live\n\t\t// columns.\n\t\tconst hydrated = await this.hydrateDraftData(result);\n\t\tif (hydrated.success && hydrated.data) {\n\t\t\tconst contentIdsToRefresh = [resolvedId];\n\t\t\tif (!usesDraftRevisions && processedData) {\n\t\t\t\ttry {\n\t\t\t\t\tcontentIdsToRefresh.push(\n\t\t\t\t\t\t...(await findNonTranslatableSiblingContentIds(\n\t\t\t\t\t\t\tthis.db,\n\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\tresolvedId,\n\t\t\t\t\t\t\thydrated.data.item.translationGroup,\n\t\t\t\t\t\t\tprocessedData,\n\t\t\t\t\t\t)),\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[media-usage] Failed to discover synced i18n siblings for ${collection}/${resolvedId}:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait markContentMediaUsageCollectionStale(\n\t\t\t\t\t\t\tthis.db,\n\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\"CONTENT_USAGE_REFRESH_ERROR\",\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (staleError) {\n\t\t\t\t\t\tconsole.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);\n\t\t} else if (draftStorageChanged) {\n\t\t\ttry {\n\t\t\t\tawait markContentMediaUsageCollectionStale(this.db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`[media-usage] Failed to mark ${collection} stale:`, error);\n\t\t\t}\n\t\t}\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (hydrated.success && hydrated.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);\n\t\t}\n\n\t\treturn hydrated;\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\t\tif (result.success) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);\n\t\t}\n\n\t\t// Run afterDelete hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection, false);\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\tconst result = await handleContentRestore(this.db, collection, id);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterRestore hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentPermanentDelete(collection: string, id: string) {\n\t\tconst result = await handleContentPermanentDelete(this.db, collection, id);\n\t\tif (result.success) {\n\t\t\tawait this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);\n\t\t}\n\n\t\t// Run afterDelete hooks so plugins (e.g. AI Search) can clean up\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection, true);\n\t\t}\n\n\t\treturn result;\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\tconst result = await handleContentDuplicate(this.db, collection, id, authorId);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Publishing & Scheduling Handlers\n\t// =========================================================================\n\n\tasync handleContentPublish(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { publishedAt?: string; requireScheduledDue?: boolean } = {},\n\t) {\n\t\tconst result = await handleContentPublish(this.db, collection, id, options);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterPublish hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnpublish(collection: string, id: string) {\n\t\tconst result = await handleContentUnpublish(this.db, collection, id);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterUnpublish hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentSchedule(collection: string, id: string, scheduledAt: string) {\n\t\tconst result = await handleContentSchedule(this.db, collection, id, scheduledAt);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterSchedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\tconst result = await handleContentUnschedule(this.db, collection, id);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterUnschedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection);\n\t\t}\n\n\t\treturn result;\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\tconst result = await handleContentDiscardDraft(this.db, collection, id);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\t\treturn result;\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: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tmimeType?: string | readonly string[];\n\t\tq?: string;\n\t}) {\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\tauthorId?: 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\tconst result = await handleMediaUpdate(this.db, id, input);\n\t\t// Resolved media references in site settings (`logo`, `favicon`,\n\t\t// `seo.defaultOgImage`) bake in the media row's `contentType`,\n\t\t// `width`, and `height`. A metadata edit invalidates that snapshot\n\t\t// for every entry point: REST routes, MCP tools, plugin code, and\n\t\t// any future caller of `handleMediaUpdate`. Cross-isolate staleness\n\t\t// remains bounded by isolate lifetime.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handleMediaDelete(id: string) {\n\t\tconst result = await handleMediaDelete(this.db, id);\n\t\t// Same reasoning as `handleMediaUpdate`: if the deleted media row\n\t\t// was referenced by a setting, the cached resolved URL now points\n\t\t// at a 404. Invalidation is unconditional on success — cheaper than\n\t\t// querying which settings reference the id.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\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\t// Discover the parent entry up front so we can branch on whether\n\t\t// the collection uses draft revisions.\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);\n\t\tconst usesDraftRevisions = collectionInfo?.supports?.includes(\"revisions\") ?? false;\n\n\t\t// Non-revision collections: keep the legacy behavior of writing the\n\t\t// revision's data straight onto the live row. This preserves\n\t\t// behavior for collections that opt out of the draft model.\n\t\tif (!usesDraftRevisions) {\n\t\t\tconst result = await handleRevisionRestore(this.db, revisionId, callerUserId);\n\t\t\tif (result.success) {\n\t\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);\n\t\t\t}\n\t\t\treturn this.hydrateDraftData(result);\n\t\t}\n\n\t\t// Revision-capable collections: restore is \"make this revision the\n\t\t// current draft\". The live row's data columns are left untouched\n\t\t// (only `draft_revision_id` and `updated_at` change). The caller\n\t\t// must then `content_publish` to promote the restored draft to\n\t\t// live, matching the documented tool contract.\n\t\ttry {\n\t\t\tconst newDraft = await revisionRepo.create({\n\t\t\t\tcollection: revision.collection,\n\t\t\t\tentryId: revision.entryId,\n\t\t\t\tdata: revision.data,\n\t\t\t\tauthorId: callerUserId,\n\t\t\t});\n\n\t\t\tvalidateIdentifier(revision.collection, \"collection\");\n\t\t\tconst tableName = `ec_${revision.collection}`;\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET draft_revision_id = ${newDraft.id},\n\t\t\t\t\tupdated_at = ${new Date().toISOString()}\n\t\t\t\tWHERE id = ${revision.entryId}\n\t\t\t`.execute(this.db);\n\n\t\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\t\tvoid revisionRepo\n\t\t\t\t.pruneOldRevisions(revision.collection, revision.entryId, 50)\n\t\t\t\t.catch(() => {});\n\n\t\t\t// Return the freshly-fetched item with the new draft hydrated\n\t\t\t// onto `data`. Without this the response would echo the live\n\t\t\t// columns and the next `content_get` would surface different\n\t\t\t// values (the bug that motivated this rewrite).\n\t\t\tconst refetched = await handleContentGet(this.db, revision.collection, revision.entryId);\n\t\t\tconst hydrated = await this.hydrateDraftData(refetched);\n\t\t\tif (hydrated.success) {\n\t\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);\n\t\t\t}\n\t\t\treturn hydrated;\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[emdash] revision restore failed:\", error);\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async refreshContentUsageAfterSuccessfulWrite(\n\t\tcollection: string,\n\t\tcontentIds: readonly string[],\n\t): Promise<void> {\n\t\tfor (const contentId of new Set(contentIds)) {\n\t\t\ttry {\n\t\t\t\tawait refreshContentMediaUsageAfterWrite(this.db, collection, contentId);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[media-usage] Failed after content write ${collection}/${contentId}:`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async deleteContentUsageAfterSuccessfulPermanentDelete(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst result = await deleteContentMediaUsage(this.db, collection, contentId);\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[media-usage] Failed after permanent content delete ${collection}/${contentId}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\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({\n\t\t\t\tdb: this.db,\n\t\t\t\tstorage: this.storage ?? undefined,\n\t\t\t\temailPipeline: this.email ?? undefined,\n\t\t\t\ttrustedProxyHeaders: getTrustedProxyHeaders(this.config),\n\t\t\t});\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): SandboxedPluginInstance | 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\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterSave\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterSave(content, collection, isNew);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterSave hook error:\", err);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sandboxed plugins\n\t\t\tconst tasks: Promise<void>[] = [];\n\t\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\t\tconst [id] = pluginKey.split(\":\");\n\t\t\t\tif (!id || !this.isPluginEnabled(id)) continue;\n\n\t\t\t\ttasks.push(\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait plugin.invokeHook(\"content:afterSave\", { content, collection, isNew });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait Promise.allSettled(tasks);\n\t\t});\n\t}\n\n\tprivate runAfterDeleteHooks(id: string, collection: string, permanent: boolean): 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, permanent)\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, permanent })\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 runDeferredContentHook(\n\t\tname:\n\t\t\t| \"content:afterPublish\"\n\t\t\t| \"content:afterUnpublish\"\n\t\t\t| \"content:afterRestore\"\n\t\t\t| \"content:afterSchedule\"\n\t\t\t| \"content:afterUnschedule\",\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): void {\n\t\tconst label = name.slice(\"content:\".length);\n\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(name)) {\n\t\t\t\ttry {\n\t\t\t\t\tswitch (name) {\n\t\t\t\t\t\tcase \"content:afterPublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterPublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnpublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnpublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterRestore\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterRestore(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterSchedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterSchedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnschedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnschedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(`EmDash ${label} hook error:`, err);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sandboxed plugins\n\t\t\tconst tasks: Promise<void>[] = [];\n\t\t\tfor (const [pluginKey, plugin] of this.sandboxedPlugins) {\n\t\t\t\tconst [pluginId] = pluginKey.split(\":\");\n\t\t\t\tif (!pluginId || !this.isPluginEnabled(pluginId)) continue;\n\n\t\t\t\ttasks.push(\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait plugin.invokeHook(name, { content, collection });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} ${label} error:`, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait Promise.allSettled(tasks);\n\t\t});\n\t}\n\n\tprivate runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterPublish\", content, collection);\n\t}\n\n\tprivate runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterUnpublish\", content, collection);\n\t}\n\n\tprivate runAfterRestoreHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterRestore\", content, collection);\n\t}\n\n\tprivate runAfterScheduleHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterSchedule\", content, collection);\n\t}\n\n\tprivate runAfterUnscheduleHooks(content: Record<string, unknown>, collection: string): void {\n\t\tthis.runDeferredContentHook(\"content:afterUnschedule\", content, collection);\n\t}\n\n\tprivate async handleSandboxedRoute(\n\t\tplugin: SandboxedPluginInstance,\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, this.config);\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 * Public media URL resolution.\n *\n * Used at render time by the Image components to decide whether a storage\n * key should be served from the configured `publicUrl` (R2 custom domain,\n * S3 CDN) or through the internal `/_emdash/api/media/file/{key}` route.\n */\nimport type { Storage } from \"../storage/types.js\";\nimport { INTERNAL_MEDIA_PREFIX } from \"./normalize.js\";\n\n// Keys accepted by the public-URL rewrite: the `{ulid}{ext}` shape produced by\n// the upload pipeline, with letters, digits, dots, dashes, and underscores.\n// Slashes, `?`, `#`, and `%` are rejected so attacker-controlled content in a\n// portable-text `asset.url` cannot traverse or reroute on the CDN origin.\nconst SAFE_STORAGE_KEY = /^[A-Za-z0-9._-]+$/;\n\n/**\n * Resolve the public URL for a locally stored media key. Returns an empty\n * string when no key is given. When a storage adapter is supplied, defers to\n * `storage.getPublicUrl()`; otherwise returns the internal proxy route.\n */\nexport function resolvePublicMediaUrl(\n\tstorage: Storage | null | undefined,\n\tstorageKey: string,\n): string {\n\tif (!storageKey) return \"\";\n\tif (storage) return storage.getPublicUrl(storageKey);\n\treturn `/_emdash/api/media/file/${storageKey}`;\n}\n\n/**\n * Build the `getPublicMediaUrl` closure attached to `Astro.locals.emdash`.\n * Shared by the anonymous fast path and the full-runtime path in middleware.\n *\n * @internal\n */\nexport function createPublicMediaUrlResolver(\n\tstorage: Storage | null | undefined,\n): (key: string) => string {\n\treturn (key) => resolvePublicMediaUrl(storage, key);\n}\n\n/** Input shape for {@link buildRenderMediaUrl}. */\nexport interface RenderMediaRef {\n\t/** Storage key with extension (the canonical shape from the upload pipeline). */\n\tstorageKey?: string;\n\t/** Pre-baked URL (either an internal proxy URL or an external URL). */\n\turl?: string;\n\t/** Bare media id (ULID without extension); only the internal proxy can look this up. */\n\tid?: string;\n}\n\n/**\n * Build a render-time media URL. Prefers `storageKey`, then rewrites an\n * internal `url` via `resolve`, then falls back to the internal proxy for a\n * bare `id`. External URLs and non-matching internal-looking URLs pass\n * through untouched. Returns `\"\"` when nothing usable is present.\n *\n * @internal\n */\nexport function buildRenderMediaUrl(\n\tresolve: ((key: string) => string) | undefined,\n\tref: RenderMediaRef,\n): string {\n\tconst { storageKey, url, id } = ref;\n\tif (storageKey) {\n\t\treturn resolve ? resolve(storageKey) : `${INTERNAL_MEDIA_PREFIX}${storageKey}`;\n\t}\n\tif (url) {\n\t\tif (resolve && url.startsWith(INTERNAL_MEDIA_PREFIX)) {\n\t\t\tconst key = url.slice(INTERNAL_MEDIA_PREFIX.length);\n\t\t\tif (SAFE_STORAGE_KEY.test(key)) return resolve(key);\n\t\t}\n\t\treturn url;\n\t}\n\tif (id) return `${INTERNAL_MEDIA_PREFIX}${id}`;\n\treturn \"\";\n}\n","/**\n * Request-scoped database lifecycle helpers.\n *\n * Extracted from middleware.ts so they can be unit-tested without pulling in\n * the virtual:emdash/* module graph. The middleware imports these to settle a\n * request-scoped db adapter's lifecycle around the response.\n */\n\n/**\n * Astro attaches AstroCookies to outgoing responses via a well-known global\n * symbol. Cloning a Response (`new Response(body, init)`) drops non-header\n * metadata, so any helper that wraps the response must explicitly forward this\n * symbol or `cookies.set()` calls will be silently dropped. `Symbol.for`\n * returns the same registry symbol everywhere, so this matches the copy in\n * middleware.ts.\n */\nexport const ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\n\n/**\n * Run a request-scoped db's `close()` once the response body has finished\n * streaming. Astro streams HTML and components issue DB queries during that\n * stream, so a connection-backed adapter (e.g. Postgres over Hyperdrive) must\n * not be torn down until the body is flushed. Bodyless responses (redirects,\n * 304s, errors) close immediately. A guard makes close idempotent and a stream\n * `cancel` (client disconnect) still triggers it so connections never leak.\n *\n * No-op for adapters without a `close` (D1): the response passes through.\n */\nexport function wrapResponseForScopedClose(response: Response, close: () => void): Response {\n\tlet closed = false;\n\tconst runClose = () => {\n\t\tif (closed) return;\n\t\tclosed = true;\n\t\ttry {\n\t\t\tclose();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[emdash] request-scoped db close failed:\", error);\n\t\t}\n\t};\n\n\tif (!response.body) {\n\t\trunClose();\n\t\treturn response;\n\t}\n\n\tconst transform = new TransformStream<Uint8Array, Uint8Array>({\n\t\tflush: runClose,\n\t\tcancel: runClose,\n\t});\n\tconst wrapped = new Response(response.body.pipeThrough(transform), response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(wrapped, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// Byte counts are preserved by the identity transform, but a stale\n\t// Content-Length on a reconstructed streaming Response risks truncation.\n\twrapped.headers.delete(\"Content-Length\");\n\treturn wrapped;\n}\n\n/**\n * Run the request body under a request-scoped db, then settle its lifecycle:\n * `commit()` runs before the response is returned (so per-request state like a\n * D1 bookmark cookie is persisted in the headers, even if render throws), while\n * `close()` (if any) is deferred to stream-end so a connection-backed adapter\n * isn't torn down mid-render. On error the connection is closed immediately\n * before rethrowing so it never leaks.\n *\n * On the error path both `commit()` and `close()` are defended: a throw from\n * either is logged and swallowed so it can't replace the propagating render\n * error (which is the one the caller needs to see). On the success path\n * `commit()` is guarded too — if it throws, the connection is closed before the\n * failure is surfaced, so it never leaks. For the current adapters `commit()`\n * is a no-op (Hyperdrive) or a cookie write (D1, no `close`) and `close()` is\n * fire-and-forget, so these guards only matter for a future connection-backed\n * adapter with throwing teardown, but the helper is generic and must not leak\n * or mask.\n */\nexport async function finishScoped(\n\tscoped: { commit: () => void; close?: () => void },\n\trun: () => Promise<Response>,\n): Promise<Response> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await run();\n\t} catch (error) {\n\t\t// A render error is already propagating; neither commit nor close may\n\t\t// mask it, and close must still run so the connection doesn't leak.\n\t\tcommitSafely(scoped.commit);\n\t\tcloseSafely(scoped.close);\n\t\tthrow error;\n\t}\n\ttry {\n\t\tscoped.commit();\n\t} catch (error) {\n\t\t// commit() failed on the success path: close the connection now (the\n\t\t// response won't be wrapped, so stream-end close would never run) and\n\t\t// surface the failure. close is swallowed so it can't mask the commit\n\t\t// error that the caller needs to see.\n\t\tcloseSafely(scoped.close);\n\t\tthrow error;\n\t}\n\treturn scoped.close ? wrapResponseForScopedClose(response, scoped.close) : response;\n}\n\n/**\n * Run commit() swallowing any error. Used where an exception is already\n * propagating (or about to be thrown) and a commit failure must neither mask it\n * nor skip the subsequent close().\n */\nfunction commitSafely(commit: () => void): void {\n\ttry {\n\t\tcommit();\n\t} catch (error) {\n\t\tconsole.error(\"[emdash] request-scoped db commit failed during error handling:\", error);\n\t}\n}\n\n/**\n * Run close() swallowing any error. Used on the error/commit-failure paths\n * where another exception is the one the caller must see; a throwing teardown\n * must not replace it.\n */\nfunction closeSafely(close: (() => void) | undefined): void {\n\tif (!close) return;\n\ttry {\n\t\tclose();\n\t} catch (error) {\n\t\tconsole.error(\"[emdash] request-scoped db close failed during error handling:\", error);\n\t}\n}\n","/**\n * Stream-end metrics\n *\n * Server-Timing db.* counters are snapshotted when middleware's next()\n * returns — but at that point only the response *headers* are final.\n * Astro streams the body afterwards, and components rendered during\n * streaming issue further DB queries that the headers can never report.\n *\n * This module wraps the response body in an identity TransformStream and\n * snapshots the request metrics in flush(), i.e. when the body actually\n * finishes streaming. The metrics object lives on the request context\n * (AsyncLocalStorage) and is mutated in-place by the Kysely log hook, so\n * a reference captured before wrapping observes every post-header query.\n * The snapshot is emitted as prefixed NDJSON on stdout (same transport as\n * [emdash-query-log] — console.log works in both Node and workerd).\n *\n * Gated on isInstrumentationEnabled() (EMDASH_QUERY_LOG=1): zero overhead\n * in normal production traffic.\n */\n\nimport { flushRecorder, isInstrumentationEnabled } from \"../../database/instrumentation.js\";\nimport { getRequestContext } from \"../../request-context.js\";\n// Reuse the single source of truth for Astro's well-known cookies symbol\n// rather than redefining `Symbol.for(\"astro.cookies\")` here — it must stay in\n// lockstep with the copy the rest of the middleware forwards.\nimport { ASTRO_COOKIES_SYMBOL } from \"./scoped-db.js\";\n\nexport const STREAM_END_PREFIX = \"[emdash-stream-end]\";\n\n/** Shape of the NDJSON snapshot emitted when the body finishes streaming. */\nexport interface StreamEndSnapshot {\n\troute?: string;\n\tmethod?: string;\n\tphase?: string;\n\t/** Total elapsed ms from middleware entry to end of body streaming. */\n\ttotalMs: number;\n\tdbCount: number;\n\tdbTotalMs: number;\n\tdbFirstOffset: number | null;\n\tdbLastOffset: number | null;\n\tcacheHits: number;\n\tcacheMisses: number;\n}\n\n/**\n * Wrap a response body so the FINAL request metrics are emitted when the\n * body finishes streaming. Returns the response unchanged when\n * instrumentation is disabled, the body is null, or no request metrics\n * are attached (e.g. outside the middleware's ALS context).\n */\nexport function wrapBodyForStreamMetrics(response: Response): Response {\n\tif (!isInstrumentationEnabled()) return response;\n\tif (!response.body) return response;\n\n\t// Capture the context's metrics object BEFORE wrapping: flush() runs\n\t// after the middleware's ALS frame may have exited, but the object\n\t// reference stays live and is mutated in-place by the Kysely log hook.\n\tconst ctx = getRequestContext();\n\tconst metrics = ctx?.metrics;\n\tif (!metrics) return response;\n\tconst recorder = ctx?.queryRecorder;\n\n\t// Claim the per-query flush: the recorder is mutated in-place by the\n\t// Kysely log hook for the whole request, including queries issued by\n\t// components while the body streams. Flushing here (rather than when\n\t// middleware returns) is what captures those streaming queries. The\n\t// flag tells the middleware's fallback flush to leave this recorder\n\t// to us.\n\tif (recorder) recorder.deferredFlush = true;\n\n\tconst transform = new TransformStream<Uint8Array, Uint8Array>({\n\t\tflush() {\n\t\t\tconst snapshot: StreamEndSnapshot = {\n\t\t\t\troute: recorder?.route,\n\t\t\t\tmethod: recorder?.method,\n\t\t\t\tphase: recorder?.phase,\n\t\t\t\ttotalMs: performance.now() - metrics.start,\n\t\t\t\tdbCount: metrics.dbCount,\n\t\t\t\tdbTotalMs: metrics.dbTotalMs,\n\t\t\t\tdbFirstOffset: metrics.dbFirstOffset,\n\t\t\t\tdbLastOffset: metrics.dbLastOffset,\n\t\t\t\tcacheHits: metrics.cacheHits,\n\t\t\t\tcacheMisses: metrics.cacheMisses,\n\t\t\t};\n\t\t\tconsole.log(`${STREAM_END_PREFIX} ${JSON.stringify(snapshot)}`);\n\t\t\t// Emit the full per-query log now that streaming is complete.\n\t\t\tif (recorder) flushRecorder(recorder);\n\t\t},\n\t});\n\n\tconst wrapped = new Response(response.body.pipeThrough(transform), response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(wrapped, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// The identity transform preserves byte counts today, but a stale\n\t// Content-Length on a re-constructed streaming Response risks\n\t// truncation if anything upstream changes; the header is recomputed\n\t// (or chunked encoding used) by the server layer anyway.\n\twrapped.headers.delete(\"Content-Length\");\n\treturn wrapped;\n}\n","/**\n * Eager, transparent prefetch of site-global \"chrome\" data.\n *\n * On a public page render, the shared layout pulls the same site-global data on\n * every request -- menus, widget areas, taxonomy term lists, site settings --\n * but each is awaited inside a separately-rendered Astro component, so they\n * execute as serial DB round trips. This fires them all CONCURRENTLY at the\n * very start of the request, before `next()`:\n *\n * - On remote backends (D1, Durable Objects) the round trips overlap instead\n * of serializing, collapsing ~N sequential RTTs into ~1 wall-clock RTT. On\n * a coalescing backend they additionally batch into a single round trip.\n * - The results land in the per-request `requestCached` store under the exact\n * keys the layout helpers use, so when the components render they hit a warm\n * (in-flight or resolved) cache entry instead of issuing their own query.\n *\n * Nothing here changes what templates call -- it warms the real helpers, so the\n * cache keys and value shapes are guaranteed identical. The caller gates this to\n * the public-page path on a request-scoped (remote) backend; it is a no-op-ish\n * waste on synchronous local SQLite, so don't call it there.\n *\n * Fire-and-forget: never awaited by middleware, never throws (a prefetch failure\n * must not affect the request -- the helpers will simply run on demand).\n */\n\nimport { getDb } from \"../loader.js\";\nimport { getMenu } from \"../menus/index.js\";\nimport { setRequestCacheEntry } from \"../request-cache.js\";\nimport { getSiteSettings } from \"../settings/index.js\";\nimport { getTaxonomyDefs, getTaxonomyTerms } from \"../taxonomies/index.js\";\nimport { getWidgetAreas } from \"../widgets/index.js\";\n\n/** Warm widget areas: one bulk load, primed under each per-area cache key. */\nasync function prefetchWidgetAreas(): Promise<void> {\n\tconst areas = await getWidgetAreas();\n\t// getWidgetArea(name) caches under `widget-area:${name}` and returns the same\n\t// WidgetArea shape getWidgetAreas yields, so priming here makes those calls hit.\n\tfor (const area of areas) {\n\t\tsetRequestCacheEntry(`widget-area:${area.name}`, area);\n\t}\n}\n\n/** Warm every taxonomy's term list via the real helper (primes per-name keys). */\nasync function prefetchTaxonomyTerms(): Promise<void> {\n\tconst defs = await getTaxonomyDefs();\n\tawait Promise.allSettled(defs.map((def) => getTaxonomyTerms(def.name)));\n}\n\n/** Warm every menu via the real helper (primes `menu:${name}:${locale}`). */\nasync function prefetchMenus(): Promise<void> {\n\tconst db = await getDb();\n\t// The layout calls getMenu(name) with hardcoded names; we can't know them, so\n\t// discover every menu name and warm them all (small, bounded chrome table).\n\tconst rows = await db.selectFrom(\"_emdash_menus\").select(\"name\").distinct().execute();\n\tconst names = [...new Set(rows.map((r) => r.name))];\n\tawait Promise.allSettled(names.map((name) => getMenu(name)));\n}\n\n/**\n * Concurrently warm the site-global layout data for the current request.\n * Safe to call only inside the request ALS frame that owns the (remote)\n * request-scoped db. Never throws.\n */\nexport async function prefetchLayoutData(): Promise<void> {\n\ttry {\n\t\tawait Promise.allSettled([\n\t\t\tgetSiteSettings(),\n\t\t\tprefetchMenus(),\n\t\t\tprefetchWidgetAreas(),\n\t\t\tprefetchTaxonomyTerms(),\n\t\t]);\n\t} catch (error) {\n\t\t// Defensive: Promise.allSettled shouldn't reject, but never let a prefetch\n\t\t// failure surface to the request.\n\t\tconsole.error(\"[emdash] layout prefetch failed (non-fatal):\", error);\n\t}\n}\n","import type { HandlerResponse } from \"./types.js\";\n\nexport type PublicPluginApiRouteHandler = (\n\tpluginId: string,\n\tmethod: string,\n\tpath: string,\n\trequest: Request,\n) => Promise<HandlerResponse>;\n\ninterface PublicPluginApiRouteRuntime {\n\tgetPluginRouteMeta(pluginId: string, path: string): { public: boolean } | null;\n\thandlePluginApiRoute(\n\t\tpluginId: string,\n\t\tmethod: string,\n\t\tpath: string,\n\t\trequest: Request,\n\t): Promise<HandlerResponse>;\n}\n\nfunction pluginRouteNotFound(): HandlerResponse {\n\treturn {\n\t\tsuccess: false,\n\t\terror: {\n\t\t\tcode: \"NOT_FOUND\",\n\t\t\tmessage: \"Plugin route not found\",\n\t\t},\n\t};\n}\n\nexport function createPublicPluginApiRouteHandler(\n\truntime: PublicPluginApiRouteRuntime,\n): PublicPluginApiRouteHandler {\n\treturn async (pluginId, method, path, request) => {\n\t\tconst meta = runtime.getPluginRouteMeta(pluginId, path);\n\t\tif (meta?.public !== true) {\n\t\t\treturn pluginRouteNotFound();\n\t\t}\n\n\t\treturn runtime.handlePluginApiRoute(pluginId, method, path, request);\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 type { 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\tcreateCoalescingDialect as virtualCreateCoalescingDialect,\n\tcreateDialect as virtualCreateDialect,\n\tcreateRequestScopedDb as virtualCreateRequestScopedDb,\n} from \"virtual:emdash/dialect\";\nimport type { RequestScopedDbOpts } 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\";\n// @ts-ignore - virtual module\nimport * as virtualSandboxRunnerModule 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 { createScheduler as virtualCreateScheduler } from \"virtual:emdash/scheduler\";\n// @ts-ignore - virtual module\nimport { createStorage as virtualCreateStorage } from \"virtual:emdash/storage\";\n\nimport { after } from \"../after.js\";\nimport {\n\tcreateRecorder,\n\tflushRecorder,\n\tisInstrumentationEnabled,\n} from \"../database/instrumentation.js\";\nimport {\n\tDB_INIT_DEADLINE_MS,\n\tEmDashRuntime,\n\ttype RuntimeDependencies,\n\ttype SandboxedPluginEntry,\n\ttype MediaProviderEntry,\n\ttype CreateSchedulerFn,\n} from \"../emdash-runtime.js\";\nimport { setI18nConfig } from \"../i18n/config.js\";\nimport type { Database, Storage } from \"../index.js\";\nimport { createPublicMediaUrlResolver } from \"../media/url.js\";\nimport type { SandboxRunner } from \"../plugins/sandbox/types.js\";\nimport type { ResolvedPlugin } from \"../plugins/types.js\";\nimport { invalidateUrlPatternCache } from \"../query.js\";\nimport {\n\tcreateRequestMetrics,\n\tgetRequestContext,\n\ttype RequestMetrics,\n\trunWithContext,\n} from \"../request-context.js\";\nimport type { PublishedRef } from \"../scheduled-publish.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport { createInitLock, type InitLock, initWithLock } from \"../utils/init-lock.js\";\nimport type { EmDashConfig } from \"./integration/runtime.js\";\nimport { ASTRO_COOKIES_SYMBOL, finishScoped } from \"./middleware/scoped-db.js\";\nimport { wrapBodyForStreamMetrics } from \"./middleware/stream-end-metrics.js\";\nimport { prefetchLayoutData } from \"./prefetch.js\";\nimport { createPublicPluginApiRouteHandler } from \"./public-plugin-api-routes.js\";\nimport { resolveSessionUser } from \"./session-user.js\";\nimport type { EmDashHandlers } from \"./types.js\";\n\n/**\n * Runtime init lock reclaim deadline. Must be strictly larger than the db\n * init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →\n * the db init lock, and equal deadlines would let this outer lock reclaim\n * (spawning a second cron scheduler and sandbox runner) while the inner db\n * init is legitimately still working through a contended migration.\n */\nconst RUNTIME_INIT_DEADLINE_MS = DB_INIT_DEADLINE_MS + 15_000;\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 *\n * Stored on globalThis behind a Symbol key so the flag is a true singleton\n * even when the bundler duplicates this module across SSR chunks (same\n * pattern as request-cache.ts). A plain module-scoped `let` becomes multiple\n * independent variables, which would make the setup probe re-run far more\n * often than intended — and every re-run is another chance for a transient\n * DB error to be misread as \"fresh install\" and bounce visitors to setup.\n */\nconst SETUP_VERIFIED_KEY = Symbol.for(\"emdash:setup-verified\");\nconst setupFlagStore = globalThis as Record<symbol, unknown>;\n\nfunction isSetupVerified(): boolean {\n\treturn setupFlagStore[SETUP_VERIFIED_KEY] === true;\n}\n\nfunction markSetupVerified(): void {\n\tsetupFlagStore[SETUP_VERIFIED_KEY] = true;\n}\n\n/**\n * The runtime singleton and its init lock live on globalThis behind a\n * Symbol — same reasoning as SETUP_VERIFIED_KEY above: the bundler can\n * duplicate this module across SSR chunks, and a duplicated instance/lock\n * would mean multiple runtimes (each with its own cron scheduler) per\n * isolate, initializing and reclaiming independently.\n */\nconst RUNTIME_HOLDER_KEY = Symbol.for(\"emdash:runtime-holder\");\ninterface RuntimeHolder {\n\tinstance: EmDashRuntime | null;\n\tlock: InitLock;\n}\n\nfunction getRuntimeHolder(): RuntimeHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = setupFlagStore[RUNTIME_HOLDER_KEY] as RuntimeHolder | undefined;\n\tif (!holder) {\n\t\tholder = { instance: null, lock: createInitLock() };\n\t\tsetupFlagStore[RUNTIME_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\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/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/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/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/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\t/* eslint-disable typescript-eslint/no-unsafe-type-assertion --\n\t The virtual:emdash/* imports above use @ts-ignore because tsgo/IDE\n\t resolution can't see virtual-modules.d.ts in every consumer setup,\n\t so they arrive as `any`. The casts here line each entry up with\n\t RuntimeDependencies's expected shape. The contract is enforced by\n\t the integration that populates these virtual modules. */\n\tconst sandboxModule = virtualSandboxRunnerModule as Record<string, unknown>;\n\treturn {\n\t\tconfig,\n\t\tplugins: getPlugins(),\n\t\tcreateDialect: virtualCreateDialect as (config: Record<string, unknown>) => unknown,\n\t\t// Optional: only batching backends (D1, DO) export this; undefined otherwise.\n\t\tcreateCoalescingDialect: virtualCreateCoalescingDialect as\n\t\t\t| ((config: Record<string, unknown>) => unknown)\n\t\t\t| undefined,\n\t\tcreateStorage: virtualCreateStorage as ((config: Record<string, unknown>) => Storage) | null,\n\t\tcreateScheduler: virtualCreateScheduler as CreateSchedulerFn | null,\n\t\tsandboxEnabled: sandboxModule.sandboxEnabled as boolean,\n\t\tsandboxBypassed: (sandboxModule.sandboxBypassed as boolean) ?? false,\n\t\tsandboxedPluginEntries: (virtualSandboxedPlugins as SandboxedPluginEntry[]) || [],\n\t\tcreateSandboxRunner: sandboxModule.createSandboxRunner as\n\t\t\t| ((opts: {\n\t\t\t\t\tdb: Kysely<Database>;\n\t\t\t\t\tmediaStorage?: {\n\t\t\t\t\t\tupload(options: {\n\t\t\t\t\t\t\tkey: string;\n\t\t\t\t\t\t\tbody: Uint8Array;\n\t\t\t\t\t\t\tcontentType: string;\n\t\t\t\t\t\t}): Promise<unknown>;\n\t\t\t\t\t\tdelete(key: string): Promise<unknown>;\n\t\t\t\t\t};\n\t\t\t }) => SandboxRunner)\n\t\t\t| null,\n\t\tmediaProviderEntries: (virtualMediaProviders as MediaProviderEntry[]) || [],\n\t};\n\t/* eslint-enable typescript-eslint/no-unsafe-type-assertion */\n}\n\n/**\n * Get or create the runtime instance.\n *\n * When `initTimings` is provided, any timing samples recorded during a\n * genuine cold init are appended. Subsequent warm calls (hitting the\n * cached instance) push nothing — callers should treat an empty array\n * as \"warm, nothing to report\".\n */\nasync function getRuntime(\n\tconfig: EmDashConfig,\n\tinitTimings?: Array<{ name: string; dur: number; desc?: string }>,\n): Promise<EmDashRuntime> {\n\t// Waiters poll rather than awaiting the initializing request's promise —\n\t// workerd flags cross-request promise resolution (warnings + potential\n\t// hangs). If the initializing request is cancelled mid-create (client\n\t// disconnect tears down its continuation, skipping any `finally`), the\n\t// anchored init keeps running under waitUntil and populates the cache;\n\t// failing that, the stale lock is reclaimed after a deadline instead of\n\t// hanging every subsequent request in the isolate until eviction.\n\tconst holder = getRuntimeHolder();\n\treturn initWithLock(\n\t\tholder.lock,\n\t\t() => holder.instance,\n\t\tasync (isCurrentClaim) => {\n\t\t\tconst deps = buildDependencies(config);\n\t\t\tconst runtime = await EmDashRuntime.create(deps, initTimings);\n\t\t\tif (isCurrentClaim()) {\n\t\t\t\tholder.instance = runtime;\n\t\t\t} else {\n\t\t\t\t// This init was reclaimed mid-flight (it ran past the deadline\n\t\t\t\t// and a waiter started its own). Don't overwrite the\n\t\t\t\t// reclaimer's published runtime, and stop this one's cron\n\t\t\t\t// scheduler so it doesn't keep firing unreferenced. The\n\t\t\t\t// runtime is still returned — it's fully functional for the\n\t\t\t\t// request that built it.\n\t\t\t\truntime.stopCron().catch((error: unknown) => {\n\t\t\t\t\tconsole.error(\"[emdash] failed to stop superseded runtime's cron:\", error);\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn runtime;\n\t\t},\n\t\t{\n\t\t\tdeadlineMs: RUNTIME_INIT_DEADLINE_MS,\n\t\t\tanchor: (promise) => after(() => promise),\n\t\t},\n\t);\n}\n\n/**\n * Run scheduled maintenance (cron tasks, scheduled publishing, system cleanup)\n * outside any request. Resolves the runtime from the build-time virtual config\n * and the cached singleton — the same instance request handlers use.\n *\n * Wired into a platform heartbeat that is not a request: the Cloudflare Worker's\n * `scheduled()` handler (Cron Trigger) calls this. On Node the runtime's own\n * timer-based scheduler already drives the same work, so this isn't needed there.\n *\n * Returns the content promoted by the publishing sweep so the caller can purge\n * edge-cache tags for it. `onPublished` (optional) is awaited after each\n * collection's batch so the caller can invalidate edge-cache tags incrementally\n * rather than only after the whole sweep.\n */\nexport async function runScheduledTasks(\n\toptions: { onPublished?: (refs: PublishedRef[]) => Promise<void> } = {},\n): Promise<{ published: PublishedRef[] }> {\n\tconst config = getConfig();\n\tif (!config) return { published: [] };\n\tconst runtime = await getRuntime(config);\n\n\t// Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse\n\t// the per-isolate singleton from a Cron Trigger: its socket belongs to the\n\t// request that opened it, and workerd rejects cross-event I/O. Open an\n\t// event-scoped connection for the sweep and run the batch under it in ALS —\n\t// the runtime's db getter, the cron executor, and plugin cron contexts all\n\t// resolve the connection from ALS — then close it. Gated on the adapter\n\t// being connection-backed (it exposes `close()`); stateless adapters (D1,\n\t// Node SQLite) return null or a close-less scope and keep using the\n\t// singleton, so their cron path is unchanged.\n\tconst scoped = createRequestScopedDb({\n\t\tconfig: config.database?.config,\n\t\tisAuthenticated: false,\n\t\t// The sweep publishes and cleans up — a write workload — so a\n\t\t// connection-backed adapter routes it to the primary.\n\t\tisWrite: true,\n\t\tcookies: NOOP_COOKIE_JAR,\n\t\turl: CRON_EVENT_URL,\n\t});\n\tif (!scoped?.close) {\n\t\t// Stateless adapter (or no per-request scoping): the singleton is safe\n\t\t// outside a request. Any close-less scope created above is discarded.\n\t\treturn runtime.runScheduledTasks(options);\n\t}\n\n\tconst parent = getRequestContext();\n\tconst ctx = parent\n\t\t? { ...parent, db: scoped.db }\n\t\t: { editMode: false, db: scoped.db, metrics: createRequestMetrics(performance.now()) };\n\ttry {\n\t\treturn await runWithContext(ctx, () => runtime.runScheduledTasks(options));\n\t} finally {\n\t\t// Guard both so a throw in teardown can't mask the sweep result or skip\n\t\t// close() and leak the connection. Mirrors closeSafely() in scoped-db.ts.\n\t\ttry {\n\t\t\tscoped.commit();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled] request-scoped db commit failed:\", error);\n\t\t}\n\t\ttry {\n\t\t\tscoped.close();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled] request-scoped db close failed:\", error);\n\t\t}\n\t}\n}\n\n/**\n * A cookie jar that reads nothing and writes nothing, for request-scoped db\n * adapters invoked outside an HTTP request (the Cron Trigger sweep). Connection\n * adapters like Hyperdrive ignore cookies entirely; the D1 session adapter\n * reads/writes a bookmark cookie, but cron never reaches that path (it has no\n * `close()`), so the no-ops are never observed.\n */\nconst NOOP_COOKIE_JAR = {\n\tget: () => undefined,\n\tset: () => {},\n};\n\n/**\n * Synthetic URL for the cron sweep's request-scoped db opts. Only the D1\n * session adapter inspects `url` (for cookie `secure`), and cron doesn't take\n * that path, so the value is never used — it exists to satisfy the contract.\n */\nconst CRON_EVENT_URL = new URL(\"https://cron.emdash.internal/\");\n\n/**\n * Baseline security headers applied to all responses.\n * Admin routes get additional headers (strict CSP) from auth middleware.\n */\nfunction finalizeResponse(\n\tresponse: Response,\n\tserverTimings?: Array<{ name: string; dur: number; desc?: string }>,\n): Response {\n\tconst res = new Response(response.body, response);\n\tconst astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);\n\tif (astroCookies !== undefined) {\n\t\tReflect.set(res, ASTRO_COOKIES_SYMBOL, astroCookies);\n\t}\n\t// Set-if-absent so a host app that sets stricter values on its own routes\n\t// wins. The middleware registers `order: 'pre'` (#1282), so on the response\n\t// path it runs *after* host middleware; unconditional `set()` would clobber\n\t// the host's headers on every public route (#1393). Mirrors the CSP guard.\n\tif (!res.headers.has(\"X-Content-Type-Options\")) {\n\t\tres.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\t}\n\tif (!res.headers.has(\"Referrer-Policy\")) {\n\t\tres.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\t}\n\tif (!res.headers.has(\"Permissions-Policy\")) {\n\t\tres.headers.set(\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=(), payment=()\");\n\t}\n\tif (!res.headers.has(\"Content-Security-Policy\")) {\n\t\tres.headers.set(\"X-Frame-Options\", \"SAMEORIGIN\");\n\t}\n\tif (serverTimings && serverTimings.length > 0) {\n\t\tres.headers.set(\n\t\t\t\"Server-Timing\",\n\t\t\tserverTimings\n\t\t\t\t.map((t) => {\n\t\t\t\t\tconst dur = Math.round(t.dur);\n\t\t\t\t\treturn t.desc ? `${t.name};dur=${dur};desc=\"${t.desc}\"` : `${t.name};dur=${dur}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \"),\n\t\t);\n\t}\n\treturn res;\n}\n\n/**\n * Append always-on counters (db.*, cache.*) to the Server-Timing list.\n *\n * dur values for `count`, `hit`, `miss` are integer counts — Server-Timing\n * spec only models milliseconds, but browsers show whatever number is given,\n * which is the convention most projects use for non-time samples.\n */\nfunction pushMetricsTimings(\n\ttimings: Array<{ name: string; dur: number; desc?: string }>,\n\tmetrics: RequestMetrics,\n): void {\n\tif (metrics.dbCount > 0) {\n\t\ttimings.push({ name: \"db.total\", dur: metrics.dbTotalMs, desc: \"DB total\" });\n\t\ttimings.push({ name: \"db.count\", dur: metrics.dbCount, desc: \"Query count\" });\n\t\tif (metrics.dbFirstOffset !== null) {\n\t\t\ttimings.push({ name: \"db.first\", dur: metrics.dbFirstOffset, desc: \"First query at\" });\n\t\t}\n\t\tif (metrics.dbLastOffset !== null) {\n\t\t\ttimings.push({ name: \"db.last\", dur: metrics.dbLastOffset, desc: \"Last query at\" });\n\t\t}\n\t}\n\tif (metrics.rpcCount > 0) {\n\t\ttimings.push({ name: \"rpc.count\", dur: metrics.rpcCount, desc: \"DB round trips\" });\n\t}\n\tif (metrics.cacheHits + metrics.cacheMisses > 0) {\n\t\ttimings.push({ name: \"cache.hit\", dur: metrics.cacheHits, desc: \"Cache hits\" });\n\t\ttimings.push({ name: \"cache.miss\", dur: metrics.cacheMisses, desc: \"Cache misses\" });\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\"]);\nconst SITEMAP_COLLECTION_RE = /^\\/sitemap-[a-z][a-z0-9_]*\\.xml$/;\n\n/**\n * Ask the configured database adapter for a per-request scoped Kysely. The\n * adapter encapsulates any per-request semantics (D1 sessions, read-replica\n * routing, bookmark cookies, etc.); core just forwards the cookie jar and\n * request flags and wraps next() in ALS if a scope was returned.\n */\nfunction createRequestScopedDb(\n\topts: RequestScopedDbOpts,\n): { db: Kysely<Database>; commit: () => void; close?: () => void } | null {\n\tif (typeof virtualCreateRequestScopedDb !== \"function\") return null;\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- adapter returns Kysely<unknown>; cast to Database since core owns that type\n\tconst fn = virtualCreateRequestScopedDb as (\n\t\to: RequestScopedDbOpts,\n\t) => { db: Kysely<Database>; commit: () => void; close?: () => void } | null;\n\treturn fn(opts);\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { request, locals, cookies } = context;\n\tconst url = context.url;\n\n\t// Fast path: routes outside /_emdash/ that plugins inject (e.g.,\n\t// /.well-known/atproto-client-metadata.json) skip the entire runtime\n\t// init + middleware chain. External servers fetch these with tight\n\t// timeouts (~1-2s) so they must respond quickly even on cold starts.\n\tif (!url.pathname.startsWith(\"/_emdash\") && virtualConfig?.authProviders) {\n\t\tconst isPluginFastRoute = virtualConfig.authProviders.some(\n\t\t\t(p: { routes?: { pattern?: string }[] }) =>\n\t\t\t\tp.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),\n\t\t);\n\t\tif (isPluginFastRoute) {\n\t\t\treturn finalizeResponse(await next());\n\t\t}\n\t}\n\n\tconst queryRecorder = isInstrumentationEnabled()\n\t\t? createRecorder(url.pathname, request.method, request.headers.get(\"x-perf-phase\") ?? \"default\")\n\t\t: undefined;\n\n\tconst metrics = createRequestMetrics(performance.now());\n\n\tconst run = async (): Promise<Response> => {\n\t\t// Process /_emdash routes and public routes with an active session\n\t\t// (logged-in editors need the runtime for toolbar/visual editing on public pages)\n\t\tconst isEmDashRoute = url.pathname.startsWith(\"/_emdash\");\n\t\tconst isPublicRuntimeRoute =\n\t\t\tPUBLIC_RUNTIME_ROUTES.has(url.pathname) || SITEMAP_COLLECTION_RE.test(url.pathname);\n\n\t\t// Check for edit mode cookie - editors viewing public pages need the runtime\n\t\t// so auth middleware can verify their session for visual editing\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t\t// Playground mode: the playground middleware stashes the per-session DO database\n\t\t// on locals.__playgroundDb. When present, use runWithContext() to make it\n\t\t// available to getDb() and the runtime's db getter via the correct ALS instance.\n\t\tconst playgroundDb = locals.__playgroundDb;\n\n\t\t// Read the Astro session user once up-front. Both the anonymous fast path\n\t\t// and the full doInit path need this, and the session store is network-backed\n\t\t// (KV / Durable Object) so we want to avoid re-fetching on the hot path.\n\t\t// Skipped entirely for:\n\t\t// - prerendered requests (no session at build time)\n\t\t// - requests without an `astro-session` cookie (no session to look up)\n\t\t// The cookie check matters on Cloudflare Workers, where Astro's session\n\t\t// backend is KV: calling session.get() on every anonymous public request\n\t\t// turns normal traffic into a flood of KV read misses. See #733.\n\t\tconst hasSessionCookie = cookies.get(\"astro-session\") !== undefined;\n\t\tconst sessionUser =\n\t\t\tcontext.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);\n\n\t\t// Credentialed API requests (API tokens `ec_pat_*`, OAuth tokens\n\t\t// `ec_oat_*`, and other Bearer credentials) carry no `astro-session`\n\t\t// cookie, so `sessionUser` is null for them -- yet they still expect\n\t\t// read-your-writes. The auth middleware that resolves the token runs\n\t\t// *after* this one, so `locals.user` isn't populated here; detect the\n\t\t// credential directly on the request. Request-scoped adapters use this to\n\t\t// keep such requests on the primary/uncached connection (not a lagging\n\t\t// read replica or the Hyperdrive query cache).\n\t\tconst hasBearerAuth = (request.headers.get(\"authorization\") ?? \"\")\n\t\t\t.toLowerCase()\n\t\t\t.startsWith(\"bearer \");\n\n\t\tif (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {\n\t\t\tif (!sessionUser && !playgroundDb) {\n\t\t\t\tconst timings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\tconst mwStart = performance.now();\n\n\t\t\t\t// On a fresh deployment the database may be completely empty.\n\t\t\t\t// Public pages call getSiteSettings() / getMenu() via getDb(), which\n\t\t\t\t// bypasses runtime init and would crash with \"no such table: options\".\n\t\t\t\t// Do a one-time lightweight probe using the same getDb() instance the\n\t\t\t\t// page will use: if the migrations table doesn't exist, no migrations\n\t\t\t\t// have ever run -- redirect to the setup wizard.\n\t\t\t\t// Skip the probe when prerendering: a prerendered route is built to\n\t\t\t\t// static HTML, so returning context.redirect(\"/_emdash/admin/setup\")\n\t\t\t\t// below would bake that redirect into the page and ship it to\n\t\t\t\t// production. The build database is legitimately empty in CI and there\n\t\t\t\t// is no live visitor to send to the wizard at build time (session reads\n\t\t\t\t// are already skipped for prerender above for the same reason).\n\t\t\t\tif (!isSetupVerified() && !context.isPrerendered) {\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst { getDb } = await import(\"../loader.js\");\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\tawait db\n\t\t\t\t\t\t\t.selectFrom(\"_emdash_migrations\" as keyof Database)\n\t\t\t\t\t\t\t.selectAll()\n\t\t\t\t\t\t\t.limit(1)\n\t\t\t\t\t\t\t.execute();\n\t\t\t\t\t\tmarkSetupVerified();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Only a genuinely-missing migrations table means a fresh,\n\t\t\t\t\t\t// un-set-up database — redirect to the setup wizard.\n\t\t\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\t\t\treturn context.redirect(\"/_emdash/admin/setup\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Any other failure (transient D1/replica error, timeout, cold-start\n\t\t\t\t\t\t// race, locked SQLite) must NOT be read as \"fresh install\" — doing so\n\t\t\t\t\t\t// bounces real visitors on a set-up site to /_emdash/admin/setup.\n\t\t\t\t\t\t// Leave the flag unset so a later request can re-verify, and fall\n\t\t\t\t\t\t// through to render the page normally.\n\t\t\t\t\t\tconsole.error(\"Setup probe failed (non-fatal):\", error);\n\t\t\t\t\t}\n\t\t\t\t\ttimings.push({ name: \"setup\", dur: performance.now() - t0, desc: \"Setup probe\" });\n\t\t\t\t}\n\n\t\t\t\t// Initialize the runtime for page:metadata and page:fragments hooks.\n\t\t\t\t// The runtime is a cached singleton — after the first request,\n\t\t\t\t// getRuntime() is just a null-check. This enables SEO plugins to\n\t\t\t\t// contribute meta tags for all visitors, not just logged-in editors.\n\t\t\t\tconst config = getConfig();\n\t\t\t\tif (config) {\n\t\t\t\t\t// Sub-phase timings are populated only on the cold init. Warm\n\t\t\t\t\t// requests hit the cached runtime and leave this empty.\n\t\t\t\t\tconst initSubTimings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst runtime = await getRuntime(config, initSubTimings);\n\t\t\t\t\t\tmarkSetupVerified();\n\t\t\t\t\t\tconst handlePublicPluginApiRoute = createPublicPluginApiRouteHandler(runtime);\n\t\t\t\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- partial object; getPageRuntime() only checks for the page-contribution methods\n\t\t\t\t\t\tlocals.emdash = {\n\t\t\t\t\t\t\thandlePublicPluginApiRoute,\n\t\t\t\t\t\t\tcollectPageMetadata: runtime.collectPageMetadata.bind(runtime),\n\t\t\t\t\t\t\tcollectPageFragments: runtime.collectPageFragments.bind(runtime),\n\t\t\t\t\t\t\tgetPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),\n\t\t\t\t\t\t\t// Exposed so the wrapped image endpoint (`/_image`) can read media\n\t\t\t\t\t\t\t// bytes from storage on the anonymous fast path -- public `<img>`\n\t\t\t\t\t\t\t// requests carry no session.\n\t\t\t\t\t\t\tstorage: runtime.storage,\n\t\t\t\t\t\t} as EmDashHandlers;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Non-fatal — EmDashHead will fall back to base SEO contributions\n\t\t\t\t\t}\n\t\t\t\t\ttimings.push({ name: \"rt\", dur: performance.now() - t0, desc: \"Runtime init\" });\n\t\t\t\t\t// Append cold-only sub-phase timings so the breakdown is visible\n\t\t\t\t\t// in Server-Timing (rt.db, rt.fts, rt.plugins, rt.site,\n\t\t\t\t\t// rt.sandbox, rt.market, rt.hooks, rt.cron).\n\t\t\t\t\tfor (const sub of initSubTimings) timings.push(sub);\n\t\t\t\t}\n\n\t\t\t\t// Even on the anonymous fast path we ask the adapter for a per-request\n\t\t\t\t// scoped db. For D1 with read replication this routes anonymous reads\n\t\t\t\t// to the nearest replica; for other adapters it's a no-op.\n\t\t\t\tconst anonScoped = createRequestScopedDb({\n\t\t\t\t\tconfig: config?.database?.config,\n\t\t\t\t\tisAuthenticated: false,\n\t\t\t\t\tisWrite: request.method !== \"GET\" && request.method !== \"HEAD\",\n\t\t\t\t\tcookies,\n\t\t\t\t\turl,\n\t\t\t\t});\n\t\t\t\tconst runAnon = async () => {\n\t\t\t\t\tconst t0 = performance.now();\n\t\t\t\t\tconst response = await next();\n\t\t\t\t\ttimings.push({ name: \"render\", dur: performance.now() - t0, desc: \"Page render\" });\n\t\t\t\t\ttimings.push({ name: \"mw\", dur: performance.now() - mwStart, desc: \"Total middleware\" });\n\t\t\t\t\tpushMetricsTimings(timings, metrics);\n\t\t\t\t\t// Server-Timing only sees pre-stream queries; the stream-end\n\t\t\t\t\t// wrapper (instrumentation-gated, no-op otherwise) emits the\n\t\t\t\t\t// final counters once the body finishes streaming.\n\t\t\t\t\treturn wrapBodyForStreamMetrics(finalizeResponse(response, timings));\n\t\t\t\t};\n\t\t\t\tif (anonScoped) {\n\t\t\t\t\tconst parent = getRequestContext();\n\t\t\t\t\tconst ctx = parent\n\t\t\t\t\t\t? { ...parent, db: anonScoped.db }\n\t\t\t\t\t\t: { editMode: false, db: anonScoped.db, metrics };\n\t\t\t\t\t// Eagerly warm site-global layout data (menus, widget areas,\n\t\t\t\t\t// taxonomy terms, settings) concurrently so the layout's\n\t\t\t\t\t// per-component reads overlap into ~one wall-clock round trip and\n\t\t\t\t\t// hit a warm cache instead of serializing. Three guards:\n\t\t\t\t\t// - request-scoped (remote) backend only -- this branch implies it;\n\t\t\t\t\t// pointless on synchronous local SQLite.\n\t\t\t\t\t// - HTML navigations only -- feeds/sitemaps/JSON don't render the\n\t\t\t\t\t// layout, so prefetching their chrome is pure waste.\n\t\t\t\t\t// - via after(): it runs immediately (still warms the render) but\n\t\t\t\t\t// hands the promise to waitUntil, so the surplus warm-up (chrome a\n\t\t\t\t\t// given page doesn't render) is kept alive past the response rather\n\t\t\t\t\t// than erroring on workerd as orphaned request I/O.\n\t\t\t\t\t// Gate on the CLIENT'S PREFERRED type (leading media range), not a\n\t\t\t\t\t// substring -- browser navigations lead with `text/html`, while feed\n\t\t\t\t\t// readers lead with `application/rss+xml` etc. and only list\n\t\t\t\t\t// `text/html;q=0.8` later, so a substring match would leak onto feeds.\n\t\t\t\t\tconst acceptsHtml = (request.headers.get(\"accept\") ?? \"\")\n\t\t\t\t\t\t.split(\",\", 1)[0]!\n\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t.startsWith(\"text/html\");\n\t\t\t\t\treturn runWithContext(ctx, async () => {\n\t\t\t\t\t\tif (acceptsHtml) after(() => prefetchLayoutData());\n\t\t\t\t\t\t// commit() persists per-request state (e.g. the D1 bookmark cookie)\n\t\t\t\t\t\t// before the response is returned, even if render throws; close()\n\t\t\t\t\t\t// (connection teardown) is deferred to stream-end. See finishScoped.\n\t\t\t\t\t\treturn finishScoped(anonScoped, runAnon);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn runAnon();\n\t\t\t}\n\t\t}\n\n\t\tconst config = getConfig();\n\t\tif (!config) {\n\t\t\tconsole.error(\"EmDash: No configuration found\");\n\t\t\treturn finalizeResponse(await next());\n\t\t}\n\n\t\t// In playground mode, wrap the entire runtime init + request handling in\n\t\t// runWithContext so that getDatabase() and all init queries use the real\n\t\t// DO database via the same AsyncLocalStorage instance as the loader.\n\t\tconst doInit = async () => {\n\t\t\tconst timings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\tconst mwStart = performance.now();\n\n\t\t\ttry {\n\t\t\t\t// Get or create runtime. Sub-phase timings (rt.db, rt.fts, rt.plugins,\n\t\t\t\t// rt.site, rt.sandbox, rt.market, rt.hooks, rt.cron) are populated\n\t\t\t\t// only on the cold init — subsequent warm calls find the cached\n\t\t\t\t// instance and `initSubTimings` stays empty.\n\t\t\t\tconst initSubTimings: Array<{ name: string; dur: number; desc?: string }> = [];\n\t\t\t\tlet t0 = performance.now();\n\t\t\t\tconst runtime = await getRuntime(config, initSubTimings);\n\t\t\t\ttimings.push({ name: \"rt\", dur: performance.now() - t0, desc: \"Runtime init\" });\n\t\t\t\t// Forward any sub-phase samples so cold-start breakdown is visible\n\t\t\t\t// in Server-Timing. Each phase appears prefixed \"rt.\" to distinguish\n\t\t\t\t// from the aggregate \"rt\" timing above.\n\t\t\t\tfor (const sub of initSubTimings) timings.push(sub);\n\n\t\t\t\t// Runtime init runs migrations, so the DB is guaranteed set up\n\t\t\t\tmarkSetupVerified();\n\n\t\t\t\t// The manifest is no longer pre-loaded here. It's admin-only\n\t\t\t\t// content that public/anonymous requests never read, and\n\t\t\t\t// loading it on every request put logged-out hot paths on\n\t\t\t\t// the same staleness budget as admin operations. Admin\n\t\t\t\t// routes call `emdash.getManifest()` directly.\n\n\t\t\t\t// Attach to locals for route handlers\n\t\t\t\tlocals.emdash = {\n\t\t\t\t\t// Content handlers\n\t\t\t\t\thandleContentList: runtime.handleContentList.bind(runtime),\n\t\t\t\t\thandleContentGet: runtime.handleContentGet.bind(runtime),\n\t\t\t\t\thandleContentAuthors: runtime.handleContentAuthors.bind(runtime),\n\t\t\t\t\thandleContentCreate: runtime.handleContentCreate.bind(runtime),\n\t\t\t\t\thandleContentUpdate: runtime.handleContentUpdate.bind(runtime),\n\t\t\t\t\thandleContentDelete: runtime.handleContentDelete.bind(runtime),\n\n\t\t\t\t\t// Trash handlers\n\t\t\t\t\thandleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),\n\t\t\t\t\thandleContentRestore: runtime.handleContentRestore.bind(runtime),\n\t\t\t\t\thandleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),\n\t\t\t\t\thandleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),\n\t\t\t\t\thandleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),\n\n\t\t\t\t\t// Duplicate handler\n\t\t\t\t\thandleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),\n\n\t\t\t\t\t// Publishing & Scheduling handlers\n\t\t\t\t\thandleContentPublish: runtime.handleContentPublish.bind(runtime),\n\t\t\t\t\thandleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),\n\t\t\t\t\thandleContentSchedule: runtime.handleContentSchedule.bind(runtime),\n\t\t\t\t\thandleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),\n\t\t\t\t\thandleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),\n\t\t\t\t\thandleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),\n\t\t\t\t\thandleContentCompare: runtime.handleContentCompare.bind(runtime),\n\t\t\t\t\thandleContentTranslations: runtime.handleContentTranslations.bind(runtime),\n\n\t\t\t\t\t// Media handlers\n\t\t\t\t\thandleMediaList: runtime.handleMediaList.bind(runtime),\n\t\t\t\t\thandleMediaGet: runtime.handleMediaGet.bind(runtime),\n\t\t\t\t\thandleMediaCreate: runtime.handleMediaCreate.bind(runtime),\n\t\t\t\t\thandleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),\n\t\t\t\t\thandleMediaDelete: runtime.handleMediaDelete.bind(runtime),\n\n\t\t\t\t\t// Revision handlers\n\t\t\t\t\thandleRevisionList: runtime.handleRevisionList.bind(runtime),\n\t\t\t\t\thandleRevisionGet: runtime.handleRevisionGet.bind(runtime),\n\t\t\t\t\thandleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),\n\n\t\t\t\t\t// Plugin routes\n\t\t\t\t\thandlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),\n\t\t\t\t\thandlePublicPluginApiRoute: createPublicPluginApiRouteHandler(runtime),\n\t\t\t\t\tgetPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),\n\n\t\t\t\t\t// Media provider methods\n\t\t\t\t\tgetMediaProvider: runtime.getMediaProvider.bind(runtime),\n\t\t\t\t\tgetMediaProviderList: runtime.getMediaProviderList.bind(runtime),\n\n\t\t\t\t\t// Page contribution methods (for EmDashHead/EmDashBodyStart/EmDashBodyEnd)\n\t\t\t\t\tcollectPageMetadata: runtime.collectPageMetadata.bind(runtime),\n\t\t\t\t\tcollectPageFragments: runtime.collectPageFragments.bind(runtime),\n\n\t\t\t\t\t// Lazy search index health check — search endpoints call this\n\t\t\t\t\t// before querying so a crash-corrupted index gets repaired on\n\t\t\t\t\t// first use rather than stalling every cold start.\n\t\t\t\t\tensureSearchHealthy: runtime.ensureSearchHealthy.bind(runtime),\n\n\t\t\t\t\t// Direct access (for advanced use cases)\n\t\t\t\t\tstorage: runtime.storage,\n\t\t\t\t\t// Lazy getter, not an eager snapshot: `locals.emdash` is built\n\t\t\t\t\t// before the per-request scoped db is installed in ALS, so reading\n\t\t\t\t\t// `runtime.db` here would capture the per-isolate singleton. Routes\n\t\t\t\t\t// access `emdash.db` later, during the request, when the scoped db\n\t\t\t\t\t// is active. For a stateless binding (D1) the two are equivalent,\n\t\t\t\t\t// but for a request-bound connection (pg/Hyperdrive) the singleton\n\t\t\t\t\t// belongs to the cold-start request and reusing it from a warm\n\t\t\t\t\t// request hangs on workerd's cross-request I/O guard.\n\t\t\t\t\tget db() {\n\t\t\t\t\t\treturn runtime.db;\n\t\t\t\t\t},\n\t\t\t\t\tgetPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),\n\t\t\t\t\thooks: runtime.hooks,\n\t\t\t\t\temail: runtime.email,\n\t\t\t\t\tconfiguredPlugins: runtime.configuredPlugins,\n\t\t\t\t\tsandboxedPluginEntries: runtime.sandboxedPluginEntries,\n\n\t\t\t\t\t// Configuration (for checking database type, auth mode, etc.)\n\t\t\t\t\tconfig,\n\n\t\t\t\t\t// Lazy manifest accessor — admin-only consumers call this on\n\t\t\t\t\t// demand. `requestCached` inside `getManifest` dedupes within\n\t\t\t\t\t// a single request.\n\t\t\t\t\tgetManifest: runtime.getManifest.bind(runtime),\n\n\t\t\t\t\t// Clear the URL pattern cache after schema mutations that\n\t\t\t\t\t// affect collection URL patterns.\n\t\t\t\t\tinvalidateUrlPatternCache,\n\n\t\t\t\t\t// Sandbox runner (for marketplace plugin install/update)\n\t\t\t\t\tgetSandboxRunner: runtime.getSandboxRunner.bind(runtime),\n\t\t\t\t\tisSandboxBypassed: runtime.isSandboxBypassed.bind(runtime),\n\n\t\t\t\t\t// Sync marketplace plugin states (after install/update/uninstall)\n\t\t\t\t\tsyncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),\n\n\t\t\t\t\t// Sync registry plugin states (after install/update/uninstall)\n\t\t\t\t\tsyncRegistryPlugins: runtime.syncRegistryPlugins.bind(runtime),\n\n\t\t\t\t\t// Update plugin enabled/disabled status and rebuild hook pipeline\n\t\t\t\t\tsetPluginStatus: runtime.setPluginStatus.bind(runtime),\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"EmDash middleware error:\", error);\n\t\t\t}\n\n\t\t\t// Ask the adapter for a request-scoped db. When it returns one, we stash\n\t\t\t// it in ALS so the runtime's db getter and loader's getDb() pick it up,\n\t\t\t// then call commit() after next() so the adapter can persist any\n\t\t\t// per-request state (e.g. a D1 bookmark cookie for read-your-writes).\n\t\t\tconst scoped = createRequestScopedDb({\n\t\t\t\tconfig: config?.database?.config,\n\t\t\t\tisAuthenticated: !!sessionUser || hasBearerAuth,\n\t\t\t\tisWrite: request.method !== \"GET\" && request.method !== \"HEAD\",\n\t\t\t\tcookies: context.cookies,\n\t\t\t\turl,\n\t\t\t});\n\n\t\t\tconst renderAndFinalize = async () => {\n\t\t\t\tconst t0 = performance.now();\n\t\t\t\tconst response = await next();\n\t\t\t\ttimings.push({ name: \"render\", dur: performance.now() - t0, desc: \"Page render\" });\n\t\t\t\ttimings.push({ name: \"mw\", dur: performance.now() - mwStart, desc: \"Total middleware\" });\n\t\t\t\tpushMetricsTimings(timings, metrics);\n\t\t\t\t// Server-Timing only sees pre-stream queries; the stream-end\n\t\t\t\t// wrapper (instrumentation-gated, no-op otherwise) emits the\n\t\t\t\t// final counters once the body finishes streaming.\n\t\t\t\treturn wrapBodyForStreamMetrics(finalizeResponse(response, timings));\n\t\t\t};\n\n\t\t\tif (scoped) {\n\t\t\t\tconst parent = getRequestContext();\n\t\t\t\tconst ctx = parent\n\t\t\t\t\t? { ...parent, db: scoped.db }\n\t\t\t\t\t: { editMode: false, db: scoped.db, metrics };\n\t\t\t\treturn runWithContext(ctx, () =>\n\t\t\t\t\t// commit() persists per-request state (e.g. the D1 bookmark cookie)\n\t\t\t\t\t// before the response returns, even if render throws; close()\n\t\t\t\t\t// (connection teardown) is deferred to stream-end. See finishScoped.\n\t\t\t\t\tfinishScoped(scoped, renderAndFinalize),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn renderAndFinalize();\n\t\t}; // end doInit\n\n\t\tif (playgroundDb) {\n\t\t\t// Read the edit-mode cookie to determine if visual editing is active.\n\t\t\t// Default to false -- editing is opt-in via the playground toolbar toggle.\n\t\t\tconst editMode = context.cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\t\t// Playground DBs are per-session isolated instances whose schema is\n\t\t\t// independent of the configured one — flag as isolated so schema-\n\t\t\t// derived caches (manifest, taxonomy defs) rebuild against it.\n\t\t\tconst parent = getRequestContext();\n\t\t\tconst ctx = parent\n\t\t\t\t? { ...parent, editMode, db: playgroundDb, dbIsIsolated: true }\n\t\t\t\t: { editMode, db: playgroundDb, dbIsIsolated: true, metrics };\n\t\t\treturn runWithContext(ctx, doInit);\n\t\t}\n\t\treturn doInit();\n\t};\n\n\ttry {\n\t\treturn await runWithContext({ editMode: false, queryRecorder, metrics }, run);\n\t} finally {\n\t\t// Streamed responses defer the flush to stream end (see\n\t\t// wrapBodyForStreamMetrics) so the log captures queries issued while\n\t\t// the body renders. Only flush here for responses that were not\n\t\t// wrapped (no body: redirects, 304s, bodyless errors), where all\n\t\t// queries have already run by the time middleware returns.\n\t\tif (queryRecorder && !queryRecorder.deferredFlush) flushRecorder(queryRecorder);\n\t}\n});\n\nexport default onRequest;\n"],"mappings":"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"}