emdash 0.19.0 → 0.20.0
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.
- package/dist/{adapters-C5AWLJSD.d.mts → adapters-BzIHV3sw.d.mts} +1 -1
- package/dist/{adapters-C5AWLJSD.d.mts.map → adapters-BzIHV3sw.d.mts.map} +1 -1
- package/dist/{allowed-origins-CyYLEJkp.mjs → allowed-origins-B1u7Qnvg.mjs} +2 -2
- package/dist/{allowed-origins-CyYLEJkp.mjs.map → allowed-origins-B1u7Qnvg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +5 -5
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +2 -2
- package/dist/{api-BZ6bhjYs.mjs → api-DStv36ik.mjs} +36 -5
- package/dist/api-DStv36ik.mjs.map +1 -0
- package/dist/{api-tokens-VrXNiNvV.mjs → api-tokens-DPfhPu5V.mjs} +2 -2
- package/dist/{api-tokens-VrXNiNvV.mjs.map → api-tokens-DPfhPu5V.mjs.map} +1 -1
- package/dist/{apply-hQkKKBCf.mjs → apply-Dr7snAMT.mjs} +7 -7
- package/dist/{apply-hQkKKBCf.mjs.map → apply-Dr7snAMT.mjs.map} +1 -1
- package/dist/astro/index.d.mts +10 -10
- package/dist/astro/index.mjs +3 -3
- package/dist/astro/middleware/auth.d.mts +9 -9
- package/dist/astro/middleware/auth.mjs +4 -4
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +63 -112
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +2 -2
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +6 -6
- package/dist/astro/routes/api/admin/bylines/index.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/bulk.mjs +4 -4
- package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/index.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +15 -15
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +14 -14
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +15 -15
- package/dist/astro/routes/api/admin/plugins/updates.mjs +14 -14
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +14 -14
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +2 -2
- package/dist/astro/routes/api/auth/dev-bypass.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/register-options.mjs +5 -5
- package/dist/astro/routes/api/auth/logout.mjs +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
- package/dist/astro/routes/api/auth/me.mjs +2 -2
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/authors.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/index.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/trash.mjs +3 -3
- package/dist/astro/routes/api/dashboard.mjs +1 -1
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/execute.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/media.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +2 -2
- package/dist/astro/routes/api/mcp.mjs +18 -18
- package/dist/astro/routes/api/media/_id_/confirm.mjs +3 -3
- package/dist/astro/routes/api/media/_id_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +1 -1
- package/dist/astro/routes/api/media/providers/index.mjs +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +4 -4
- package/dist/astro/routes/api/media.mjs +4 -4
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/items.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_/translations.mjs +3 -3
- package/dist/astro/routes/api/menus/_name_.mjs +3 -3
- package/dist/astro/routes/api/menus/index.mjs +3 -3
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
- package/dist/astro/routes/api/oauth/register.mjs +1 -1
- package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
- package/dist/astro/routes/api/oauth/token.mjs +4 -4
- package/dist/astro/routes/api/openapi.json.mjs +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/summary.mjs +4 -4
- package/dist/astro/routes/api/redirects/_id_.mjs +4 -4
- package/dist/astro/routes/api/redirects/index.mjs +4 -4
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +14 -14
- package/dist/astro/routes/api/schema/collections/index.mjs +14 -14
- package/dist/astro/routes/api/schema/index.mjs +5 -10
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +14 -14
- package/dist/astro/routes/api/schema/orphans/index.mjs +14 -14
- package/dist/astro/routes/api/search/enable.mjs +5 -5
- package/dist/astro/routes/api/search/index.mjs +4 -4
- package/dist/astro/routes/api/search/rebuild.mjs +5 -5
- package/dist/astro/routes/api/search/stats.mjs +3 -3
- package/dist/astro/routes/api/search/suggest.mjs +4 -4
- package/dist/astro/routes/api/sections/_slug_.mjs +5 -5
- package/dist/astro/routes/api/sections/index.mjs +5 -5
- package/dist/astro/routes/api/settings/email.mjs +1 -1
- package/dist/astro/routes/api/settings.mjs +6 -6
- package/dist/astro/routes/api/setup/admin-verify.mjs +7 -7
- package/dist/astro/routes/api/setup/admin.mjs +6 -6
- package/dist/astro/routes/api/setup/dev-bypass.mjs +10 -10
- package/dist/astro/routes/api/setup/index.mjs +9 -9
- package/dist/astro/routes/api/setup/status.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs +3 -3
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/index.mjs +6 -6
- package/dist/astro/routes/api/themes/preview.mjs +3 -3
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -3
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +1 -1
- package/dist/astro/routes/robots.txt.mjs +3 -3
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +12 -5
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +4 -4
- package/dist/astro/types.d.mts +12 -12
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-C_8t2KGa.mjs → authorize-DsMSVSaY.mjs} +1 -1
- package/dist/{authorize-C_8t2KGa.mjs.map → authorize-DsMSVSaY.mjs.map} +1 -1
- package/dist/{byline-fields-C_OsR-KF.mjs → byline-fields--WxSNS79.mjs} +1 -1
- package/dist/{byline-fields-C_OsR-KF.mjs.map → byline-fields--WxSNS79.mjs.map} +1 -1
- package/dist/{byline-fields-51kg6Vuv.mjs → byline-fields-8TMtkBnH.mjs} +2 -2
- package/dist/{byline-fields-51kg6Vuv.mjs.map → byline-fields-8TMtkBnH.mjs.map} +1 -1
- package/dist/{byline-fields-DYXKDuNX.d.mts → byline-fields-DbibsvTl.d.mts} +5 -1
- package/dist/byline-fields-DbibsvTl.d.mts.map +1 -0
- package/dist/{bylines-Cx5n-WqP.mjs → bylines-BdxWCnPL.mjs} +1 -1
- package/dist/{bylines-Cx5n-WqP.mjs.map → bylines-BdxWCnPL.mjs.map} +1 -1
- package/dist/{bylines-wurS258E.mjs → bylines-s8c2DXbH.mjs} +3 -3
- package/dist/{bylines-wurS258E.mjs.map → bylines-s8c2DXbH.mjs.map} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs → challenge-store-DXX3rfdI.mjs} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs.map → challenge-store-DXX3rfdI.mjs.map} +1 -1
- package/dist/cli/index.mjs +11 -10
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{comments-CJ0RZsYR.mjs → comments-Vkivawyl.mjs} +1 -1
- package/dist/{comments-CJ0RZsYR.mjs.map → comments-Vkivawyl.mjs.map} +1 -1
- package/dist/{components-CTfpu3PZ.mjs → components-CK0cuUoH.mjs} +1 -1
- package/dist/{components-CTfpu3PZ.mjs.map → components-CK0cuUoH.mjs.map} +1 -1
- package/dist/{context-GG52SPgh.mjs → context-Y7BRkWes.mjs} +2 -2
- package/dist/{context-GG52SPgh.mjs.map → context-Y7BRkWes.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +10 -1
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +13 -1
- package/dist/database/instrumentation.mjs.map +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-xLFNSsZ9.mjs → default-IlBaTFxM.mjs} +1 -1
- package/dist/{default-xLFNSsZ9.mjs.map → default-IlBaTFxM.mjs.map} +1 -1
- package/dist/{device-flow-s6_q3T7A.mjs → device-flow-R23SIbQ2.mjs} +4 -4
- package/dist/{device-flow-s6_q3T7A.mjs.map → device-flow-R23SIbQ2.mjs.map} +1 -1
- package/dist/{escape-bIyGoW5W.mjs → escape-Ds07EEyu.mjs} +1 -1
- package/dist/{escape-bIyGoW5W.mjs.map → escape-Ds07EEyu.mjs.map} +1 -1
- package/dist/{index-FfiTQJq2.d.mts → index-B1keaX5Y.d.mts} +43 -12
- package/dist/{index-FfiTQJq2.d.mts.map → index-B1keaX5Y.d.mts.map} +1 -1
- package/dist/{index-BpYeJO1E.d.mts → index-DR56od45.d.mts} +3 -3
- package/dist/{index-BpYeJO1E.d.mts.map → index-DR56od45.d.mts.map} +1 -1
- package/dist/index.d.mts +16 -16
- package/dist/index.mjs +22 -22
- package/dist/{load-B84ohfBk.mjs → load-BBetCvLC.mjs} +1 -1
- package/dist/{load-B84ohfBk.mjs.map → load-BBetCvLC.mjs.map} +1 -1
- package/dist/{loader-CpZKpFz0.mjs → loader-ZN1ll-d-.mjs} +11 -14
- package/dist/loader-ZN1ll-d-.mjs.map +1 -0
- package/dist/{manifest-schema-Cj-YrzrF.mjs → manifest-schema-BtwbL_vj.mjs} +55 -2
- package/dist/manifest-schema-BtwbL_vj.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +11 -11
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-allowlist-CMcoYIjQ.mjs → media-allowlist-Dknq-OFY.mjs} +1 -1
- package/dist/{media-allowlist-CMcoYIjQ.mjs.map → media-allowlist-Dknq-OFY.mjs.map} +1 -1
- package/dist/media-url-VClf8glU.mjs +26 -0
- package/dist/media-url-VClf8glU.mjs.map +1 -0
- package/dist/{menus-Dp9xporj.mjs → menus-DrQLusqj.mjs} +6 -33
- package/dist/menus-DrQLusqj.mjs.map +1 -0
- package/dist/{mode-BjlXswIw.mjs → mode-CO2vQHfq.mjs} +1 -1
- package/dist/{mode-BjlXswIw.mjs.map → mode-CO2vQHfq.mjs.map} +1 -1
- package/dist/{oauth-authorization-1aPAYjiC.mjs → oauth-authorization-Bw4NdF_S.mjs} +4 -4
- package/dist/{oauth-authorization-1aPAYjiC.mjs.map → oauth-authorization-Bw4NdF_S.mjs.map} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs → oauth-clients-BGGFp57s.mjs} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs.map → oauth-clients-BGGFp57s.mjs.map} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs → oauth-state-store-97x0xtN2.mjs} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs.map → oauth-state-store-97x0xtN2.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs → oauth-user-lookup-B_vnZHKO.mjs} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs.map → oauth-user-lookup-B_vnZHKO.mjs.map} +1 -1
- package/dist/{options-D4MnavW_.d.mts → options-DyYIYpPd.d.mts} +3 -3
- package/dist/{options-D4MnavW_.d.mts.map → options-DyYIYpPd.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{passkey-config-BDVM86Tj.mjs → passkey-config-C3QgnQnU.mjs} +1 -1
- package/dist/{passkey-config-BDVM86Tj.mjs.map → passkey-config-C3QgnQnU.mjs.map} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts → placeholder-CVBv5z8k.d.mts} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts.map → placeholder-CVBv5z8k.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-egRHCy1m.mjs → public-url-BFVC2OTJ.mjs} +1 -1
- package/dist/{public-url-egRHCy1m.mjs.map → public-url-BFVC2OTJ.mjs.map} +1 -1
- package/dist/{query-BFQ029Ts.mjs → query-CbUcI4Xk.mjs} +18 -8
- package/dist/query-CbUcI4Xk.mjs.map +1 -0
- package/dist/{rate-limit-ClFFUga6.mjs → rate-limit-C7hjdkS5.mjs} +1 -1
- package/dist/{rate-limit-ClFFUga6.mjs.map → rate-limit-C7hjdkS5.mjs.map} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs → redirect-B_q19j4v.mjs} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs.map → redirect-B_q19j4v.mjs.map} +1 -1
- package/dist/{redirects-DEygMrRO.mjs → redirects-CCbCqCCd.mjs} +4 -2
- package/dist/redirects-CCbCqCCd.mjs.map +1 -0
- package/dist/{redirects-OIu6vQ2i.mjs → redirects-DxVoR7PI.mjs} +1 -1
- package/dist/{redirects-OIu6vQ2i.mjs.map → redirects-DxVoR7PI.mjs.map} +1 -1
- package/dist/request-context.d.mts +7 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +2 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-BcRuXq_h.d.mts → runner-DTdhuI9i.d.mts} +2 -2
- package/dist/{runner-BcRuXq_h.d.mts.map → runner-DTdhuI9i.d.mts.map} +1 -1
- package/dist/runtime.d.mts +10 -10
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-CS7Eg5gh.mjs → schema-C1E70ug_.mjs} +2 -2
- package/dist/{schema-CS7Eg5gh.mjs.map → schema-C1E70ug_.mjs.map} +1 -1
- package/dist/{search-o-aQzHI1.mjs → search-B3SGZw91.mjs} +2 -2
- package/dist/{search-o-aQzHI1.mjs.map → search-B3SGZw91.mjs.map} +1 -1
- package/dist/{secrets-C_ZtRos3.mjs → secrets-ChPTmy9x.mjs} +1 -1
- package/dist/{secrets-C_ZtRos3.mjs.map → secrets-ChPTmy9x.mjs.map} +1 -1
- package/dist/{sections-DhsZ0ns9.mjs → sections-D_lVzwRZ.mjs} +2 -2
- package/dist/{sections-DhsZ0ns9.mjs.map → sections-D_lVzwRZ.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +6 -6
- package/dist/seo/index.d.mts +1 -1
- package/dist/seo/index.d.mts.map +1 -1
- package/dist/seo/index.mjs +3 -12
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-DfjLvu8i.mjs → seo-D_LPkOtu.mjs} +4 -3
- package/dist/seo-D_LPkOtu.mjs.map +1 -0
- package/dist/{service-DAxg8RPR.mjs → service-ChDcsTBs.mjs} +2 -2
- package/dist/{service-DAxg8RPR.mjs.map → service-ChDcsTBs.mjs.map} +1 -1
- package/dist/{settings-DIsbHTRE.mjs → settings-Cv47v9u8.mjs} +2 -2
- package/dist/{settings-DIsbHTRE.mjs.map → settings-Cv47v9u8.mjs.map} +1 -1
- package/dist/settings-DfxiWY_s.mjs +411 -0
- package/dist/settings-DfxiWY_s.mjs.map +1 -0
- package/dist/{setup-complete-Yuv78yua.mjs → setup-complete-yvPE4OsP.mjs} +1 -1
- package/dist/{setup-complete-Yuv78yua.mjs.map → setup-complete-yvPE4OsP.mjs.map} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs → setup-nonce-C9aFzb94.mjs} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs.map → setup-nonce-C9aFzb94.mjs.map} +1 -1
- package/dist/{site-url-mEVmwIFi.mjs → site-url-CnHlmAs9.mjs} +1 -1
- package/dist/{site-url-mEVmwIFi.mjs.map → site-url-CnHlmAs9.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/{taxonomies-UusDXv3C.mjs → taxonomies-BILwiyGk.mjs} +2 -2
- package/dist/{taxonomies-UusDXv3C.mjs.map → taxonomies-BILwiyGk.mjs.map} +1 -1
- package/dist/{taxonomies-BEW7S5AI.mjs → taxonomies-BdAmbOwx.mjs} +46 -9
- package/dist/taxonomies-BdAmbOwx.mjs.map +1 -0
- package/dist/{transport-BwQeeY2p.d.mts → transport-B7PPP2CC.d.mts} +1 -1
- package/dist/{transport-BwQeeY2p.d.mts.map → transport-B7PPP2CC.d.mts.map} +1 -1
- package/dist/{transport--Ck3RBin.mjs → transport-CmpLD7W3.mjs} +1 -1
- package/dist/{transport--Ck3RBin.mjs.map → transport-CmpLD7W3.mjs.map} +1 -1
- package/dist/{types-DWnN7weG.d.mts → types-BFgrqwSk.d.mts} +1 -1
- package/dist/{types-DWnN7weG.d.mts.map → types-BFgrqwSk.d.mts.map} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts → types-BH8-30hc.d.mts} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts.map → types-BH8-30hc.d.mts.map} +1 -1
- package/dist/{types-OT_Es5mp.d.mts → types-BPzXTV9x.d.mts} +1 -1
- package/dist/{types-OT_Es5mp.d.mts.map → types-BPzXTV9x.d.mts.map} +1 -1
- package/dist/{types-DbCWhHet.d.mts → types-BUUVn1zr.d.mts} +2 -2
- package/dist/types-BUUVn1zr.d.mts.map +1 -0
- package/dist/{types-DMwSpvcw.d.mts → types-CPAPl93j.d.mts} +9 -3
- package/dist/{types-DMwSpvcw.d.mts.map → types-CPAPl93j.d.mts.map} +1 -1
- package/dist/types-CZI4E3qG.mjs +3 -0
- package/dist/{types-kwqCOUxj.d.mts → types-D4kUqbHh.d.mts} +1 -1
- package/dist/{types-kwqCOUxj.d.mts.map → types-D4kUqbHh.d.mts.map} +1 -1
- package/dist/{types-WVmpZBJV.d.mts → types-DTniiNto.d.mts} +2 -2
- package/dist/{types-WVmpZBJV.d.mts.map → types-DTniiNto.d.mts.map} +1 -1
- package/dist/types-DZk_y-MU.mjs.map +1 -1
- package/dist/{types-DX6v9KzJ.d.mts → types-S15DXXNi.d.mts} +1 -1
- package/dist/{types-DX6v9KzJ.d.mts.map → types-S15DXXNi.d.mts.map} +1 -1
- package/dist/{validate-ZP9Dvg0P.mjs → validate-Bz4vqcX1.mjs} +1 -1
- package/dist/{validate-ZP9Dvg0P.mjs.map → validate-Bz4vqcX1.mjs.map} +1 -1
- package/dist/{validate-BPAHUSge.d.mts → validate-CNwkPWzz.d.mts} +5 -5
- package/dist/{validate-BPAHUSge.d.mts.map → validate-CNwkPWzz.d.mts.map} +1 -1
- package/dist/{validation-CE5i4q0c.mjs → validation-DgGTJm3u.mjs} +1 -1
- package/dist/{validation-CE5i4q0c.mjs.map → validation-DgGTJm3u.mjs.map} +1 -1
- package/dist/version-D-5txk2m.mjs +7 -0
- package/dist/{version-Dw0JXu45.mjs.map → version-D-5txk2m.mjs.map} +1 -1
- package/dist/{widgets-ClEnYQCH.mjs → widgets-DZfmAbE4.mjs} +47 -44
- package/dist/widgets-DZfmAbE4.mjs.map +1 -0
- package/package.json +10 -10
- package/src/api/handlers/marketplace.ts +2 -5
- package/src/api/handlers/registry.ts +70 -0
- package/src/api/handlers/seo.ts +9 -1
- package/src/api/schemas/schema.ts +13 -1
- package/src/astro/middleware.ts +20 -6
- package/src/astro/routes/api/schema/index.ts +7 -15
- package/src/astro/routes/sitemap-[collection].xml.ts +13 -2
- package/src/cli/commands/bundle-utils.ts +2 -0
- package/src/cli/commands/secrets.ts +2 -2
- package/src/database/instrumentation.ts +13 -0
- package/src/emdash-runtime.ts +31 -25
- package/src/loader.ts +24 -15
- package/src/plugins/manifest-schema.ts +75 -0
- package/src/plugins/marketplace.ts +2 -5
- package/src/plugins/types.ts +12 -0
- package/src/query.ts +13 -2
- package/src/request-context.ts +8 -0
- package/src/schema/types.ts +11 -1
- package/src/seo/index.ts +2 -28
- package/src/seo/media-url.ts +32 -0
- package/src/settings/index.ts +32 -40
- package/src/taxonomies/index.ts +78 -12
- package/src/utils/isolate-cache.ts +189 -0
- package/src/widgets/index.ts +57 -54
- package/dist/api-BZ6bhjYs.mjs.map +0 -1
- package/dist/byline-fields-DYXKDuNX.d.mts.map +0 -1
- package/dist/loader-CpZKpFz0.mjs.map +0 -1
- package/dist/manifest-schema-Cj-YrzrF.mjs.map +0 -1
- package/dist/menus-Dp9xporj.mjs.map +0 -1
- package/dist/query-BFQ029Ts.mjs.map +0 -1
- package/dist/redirects-DEygMrRO.mjs.map +0 -1
- package/dist/seo-DfjLvu8i.mjs.map +0 -1
- package/dist/settings-B1p-gPUK.mjs +0 -235
- package/dist/settings-B1p-gPUK.mjs.map +0 -1
- package/dist/taxonomies-BEW7S5AI.mjs.map +0 -1
- package/dist/types-Cj2S6FuC.mjs +0 -3
- package/dist/types-DbCWhHet.d.mts.map +0 -1
- package/dist/version-Dw0JXu45.mjs +0 -7
- package/dist/widgets-ClEnYQCH.mjs.map +0 -1
- /package/dist/{api-tokens-B6VgoE6M.mjs → api-tokens-Oq39ba-Z.mjs} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","ASTRO_COOKIES_SYMBOL","virtualPlugins","virtualCreateDialect","virtualCreateStorage","virtualCreateScheduler","virtualSandboxedPlugins","virtualMediaProviders","createRequestScopedDb","virtualCreateRequestScopedDb"],"sources":["../../src/utils/init-lock.ts","../../src/cleanup.ts","../../src/comments/moderator.ts","../../src/scheduled-publish.ts","../../src/emdash-runtime.ts","../../src/media/url.ts","../../src/astro/middleware/stream-end-metrics.ts","../../src/astro/public-plugin-api-routes.ts","../../src/astro/middleware.ts"],"sourcesContent":["/**\n * Reclaimable initialization lock for isolate-lifetime singletons.\n *\n * Guards \"first request initializes, everyone else waits\" sections\n * (runtime creation, database init) against a workerd failure mode: if the\n * request that owns the initialization is cancelled mid-await (client\n * disconnect, context teardown), its continuation — including any `finally`\n * that would release the lock — never runs. A plain boolean or shared\n * promise then stays stuck forever and every subsequent request in the\n * isolate hangs until the platform kills it (observed as 524s at the\n * 100-second wall limit, with the isolate poisoned until eviction).\n *\n * This lock instead records *when* the owner started. Waiters poll — we\n * deliberately never await a promise created by another request, which\n * workerd flags — and if the owner has held the lock past `deadlineMs`,\n * the next waiter assumes the owner is dead, reclaims the lock, and runs\n * the initialization itself. Waiters also give up after `maxWaitMs` so a\n * request degrades to an error response rather than hanging.\n */\n\nexport interface InitLock {\n\t/** Epoch ms when the current owner claimed the lock, or null when free. */\n\townerStartedAt: number | null;\n\t/**\n\t * Monotonic claim counter identifying the current owner. Release is\n\t * gated on it: a slow owner that finishes after a waiter has reclaimed\n\t * the lock must not clear the reclaimer's claim — that would let yet\n\t * another caller claim the lock and start a third concurrent init.\n\t */\n\tgeneration: number;\n}\n\nexport function createInitLock(): InitLock {\n\treturn { ownerStartedAt: null, generation: 0 };\n}\n\nexport interface InitLockOptions {\n\t/**\n\t * Reclaim the lock if the owner has held it longer than this. Must be\n\t * comfortably above the slowest legitimate init (cold migrations on a\n\t * contended D1, including the concurrent-migrator wait) — a too-short\n\t * deadline risks two concurrent inits, a too-long one delays recovery\n\t * of a poisoned isolate. Nested locks must compose: an outer lock's\n\t * deadline must exceed the deadline of any lock its init acquires.\n\t */\n\tdeadlineMs?: number;\n\t/** Waiter poll interval. */\n\tpollMs?: number;\n\t/**\n\t * Give up waiting after this long and throw instead of hanging.\n\t * Defaults to `deadlineMs` plus headroom so a waiter always survives\n\t * long enough to reclaim a dead owner before giving up.\n\t */\n\tmaxWaitMs?: number;\n\t/**\n\t * Called with the in-flight init promise (errors pre-swallowed) so the\n\t * caller can hand it to the host's lifetime extender (waitUntil via\n\t * `after()`). If the owning request is cancelled mid-init, the anchored\n\t * promise keeps the context alive: init completes, populates the cache,\n\t * and the `finally` below releases the lock — preventing the poisoning\n\t * instead of merely recovering from it via reclaim.\n\t */\n\tanchor?: (promise: Promise<void>) => void;\n}\n\nconst DEFAULT_DEADLINE_MS = 15_000;\nconst DEFAULT_POLL_MS = 50;\nconst MAX_WAIT_HEADROOM_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Return the cached value if present, otherwise initialize it under the\n * lock. `init` is responsible for storing the value so that `getCached`\n * returns it on subsequent calls — waiters re-check `getCached` after the\n * owner finishes rather than sharing the owner's promise.\n *\n * `init` receives an `isCurrentClaim` predicate and must gate its cache\n * publication on it: a slow init that was reclaimed past the deadline\n * must not overwrite the value published by the reclaimer (for the\n * runtime singleton that would orphan the reclaimer's active cron\n * scheduler). A losing init should also tear down any side resources it\n * started, since its result will never be published.\n */\nexport async function initWithLock<T>(\n\tlock: InitLock,\n\tgetCached: () => T | null | undefined,\n\tinit: (isCurrentClaim: () => boolean) => Promise<T>,\n\toptions?: InitLockOptions,\n): Promise<T> {\n\tconst deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;\n\tconst pollMs = options?.pollMs ?? DEFAULT_POLL_MS;\n\tconst maxWaitMs = options?.maxWaitMs ?? deadlineMs + MAX_WAIT_HEADROOM_MS;\n\t// Date.now() is deliberate and only works because every loop iteration\n\t// awaits: in workerd the clock only advances across I/O, so a sync spin\n\t// would never observe the deadline. Don't \"optimize\" away the sleep.\n\tconst waitStart = Date.now();\n\n\tfor (;;) {\n\t\tconst cached = getCached();\n\t\tif (cached !== null && cached !== undefined) {\n\t\t\treturn cached;\n\t\t}\n\n\t\tconst ownerStartedAt = lock.ownerStartedAt;\n\t\tif (ownerStartedAt === null || Date.now() - ownerStartedAt > deadlineMs) {\n\t\t\t// Free, or the owner has been gone past the deadline — claim it.\n\t\t\t// Synchronous between awaits, so two waiters can't both claim.\n\t\t\tlock.generation += 1;\n\t\t\tconst claim = lock.generation;\n\t\t\tlock.ownerStartedAt = Date.now();\n\t\t\ttry {\n\t\t\t\t// Promise.resolve().then(...) so a synchronous throw from\n\t\t\t\t// init still becomes a rejection after the anchor attaches.\n\t\t\t\tconst isCurrentClaim = () => lock.generation === claim;\n\t\t\t\tconst initPromise = Promise.resolve().then(() => init(isCurrentClaim));\n\t\t\t\toptions?.anchor?.(\n\t\t\t\t\tinitPromise.then(\n\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn await initPromise;\n\t\t\t} finally {\n\t\t\t\t// If this request dies mid-init unanchored this never runs;\n\t\t\t\t// the next waiter reclaims after deadlineMs instead. Release\n\t\t\t\t// only while still the current owner: a reclaimer may have\n\t\t\t\t// taken the lock while this (slow) init was running, and\n\t\t\t\t// clearing its claim would admit a third concurrent init.\n\t\t\t\tif (lock.generation === claim) {\n\t\t\t\t\tlock.ownerStartedAt = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (Date.now() - waitStart > maxWaitMs) {\n\t\t\tthrow new Error(`initWithLock: timed out after ${maxWaitMs}ms waiting for initialization`);\n\t\t}\n\t\tawait sleep(pollMs);\n\t}\n}\n","/**\n * System cleanup\n *\n * Runs periodic maintenance tasks that prevent unbounded accumulation of\n * expired or stale data. Called from cron scheduler ticks and (for latency-\n * sensitive subsystems) inline during relevant requests.\n *\n * Each subsystem cleanup is independent and non-fatal -- if one fails, the\n * rest still run. Failures are logged but never surface to callers.\n */\n\nimport { createKyselyAdapter, type AuthTables } from \"@emdash-cms/auth/adapters/kysely\";\nimport { sql, type Kysely } from \"kysely\";\n\nimport { cleanupExpiredChallenges } from \"./auth/challenge-store.js\";\nimport { MediaRepository } from \"./database/repositories/media.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { Database } from \"./database/types.js\";\nimport type { Storage } from \"./storage/types.js\";\n\n/**\n * Result of a system cleanup run.\n * Each field is the number of rows deleted, or -1 if the cleanup failed.\n */\nexport interface CleanupResult {\n\tchallenges: number;\n\texpiredTokens: number;\n\tpendingUploads: number;\n\tpendingUploadFiles: number;\n\trevisionsPruned: number;\n}\n\n/** Max revisions to keep per entry during periodic pruning */\nconst REVISION_KEEP_COUNT = 50;\n\n/** Only prune entries that exceed this threshold */\nconst REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;\n\n/**\n * Run all system cleanup tasks.\n *\n * Safe to call frequently -- each task is a single DELETE with a WHERE clause,\n * so repeated calls with nothing to clean are cheap (no-op queries).\n *\n * @param db - The database instance\n * @param storage - Optional storage backend for deleting orphaned files.\n * When omitted, pending upload DB rows are still deleted but the\n * corresponding files in object storage are not removed.\n */\nexport async function runSystemCleanup(\n\tdb: Kysely<Database>,\n\tstorage?: Storage,\n): Promise<CleanupResult> {\n\tconst result: CleanupResult = {\n\t\tchallenges: -1,\n\t\texpiredTokens: -1,\n\t\tpendingUploads: -1,\n\t\tpendingUploadFiles: -1,\n\t\trevisionsPruned: -1,\n\t};\n\n\t// 1. Passkey challenges (expire after 60s, clean anything past 5 min)\n\ttry {\n\t\tresult.challenges = await cleanupExpiredChallenges(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired challenges:\", error);\n\t}\n\n\t// 2. Magic link / invite / signup tokens\n\ttry {\n\t\t// Cast needed: Database extends AuthTables but uses Generated<> wrappers\n\t\t// that confuse structural checks. The adapter casts internally anyway.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Database uses Generated<> wrappers incompatible with AuthTables structurally; safe at runtime\n\t\tconst authAdapter = createKyselyAdapter(db as unknown as Kysely<AuthTables>);\n\t\tawait authAdapter.deleteExpiredTokens();\n\t\tresult.expiredTokens = 0; // deleteExpiredTokens returns void\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired tokens:\", error);\n\t}\n\n\t// 3. Pending media uploads (abandoned after 1 hour)\n\t// Delete DB rows first, then remove corresponding files from storage.\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst orphanedKeys = await mediaRepo.cleanupPendingUploads();\n\t\tresult.pendingUploads = orphanedKeys.length;\n\n\t\t// Delete orphaned files from object storage\n\t\tif (storage && orphanedKeys.length > 0) {\n\t\t\tlet filesDeleted = 0;\n\t\t\tfor (const key of orphanedKeys) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tfilesDeleted++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Log per-file failures but continue -- storage.delete is\n\t\t\t\t\t// documented as idempotent, so this is an unexpected error.\n\t\t\t\t\tconsole.error(`[cleanup] Failed to delete storage file ${key}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.pendingUploadFiles = filesDeleted;\n\t\t} else {\n\t\t\tresult.pendingUploadFiles = 0;\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean pending uploads:\", error);\n\t}\n\n\t// 4. Revision pruning -- trim entries with excessive revision counts\n\ttry {\n\t\tresult.revisionsPruned = await pruneExcessiveRevisions(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to prune revisions:\", error);\n\t}\n\n\treturn result;\n}\n\n/**\n * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune\n * them down to REVISION_KEEP_COUNT.\n */\nasync function pruneExcessiveRevisions(db: Kysely<Database>): Promise<number> {\n\tconst entries = await sql<{ collection: string; entry_id: string }>`\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} 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 { 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/** Admin entry module */\n\tadminEntry?: string;\n\t/**\n\t * Exclusive hooks this plugin should be auto-selected for.\n\t * Weaker than an existing admin DB selection — config order wins when no selection exists.\n\t */\n\tpreferred?: string[];\n}\n\n/**\n * Media provider entry from virtual module\n */\nexport interface MediaProviderEntry {\n\tid: string;\n\tname: string;\n\ticon?: string;\n\tcapabilities: MediaProviderCapabilities;\n\t/** Factory function to create the provider instance */\n\tcreateProvider: (ctx: MediaProviderContext) => MediaProvider;\n}\n\n/**\n * Context passed to media provider factory functions\n */\nexport interface MediaProviderContext {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * 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// 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\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}\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?: { pages?: PluginAdminPage[]; widgets?: PluginDashboardWidget[] };\n\t}\n>();\n/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */\nconst sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();\nlet sandboxRunner: SandboxRunner | null = null;\n\n/**\n * EmDashRuntime - singleton per worker\n */\nexport class EmDashRuntime {\n\t/**\n\t * The singleton database instance (worker-lifetime cached).\n\t * Use the `db` getter instead — it checks the request context first\n\t * for per-request overrides (D1 read replica sessions, DO multi-site).\n\t */\n\tprivate readonly _db: Kysely<Database>;\n\treadonly storage: Storage | null;\n\treadonly configuredPlugins: ResolvedPlugin[];\n\treadonly sandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\treadonly schemaRegistry: SchemaRegistry;\n\tprivate _hooks!: HookPipeline;\n\treadonly config: EmDashConfig;\n\treadonly mediaProviders: Map<string, MediaProvider>;\n\treadonly mediaProviderEntries: MediaProviderEntry[];\n\treadonly cronExecutor: CronExecutor | null;\n\treadonly email: EmailPipeline | null;\n\n\tprivate cronScheduler: CronScheduler | null;\n\tprivate enabledPlugins: Set<string>;\n\tprivate pluginStates: Map<string, string>;\n\n\t/**\n\t * Set to true after FTS indexes have been verified for this worker\n\t * lifetime so we don't re-scan on every admin request. See\n\t * ensureSearchHealthy().\n\t */\n\tprivate _searchHealthChecked = false;\n\tprivate _searchHealthPromise: Promise<void> | null = null;\n\n\t/** Current hook pipeline. Use the `hooks` getter for external access. */\n\tget hooks(): HookPipeline {\n\t\treturn this._hooks;\n\t}\n\n\t/** All plugins eligible for the hook pipeline (includes built-in plugins).\n\t * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */\n\tprivate allPipelinePlugins: ResolvedPlugin[];\n\t/** Factory options for the hook pipeline context factory */\n\tprivate pipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tstorage?: Storage;\n\t\tsiteInfo?: { siteName?: string; siteUrl?: string; locale?: string };\n\t};\n\t/** Dependencies needed for exclusive hook resolution */\n\tprivate runtimeDeps: RuntimeDependencies;\n\t/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */\n\tprivate pipelineRef!: { current: HookPipeline };\n\n\t/**\n\t * Get the database instance for the current request.\n\t *\n\t * Checks the ALS-based request context first — middleware sets a\n\t * per-request Kysely instance there for D1 read replica sessions\n\t * or DO preview databases. Falls back to the singleton instance.\n\t */\n\tget db(): Kysely<Database> {\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript/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.schemaRegistry = new SchemaRegistry(parts.db);\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\tnewPipeline.setContextFactory({ db: this.db, emailPipeline: this.email });\n\t\t}\n\t\tif (this.cronScheduler) {\n\t\t\tconst scheduler = this.cronScheduler;\n\t\t\tnewPipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => scheduler.reschedule(),\n\t\t\t});\n\t\t}\n\n\t\t// Update the email pipeline to use the new hook pipeline\n\t\tif (this.email) {\n\t\t\tthis.email.setPipeline(newPipeline);\n\t\t}\n\n\t\t// Update the mutable ref so the cron closure dispatches through\n\t\t// the new pipeline without needing to reconstruct the CronExecutor.\n\t\tthis.pipelineRef.current = newPipeline;\n\n\t\tthis._hooks = newPipeline;\n\t}\n\n\t/**\n\t * Synchronize marketplace plugin runtime state with DB + storage.\n\t *\n\t * Ensures install/update/uninstall changes take effect immediately in the\n\t * current worker: loads newly active plugins and removes uninstalled ones.\n\t */\n\tasync syncMarketplacePlugins(): Promise<void> {\n\t\tif (!this.config.marketplace) 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// 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\t// Fetch plugin states and site info concurrently — independent reads\n\t\t// against different tables (_plugin_state vs options), so they share\n\t\t// one round-trip window instead of paying two sequential ones. Each\n\t\t// phase() wrapper still records that phase's own duration, and each\n\t\t// body keeps its own non-fatal catch.\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\tawait Promise.all([\n\t\t\t// Fetch plugin states from database\n\t\t\tphase(\"rt.plugins\", \"Plugin states\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst states = await db\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 table may not exist yet\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// Load site info for plugin context extensions (1 batch query instead of 3)\n\t\t\tphase(\"rt.site\", \"Site info options\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst optionsRepo = new OptionsRepository(db);\n\t\t\t\t\tconst siteOpts = await optionsRepo.getMany<string>([\n\t\t\t\t\t\t\"emdash:site_title\",\n\t\t\t\t\t\t\"emdash:site_url\",\n\t\t\t\t\t\t\"emdash:locale\",\n\t\t\t\t\t]);\n\t\t\t\t\tsiteInfo = {\n\t\t\t\t\t\tsiteName: siteOpts.get(\"emdash:site_title\") ?? undefined,\n\t\t\t\t\t\tsiteUrl: siteOpts.get(\"emdash:site_url\") ?? undefined,\n\t\t\t\t\t\tlocale: siteOpts.get(\"emdash:locale\") ?? undefined,\n\t\t\t\t\t};\n\t\t\t\t} catch {\n\t\t\t\t\t// Options table may not exist yet (pre-setup)\n\t\t\t\t}\n\t\t\t}),\n\t\t]);\n\n\t\t// Build set of enabled plugins\n\t\tconst enabledPlugins = new Set<string>();\n\t\tfor (const plugin of deps.plugins) {\n\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t}\n\t\t}\n\n\t\t// 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\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tstorage: storage ?? undefined,\n\t\t\tsiteInfo,\n\t\t};\n\t\tconst pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);\n\n\t\t// Load sandboxed plugins (build-time, 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 };\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\tpipeline.setContextFactory({ db, 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(db, 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// Auto-seed schema if no collections exist and setup hasn't run.\n\t\t\t\t// This covers first-load on sites that skip the setup wizard.\n\t\t\t\t// Dev-bypass and the wizard apply seeds explicitly.\n\t\t\t\ttry {\n\t\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t\t]);\n\n\t\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\n\t\t\t\t\tif (collectionCount.count === 0 && !setupDone) {\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}\n\t\t\t\t} catch {\n\t\t\t\t\t// Tables may not exist yet. Non-fatal.\n\t\t\t\t}\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});\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\t// TODO: sandboxed plugins need fieldWidgets extracted from their manifest\n\t\t// to support Block Kit field widgets. Currently only trusted plugins carry\n\t\t// fieldWidgets through the admin.fieldWidgets path.\n\t\tfor (const entry of this.sandboxedPluginEntries) {\n\t\t\tconst status = this.pluginStates.get(entry.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\tconst hasAdminPages = (entry.adminPages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[entry.id] = {\n\t\t\t\tversion: entry.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: entry.adminPages ?? [],\n\t\t\t\tdashboardWidgets: entry.adminWidgets ?? [],\n\t\t\t};\n\t\t}\n\n\t\t// Add marketplace-installed plugins (dynamically loaded from R2)\n\t\tfor (const [pluginId, meta] of marketplaceManifestCache) {\n\t\t\t// Skip if already included from build-time config\n\t\t\tif (manifestPlugins[pluginId]) continue;\n\n\t\t\tconst status = this.pluginStates.get(pluginId);\n\t\t\tconst enabled = status === \"active\";\n\n\t\t\tconst pages = meta.admin?.pages;\n\t\t\tconst widgets = meta.admin?.widgets;\n\t\t\tconst hasAdminPages = (pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (widgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[pluginId] = {\n\t\t\t\tversion: meta.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: pages ?? [],\n\t\t\t\tdashboardWidgets: widgets ?? [],\n\t\t\t};\n\t\t}\n\n\t\t// 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\tif (this._searchHealthChecked) return;\n\t\tif (this._searchHealthPromise) return this._searchHealthPromise;\n\t\tif (!isSqlite(this._db)) {\n\t\t\tthis._searchHealthChecked = true;\n\t\t\treturn;\n\t\t}\n\t\tthis._searchHealthPromise = (async () => {\n\t\t\ttry {\n\t\t\t\tconst ftsManager = new FTSManager(this._db);\n\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\tif (repaired > 0) {\n\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es)`);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal.\n\t\t\t} finally {\n\t\t\t\tthis._searchHealthChecked = true;\n\t\t\t\tthis._searchHealthPromise = null;\n\t\t\t}\n\t\t})();\n\t\treturn this._searchHealthPromise;\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\treturn handleContentRestore(this.db, collection, id);\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\treturn handleContentSchedule(this.db, collection, id, scheduledAt);\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\treturn handleContentUnschedule(this.db, collection, id);\n\t}\n\n\tasync handleContentCountScheduled(collection: string) {\n\t\treturn handleContentCountScheduled(this.db, collection);\n\t}\n\n\tasync handleContentDiscardDraft(collection: string, id: string) {\n\t\treturn handleContentDiscardDraft(this.db, collection, id);\n\t}\n\n\tasync handleContentCompare(collection: string, id: string) {\n\t\treturn handleContentCompare(this.db, collection, id);\n\t}\n\n\tasync handleContentTranslations(collection: string, id: string) {\n\t\treturn handleContentTranslations(this.db, collection, id);\n\t}\n\n\t// =========================================================================\n\t// Media Handlers\n\t// =========================================================================\n\n\tasync handleMediaList(params: {\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\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 runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterPublish\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterPublish(content, collection);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterPublish 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(\"content:afterPublish\", { content, collection });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish 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 runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterUnpublish\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterUnpublish(content, collection)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUnpublish 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:afterUnpublish\", { content, collection })\n\t\t\t\t.catch((err) =>\n\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterUnpublish error:`, err),\n\t\t\t\t);\n\t\t}\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 * 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 { isInstrumentationEnabled } from \"../../database/instrumentation.js\";\nimport { getRequestContext } from \"../../request-context.js\";\n\nexport const STREAM_END_PREFIX = \"[emdash-stream-end]\";\n\n/**\n * Astro attaches AstroCookies to outgoing responses via a well-known global\n * symbol. Constructing a new Response drops non-header metadata, so the\n * symbol must be forwarded explicitly or `cookies.set()` calls are silently\n * dropped. Same pattern as finalizeResponse in ../middleware.ts.\n */\nconst ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\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\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},\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","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\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 { wrapBodyForStreamMetrics } from \"./middleware/stream-end-metrics.js\";\nimport { createPublicPluginApiRouteHandler } from \"./public-plugin-api-routes.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\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\treturn runtime.runScheduledTasks(options);\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 middleware that wraps the response must explicitly forward\n * this symbol or `cookies.set()` calls will be silently dropped.\n */\nconst ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\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\tres.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\tres.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\tres.headers.set(\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=(), payment=()\");\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.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 } | 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 } | 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 context.session?.get(\"user\");\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\tif (!isSetupVerified()) {\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} 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\treturn runWithContext(ctx, async () => {\n\t\t\t\t\t\tconst response = await runAnon();\n\t\t\t\t\t\tanonScoped.commit();\n\t\t\t\t\t\treturn response;\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\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\tdb: runtime.db,\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\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,\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, async () => {\n\t\t\t\t\tconst response = await renderAndFinalize();\n\t\t\t\t\tscoped.commit();\n\t\t\t\t\treturn response;\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\tif (queryRecorder) flushRecorder(queryRecorder);\n\t}\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":["resolveExclusiveHooksShared","ASTRO_COOKIES_SYMBOL","virtualPlugins","virtualCreateDialect","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/stream-end-metrics.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} 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 { createIsolateCache, isolateCachedAsync } from \"./utils/isolate-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/** Admin entry module */\n\tadminEntry?: string;\n\t/**\n\t * Exclusive hooks this plugin should be auto-selected for.\n\t * Weaker than an existing admin DB selection — config order wins when no selection exists.\n\t */\n\tpreferred?: string[];\n}\n\n/**\n * Media provider entry from virtual module\n */\nexport interface MediaProviderEntry {\n\tid: string;\n\tname: string;\n\ticon?: string;\n\tcapabilities: MediaProviderCapabilities;\n\t/** Factory function to create the provider instance */\n\tcreateProvider: (ctx: MediaProviderContext) => MediaProvider;\n}\n\n/**\n * Context passed to media provider factory functions\n */\nexport interface MediaProviderContext {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * 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// 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\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}\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?: { pages?: PluginAdminPage[]; widgets?: PluginDashboardWidget[] };\n\t}\n>();\n/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */\nconst sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();\nlet sandboxRunner: SandboxRunner | null = null;\n\n/**\n * EmDashRuntime - singleton per worker\n */\nexport class EmDashRuntime {\n\t/**\n\t * The singleton database instance (worker-lifetime cached).\n\t * Use the `db` getter instead — it checks the request context first\n\t * for per-request overrides (D1 read replica sessions, DO multi-site).\n\t */\n\tprivate readonly _db: Kysely<Database>;\n\treadonly storage: Storage | null;\n\treadonly configuredPlugins: ResolvedPlugin[];\n\treadonly sandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\treadonly schemaRegistry: SchemaRegistry;\n\tprivate _hooks!: HookPipeline;\n\treadonly config: EmDashConfig;\n\treadonly mediaProviders: Map<string, MediaProvider>;\n\treadonly mediaProviderEntries: MediaProviderEntry[];\n\treadonly cronExecutor: CronExecutor | null;\n\treadonly email: EmailPipeline | null;\n\n\tprivate cronScheduler: CronScheduler | null;\n\tprivate enabledPlugins: Set<string>;\n\tprivate pluginStates: Map<string, string>;\n\n\t/**\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 isolate cache (never a shared awaitable promise)\n\t * so a cancelled first caller can't wedge later ones.\n\t */\n\tprivate readonly _searchHealthCache = createIsolateCache<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\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.schemaRegistry = new SchemaRegistry(parts.db);\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\tnewPipeline.setContextFactory({ db: this.db, emailPipeline: this.email });\n\t\t}\n\t\tif (this.cronScheduler) {\n\t\t\tconst scheduler = this.cronScheduler;\n\t\t\tnewPipeline.setContextFactory({\n\t\t\t\tcronReschedule: () => scheduler.reschedule(),\n\t\t\t});\n\t\t}\n\n\t\t// Update the email pipeline to use the new hook pipeline\n\t\tif (this.email) {\n\t\t\tthis.email.setPipeline(newPipeline);\n\t\t}\n\n\t\t// Update the mutable ref so the cron closure dispatches through\n\t\t// the new pipeline without needing to reconstruct the CronExecutor.\n\t\tthis.pipelineRef.current = newPipeline;\n\n\t\tthis._hooks = newPipeline;\n\t}\n\n\t/**\n\t * Synchronize marketplace plugin runtime state with DB + storage.\n\t *\n\t * Ensures install/update/uninstall changes take effect immediately in the\n\t * current worker: loads newly active plugins and removes uninstalled ones.\n\t */\n\tasync syncMarketplacePlugins(): Promise<void> {\n\t\tif (!this.config.marketplace) 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// 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\t// Fetch plugin states and site info concurrently — independent reads\n\t\t// against different tables (_plugin_state vs options), so they share\n\t\t// one round-trip window instead of paying two sequential ones. Each\n\t\t// phase() wrapper still records that phase's own duration, and each\n\t\t// body keeps its own non-fatal catch.\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\tlet siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;\n\t\tawait Promise.all([\n\t\t\t// Fetch plugin states from database\n\t\t\tphase(\"rt.plugins\", \"Plugin states\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst states = await db\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 table may not exist yet\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// Load site info for plugin context extensions (1 batch query instead of 3)\n\t\t\tphase(\"rt.site\", \"Site info options\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst optionsRepo = new OptionsRepository(db);\n\t\t\t\t\tconst siteOpts = await optionsRepo.getMany<string>([\n\t\t\t\t\t\t\"emdash:site_title\",\n\t\t\t\t\t\t\"emdash:site_url\",\n\t\t\t\t\t\t\"emdash:locale\",\n\t\t\t\t\t]);\n\t\t\t\t\tsiteInfo = {\n\t\t\t\t\t\tsiteName: siteOpts.get(\"emdash:site_title\") ?? undefined,\n\t\t\t\t\t\tsiteUrl: siteOpts.get(\"emdash:site_url\") ?? undefined,\n\t\t\t\t\t\tlocale: siteOpts.get(\"emdash:locale\") ?? undefined,\n\t\t\t\t\t};\n\t\t\t\t} catch {\n\t\t\t\t\t// Options table may not exist yet (pre-setup)\n\t\t\t\t}\n\t\t\t}),\n\t\t]);\n\n\t\t// Build set of enabled plugins\n\t\tconst enabledPlugins = new Set<string>();\n\t\tfor (const plugin of deps.plugins) {\n\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t}\n\t\t}\n\n\t\t// 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\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tstorage: storage ?? undefined,\n\t\t\tsiteInfo,\n\t\t};\n\t\tconst pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);\n\n\t\t// Load sandboxed plugins (build-time, 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 };\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\tpipeline.setContextFactory({ db, 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(db, 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// Auto-seed schema if no collections exist and setup hasn't run.\n\t\t\t\t// This covers first-load on sites that skip the setup wizard.\n\t\t\t\t// Dev-bypass and the wizard apply seeds explicitly.\n\t\t\t\ttry {\n\t\t\t\t\tconst [collectionCount, setupOption] = await Promise.all([\n\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t\t\t\t.executeTakeFirstOrThrow(),\n\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t\t]);\n\n\t\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\n\t\t\t\t\tif (collectionCount.count === 0 && !setupDone) {\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}\n\t\t\t\t} catch {\n\t\t\t\t\t// Tables may not exist yet. Non-fatal.\n\t\t\t\t}\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});\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\t// TODO: sandboxed plugins need fieldWidgets extracted from their manifest\n\t\t// to support Block Kit field widgets. Currently only trusted plugins carry\n\t\t// fieldWidgets through the admin.fieldWidgets path.\n\t\tfor (const entry of this.sandboxedPluginEntries) {\n\t\t\tconst status = this.pluginStates.get(entry.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\tconst hasAdminPages = (entry.adminPages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[entry.id] = {\n\t\t\t\tversion: entry.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: entry.adminPages ?? [],\n\t\t\t\tdashboardWidgets: entry.adminWidgets ?? [],\n\t\t\t};\n\t\t}\n\n\t\t// Add marketplace-installed plugins (dynamically loaded from R2)\n\t\tfor (const [pluginId, meta] of marketplaceManifestCache) {\n\t\t\t// Skip if already included from build-time config\n\t\t\tif (manifestPlugins[pluginId]) continue;\n\n\t\t\tconst status = this.pluginStates.get(pluginId);\n\t\t\tconst enabled = status === \"active\";\n\n\t\t\tconst pages = meta.admin?.pages;\n\t\t\tconst widgets = meta.admin?.widgets;\n\t\t\tconst hasAdminPages = (pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (widgets?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[pluginId] = {\n\t\t\t\tversion: meta.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: pages ?? [],\n\t\t\t\tdashboardWidgets: widgets ?? [],\n\t\t\t};\n\t\t}\n\n\t\t// 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 isolateCachedAsync(\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\treturn handleContentRestore(this.db, collection, id);\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\treturn handleContentSchedule(this.db, collection, id, scheduledAt);\n\t}\n\n\tasync handleContentUnschedule(collection: string, id: string) {\n\t\treturn handleContentUnschedule(this.db, collection, id);\n\t}\n\n\tasync handleContentCountScheduled(collection: string) {\n\t\treturn handleContentCountScheduled(this.db, collection);\n\t}\n\n\tasync handleContentDiscardDraft(collection: string, id: string) {\n\t\treturn handleContentDiscardDraft(this.db, collection, id);\n\t}\n\n\tasync handleContentCompare(collection: string, id: string) {\n\t\treturn handleContentCompare(this.db, collection, id);\n\t}\n\n\tasync handleContentTranslations(collection: string, id: string) {\n\t\treturn handleContentTranslations(this.db, collection, id);\n\t}\n\n\t// =========================================================================\n\t// Media Handlers\n\t// =========================================================================\n\n\tasync handleMediaList(params: {\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\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 runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterPublish\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterPublish(content, collection);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterPublish 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(\"content:afterPublish\", { content, collection });\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish 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 runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {\n\t\t// Trusted plugins\n\t\tif (this.hooks.hasHooks(\"content:afterUnpublish\")) {\n\t\t\tthis.hooks\n\t\t\t\t.runContentAfterUnpublish(content, collection)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUnpublish 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:afterUnpublish\", { content, collection })\n\t\t\t\t.catch((err) =>\n\t\t\t\t\tconsole.error(`EmDash: Sandboxed plugin ${pluginId} afterUnpublish error:`, err),\n\t\t\t\t);\n\t\t}\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 * 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 { isInstrumentationEnabled } from \"../../database/instrumentation.js\";\nimport { getRequestContext } from \"../../request-context.js\";\n\nexport const STREAM_END_PREFIX = \"[emdash-stream-end]\";\n\n/**\n * Astro attaches AstroCookies to outgoing responses via a well-known global\n * symbol. Constructing a new Response drops non-header metadata, so the\n * symbol must be forwarded explicitly or `cookies.set()` calls are silently\n * dropped. Same pattern as finalizeResponse in ../middleware.ts.\n */\nconst ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\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\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},\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","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\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 { wrapBodyForStreamMetrics } from \"./middleware/stream-end-metrics.js\";\nimport { createPublicPluginApiRouteHandler } from \"./public-plugin-api-routes.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\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\treturn runtime.runScheduledTasks(options);\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 middleware that wraps the response must explicitly forward\n * this symbol or `cookies.set()` calls will be silently dropped.\n */\nconst ASTRO_COOKIES_SYMBOL = Symbol.for(\"astro.cookies\");\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\tres.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\tres.headers.set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\");\n\tres.headers.set(\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=(), payment=()\");\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 } | 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 } | 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 context.session?.get(\"user\");\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\tif (!isSetupVerified()) {\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} 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\treturn runWithContext(ctx, async () => {\n\t\t\t\t\t\t// commit() in finally: the write reached the primary independently\n\t\t\t\t\t\t// of render, so the bookmark cookie must be persisted even if\n\t\t\t\t\t\t// render throws -- otherwise a write-then-failed-render leaves the\n\t\t\t\t\t\t// next request able to read pre-write state off a lagging replica.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn await runAnon();\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tanonScoped.commit();\n\t\t\t\t\t\t}\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\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\tdb: runtime.db,\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\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,\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, async () => {\n\t\t\t\t\t// commit() in finally: persist the bookmark cookie even if render\n\t\t\t\t\t// throws -- the write already reached the primary, so a failed\n\t\t\t\t\t// render must not strand the next request on a stale replica read.\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await renderAndFinalize();\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tscoped.commit();\n\t\t\t\t\t}\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\tif (queryRecorder) flushRecorder(queryRecorder);\n\t}\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|