emdash 0.40.0 → 0.41.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.
Files changed (1258) hide show
  1. package/dist/action-token-CbKDcPJi.mjs +26 -0
  2. package/dist/action-token-CbKDcPJi.mjs.map +1 -0
  3. package/dist/activation-PtniBLt9.mjs +382 -0
  4. package/dist/activation-PtniBLt9.mjs.map +1 -0
  5. package/dist/adapters-C_Tf5x90.d.mts +171 -0
  6. package/dist/adapters-C_Tf5x90.d.mts.map +1 -0
  7. package/dist/allowed-origins-U1z4TdEZ.mjs +116 -0
  8. package/dist/allowed-origins-U1z4TdEZ.mjs.map +1 -0
  9. package/dist/api/route-utils.d.mts +4 -4
  10. package/dist/api/route-utils.mjs +46 -45
  11. package/dist/api/route-utils.mjs.map +1 -1
  12. package/dist/api/schemas/index.d.mts +2 -2
  13. package/dist/api/schemas/index.mjs +10 -10
  14. package/dist/api-CXwQkV_m.mjs +5122 -0
  15. package/dist/api-CXwQkV_m.mjs.map +1 -0
  16. package/dist/api-tokens-89WlohEN.mjs +166 -0
  17. package/dist/api-tokens-89WlohEN.mjs.map +1 -0
  18. package/dist/apply-Do3KNnUk.mjs +1659 -0
  19. package/dist/apply-Do3KNnUk.mjs.map +1 -0
  20. package/dist/approvals-CAX9Yw33.mjs +460 -0
  21. package/dist/approvals-CAX9Yw33.mjs.map +1 -0
  22. package/dist/artifact-fetch-B9W4CEST.mjs +659 -0
  23. package/dist/artifact-fetch-B9W4CEST.mjs.map +1 -0
  24. package/dist/astro/index.d.mts +14 -14
  25. package/dist/astro/index.mjs +26 -30
  26. package/dist/astro/index.mjs.map +1 -1
  27. package/dist/astro/middleware/auth.d.mts +12 -12
  28. package/dist/astro/middleware/auth.mjs +10 -10
  29. package/dist/astro/middleware/media-usage-write-fence.mjs +5 -5
  30. package/dist/astro/middleware/redirect.mjs +7 -7
  31. package/dist/astro/middleware/request-context.mjs +3 -3
  32. package/dist/astro/middleware/setup.mjs +1 -1
  33. package/dist/astro/middleware.d.mts +12 -12
  34. package/dist/astro/middleware.d.mts.map +1 -1
  35. package/dist/astro/middleware.mjs +110 -106
  36. package/dist/astro/middleware.mjs.map +1 -1
  37. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +14 -14
  38. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +14 -14
  39. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +6 -6
  40. package/dist/astro/routes/api/admin/api-tokens/index.mjs +7 -7
  41. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +7 -7
  42. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +17 -17
  43. package/dist/astro/routes/api/admin/byline-fields/index.mjs +17 -17
  44. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +17 -17
  45. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +23 -23
  46. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +23 -23
  47. package/dist/astro/routes/api/admin/bylines/index.mjs +23 -23
  48. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -18
  49. package/dist/astro/routes/api/admin/comments/_id_.mjs +7 -7
  50. package/dist/astro/routes/api/admin/comments/bulk.mjs +17 -17
  51. package/dist/astro/routes/api/admin/comments/counts.mjs +7 -7
  52. package/dist/astro/routes/api/admin/comments/index.mjs +17 -17
  53. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
  54. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
  55. package/dist/astro/routes/api/admin/media-usage/activation.mjs +20 -20
  56. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +22 -22
  57. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +22 -22
  58. package/dist/astro/routes/api/admin/media-usage/progress.mjs +25 -25
  59. package/dist/astro/routes/api/admin/media-usage/repair.mjs +31 -31
  60. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +22 -22
  61. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +22 -22
  62. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +6 -6
  63. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +6 -6
  64. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +8 -8
  65. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +8 -8
  66. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +72 -68
  67. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  68. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +7 -7
  69. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +71 -67
  70. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  71. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +72 -68
  72. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +82 -78
  74. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/index.mjs +72 -68
  76. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +5 -5
  78. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +71 -67
  79. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +81 -77
  81. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +71 -67
  83. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +72 -68
  85. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +83 -79
  87. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +9 -9
  89. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +74 -70
  90. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/registry/publisher-handle.mjs +7 -7
  92. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +72 -68
  93. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
  94. package/dist/astro/routes/api/admin/plugins/updates.mjs +71 -67
  95. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  96. package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs +6 -6
  97. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +71 -67
  98. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  99. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +5 -5
  100. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +71 -67
  101. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  102. package/dist/astro/routes/api/admin/transfer/_...path_.mjs +4 -4
  103. package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +40 -40
  104. package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +40 -40
  105. package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +42 -42
  106. package/dist/astro/routes/api/admin/transfer/capabilities.mjs +42 -42
  107. package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +42 -42
  108. package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +43 -43
  109. package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +43 -43
  110. package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +42 -42
  111. package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +43 -43
  112. package/dist/astro/routes/api/admin/transfer/exports/index.mjs +44 -44
  113. package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +42 -42
  114. package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +42 -42
  115. package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +44 -44
  116. package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +42 -42
  117. package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +44 -44
  118. package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +42 -42
  119. package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +42 -42
  120. package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +44 -44
  121. package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +42 -42
  122. package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +42 -42
  123. package/dist/astro/routes/api/admin/transfer/imports/index.mjs +44 -44
  124. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +5 -5
  125. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +4 -4
  126. package/dist/astro/routes/api/admin/users/_id_/index.mjs +15 -15
  127. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
  128. package/dist/astro/routes/api/admin/users/index.mjs +14 -14
  129. package/dist/astro/routes/api/auth/dev-bypass.mjs +6 -6
  130. package/dist/astro/routes/api/auth/invite/accept.mjs +4 -4
  131. package/dist/astro/routes/api/auth/invite/complete.mjs +18 -18
  132. package/dist/astro/routes/api/auth/invite/index.mjs +16 -16
  133. package/dist/astro/routes/api/auth/invite/register-options.mjs +17 -17
  134. package/dist/astro/routes/api/auth/logout.mjs +5 -5
  135. package/dist/astro/routes/api/auth/magic-link/send.mjs +18 -18
  136. package/dist/astro/routes/api/auth/magic-link/verify.mjs +14 -14
  137. package/dist/astro/routes/api/auth/me.mjs +15 -15
  138. package/dist/astro/routes/api/auth/mode.mjs +5 -5
  139. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  140. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  141. package/dist/astro/routes/api/auth/passkey/_id_.mjs +14 -14
  142. package/dist/astro/routes/api/auth/passkey/index.mjs +4 -4
  143. package/dist/astro/routes/api/auth/passkey/options.mjs +19 -19
  144. package/dist/astro/routes/api/auth/passkey/register/options.mjs +17 -17
  145. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +18 -18
  146. package/dist/astro/routes/api/auth/passkey/verify.mjs +18 -18
  147. package/dist/astro/routes/api/auth/signup/complete.mjs +18 -18
  148. package/dist/astro/routes/api/auth/signup/request.mjs +17 -17
  149. package/dist/astro/routes/api/auth/signup/verify.mjs +4 -4
  150. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +12 -12
  151. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +18 -18
  152. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +5 -5
  153. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +17 -17
  154. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  155. package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +17 -17
  156. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +5 -5
  157. package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs +11 -11
  158. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +31 -31
  159. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +17 -17
  160. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.d.mts +1 -2
  161. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.d.mts.map +1 -1
  162. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +42 -53
  163. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  164. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +34 -29
  165. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  166. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  167. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +5 -5
  168. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +17 -17
  169. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +25 -24
  170. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  171. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +5 -5
  172. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +17 -17
  173. package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
  174. package/dist/astro/routes/api/content/_collection_/_id_.mjs +18 -18
  175. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  176. package/dist/astro/routes/api/content/_collection_/authors.mjs +5 -5
  177. package/dist/astro/routes/api/content/_collection_/index.mjs +15 -15
  178. package/dist/astro/routes/api/content/_collection_/trash.mjs +15 -15
  179. package/dist/astro/routes/api/dashboard.mjs +10 -9
  180. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  181. package/dist/astro/routes/api/dev/emails.mjs +5 -5
  182. package/dist/astro/routes/api/health.mjs +6 -6
  183. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  184. package/dist/astro/routes/api/import/probe.mjs +23 -23
  185. package/dist/astro/routes/api/import/wordpress/analyze.mjs +6 -6
  186. package/dist/astro/routes/api/import/wordpress/execute.d.mts +13 -13
  187. package/dist/astro/routes/api/import/wordpress/execute.mjs +16 -16
  188. package/dist/astro/routes/api/import/wordpress/media.d.mts +12 -12
  189. package/dist/astro/routes/api/import/wordpress/media.mjs +19 -19
  190. package/dist/astro/routes/api/import/wordpress/prepare.mjs +18 -18
  191. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +13 -13
  192. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +28 -28
  193. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  194. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +23 -23
  195. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +13 -13
  196. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +34 -34
  197. package/dist/astro/routes/api/manifest.mjs +6 -6
  198. package/dist/astro/routes/api/mcp.mjs +98 -118
  199. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  200. package/dist/astro/routes/api/media/_id_/confirm.mjs +19 -19
  201. package/dist/astro/routes/api/media/_id_/replace.mjs +16 -16
  202. package/dist/astro/routes/api/media/_id_/upload.mjs +9 -9
  203. package/dist/astro/routes/api/media/_id_/usage.mjs +33 -33
  204. package/dist/astro/routes/api/media/_id_.mjs +31 -31
  205. package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +6 -6
  206. package/dist/astro/routes/api/media/file/_...key_.mjs +9 -9
  207. package/dist/astro/routes/api/media/folders/_id_.mjs +16 -16
  208. package/dist/astro/routes/api/media/folders/index.mjs +16 -16
  209. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +5 -5
  210. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +5 -5
  211. package/dist/astro/routes/api/media/providers/index.mjs +5 -5
  212. package/dist/astro/routes/api/media/upload-url.mjs +18 -18
  213. package/dist/astro/routes/api/media.mjs +35 -35
  214. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +17 -17
  215. package/dist/astro/routes/api/menus/_name_/items.mjs +17 -17
  216. package/dist/astro/routes/api/menus/_name_/reorder.mjs +17 -17
  217. package/dist/astro/routes/api/menus/_name_/translations.mjs +17 -17
  218. package/dist/astro/routes/api/menus/_name_.mjs +17 -17
  219. package/dist/astro/routes/api/menus/index.mjs +17 -17
  220. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  221. package/dist/astro/routes/api/oauth/device/authorize.d.mts +2 -1
  222. package/dist/astro/routes/api/oauth/device/authorize.d.mts.map +1 -1
  223. package/dist/astro/routes/api/oauth/device/authorize.mjs +23 -9
  224. package/dist/astro/routes/api/oauth/device/authorize.mjs.map +1 -1
  225. package/dist/astro/routes/api/oauth/device/code.mjs +11 -11
  226. package/dist/astro/routes/api/oauth/device/token.mjs +10 -10
  227. package/dist/astro/routes/api/oauth/register.mjs +5 -5
  228. package/dist/astro/routes/api/oauth/token/refresh.mjs +8 -8
  229. package/dist/astro/routes/api/oauth/token/revoke.mjs +8 -8
  230. package/dist/astro/routes/api/oauth/token.mjs +8 -8
  231. package/dist/astro/routes/api/openapi.json.mjs +20 -20
  232. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +11 -11
  233. package/dist/astro/routes/api/redirects/404s/index.mjs +19 -19
  234. package/dist/astro/routes/api/redirects/404s/summary.mjs +19 -19
  235. package/dist/astro/routes/api/redirects/_id_.mjs +19 -19
  236. package/dist/astro/routes/api/redirects/index.mjs +19 -19
  237. package/dist/astro/routes/api/relations/_id_/index.mjs +34 -29
  238. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  239. package/dist/astro/routes/api/relations/index.mjs +36 -31
  240. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  241. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +5 -5
  242. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  243. package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +71 -67
  244. package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs.map +1 -1
  245. package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +71 -67
  246. package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs.map +1 -1
  247. package/dist/astro/routes/api/schema/block-types/index.mjs +71 -67
  248. package/dist/astro/routes/api/schema/block-types/index.mjs.map +1 -1
  249. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +73 -69
  250. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  251. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +71 -67
  252. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  253. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +71 -67
  254. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  255. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +71 -67
  256. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  257. package/dist/astro/routes/api/schema/collections/index.mjs +71 -67
  258. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  259. package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -67
  260. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  261. package/dist/astro/routes/api/schema/index.mjs +16 -16
  262. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +71 -67
  263. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  264. package/dist/astro/routes/api/schema/orphans/index.mjs +71 -67
  265. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  266. package/dist/astro/routes/api/search/enable.mjs +19 -19
  267. package/dist/astro/routes/api/search/index.mjs +18 -18
  268. package/dist/astro/routes/api/search/rebuild.mjs +19 -19
  269. package/dist/astro/routes/api/search/stats.mjs +9 -9
  270. package/dist/astro/routes/api/search/suggest.mjs +18 -18
  271. package/dist/astro/routes/api/sections/_slug_.mjs +18 -18
  272. package/dist/astro/routes/api/sections/index.mjs +18 -18
  273. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
  274. package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
  275. package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
  276. package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
  277. package/dist/astro/routes/api/settings/email.mjs +6 -6
  278. package/dist/astro/routes/api/settings.mjs +21 -21
  279. package/dist/astro/routes/api/setup/admin-verify.mjs +19 -19
  280. package/dist/astro/routes/api/setup/admin.mjs +18 -18
  281. package/dist/astro/routes/api/setup/dev-bypass.mjs +45 -39
  282. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  283. package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
  284. package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
  285. package/dist/astro/routes/api/setup/index.mjs +80 -49
  286. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  287. package/dist/astro/routes/api/setup/status.mjs +6 -6
  288. package/dist/astro/routes/api/snapshot.mjs +9 -9
  289. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  290. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +23 -23
  291. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +23 -23
  292. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +23 -23
  293. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +23 -23
  294. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +22 -22
  295. package/dist/astro/routes/api/taxonomies/_name_.mjs +23 -23
  296. package/dist/astro/routes/api/taxonomies/bulk-tag.mjs +36 -35
  297. package/dist/astro/routes/api/taxonomies/bulk-tag.mjs.map +1 -1
  298. package/dist/astro/routes/api/taxonomies/index.mjs +23 -23
  299. package/dist/astro/routes/api/themes/preview.mjs +6 -6
  300. package/dist/astro/routes/api/typegen.mjs +8 -8
  301. package/dist/astro/routes/api/visual-editing/action-token.mjs +6 -6
  302. package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs +19 -19
  303. package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs +4 -4
  304. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  305. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  306. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  307. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +16 -16
  308. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +19 -19
  309. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +19 -19
  310. package/dist/astro/routes/api/widget-areas/_name_.mjs +9 -9
  311. package/dist/astro/routes/api/widget-areas/index.mjs +19 -19
  312. package/dist/astro/routes/api/widget-components.mjs +5 -5
  313. package/dist/astro/routes/robots.txt.mjs +6 -6
  314. package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
  315. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  316. package/dist/astro/types.d.mts +28 -18
  317. package/dist/astro/types.d.mts.map +1 -1
  318. package/dist/audit-CrgmE9LE.mjs +127 -0
  319. package/dist/audit-CrgmE9LE.mjs.map +1 -0
  320. package/dist/auth/providers/github.d.mts +1 -1
  321. package/dist/auth/providers/google.d.mts +1 -1
  322. package/dist/authoritative-records-C2YzIdbu.mjs +5172 -0
  323. package/dist/authoritative-records-C2YzIdbu.mjs.map +1 -0
  324. package/dist/authorize-WgdKF8QY.mjs +49 -0
  325. package/dist/authorize-WgdKF8QY.mjs.map +1 -0
  326. package/dist/backup-Ck8_lMO3.mjs +246 -0
  327. package/dist/backup-Ck8_lMO3.mjs.map +1 -0
  328. package/dist/block-types-BQ90asVI.mjs +132 -0
  329. package/dist/block-types-BQ90asVI.mjs.map +1 -0
  330. package/dist/block-values-ACFqgB1D.mjs +175 -0
  331. package/dist/block-values-ACFqgB1D.mjs.map +1 -0
  332. package/dist/bundle-Db1iNMv_.mjs +667 -0
  333. package/dist/bundle-Db1iNMv_.mjs.map +1 -0
  334. package/dist/byline-BayaYGoj.mjs +909 -0
  335. package/dist/byline-BayaYGoj.mjs.map +1 -0
  336. package/dist/byline-fields-B8HH-35f.mjs +254 -0
  337. package/dist/byline-fields-B8HH-35f.mjs.map +1 -0
  338. package/dist/byline-fields-hMPjf6Eu.mjs +123 -0
  339. package/dist/byline-fields-hMPjf6Eu.mjs.map +1 -0
  340. package/dist/byline-registry-B_EK_TQf.mjs +406 -0
  341. package/dist/byline-registry-B_EK_TQf.mjs.map +1 -0
  342. package/dist/bylines-DFAaFXWi.mjs +214 -0
  343. package/dist/bylines-DFAaFXWi.mjs.map +1 -0
  344. package/dist/bylines-DkIxUxmr.mjs +232 -0
  345. package/dist/bylines-DkIxUxmr.mjs.map +1 -0
  346. package/dist/cache-DkvhQ8XE.mjs +108 -0
  347. package/dist/cache-DkvhQ8XE.mjs.map +1 -0
  348. package/dist/canonical-CcSuuTlH.mjs +174 -0
  349. package/dist/canonical-CcSuuTlH.mjs.map +1 -0
  350. package/dist/capture-triggers-DJEoOTOq.mjs +362 -0
  351. package/dist/capture-triggers-DJEoOTOq.mjs.map +1 -0
  352. package/dist/challenge-store-CvBl81AQ.mjs +51 -0
  353. package/dist/challenge-store-CvBl81AQ.mjs.map +1 -0
  354. package/dist/checksum-CClweQzI.mjs +162 -0
  355. package/dist/checksum-CClweQzI.mjs.map +1 -0
  356. package/dist/chrome-tags-D5caJwwp.mjs +24 -0
  357. package/dist/chrome-tags-D5caJwwp.mjs.map +1 -0
  358. package/dist/chunks-DwBaj8aT.mjs +25 -0
  359. package/dist/chunks-DwBaj8aT.mjs.map +1 -0
  360. package/dist/cli/index.mjs +374 -761
  361. package/dist/cli/index.mjs.map +1 -1
  362. package/dist/client/cf-access.d.mts +1 -1
  363. package/dist/client/index.d.mts +18 -18
  364. package/dist/client/index.mjs +2 -2
  365. package/dist/collection-deletion-CLCdXsnV.mjs +436 -0
  366. package/dist/collection-deletion-CLCdXsnV.mjs.map +1 -0
  367. package/dist/collection-deletion-processor-hNw8x01F.mjs +217 -0
  368. package/dist/collection-deletion-processor-hNw8x01F.mjs.map +1 -0
  369. package/dist/collection-slugs-cache-CUuiduLq.mjs +85 -0
  370. package/dist/collection-slugs-cache-CUuiduLq.mjs.map +1 -0
  371. package/dist/comment-D2uvL63j.mjs +313 -0
  372. package/dist/comment-D2uvL63j.mjs.map +1 -0
  373. package/dist/comment-reaction-DevUuzg4.mjs +85 -0
  374. package/dist/comment-reaction-DevUuzg4.mjs.map +1 -0
  375. package/dist/comments-Cq0s7_hO.mjs +195 -0
  376. package/dist/comments-Cq0s7_hO.mjs.map +1 -0
  377. package/dist/comments-M4EDYAHw.mjs +99 -0
  378. package/dist/comments-M4EDYAHw.mjs.map +1 -0
  379. package/dist/components-CuIGwXMs.mjs +108 -0
  380. package/dist/components-CuIGwXMs.mjs.map +1 -0
  381. package/dist/config-52Ea5IWt.d.mts +39 -0
  382. package/dist/config-52Ea5IWt.d.mts.map +1 -0
  383. package/dist/config-CCz3xOLc.mjs +242 -0
  384. package/dist/config-CCz3xOLc.mjs.map +1 -0
  385. package/dist/config-D3d5Qudq.d.mts +6669 -0
  386. package/dist/config-D3d5Qudq.d.mts.map +1 -0
  387. package/dist/content-CbnufDsM.mjs +2018 -0
  388. package/dist/content-CbnufDsM.mjs.map +1 -0
  389. package/dist/content-list-query-D_7B1FE5.d.mts +26 -0
  390. package/dist/content-list-query-D_7B1FE5.d.mts.map +1 -0
  391. package/dist/content-policy-CYu6K1KB.mjs +46 -0
  392. package/dist/content-policy-CYu6K1KB.mjs.map +1 -0
  393. package/dist/content-repair-DlDUgFGp.mjs +442 -0
  394. package/dist/content-repair-DlDUgFGp.mjs.map +1 -0
  395. package/dist/context-CGQLA1-a.mjs +1922 -0
  396. package/dist/context-CGQLA1-a.mjs.map +1 -0
  397. package/dist/cron-c4D5C5Fx.mjs +284 -0
  398. package/dist/cron-c4D5C5Fx.mjs.map +1 -0
  399. package/dist/dashboard-CBq9kmOn.mjs +151 -0
  400. package/dist/dashboard-CBq9kmOn.mjs.map +1 -0
  401. package/dist/db/index.d.mts +3 -3
  402. package/dist/db/index.mjs +1 -1
  403. package/dist/db/libsql-migrations.d.mts +1 -1
  404. package/dist/db/libsql-migrations.mjs +4 -4
  405. package/dist/db/libsql.d.mts +1 -1
  406. package/dist/db/postgres-migrations.d.mts +1 -1
  407. package/dist/db/postgres-migrations.mjs +4 -4
  408. package/dist/db/postgres.d.mts +1 -1
  409. package/dist/db/sqlite-migrations.d.mts +1 -1
  410. package/dist/db/sqlite-migrations.mjs +4 -4
  411. package/dist/db/sqlite.d.mts +1 -1
  412. package/dist/db/sqlite.mjs +1 -1
  413. package/dist/default-DPsys51U.mjs +81 -0
  414. package/dist/default-DPsys51U.mjs.map +1 -0
  415. package/dist/deferred-tasks-CZ7rzBdo.d.mts +9 -0
  416. package/dist/deferred-tasks-CZ7rzBdo.d.mts.map +1 -0
  417. package/dist/dev-typegen-HRxwLRcT.mjs +29 -0
  418. package/dist/dev-typegen-HRxwLRcT.mjs.map +1 -0
  419. package/dist/device-flow-OcSqE9uY.mjs +521 -0
  420. package/dist/device-flow-OcSqE9uY.mjs.map +1 -0
  421. package/dist/digest-DVZ--cE9.mjs +194 -0
  422. package/dist/digest-DVZ--cE9.mjs.map +1 -0
  423. package/dist/direct-executor-DyjAqkpT.mjs +104 -0
  424. package/dist/direct-executor-DyjAqkpT.mjs.map +1 -0
  425. package/dist/dist-FPR93_xf.mjs +810 -0
  426. package/dist/dist-FPR93_xf.mjs.map +1 -0
  427. package/dist/email-console-DyQIgEnz.mjs +50 -0
  428. package/dist/email-console-DyQIgEnz.mjs.map +1 -0
  429. package/dist/email-locale-BYb16rVd.mjs +25 -0
  430. package/dist/email-locale-BYb16rVd.mjs.map +1 -0
  431. package/dist/emdash-runtime-CrzxJFmF.mjs +4214 -0
  432. package/dist/emdash-runtime-CrzxJFmF.mjs.map +1 -0
  433. package/dist/enrich-DBtCncE2.mjs +194 -0
  434. package/dist/enrich-DBtCncE2.mjs.map +1 -0
  435. package/dist/entry-lock-BN5y4YbO.d.mts +4481 -0
  436. package/dist/entry-lock-BN5y4YbO.d.mts.map +1 -0
  437. package/dist/entry-lock-BaRBHCsH.mjs +1173 -0
  438. package/dist/entry-lock-BaRBHCsH.mjs.map +1 -0
  439. package/dist/entry-lock-C6NcnZPV.mjs +163 -0
  440. package/dist/entry-lock-C6NcnZPV.mjs.map +1 -0
  441. package/dist/entry-locks-BTyQLdbC.mjs +167 -0
  442. package/dist/entry-locks-BTyQLdbC.mjs.map +1 -0
  443. package/dist/error-BfsNNrIE.mjs +90 -0
  444. package/dist/error-BfsNNrIE.mjs.map +1 -0
  445. package/dist/errors-BN1QAnEY.mjs +460 -0
  446. package/dist/errors-BN1QAnEY.mjs.map +1 -0
  447. package/dist/errors-Bt3goF8I.mjs +120 -0
  448. package/dist/errors-Bt3goF8I.mjs.map +1 -0
  449. package/dist/escape-BveidMa2.mjs +9 -0
  450. package/dist/escape-BveidMa2.mjs.map +1 -0
  451. package/dist/fence-Bb_IbCxI.mjs +162 -0
  452. package/dist/fence-Bb_IbCxI.mjs.map +1 -0
  453. package/dist/field-defs-cache-CsZctvbu.mjs +77 -0
  454. package/dist/field-defs-cache-CsZctvbu.mjs.map +1 -0
  455. package/dist/field-map-6m9rF49t.mjs +83 -0
  456. package/dist/field-map-6m9rF49t.mjs.map +1 -0
  457. package/dist/focal-point-CZ3d1wK5.mjs +23 -0
  458. package/dist/focal-point-CZ3d1wK5.mjs.map +1 -0
  459. package/dist/fts-manager-CCaZTp8y.mjs +443 -0
  460. package/dist/fts-manager-CCaZTp8y.mjs.map +1 -0
  461. package/dist/gc-xowRjczI.mjs +75 -0
  462. package/dist/gc-xowRjczI.mjs.map +1 -0
  463. package/dist/hash-COVK27Pc.mjs +37 -0
  464. package/dist/hash-COVK27Pc.mjs.map +1 -0
  465. package/dist/hook-result-CYSk0XFe.d.mts +31 -0
  466. package/dist/hook-result-CYSk0XFe.d.mts.map +1 -0
  467. package/dist/hreflang-CChPV7jx.mjs +105 -0
  468. package/dist/hreflang-CChPV7jx.mjs.map +1 -0
  469. package/dist/hreflang-DRslq3QI.d.mts +50 -0
  470. package/dist/hreflang-DRslq3QI.d.mts.map +1 -0
  471. package/dist/http-route-dispatch-CAdp2onn.mjs +339 -0
  472. package/dist/http-route-dispatch-CAdp2onn.mjs.map +1 -0
  473. package/dist/identity-NVTOOQyq.mjs +35 -0
  474. package/dist/identity-NVTOOQyq.mjs.map +1 -0
  475. package/dist/import-wpSO2N0x.mjs +2020 -0
  476. package/dist/import-wpSO2N0x.mjs.map +1 -0
  477. package/dist/index-CDs2wq4X.d.mts +623 -0
  478. package/dist/index-CDs2wq4X.d.mts.map +1 -0
  479. package/dist/index.d.mts +24 -24
  480. package/dist/index.mjs +103 -101
  481. package/dist/install-finalization-D9R9R-W8.mjs +58 -0
  482. package/dist/install-finalization-D9R9R-W8.mjs.map +1 -0
  483. package/dist/keys-Bck6LBfF.mjs +61 -0
  484. package/dist/keys-Bck6LBfF.mjs.map +1 -0
  485. package/dist/kinds-CYmRESWj.mjs +974 -0
  486. package/dist/kinds-CYmRESWj.mjs.map +1 -0
  487. package/dist/lifecycle-C815ZQfV.mjs +25 -0
  488. package/dist/lifecycle-C815ZQfV.mjs.map +1 -0
  489. package/dist/load-D99axixL.mjs +28 -0
  490. package/dist/load-D99axixL.mjs.map +1 -0
  491. package/dist/loader-CrRPfkHS.mjs +1197 -0
  492. package/dist/loader-CrRPfkHS.mjs.map +1 -0
  493. package/dist/loops-DMKlygqX.mjs +203 -0
  494. package/dist/loops-DMKlygqX.mjs.map +1 -0
  495. package/dist/manifest-builder-BQzCHNgB.mjs +189 -0
  496. package/dist/manifest-builder-BQzCHNgB.mjs.map +1 -0
  497. package/dist/manifest-schema-Ci2gmd0u.mjs +492 -0
  498. package/dist/manifest-schema-Ci2gmd0u.mjs.map +1 -0
  499. package/dist/media/image-endpoint.mjs +1 -1
  500. package/dist/media/index.d.mts +1 -1
  501. package/dist/media/index.mjs +4 -4
  502. package/dist/media/local-runtime.d.mts +14 -14
  503. package/dist/media/local-runtime.mjs +6 -6
  504. package/dist/media-BUQePMxP.mjs +373 -0
  505. package/dist/media-BUQePMxP.mjs.map +1 -0
  506. package/dist/media-DH8ibNlY.mjs +294 -0
  507. package/dist/media-DH8ibNlY.mjs.map +1 -0
  508. package/dist/media-OEIQ3bVc.mjs +408 -0
  509. package/dist/media-OEIQ3bVc.mjs.map +1 -0
  510. package/dist/media-allowlist-pYtaBA95.mjs +44 -0
  511. package/dist/media-allowlist-pYtaBA95.mjs.map +1 -0
  512. package/dist/media-folders-Bf8_s4de.mjs +210 -0
  513. package/dist/media-folders-Bf8_s4de.mjs.map +1 -0
  514. package/dist/media-url-C6klEuNd.mjs +43 -0
  515. package/dist/media-url-C6klEuNd.mjs.map +1 -0
  516. package/dist/media-usage-D7Y64m6v.mjs +1629 -0
  517. package/dist/media-usage-D7Y64m6v.mjs.map +1 -0
  518. package/dist/media-usage-activation-DrqbmgLA.mjs +78 -0
  519. package/dist/media-usage-activation-DrqbmgLA.mjs.map +1 -0
  520. package/dist/media-usage-work-DommYhYA.mjs +152 -0
  521. package/dist/media-usage-work-DommYhYA.mjs.map +1 -0
  522. package/dist/media-usage-work-HU63hNDu.mjs +586 -0
  523. package/dist/media-usage-work-HU63hNDu.mjs.map +1 -0
  524. package/dist/menus-BQfusWEC.mjs +3608 -0
  525. package/dist/menus-BQfusWEC.mjs.map +1 -0
  526. package/dist/menus-BtwjQt5s.mjs +753 -0
  527. package/dist/menus-BtwjQt5s.mjs.map +1 -0
  528. package/dist/menus-p_M1V7J_.mjs +139 -0
  529. package/dist/menus-p_M1V7J_.mjs.map +1 -0
  530. package/dist/migrations/index.d.mts +1 -1
  531. package/dist/migrations/index.mjs +3 -3
  532. package/dist/mime-C94qBG-P.mjs +36 -0
  533. package/dist/mime-C94qBG-P.mjs.map +1 -0
  534. package/dist/mode-DpYW3w6U.mjs +23 -0
  535. package/dist/mode-DpYW3w6U.mjs.map +1 -0
  536. package/dist/node-sqlite-compat-pWFw0lcc.mjs +46 -0
  537. package/dist/node-sqlite-compat-pWFw0lcc.mjs.map +1 -0
  538. package/dist/normalize-DduyxBjf.mjs +191 -0
  539. package/dist/normalize-DduyxBjf.mjs.map +1 -0
  540. package/dist/oauth-authorization-DAXsH1H9.mjs +274 -0
  541. package/dist/oauth-authorization-DAXsH1H9.mjs.map +1 -0
  542. package/dist/oauth-clients-DENiDQri.mjs +266 -0
  543. package/dist/oauth-clients-DENiDQri.mjs.map +1 -0
  544. package/dist/oauth-state-store-y8mEYVq9.mjs +50 -0
  545. package/dist/oauth-state-store-y8mEYVq9.mjs.map +1 -0
  546. package/dist/oauth-user-lookup-CtTYAy-O.mjs +26 -0
  547. package/dist/oauth-user-lookup-CtTYAy-O.mjs.map +1 -0
  548. package/dist/object-cache/memory.d.mts +1 -1
  549. package/dist/options-BYa1FgIM.d.mts +81 -0
  550. package/dist/options-BYa1FgIM.d.mts.map +1 -0
  551. package/dist/page/index.d.mts +2 -2
  552. package/dist/page/index.mjs +5 -5
  553. package/dist/parse-CORGGhHf.mjs +81 -0
  554. package/dist/parse-CORGGhHf.mjs.map +1 -0
  555. package/dist/parse-avNM1xWV.d.mts +168 -0
  556. package/dist/parse-avNM1xWV.d.mts.map +1 -0
  557. package/dist/passkey-config-Dih5YeIS.mjs +46 -0
  558. package/dist/passkey-config-Dih5YeIS.mjs.map +1 -0
  559. package/dist/paths-d51RFm8M.mjs +77 -0
  560. package/dist/paths-d51RFm8M.mjs.map +1 -0
  561. package/dist/patterns-CoqlXUvk.mjs +145 -0
  562. package/dist/patterns-CoqlXUvk.mjs.map +1 -0
  563. package/dist/placeholder-D2aksPMF.d.mts +306 -0
  564. package/dist/placeholder-D2aksPMF.d.mts.map +1 -0
  565. package/dist/plan-DqeZKuaR.mjs +135 -0
  566. package/dist/plan-DqeZKuaR.mjs.map +1 -0
  567. package/dist/plugin-test-runtime.d.mts +14 -14
  568. package/dist/plugin-test-runtime.mjs +110 -107
  569. package/dist/plugin-types.d.mts +2 -2
  570. package/dist/plugin-utils.d.mts +12 -12
  571. package/dist/plugins/adapt-sandbox-entry.d.mts +12 -12
  572. package/dist/plugins/adapt-sandbox-entry.mjs +3 -3
  573. package/dist/plugins/host.d.mts +5 -5
  574. package/dist/plugins/host.mjs +42 -41
  575. package/dist/plugins-Bb7oqjQA.mjs +277 -0
  576. package/dist/plugins-Bb7oqjQA.mjs.map +1 -0
  577. package/dist/policy-BcEqut3n.mjs +45 -0
  578. package/dist/policy-BcEqut3n.mjs.map +1 -0
  579. package/dist/portable-text-DXE8ymtD.mjs +307 -0
  580. package/dist/portable-text-DXE8ymtD.mjs.map +1 -0
  581. package/dist/preview-DEDWbwA_.mjs +107 -0
  582. package/dist/preview-DEDWbwA_.mjs.map +1 -0
  583. package/dist/protocol-BJbTzvwl.d.mts +61 -0
  584. package/dist/protocol-BJbTzvwl.d.mts.map +1 -0
  585. package/dist/public-submission-D9c9S14m.mjs +250 -0
  586. package/dist/public-submission-D9c9S14m.mjs.map +1 -0
  587. package/dist/public-url-CIRe-2T6.mjs +101 -0
  588. package/dist/public-url-CIRe-2T6.mjs.map +1 -0
  589. package/dist/query-BzifXlPS.mjs +1156 -0
  590. package/dist/query-BzifXlPS.mjs.map +1 -0
  591. package/dist/query-OzMJ6vJP.mjs +41 -0
  592. package/dist/query-OzMJ6vJP.mjs.map +1 -0
  593. package/dist/rate-limit-Csp60Vik.mjs +120 -0
  594. package/dist/rate-limit-Csp60Vik.mjs.map +1 -0
  595. package/dist/receipt-B-aU4hpJ.mjs +43 -0
  596. package/dist/receipt-B-aU4hpJ.mjs.map +1 -0
  597. package/dist/redirect-CDSw1sU3.mjs +17 -0
  598. package/dist/redirect-CDSw1sU3.mjs.map +1 -0
  599. package/dist/redirect-D1ArbO85.mjs +411 -0
  600. package/dist/redirect-D1ArbO85.mjs.map +1 -0
  601. package/dist/redirects-BjTwDTsV.mjs +148 -0
  602. package/dist/redirects-BjTwDTsV.mjs.map +1 -0
  603. package/dist/redirects-Ck50kH3n.mjs +446 -0
  604. package/dist/redirects-Ck50kH3n.mjs.map +1 -0
  605. package/dist/registry/testing.mjs +3 -3
  606. package/dist/registry-CFcNCayZ.mjs +6754 -0
  607. package/dist/registry-CFcNCayZ.mjs.map +1 -0
  608. package/dist/relations-Cfaa1fDl.mjs +776 -0
  609. package/dist/relations-Cfaa1fDl.mjs.map +1 -0
  610. package/dist/request-cache-iHRlHx1d.mjs +104 -0
  611. package/dist/request-cache-iHRlHx1d.mjs.map +1 -0
  612. package/dist/request-context.d.mts +1 -1
  613. package/dist/request-meta-D47mqyev.mjs +141 -0
  614. package/dist/request-meta-D47mqyev.mjs.map +1 -0
  615. package/dist/resolve-BPE8sMvx.mjs +99 -0
  616. package/dist/resolve-BPE8sMvx.mjs.map +1 -0
  617. package/dist/resolve-C0s1E_fH.mjs +199 -0
  618. package/dist/resolve-C0s1E_fH.mjs.map +1 -0
  619. package/dist/revision-j1p4-bOj.mjs +302 -0
  620. package/dist/revision-j1p4-bOj.mjs.map +1 -0
  621. package/dist/route-wire-C9PJV9aL.mjs +197 -0
  622. package/dist/route-wire-C9PJV9aL.mjs.map +1 -0
  623. package/dist/routes-lyYw0mMf.mjs +272 -0
  624. package/dist/routes-lyYw0mMf.mjs.map +1 -0
  625. package/dist/runner-B3eyB9Gb.mjs +6121 -0
  626. package/dist/runner-B3eyB9Gb.mjs.map +1 -0
  627. package/dist/runner-DoYH2Jcu.d.mts +68 -0
  628. package/dist/runner-DoYH2Jcu.d.mts.map +1 -0
  629. package/dist/runtime.d.mts +13 -13
  630. package/dist/runtime.mjs +3 -3
  631. package/dist/schema-BAayK46S.mjs +13 -0
  632. package/dist/schema-BAayK46S.mjs.map +1 -0
  633. package/dist/schema-Gqcl-8G4.mjs +815 -0
  634. package/dist/schema-Gqcl-8G4.mjs.map +1 -0
  635. package/dist/scopes-BqPOKoKf.mjs +34 -0
  636. package/dist/scopes-BqPOKoKf.mjs.map +1 -0
  637. package/dist/search-YJKPIEV6.mjs +448 -0
  638. package/dist/search-YJKPIEV6.mjs.map +1 -0
  639. package/dist/sections-BR_e-wK5.mjs +346 -0
  640. package/dist/sections-BR_e-wK5.mjs.map +1 -0
  641. package/dist/seed/index.d.mts +2 -2
  642. package/dist/seed/index.mjs +37 -31
  643. package/dist/seo/index.d.mts +3 -3
  644. package/dist/seo/index.mjs +22 -21
  645. package/dist/seo/index.mjs.map +1 -1
  646. package/dist/seo-Dsj8hy3j.mjs +114 -0
  647. package/dist/seo-Dsj8hy3j.mjs.map +1 -0
  648. package/dist/seo-XvH4LNNp.mjs +133 -0
  649. package/dist/seo-XvH4LNNp.mjs.map +1 -0
  650. package/dist/service-BsVG05gS.mjs +150 -0
  651. package/dist/service-BsVG05gS.mjs.map +1 -0
  652. package/dist/session-user-DgKbYrlj.mjs +51 -0
  653. package/dist/session-user-DgKbYrlj.mjs.map +1 -0
  654. package/dist/settings-CNPUntkh.mjs +51 -0
  655. package/dist/settings-CNPUntkh.mjs.map +1 -0
  656. package/dist/settings-D5EHi0Tk.mjs +439 -0
  657. package/dist/settings-D5EHi0Tk.mjs.map +1 -0
  658. package/dist/settings-DSfE7wOa.mjs +61 -0
  659. package/dist/settings-DSfE7wOa.mjs.map +1 -0
  660. package/dist/setup-complete-B8W8sJ9v.mjs +26 -0
  661. package/dist/setup-complete-B8W8sJ9v.mjs.map +1 -0
  662. package/dist/setup-nonce-DnSBP260.mjs +25 -0
  663. package/dist/setup-nonce-DnSBP260.mjs.map +1 -0
  664. package/dist/site-settings-NGzdrt7O.mjs +50 -0
  665. package/dist/site-settings-NGzdrt7O.mjs.map +1 -0
  666. package/dist/site-url-D6JB7tLW.mjs +16 -0
  667. package/dist/site-url-D6JB7tLW.mjs.map +1 -0
  668. package/dist/snapshot-CdcwwWAf.mjs +414 -0
  669. package/dist/snapshot-CdcwwWAf.mjs.map +1 -0
  670. package/dist/ssrf-Dv0_tRpo.mjs +406 -0
  671. package/dist/ssrf-Dv0_tRpo.mjs.map +1 -0
  672. package/dist/stage-CUjhED3P.mjs +662 -0
  673. package/dist/stage-CUjhED3P.mjs.map +1 -0
  674. package/dist/staged-C36IWXff.mjs +659 -0
  675. package/dist/staged-C36IWXff.mjs.map +1 -0
  676. package/dist/state-CKouMCSY.mjs +153 -0
  677. package/dist/state-CKouMCSY.mjs.map +1 -0
  678. package/dist/status-lzyj4Uu7.mjs +30 -0
  679. package/dist/status-lzyj4Uu7.mjs.map +1 -0
  680. package/dist/storage/local.d.mts +1 -1
  681. package/dist/storage/local.mjs +1 -1
  682. package/dist/storage/s3.d.mts +1 -1
  683. package/dist/storage/s3.mjs +1 -1
  684. package/dist/storage-query-B-cWcTjJ.d.mts +1594 -0
  685. package/dist/storage-query-B-cWcTjJ.d.mts.map +1 -0
  686. package/dist/tar-DsTXX6ch.mjs +449 -0
  687. package/dist/tar-DsTXX6ch.mjs.map +1 -0
  688. package/dist/target-DLQCwp_Z.mjs +27 -0
  689. package/dist/target-DLQCwp_Z.mjs.map +1 -0
  690. package/dist/taxonomies-BlAjr4BL.mjs +697 -0
  691. package/dist/taxonomies-BlAjr4BL.mjs.map +1 -0
  692. package/dist/taxonomies-Byyo7LOZ.mjs +939 -0
  693. package/dist/taxonomies-Byyo7LOZ.mjs.map +1 -0
  694. package/dist/taxonomy-CKDwM61Y.mjs +525 -0
  695. package/dist/taxonomy-CKDwM61Y.mjs.map +1 -0
  696. package/dist/tokens-BojXlk49.mjs +171 -0
  697. package/dist/tokens-BojXlk49.mjs.map +1 -0
  698. package/dist/transaction-DLPQWik1.mjs +29 -0
  699. package/dist/transaction-DLPQWik1.mjs.map +1 -0
  700. package/dist/transfer-XSTh2BCr.mjs +8706 -0
  701. package/dist/transfer-XSTh2BCr.mjs.map +1 -0
  702. package/dist/transfer-download-DkoCDA9d.mjs +33 -0
  703. package/dist/transfer-download-DkoCDA9d.mjs.map +1 -0
  704. package/dist/transfer-iQ26J-RT.mjs +166 -0
  705. package/dist/transfer-iQ26J-RT.mjs.map +1 -0
  706. package/dist/transformations-C7RQabKQ.mjs +196 -0
  707. package/dist/transformations-C7RQabKQ.mjs.map +1 -0
  708. package/dist/transport-D3RulfD6.mjs +136 -0
  709. package/dist/transport-D3RulfD6.mjs.map +1 -0
  710. package/dist/transport-DhlZlf4D.d.mts +42 -0
  711. package/dist/transport-DhlZlf4D.d.mts.map +1 -0
  712. package/dist/trusted-proxy-B6ECOZJc.mjs +51 -0
  713. package/dist/trusted-proxy-B6ECOZJc.mjs.map +1 -0
  714. package/dist/turnstile-lqUiY7lP.mjs +53 -0
  715. package/dist/turnstile-lqUiY7lP.mjs.map +1 -0
  716. package/dist/types-4Dn9bZhu.mjs +77 -0
  717. package/dist/types-4Dn9bZhu.mjs.map +1 -0
  718. package/dist/types-5LSo7PPx.d.mts +85 -0
  719. package/dist/types-5LSo7PPx.d.mts.map +1 -0
  720. package/dist/types-BQ3hwYJT.d.mts +896 -0
  721. package/dist/types-BQ3hwYJT.d.mts.map +1 -0
  722. package/dist/types-BYEk7qo3.d.mts +198 -0
  723. package/dist/types-BYEk7qo3.d.mts.map +1 -0
  724. package/dist/types-CJKsWVFR.mjs +199 -0
  725. package/dist/types-CJKsWVFR.mjs.map +1 -0
  726. package/dist/types-Ca_w8mmY.d.mts +363 -0
  727. package/dist/types-Ca_w8mmY.d.mts.map +1 -0
  728. package/dist/types-CkE41ZAG.mjs +18 -0
  729. package/dist/types-CkE41ZAG.mjs.map +1 -0
  730. package/dist/types-D0aJW2uR.d.mts +1641 -0
  731. package/dist/types-D0aJW2uR.d.mts.map +1 -0
  732. package/dist/types-DbpN4Qto.d.mts +372 -0
  733. package/dist/types-DbpN4Qto.d.mts.map +1 -0
  734. package/dist/types-ILDI1sdY.mjs +16 -0
  735. package/dist/types-ILDI1sdY.mjs.map +1 -0
  736. package/dist/types-OC5c-edM.mjs +22 -0
  737. package/dist/types-OC5c-edM.mjs.map +1 -0
  738. package/dist/types-aOOxRSx6.mjs +103 -0
  739. package/dist/types-aOOxRSx6.mjs.map +1 -0
  740. package/dist/types-m1WbUffD.d.mts +198 -0
  741. package/dist/types-m1WbUffD.d.mts.map +1 -0
  742. package/dist/types-uxLoPBio.d.mts +124 -0
  743. package/dist/types-uxLoPBio.d.mts.map +1 -0
  744. package/dist/types-we5uR-tV.d.mts +287 -0
  745. package/dist/types-we5uR-tV.d.mts.map +1 -0
  746. package/dist/upload-attempts-DduMy6B0.mjs +32 -0
  747. package/dist/upload-attempts-DduMy6B0.mjs.map +1 -0
  748. package/dist/url-pattern-B3EwxAAk.mjs +33 -0
  749. package/dist/url-pattern-B3EwxAAk.mjs.map +1 -0
  750. package/dist/user-ElwQjTQT.mjs +171 -0
  751. package/dist/user-ElwQjTQT.mjs.map +1 -0
  752. package/dist/utils-BlAXmfOG.mjs +439 -0
  753. package/dist/utils-BlAXmfOG.mjs.map +1 -0
  754. package/dist/validate-CH0X5b2c.d.mts +468 -0
  755. package/dist/validate-CH0X5b2c.d.mts.map +1 -0
  756. package/dist/validate-CfgRpcjA.mjs +492 -0
  757. package/dist/validate-CfgRpcjA.mjs.map +1 -0
  758. package/dist/validation-DvYJtv4k.mjs +218 -0
  759. package/dist/validation-DvYJtv4k.mjs.map +1 -0
  760. package/dist/version-DgYjhTud.mjs +9 -0
  761. package/dist/version-DgYjhTud.mjs.map +1 -0
  762. package/dist/version-Dq71scUb.mjs +7 -0
  763. package/dist/version-Dq71scUb.mjs.map +1 -0
  764. package/dist/widgets-ChFIj0be.mjs +126 -0
  765. package/dist/widgets-ChFIj0be.mjs.map +1 -0
  766. package/dist/wxr-taxonomies-CBZMp2YX.mjs +372 -0
  767. package/dist/wxr-taxonomies-CBZMp2YX.mjs.map +1 -0
  768. package/dist/wxr-taxonomies-DlKDpWN1.d.mts +34 -0
  769. package/dist/wxr-taxonomies-DlKDpWN1.d.mts.map +1 -0
  770. package/dist/zod-generator-DmTKI-Ig.mjs +426 -0
  771. package/dist/zod-generator-DmTKI-Ig.mjs.map +1 -0
  772. package/package.json +7 -6
  773. package/src/api/handlers/content.ts +593 -16
  774. package/src/api/handlers/device-flow.ts +74 -21
  775. package/src/api/handlers/index.ts +1 -0
  776. package/src/api/handlers/manifest.ts +79 -5
  777. package/src/api/handlers/media-upload.ts +17 -71
  778. package/src/api/handlers/media.ts +97 -3
  779. package/src/api/handlers/relations.ts +638 -157
  780. package/src/api/handlers/revision.ts +30 -0
  781. package/src/api/handlers/schema.ts +403 -1
  782. package/src/api/handlers/snapshot.ts +257 -3
  783. package/src/api/handlers/staged-references.ts +142 -0
  784. package/src/api/handlers/validate-references.ts +146 -0
  785. package/src/api/schemas/content.ts +13 -0
  786. package/src/api/schemas/relations.ts +54 -44
  787. package/src/api/schemas/schema.ts +14 -0
  788. package/src/astro/integration/routes.ts +15 -19
  789. package/src/astro/integration/vite-config.ts +10 -4
  790. package/src/astro/middleware.ts +1 -0
  791. package/src/astro/routes/api/content/[collection]/[id]/references/[relation]/children.ts +11 -54
  792. package/src/astro/routes/api/content/[collection]/[id].ts +3 -1
  793. package/src/astro/routes/api/oauth/device/authorize.ts +33 -4
  794. package/src/astro/routes/api/relations/index.ts +4 -4
  795. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +6 -2
  796. package/src/astro/routes/api/setup/index.ts +66 -23
  797. package/src/astro/routes/api/snapshot.ts +2 -1
  798. package/src/astro/types.ts +10 -0
  799. package/src/cleanup.ts +10 -0
  800. package/src/cli/commands/export-seed.ts +510 -89
  801. package/src/components/Columns.astro +3 -1
  802. package/src/components/Cover.astro +3 -1
  803. package/src/components/WidgetArea.astro +3 -2
  804. package/src/database/migrations/086_relations_structural.ts +197 -0
  805. package/src/database/migrations/087_reference_field_relations.ts +407 -0
  806. package/src/database/migrations/runner.ts +4 -0
  807. package/src/database/reference-backfill.ts +141 -0
  808. package/src/database/repositories/content.ts +43 -8
  809. package/src/database/repositories/media.ts +40 -1
  810. package/src/database/repositories/redirect.ts +17 -34
  811. package/src/database/repositories/relation.ts +620 -208
  812. package/src/database/repositories/revision.ts +16 -0
  813. package/src/database/repositories/types.ts +38 -0
  814. package/src/database/transaction.ts +12 -1
  815. package/src/database/types.ts +14 -5
  816. package/src/emdash-runtime.ts +114 -11
  817. package/src/index.ts +11 -0
  818. package/src/loader.ts +169 -22
  819. package/src/mcp/server.ts +13 -48
  820. package/src/query.ts +455 -25
  821. package/src/references/field-map.ts +115 -0
  822. package/src/references/resolve.ts +160 -0
  823. package/src/references/staged.ts +117 -0
  824. package/src/references/types.ts +16 -0
  825. package/src/registry/artifact-fetch.ts +45 -271
  826. package/src/schema/registry.ts +99 -24
  827. package/src/schema/types.ts +58 -0
  828. package/src/schema/zod-generator.ts +67 -1
  829. package/src/seed/apply.ts +841 -103
  830. package/src/seed/types.ts +36 -2
  831. package/src/seed/validate.ts +78 -2
  832. package/src/transfer/analyze/unique-keys.ts +3 -12
  833. package/src/transfer/analyze/validate.ts +65 -39
  834. package/src/transfer/analyze/values.ts +12 -1
  835. package/src/transfer/export/readers.ts +12 -8
  836. package/src/transfer/format/columns.ts +26 -6
  837. package/src/transfer/format/kinds.ts +21 -5
  838. package/src/transfer/import/context.ts +8 -1
  839. package/src/transfer/ops/package-index.ts +1 -1
  840. package/dist/action-token-DYwe4Pk0.mjs +0 -26
  841. package/dist/action-token-DYwe4Pk0.mjs.map +0 -1
  842. package/dist/activation-LOkDM5WT.mjs +0 -382
  843. package/dist/activation-LOkDM5WT.mjs.map +0 -1
  844. package/dist/adapters-B64LbYcB.d.mts +0 -171
  845. package/dist/adapters-B64LbYcB.d.mts.map +0 -1
  846. package/dist/allowed-origins-CHB8oZNM.mjs +0 -116
  847. package/dist/allowed-origins-CHB8oZNM.mjs.map +0 -1
  848. package/dist/api-cFFZpyS0.mjs +0 -4718
  849. package/dist/api-cFFZpyS0.mjs.map +0 -1
  850. package/dist/api-tokens-s4i4aUp_.mjs +0 -166
  851. package/dist/api-tokens-s4i4aUp_.mjs.map +0 -1
  852. package/dist/apply-CoT603H3.mjs +0 -1163
  853. package/dist/apply-CoT603H3.mjs.map +0 -1
  854. package/dist/approvals-CqMfNxak.mjs +0 -460
  855. package/dist/approvals-CqMfNxak.mjs.map +0 -1
  856. package/dist/artifact-fetch-DZMOGZ3-.mjs +0 -818
  857. package/dist/artifact-fetch-DZMOGZ3-.mjs.map +0 -1
  858. package/dist/astro/routes/api/relations/_id_/translations.d.mts +0 -8
  859. package/dist/astro/routes/api/relations/_id_/translations.d.mts.map +0 -1
  860. package/dist/astro/routes/api/relations/_id_/translations.mjs +0 -47
  861. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +0 -1
  862. package/dist/audit-RfCugiSB.mjs +0 -127
  863. package/dist/audit-RfCugiSB.mjs.map +0 -1
  864. package/dist/authoritative-records-CoJvBXAD.mjs +0 -5172
  865. package/dist/authoritative-records-CoJvBXAD.mjs.map +0 -1
  866. package/dist/authorize-seCifOMJ.mjs +0 -49
  867. package/dist/authorize-seCifOMJ.mjs.map +0 -1
  868. package/dist/backup-CBu0LrXf.mjs +0 -246
  869. package/dist/backup-CBu0LrXf.mjs.map +0 -1
  870. package/dist/block-type-registry-DUI_3KxT.mjs +0 -6709
  871. package/dist/block-type-registry-DUI_3KxT.mjs.map +0 -1
  872. package/dist/block-types-BmZmLmAD.mjs +0 -132
  873. package/dist/block-types-BmZmLmAD.mjs.map +0 -1
  874. package/dist/block-values-C_VIPr5t.mjs +0 -175
  875. package/dist/block-values-C_VIPr5t.mjs.map +0 -1
  876. package/dist/byline-DYmkXqvo.mjs +0 -909
  877. package/dist/byline-DYmkXqvo.mjs.map +0 -1
  878. package/dist/byline-fields-D28AT-Fh.mjs +0 -123
  879. package/dist/byline-fields-D28AT-Fh.mjs.map +0 -1
  880. package/dist/byline-registry-gnKscbQX.mjs +0 -406
  881. package/dist/byline-registry-gnKscbQX.mjs.map +0 -1
  882. package/dist/bylines-BPwJqoYH.mjs +0 -232
  883. package/dist/bylines-BPwJqoYH.mjs.map +0 -1
  884. package/dist/bylines-ukLaYpVZ.mjs +0 -214
  885. package/dist/bylines-ukLaYpVZ.mjs.map +0 -1
  886. package/dist/cache-m7UEkRFV.mjs +0 -108
  887. package/dist/cache-m7UEkRFV.mjs.map +0 -1
  888. package/dist/canonical-YhEQezjg.mjs +0 -174
  889. package/dist/canonical-YhEQezjg.mjs.map +0 -1
  890. package/dist/capture-triggers-kJyl_yyM.mjs +0 -362
  891. package/dist/capture-triggers-kJyl_yyM.mjs.map +0 -1
  892. package/dist/challenge-store-ppU6Uv13.mjs +0 -51
  893. package/dist/challenge-store-ppU6Uv13.mjs.map +0 -1
  894. package/dist/checksum-B2IINIlN.mjs +0 -162
  895. package/dist/checksum-B2IINIlN.mjs.map +0 -1
  896. package/dist/chrome-tags-DApCQSIk.mjs +0 -24
  897. package/dist/chrome-tags-DApCQSIk.mjs.map +0 -1
  898. package/dist/chunks-Dms7Uxbt.mjs +0 -25
  899. package/dist/chunks-Dms7Uxbt.mjs.map +0 -1
  900. package/dist/collection-deletion-BIm28mo3.mjs +0 -436
  901. package/dist/collection-deletion-BIm28mo3.mjs.map +0 -1
  902. package/dist/collection-deletion-processor-Br6rxs3u.mjs +0 -217
  903. package/dist/collection-deletion-processor-Br6rxs3u.mjs.map +0 -1
  904. package/dist/collection-slugs-cache-CjGk__zp.mjs +0 -85
  905. package/dist/collection-slugs-cache-CjGk__zp.mjs.map +0 -1
  906. package/dist/comment-CrBl22OF.mjs +0 -313
  907. package/dist/comment-CrBl22OF.mjs.map +0 -1
  908. package/dist/comment-reaction-JEQ2Fpd8.mjs +0 -85
  909. package/dist/comment-reaction-JEQ2Fpd8.mjs.map +0 -1
  910. package/dist/comments-2ah9du9y.mjs +0 -195
  911. package/dist/comments-2ah9du9y.mjs.map +0 -1
  912. package/dist/comments-fHZ8ON9O.mjs +0 -99
  913. package/dist/comments-fHZ8ON9O.mjs.map +0 -1
  914. package/dist/components-CXbq7CSM.mjs +0 -108
  915. package/dist/components-CXbq7CSM.mjs.map +0 -1
  916. package/dist/config-5ii2haYu.d.mts +0 -6528
  917. package/dist/config-5ii2haYu.d.mts.map +0 -1
  918. package/dist/config-IhybvkIQ.mjs +0 -242
  919. package/dist/config-IhybvkIQ.mjs.map +0 -1
  920. package/dist/config-Py43JK9r.d.mts +0 -39
  921. package/dist/config-Py43JK9r.d.mts.map +0 -1
  922. package/dist/content-DFaIEDj9.mjs +0 -2281
  923. package/dist/content-DFaIEDj9.mjs.map +0 -1
  924. package/dist/content-list-query-Dra4Dalc.d.mts +0 -26
  925. package/dist/content-list-query-Dra4Dalc.d.mts.map +0 -1
  926. package/dist/content-policy-DFNw91-d.mjs +0 -46
  927. package/dist/content-policy-DFNw91-d.mjs.map +0 -1
  928. package/dist/content-repair-Q8IUrpLL.mjs +0 -442
  929. package/dist/content-repair-Q8IUrpLL.mjs.map +0 -1
  930. package/dist/context-BeGccdUX.mjs +0 -1921
  931. package/dist/context-BeGccdUX.mjs.map +0 -1
  932. package/dist/cron-7DCJwPXq.mjs +0 -284
  933. package/dist/cron-7DCJwPXq.mjs.map +0 -1
  934. package/dist/dashboard-DjrB-xxZ.mjs +0 -151
  935. package/dist/dashboard-DjrB-xxZ.mjs.map +0 -1
  936. package/dist/default-DQ6vsviu.mjs +0 -81
  937. package/dist/default-DQ6vsviu.mjs.map +0 -1
  938. package/dist/deferred-tasks-BPxXUB4x.d.mts +0 -9
  939. package/dist/deferred-tasks-BPxXUB4x.d.mts.map +0 -1
  940. package/dist/dev-typegen-CZaPLl3V.mjs +0 -29
  941. package/dist/dev-typegen-CZaPLl3V.mjs.map +0 -1
  942. package/dist/device-flow-CRd6DXmW.mjs +0 -466
  943. package/dist/device-flow-CRd6DXmW.mjs.map +0 -1
  944. package/dist/digest-BarSh-pN.mjs +0 -194
  945. package/dist/digest-BarSh-pN.mjs.map +0 -1
  946. package/dist/direct-executor-BmuFJJZV.mjs +0 -104
  947. package/dist/direct-executor-BmuFJJZV.mjs.map +0 -1
  948. package/dist/dist-BNwANFJR.mjs +0 -810
  949. package/dist/dist-BNwANFJR.mjs.map +0 -1
  950. package/dist/email-console-bJn3ctqA.mjs +0 -50
  951. package/dist/email-console-bJn3ctqA.mjs.map +0 -1
  952. package/dist/email-locale-CN6WAMx0.mjs +0 -25
  953. package/dist/email-locale-CN6WAMx0.mjs.map +0 -1
  954. package/dist/emdash-runtime-DAjghyEG.mjs +0 -4186
  955. package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
  956. package/dist/enrich-Dl3kZ-lP.mjs +0 -194
  957. package/dist/enrich-Dl3kZ-lP.mjs.map +0 -1
  958. package/dist/entry-lock-BJYdBfXb.mjs +0 -1097
  959. package/dist/entry-lock-BJYdBfXb.mjs.map +0 -1
  960. package/dist/entry-lock-DHQOxN7R.mjs +0 -163
  961. package/dist/entry-lock-DHQOxN7R.mjs.map +0 -1
  962. package/dist/entry-lock-DVhCt49A.d.mts +0 -4368
  963. package/dist/entry-lock-DVhCt49A.d.mts.map +0 -1
  964. package/dist/entry-locks-D0foLdjQ.mjs +0 -167
  965. package/dist/entry-locks-D0foLdjQ.mjs.map +0 -1
  966. package/dist/error-CfvOHP0M.mjs +0 -90
  967. package/dist/error-CfvOHP0M.mjs.map +0 -1
  968. package/dist/errors-BF0_LFhW.mjs +0 -460
  969. package/dist/errors-BF0_LFhW.mjs.map +0 -1
  970. package/dist/errors-DgE1zJGG.mjs +0 -120
  971. package/dist/errors-DgE1zJGG.mjs.map +0 -1
  972. package/dist/escape-CuaZhZvL.mjs +0 -9
  973. package/dist/escape-CuaZhZvL.mjs.map +0 -1
  974. package/dist/fence-CMDXJYc_.mjs +0 -162
  975. package/dist/fence-CMDXJYc_.mjs.map +0 -1
  976. package/dist/field-defs-cache-HIgBbqZZ.mjs +0 -77
  977. package/dist/field-defs-cache-HIgBbqZZ.mjs.map +0 -1
  978. package/dist/focal-point-Gr34o-4o.mjs +0 -23
  979. package/dist/focal-point-Gr34o-4o.mjs.map +0 -1
  980. package/dist/fts-manager-S-K0laoS.mjs +0 -443
  981. package/dist/fts-manager-S-K0laoS.mjs.map +0 -1
  982. package/dist/gc-DjKY0Ay1.mjs +0 -75
  983. package/dist/gc-DjKY0Ay1.mjs.map +0 -1
  984. package/dist/hash-SDAW6zC-.mjs +0 -37
  985. package/dist/hash-SDAW6zC-.mjs.map +0 -1
  986. package/dist/hook-result-B5bXDgS2.d.mts +0 -31
  987. package/dist/hook-result-B5bXDgS2.d.mts.map +0 -1
  988. package/dist/hreflang-D5uNnsGJ.d.mts +0 -50
  989. package/dist/hreflang-D5uNnsGJ.d.mts.map +0 -1
  990. package/dist/hreflang-DMFBL2eb.mjs +0 -105
  991. package/dist/hreflang-DMFBL2eb.mjs.map +0 -1
  992. package/dist/http-route-dispatch-BitqzwMy.mjs +0 -339
  993. package/dist/http-route-dispatch-BitqzwMy.mjs.map +0 -1
  994. package/dist/identity-IyNWwXr6.mjs +0 -35
  995. package/dist/identity-IyNWwXr6.mjs.map +0 -1
  996. package/dist/import-C3JZplWq.mjs +0 -2020
  997. package/dist/import-C3JZplWq.mjs.map +0 -1
  998. package/dist/index-D-hdtLe8.d.mts +0 -623
  999. package/dist/index-D-hdtLe8.d.mts.map +0 -1
  1000. package/dist/install-finalization-DfM1fj5x.mjs +0 -58
  1001. package/dist/install-finalization-DfM1fj5x.mjs.map +0 -1
  1002. package/dist/keys-yxLm5l35.mjs +0 -61
  1003. package/dist/keys-yxLm5l35.mjs.map +0 -1
  1004. package/dist/kinds-CRu_T5T5.mjs +0 -961
  1005. package/dist/kinds-CRu_T5T5.mjs.map +0 -1
  1006. package/dist/lifecycle-BzeGPeQ-.mjs +0 -25
  1007. package/dist/lifecycle-BzeGPeQ-.mjs.map +0 -1
  1008. package/dist/load-XgJiE6_3.mjs +0 -28
  1009. package/dist/load-XgJiE6_3.mjs.map +0 -1
  1010. package/dist/loader-BzqeR1ZN.mjs +0 -1086
  1011. package/dist/loader-BzqeR1ZN.mjs.map +0 -1
  1012. package/dist/loops-BQSzv5Zn.mjs +0 -203
  1013. package/dist/loops-BQSzv5Zn.mjs.map +0 -1
  1014. package/dist/manifest-builder-B8zR-73J.mjs +0 -189
  1015. package/dist/manifest-builder-B8zR-73J.mjs.map +0 -1
  1016. package/dist/manifest-schema-CY3zng_M.mjs +0 -492
  1017. package/dist/manifest-schema-CY3zng_M.mjs.map +0 -1
  1018. package/dist/media-Pu1L6xir.mjs +0 -408
  1019. package/dist/media-Pu1L6xir.mjs.map +0 -1
  1020. package/dist/media-Qp5S3eH3.mjs +0 -355
  1021. package/dist/media-Qp5S3eH3.mjs.map +0 -1
  1022. package/dist/media-allowlist-BSoa9Vvm.mjs +0 -44
  1023. package/dist/media-allowlist-BSoa9Vvm.mjs.map +0 -1
  1024. package/dist/media-folders-BE0oo2eX.mjs +0 -210
  1025. package/dist/media-folders-BE0oo2eX.mjs.map +0 -1
  1026. package/dist/media-h7tOiDLj.mjs +0 -221
  1027. package/dist/media-h7tOiDLj.mjs.map +0 -1
  1028. package/dist/media-url-jArBpzIG.mjs +0 -43
  1029. package/dist/media-url-jArBpzIG.mjs.map +0 -1
  1030. package/dist/media-usage-My-Vi-ex.mjs +0 -1629
  1031. package/dist/media-usage-My-Vi-ex.mjs.map +0 -1
  1032. package/dist/media-usage-activation-HKp7zk7W.mjs +0 -78
  1033. package/dist/media-usage-activation-HKp7zk7W.mjs.map +0 -1
  1034. package/dist/media-usage-work-C2OZLCAd.mjs +0 -586
  1035. package/dist/media-usage-work-C2OZLCAd.mjs.map +0 -1
  1036. package/dist/media-usage-work-DwUNbgvS.mjs +0 -152
  1037. package/dist/media-usage-work-DwUNbgvS.mjs.map +0 -1
  1038. package/dist/menus-BM7BPLOb.mjs +0 -139
  1039. package/dist/menus-BM7BPLOb.mjs.map +0 -1
  1040. package/dist/menus-ITeAbcIF.mjs +0 -753
  1041. package/dist/menus-ITeAbcIF.mjs.map +0 -1
  1042. package/dist/menus-TsSGmSR4.mjs +0 -3608
  1043. package/dist/menus-TsSGmSR4.mjs.map +0 -1
  1044. package/dist/mime-ChLqYwDk.mjs +0 -36
  1045. package/dist/mime-ChLqYwDk.mjs.map +0 -1
  1046. package/dist/mode-CCuKXUci.mjs +0 -23
  1047. package/dist/mode-CCuKXUci.mjs.map +0 -1
  1048. package/dist/node-sqlite-compat-CONMsl-x.mjs +0 -46
  1049. package/dist/node-sqlite-compat-CONMsl-x.mjs.map +0 -1
  1050. package/dist/normalize-CO2P8-iE.mjs +0 -191
  1051. package/dist/normalize-CO2P8-iE.mjs.map +0 -1
  1052. package/dist/oauth-authorization-DlMIL5_h.mjs +0 -274
  1053. package/dist/oauth-authorization-DlMIL5_h.mjs.map +0 -1
  1054. package/dist/oauth-clients-Djq9r_KP.mjs +0 -266
  1055. package/dist/oauth-clients-Djq9r_KP.mjs.map +0 -1
  1056. package/dist/oauth-state-store-BbjnZXNC.mjs +0 -50
  1057. package/dist/oauth-state-store-BbjnZXNC.mjs.map +0 -1
  1058. package/dist/oauth-user-lookup-vue9a64A.mjs +0 -26
  1059. package/dist/oauth-user-lookup-vue9a64A.mjs.map +0 -1
  1060. package/dist/options-SQCHQCPH.d.mts +0 -81
  1061. package/dist/options-SQCHQCPH.d.mts.map +0 -1
  1062. package/dist/parse-CVgO-Qai.d.mts +0 -168
  1063. package/dist/parse-CVgO-Qai.d.mts.map +0 -1
  1064. package/dist/parse-DY6OD1j_.mjs +0 -81
  1065. package/dist/parse-DY6OD1j_.mjs.map +0 -1
  1066. package/dist/passkey-config-X_H8128a.mjs +0 -46
  1067. package/dist/passkey-config-X_H8128a.mjs.map +0 -1
  1068. package/dist/paths-JxokKIbr.mjs +0 -77
  1069. package/dist/paths-JxokKIbr.mjs.map +0 -1
  1070. package/dist/patterns-BhAvqO0S.mjs +0 -145
  1071. package/dist/patterns-BhAvqO0S.mjs.map +0 -1
  1072. package/dist/placeholder-MSEd8gty.d.mts +0 -306
  1073. package/dist/placeholder-MSEd8gty.d.mts.map +0 -1
  1074. package/dist/plan-xjcWrAah.mjs +0 -135
  1075. package/dist/plan-xjcWrAah.mjs.map +0 -1
  1076. package/dist/plugins-DUXUt8B9.mjs +0 -277
  1077. package/dist/plugins-DUXUt8B9.mjs.map +0 -1
  1078. package/dist/policy-CUvwAARc.mjs +0 -45
  1079. package/dist/policy-CUvwAARc.mjs.map +0 -1
  1080. package/dist/portable-text-5O_ZGUjB.mjs +0 -307
  1081. package/dist/portable-text-5O_ZGUjB.mjs.map +0 -1
  1082. package/dist/preview-CIUcPYr6.mjs +0 -107
  1083. package/dist/preview-CIUcPYr6.mjs.map +0 -1
  1084. package/dist/protocol-B97DZaFr.d.mts +0 -61
  1085. package/dist/protocol-B97DZaFr.d.mts.map +0 -1
  1086. package/dist/public-submission-BFBUrqst.mjs +0 -250
  1087. package/dist/public-submission-BFBUrqst.mjs.map +0 -1
  1088. package/dist/public-url-Bn7U0D7-.mjs +0 -101
  1089. package/dist/public-url-Bn7U0D7-.mjs.map +0 -1
  1090. package/dist/query-B48peoRT.mjs +0 -890
  1091. package/dist/query-B48peoRT.mjs.map +0 -1
  1092. package/dist/query-C4IH0uhR.mjs +0 -41
  1093. package/dist/query-C4IH0uhR.mjs.map +0 -1
  1094. package/dist/rate-limit-BZ6nybgc.mjs +0 -120
  1095. package/dist/rate-limit-BZ6nybgc.mjs.map +0 -1
  1096. package/dist/receipt-B4wJj5Jq.mjs +0 -43
  1097. package/dist/receipt-B4wJj5Jq.mjs.map +0 -1
  1098. package/dist/redirect-Cd1E7VUt.mjs +0 -17
  1099. package/dist/redirect-Cd1E7VUt.mjs.map +0 -1
  1100. package/dist/redirect-iJf5fltD.mjs +0 -415
  1101. package/dist/redirect-iJf5fltD.mjs.map +0 -1
  1102. package/dist/redirects-CYu1u1aK.mjs +0 -446
  1103. package/dist/redirects-CYu1u1aK.mjs.map +0 -1
  1104. package/dist/redirects-Cv9apQKj.mjs +0 -148
  1105. package/dist/redirects-Cv9apQKj.mjs.map +0 -1
  1106. package/dist/relation-M4NUCb0b.mjs +0 -334
  1107. package/dist/relation-M4NUCb0b.mjs.map +0 -1
  1108. package/dist/relations-CCif47Oq.mjs +0 -403
  1109. package/dist/relations-CCif47Oq.mjs.map +0 -1
  1110. package/dist/relations-CK9CYUyr.mjs +0 -310
  1111. package/dist/relations-CK9CYUyr.mjs.map +0 -1
  1112. package/dist/request-cache-DzLYKm_x.mjs +0 -104
  1113. package/dist/request-cache-DzLYKm_x.mjs.map +0 -1
  1114. package/dist/request-meta-sZXt3sRo.mjs +0 -141
  1115. package/dist/request-meta-sZXt3sRo.mjs.map +0 -1
  1116. package/dist/resolve-2WbQsCvT.mjs +0 -199
  1117. package/dist/resolve-2WbQsCvT.mjs.map +0 -1
  1118. package/dist/route-wire--1hLuACz.mjs +0 -197
  1119. package/dist/route-wire--1hLuACz.mjs.map +0 -1
  1120. package/dist/routes-CUsM40Cc.mjs +0 -272
  1121. package/dist/routes-CUsM40Cc.mjs.map +0 -1
  1122. package/dist/runner-1y4J9fJB.mjs +0 -5742
  1123. package/dist/runner-1y4J9fJB.mjs.map +0 -1
  1124. package/dist/runner-D5_LzKXI.d.mts +0 -68
  1125. package/dist/runner-D5_LzKXI.d.mts.map +0 -1
  1126. package/dist/schema-BABiRdsH.mjs +0 -13
  1127. package/dist/schema-BABiRdsH.mjs.map +0 -1
  1128. package/dist/schema-Du3MzCG0.mjs +0 -446
  1129. package/dist/schema-Du3MzCG0.mjs.map +0 -1
  1130. package/dist/scopes-Dbk-ElhT.mjs +0 -34
  1131. package/dist/scopes-Dbk-ElhT.mjs.map +0 -1
  1132. package/dist/search-Bt1PL1jf.mjs +0 -448
  1133. package/dist/search-Bt1PL1jf.mjs.map +0 -1
  1134. package/dist/sections-Ddptx_o9.mjs +0 -346
  1135. package/dist/sections-Ddptx_o9.mjs.map +0 -1
  1136. package/dist/seo-CZT2oLtT.mjs +0 -114
  1137. package/dist/seo-CZT2oLtT.mjs.map +0 -1
  1138. package/dist/seo-D5w3-Azy.mjs +0 -133
  1139. package/dist/seo-D5w3-Azy.mjs.map +0 -1
  1140. package/dist/service-DuykXPyW.mjs +0 -150
  1141. package/dist/service-DuykXPyW.mjs.map +0 -1
  1142. package/dist/session-user-BhaVp8w0.mjs +0 -51
  1143. package/dist/session-user-BhaVp8w0.mjs.map +0 -1
  1144. package/dist/settings-2xNYr90o.mjs +0 -51
  1145. package/dist/settings-2xNYr90o.mjs.map +0 -1
  1146. package/dist/settings-CgjRWGW9.mjs +0 -439
  1147. package/dist/settings-CgjRWGW9.mjs.map +0 -1
  1148. package/dist/settings-DYidIB26.mjs +0 -61
  1149. package/dist/settings-DYidIB26.mjs.map +0 -1
  1150. package/dist/setup-complete-bOogcHWG.mjs +0 -26
  1151. package/dist/setup-complete-bOogcHWG.mjs.map +0 -1
  1152. package/dist/setup-nonce-0EGuKM4s.mjs +0 -25
  1153. package/dist/setup-nonce-0EGuKM4s.mjs.map +0 -1
  1154. package/dist/site-settings-DOgRUoFX.mjs +0 -50
  1155. package/dist/site-settings-DOgRUoFX.mjs.map +0 -1
  1156. package/dist/site-url-BU_bbxwj.mjs +0 -16
  1157. package/dist/site-url-BU_bbxwj.mjs.map +0 -1
  1158. package/dist/snapshot-CugwUqKQ.mjs +0 -237
  1159. package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
  1160. package/dist/ssrf-CJUnJymJ.mjs +0 -406
  1161. package/dist/ssrf-CJUnJymJ.mjs.map +0 -1
  1162. package/dist/stage-DgrBz_du.mjs +0 -662
  1163. package/dist/stage-DgrBz_du.mjs.map +0 -1
  1164. package/dist/state-DDiPySSG.mjs +0 -153
  1165. package/dist/state-DDiPySSG.mjs.map +0 -1
  1166. package/dist/status-DZKIG4a9.mjs +0 -30
  1167. package/dist/status-DZKIG4a9.mjs.map +0 -1
  1168. package/dist/storage-query-Bdv7A62k.d.mts +0 -1576
  1169. package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
  1170. package/dist/tar-_1AgTYFu.mjs +0 -449
  1171. package/dist/tar-_1AgTYFu.mjs.map +0 -1
  1172. package/dist/target-_7pn2Gbg.mjs +0 -27
  1173. package/dist/target-_7pn2Gbg.mjs.map +0 -1
  1174. package/dist/taxonomies-C1IwNe8Q.mjs +0 -697
  1175. package/dist/taxonomies-C1IwNe8Q.mjs.map +0 -1
  1176. package/dist/taxonomies-C_XMoRcX.mjs +0 -939
  1177. package/dist/taxonomies-C_XMoRcX.mjs.map +0 -1
  1178. package/dist/taxonomy-C3Fu1YeH.mjs +0 -525
  1179. package/dist/taxonomy-C3Fu1YeH.mjs.map +0 -1
  1180. package/dist/tokens-u78jETkX.mjs +0 -171
  1181. package/dist/tokens-u78jETkX.mjs.map +0 -1
  1182. package/dist/transaction-BcJO4Tmc.mjs +0 -29
  1183. package/dist/transaction-BcJO4Tmc.mjs.map +0 -1
  1184. package/dist/transfer-BhUCrgDc.mjs +0 -8675
  1185. package/dist/transfer-BhUCrgDc.mjs.map +0 -1
  1186. package/dist/transfer-DIGvR9Af.mjs +0 -166
  1187. package/dist/transfer-DIGvR9Af.mjs.map +0 -1
  1188. package/dist/transfer-download-Pml7RMio.mjs +0 -33
  1189. package/dist/transfer-download-Pml7RMio.mjs.map +0 -1
  1190. package/dist/transformations-9gfpwY65.mjs +0 -196
  1191. package/dist/transformations-9gfpwY65.mjs.map +0 -1
  1192. package/dist/transport-BIfH7_hx.d.mts +0 -42
  1193. package/dist/transport-BIfH7_hx.d.mts.map +0 -1
  1194. package/dist/transport-DKWIq53q.mjs +0 -136
  1195. package/dist/transport-DKWIq53q.mjs.map +0 -1
  1196. package/dist/trusted-proxy-tKiVtyTD.mjs +0 -51
  1197. package/dist/trusted-proxy-tKiVtyTD.mjs.map +0 -1
  1198. package/dist/turnstile-DpsoyOfg.mjs +0 -53
  1199. package/dist/turnstile-DpsoyOfg.mjs.map +0 -1
  1200. package/dist/types-B4sUC2p1.d.mts +0 -198
  1201. package/dist/types-B4sUC2p1.d.mts.map +0 -1
  1202. package/dist/types-B7Lmsh1E.d.mts +0 -887
  1203. package/dist/types-B7Lmsh1E.d.mts.map +0 -1
  1204. package/dist/types-BZakh7Ym.mjs +0 -22
  1205. package/dist/types-BZakh7Ym.mjs.map +0 -1
  1206. package/dist/types-B_CppEmv.d.mts +0 -264
  1207. package/dist/types-B_CppEmv.d.mts.map +0 -1
  1208. package/dist/types-BcrBrE6-.mjs +0 -159
  1209. package/dist/types-BcrBrE6-.mjs.map +0 -1
  1210. package/dist/types-BkUg2yHx.mjs +0 -92
  1211. package/dist/types-BkUg2yHx.mjs.map +0 -1
  1212. package/dist/types-CAZgq9fO.d.mts +0 -124
  1213. package/dist/types-CAZgq9fO.d.mts.map +0 -1
  1214. package/dist/types-Cd-O03VG.d.mts +0 -363
  1215. package/dist/types-Cd-O03VG.d.mts.map +0 -1
  1216. package/dist/types-Cr-Kg-3D.d.mts +0 -1641
  1217. package/dist/types-Cr-Kg-3D.d.mts.map +0 -1
  1218. package/dist/types-D2fvWd1i.d.mts +0 -359
  1219. package/dist/types-D2fvWd1i.d.mts.map +0 -1
  1220. package/dist/types-D7HUsguf.mjs +0 -77
  1221. package/dist/types-D7HUsguf.mjs.map +0 -1
  1222. package/dist/types-DA1Te3Oy.d.mts +0 -198
  1223. package/dist/types-DA1Te3Oy.d.mts.map +0 -1
  1224. package/dist/types-DhV6Q5bP.mjs +0 -18
  1225. package/dist/types-DhV6Q5bP.mjs.map +0 -1
  1226. package/dist/types-DnnVSl6Y.d.mts +0 -85
  1227. package/dist/types-DnnVSl6Y.d.mts.map +0 -1
  1228. package/dist/types-VT4-BugW.mjs +0 -16
  1229. package/dist/types-VT4-BugW.mjs.map +0 -1
  1230. package/dist/upload-attempts-B68ZGNu9.mjs +0 -32
  1231. package/dist/upload-attempts-B68ZGNu9.mjs.map +0 -1
  1232. package/dist/url-pattern-CCeZMGOh.mjs +0 -33
  1233. package/dist/url-pattern-CCeZMGOh.mjs.map +0 -1
  1234. package/dist/user-DDSL-sGw.mjs +0 -171
  1235. package/dist/user-DDSL-sGw.mjs.map +0 -1
  1236. package/dist/utils-DdhmO9w9.mjs +0 -439
  1237. package/dist/utils-DdhmO9w9.mjs.map +0 -1
  1238. package/dist/validate-CHosPEhs.mjs +0 -453
  1239. package/dist/validate-CHosPEhs.mjs.map +0 -1
  1240. package/dist/validate-CM79QXYE.d.mts +0 -432
  1241. package/dist/validate-CM79QXYE.d.mts.map +0 -1
  1242. package/dist/validation-BFecQczG.mjs +0 -218
  1243. package/dist/validation-BFecQczG.mjs.map +0 -1
  1244. package/dist/version-BRZXVifl.mjs +0 -9
  1245. package/dist/version-BRZXVifl.mjs.map +0 -1
  1246. package/dist/version-DUQDHgrf.mjs +0 -7
  1247. package/dist/version-DUQDHgrf.mjs.map +0 -1
  1248. package/dist/widgets-WdV72brA.mjs +0 -126
  1249. package/dist/widgets-WdV72brA.mjs.map +0 -1
  1250. package/dist/wxr-taxonomies-BHTdqkmZ.d.mts +0 -34
  1251. package/dist/wxr-taxonomies-BHTdqkmZ.d.mts.map +0 -1
  1252. package/dist/wxr-taxonomies-DP6ESgMN.mjs +0 -372
  1253. package/dist/wxr-taxonomies-DP6ESgMN.mjs.map +0 -1
  1254. package/dist/zod-generator-D6uE4WiS.mjs +0 -389
  1255. package/dist/zod-generator-D6uE4WiS.mjs.map +0 -1
  1256. package/src/astro/routes/api/relations/[id]/translations.ts +0 -33
  1257. /package/dist/{api-tokens-CE1ECIg0.mjs → api-tokens-FaG1zPsj.mjs} +0 -0
  1258. /package/dist/{ssrf-DpBH7aOt.mjs → ssrf-DZOt8Qof.mjs} +0 -0
@@ -0,0 +1,4214 @@
1
+ import { r as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "./runner-B3eyB9Gb.mjs";
2
+ import { n as isMissingTableError } from "./db-errors-CBD5yMro.mjs";
3
+ import { d as isSqlite, m as listTablesLike } from "./dialect-helpers-7_IqPdh9.mjs";
4
+ import { r as getI18nConfig, s as resolveContentCreateLocale } from "./config-CvmjJcVw.mjs";
5
+ import { S as definePlugin, _ as isContentSaveRejection, b as getActiveContentSaveHookName, c as inspectSandboxHookResult, g as ContentSaveRejectedError, h as EmailPipeline, x as resolveExclusiveHooks, y as createHookPipeline } from "./menus-BQfusWEC.mjs";
6
+ import { t as after } from "./after-PcEvMY_6.mjs";
7
+ import { getRequestContext } from "./request-context.mjs";
8
+ import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
9
+ import { n as decodeBase64Bytes } from "./base64-DvnNgP7e.mjs";
10
+ import { n as initWithLock, t as createInitLock } from "./init-lock-4eRRB4Le.mjs";
11
+ import { r as singleFlightCached, t as createSingleFlightCache } from "./single-flight-cache-B5O2qhO1.mjs";
12
+ import { o as validateEncryptionKeyAtStartup } from "./secrets-CIi0nFiR.mjs";
13
+ import { a as staleStoredKeys, i as keepKnownFields, t as ContentRepository } from "./content-CbnufDsM.mjs";
14
+ import { f as isStoragelessField, p as isStoragelessFieldRow } from "./types-CJKsWVFR.mjs";
15
+ import { n as ContentMutationConflictError } from "./types-aOOxRSx6.mjs";
16
+ import { t as RevisionRepository } from "./revision-j1p4-bOj.mjs";
17
+ import { t as MediaRepository } from "./media-BUQePMxP.mjs";
18
+ import { t as CommentRepository } from "./comment-D2uvL63j.mjs";
19
+ import { t as AuditRepository } from "./audit-CrgmE9LE.mjs";
20
+ import { d as createTaxonomyAccessWithWrite, l as createSiteInfo, r as createCommentAccess, y as updatePluginMediaMetadata } from "./context-CGQLA1-a.mjs";
21
+ import { t as RedirectRepository } from "./redirect-D1ArbO85.mjs";
22
+ import { r as requestCached } from "./request-cache-iHRlHx1d.mjs";
23
+ import { i as mergeStagedReferences, r as STAGED_REFERENCES_KEY } from "./staged-C36IWXff.mjs";
24
+ import { n as normalizeMediaValue } from "./normalize-DduyxBjf.mjs";
25
+ import { n as normalizeMime, t as matchesMimeAllowlist } from "./mime-C94qBG-P.mjs";
26
+ import { t as enrichImageMetadata } from "./enrich-DBtCncE2.mjs";
27
+ import { i as scheduledPolicyRejectionKey } from "./content-policy-CYu6K1KB.mjs";
28
+ import { t as FTSManager } from "./fts-manager-CCaZTp8y.mjs";
29
+ import { h as selectTaxonomyDefs, s as getDb } from "./loader-CrRPfkHS.mjs";
30
+ import { kyselyLogOption } from "./database/instrumentation.mjs";
31
+ import { r as primeRegisteredCollections } from "./collection-slugs-cache-CUuiduLq.mjs";
32
+ import { A as handleContentDelete, B as handleContentSchedule, D as handleContentCountScheduled, E as handleContentCompare, F as handleContentList, G as handleScheduledPolicyRejection, H as handleContentUnpublish, I as handleContentListTrashed, K as storagelessDataKeyError, L as handleContentPermanentDelete, M as handleContentDuplicate, N as handleContentGet, O as handleContentCountTrashed, P as handleContentGetIncludingTrashed, R as handleContentPublish, S as buildManifestCollections, T as handleContentAuthors, U as handleContentUnschedule, V as handleContentTranslations, W as handleContentUpdate, b as handleRevisionList, h as syncDeclaredStorageIndexes, j as handleContentDiscardDraft, k as handleContentCreate, p as loadBundleFromR2, q as validateRev, w as changedStoragelessDataKeys, x as handleRevisionRestore, y as handleRevisionGet, z as handleContentRestore } from "./api-CXwQkV_m.mjs";
33
+ import { r as activateMediaUsageCapture } from "./activation-PtniBLt9.mjs";
34
+ import { D as refreshContentMediaUsageAfterWrite, E as findNonTranslatableSiblingContentIds, H as markContentMediaUsageCollectionStale, T as deleteContentMediaUsage, W as MediaUsageRepository, n as SchemaRegistry, t as SchemaError } from "./registry-CFcNCayZ.mjs";
35
+ import { n as computeContentHash, r as hashString } from "./hash-COVK27Pc.mjs";
36
+ import { i as resolveBlockTypes, r as normalizeBlocksData } from "./block-values-ACFqgB1D.mjs";
37
+ import { f as resolveReferenceSelection } from "./relations-Cfaa1fDl.mjs";
38
+ import { n as recordSchedulerHeartbeatSafely } from "./dashboard-CBq9kmOn.mjs";
39
+ import { t as removeUploadAttempt } from "./upload-attempts-DduMy6B0.mjs";
40
+ import { a as handleMediaRegisterUpload, i as handleMediaList, n as handleMediaDelete, o as handleMediaReplaceMetadata, r as handleMediaGet, s as handleMediaUpdate, t as handleMediaCreate } from "./media-DH8ibNlY.mjs";
41
+ import { s as processMediaUsageWorkAfterWrite } from "./media-usage-D7Y64m6v.mjs";
42
+ import { t as ErrorCode } from "./errors-BN1QAnEY.mjs";
43
+ import { t as PluginStateRepository } from "./state-CKouMCSY.mjs";
44
+ import { s as invalidateSiteSettingsCache } from "./settings-D5EHi0Tk.mjs";
45
+ import { i as normalizeManifestRoute$1, r as normalizeManifestHook, t as HOOK_NAMES } from "./manifest-schema-Ci2gmd0u.mjs";
46
+ import { i as getSandboxRouteErrorDetails } from "./types-4Dn9bZhu.mjs";
47
+ import { s as normalizePluginCapabilities } from "./types-OC5c-edM.mjs";
48
+ import { c as resolveRegistryConfigForSandbox, r as getRegistryConfigInput, s as resolveManifestRegistryConfig } from "./config-CCz3xOLc.mjs";
49
+ import { n as DEFAULT_MAX_UPLOAD_SIZE, r as formatFileSize, t as CONTENT_TYPE_RE } from "./media-OEIQ3bVc.mjs";
50
+ import { t as getTrustedProxyHeaders } from "./trusted-proxy-B6ECOZJc.mjs";
51
+ import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "./request-meta-D47mqyev.mjs";
52
+ import { adaptSandboxEntry } from "./plugins/adapt-sandbox-entry.mjs";
53
+ import { t as GLOBAL_UPLOAD_ALLOWLIST } from "./media-allowlist-pYtaBA95.mjs";
54
+ import { n as CronExecutor } from "./cron-c4D5C5Fx.mjs";
55
+ import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "./email-console-DyQIgEnz.mjs";
56
+ import { n as assertSiteWriteAllowed } from "./fence-Bb_IbCxI.mjs";
57
+ import { t as PluginRouteRequestError } from "./route-wire-C9PJV9aL.mjs";
58
+ import { a as toRouteCallerInfo, n as buildRouteMeta, r as parseRouteInput, t as PluginRouteRegistry } from "./routes-lyYw0mMf.mjs";
59
+ import { n as enforceRuntimeMigrationPolicy, t as PendingMigrationsError } from "./policy-BcEqut3n.mjs";
60
+ import { n as VERSION, t as COMMIT } from "./version-Dq71scUb.mjs";
61
+ import { t as getSiteBaseUrl } from "./site-url-D6JB7tLW.mjs";
62
+ import { t as getAuthMode } from "./mode-DpYW3w6U.mjs";
63
+ import { n as lookupContentAuthor, r as sendCommentNotification, t as submitPublicComment } from "./public-submission-D9c9S14m.mjs";
64
+ import { u as maybeRunScheduledBackup } from "./backup-Ck8_lMO3.mjs";
65
+ import { t as cleanupExpiredChallenges } from "./challenge-store-CvBl81AQ.mjs";
66
+ import { n as createComment, r as moderateComment, t as CommentStatusConflictError } from "./service-BsVG05gS.mjs";
67
+ import { isRecord } from "./plugin-utils.mjs";
68
+ import { n as enableRuntimePlugin, t as disableRuntimePlugin } from "./lifecycle-C815ZQfV.mjs";
69
+ import { Kysely, sql } from "kysely";
70
+ import { z } from "zod";
71
+ import { ulid } from "ulidx";
72
+ import { Permissions } from "@emdash-cms/auth";
73
+ import { isJsonPostRouteContract } from "@emdash-cms/plugin-types";
74
+ import * as path from "node:path";
75
+ import virtualConfig from "virtual:emdash/config";
76
+ import { getLocaleDir, resolveLocale } from "@emdash-cms/admin/locales";
77
+ import { normalizePluginPagePath, validateBlockResponse, validateContentEditorActionResponse } from "@emdash-cms/blocks/server";
78
+ import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
79
+
80
+ //#region src/api/handlers/media-upload.ts
81
+ /**
82
+ * Programmatic media upload handler (MCP `media_upload` tool).
83
+ *
84
+ * Accepts file bytes as base64, then runs the same pipeline as the multipart
85
+ * REST upload route: allowlist + size validation, content-hash deduplication,
86
+ * storage upload, image metadata enrichment, and record creation.
87
+ */
88
+ function fail(code, message) {
89
+ return {
90
+ success: false,
91
+ error: {
92
+ code,
93
+ message
94
+ }
95
+ };
96
+ }
97
+ /** Same relative-URL shape the REST media routes return. */
98
+ function withUrl(item) {
99
+ return {
100
+ ...item,
101
+ url: `/_emdash/api/media/file/${item.storageKey}`
102
+ };
103
+ }
104
+ /** Decode the file bytes after rejecting an oversized encoded payload. */
105
+ async function acquireBytes(input, maxUploadSize) {
106
+ if (input.base64.length * 3 / 4 > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
107
+ try {
108
+ return {
109
+ bytes: decodeBase64Bytes(input.base64),
110
+ mimeType: input.contentType
111
+ };
112
+ } catch {
113
+ return fail("VALIDATION_ERROR", "Invalid base64 data");
114
+ }
115
+ }
116
+ /**
117
+ * Upload a media file from base64 data.
118
+ *
119
+ * Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,
120
+ * size limit, content-hash dedupe (returns the existing item with
121
+ * `deduplicated: true`), storage upload with cleanup on failure, and
122
+ * image metadata enrichment (dimensions, blurhash, dominant color).
123
+ */
124
+ async function handleMediaUpload(db, storage, input, hooks = {}) {
125
+ if (!input.base64 || !input.contentType) return fail("VALIDATION_ERROR", "base64 and contentType are required");
126
+ const rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
127
+ if (!Number.isFinite(rawMax) || rawMax <= 0) return fail("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration");
128
+ const acquired = await acquireBytes(input, rawMax);
129
+ if ("success" in acquired) return acquired;
130
+ const { bytes } = acquired;
131
+ if (!CONTENT_TYPE_RE.test(acquired.mimeType)) return fail("VALIDATION_ERROR", "Invalid content type");
132
+ let filename = input.filename;
133
+ let mimeType = normalizeMime(acquired.mimeType);
134
+ let size = bytes.byteLength;
135
+ if (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) return fail("INVALID_TYPE", "File type not allowed");
136
+ if (bytes.byteLength > rawMax) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(rawMax)}`);
137
+ try {
138
+ if (hooks.beforeUpload) {
139
+ const processed = await hooks.beforeUpload({
140
+ name: filename,
141
+ type: mimeType,
142
+ size: bytes.byteLength
143
+ });
144
+ filename = processed.name;
145
+ mimeType = normalizeMime(processed.type);
146
+ size = processed.size;
147
+ if (!CONTENT_TYPE_RE.test(processed.type)) return fail("VALIDATION_ERROR", "Invalid content type");
148
+ if (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) return fail("INVALID_TYPE", "File type not allowed");
149
+ }
150
+ const contentHash = await computeContentHash(bytes);
151
+ const repo = new MediaRepository(db);
152
+ const existing = await repo.findByContentHash(contentHash);
153
+ if (existing) return {
154
+ success: true,
155
+ data: {
156
+ item: withUrl(existing),
157
+ deduplicated: true
158
+ }
159
+ };
160
+ const storageKey = `${ulid()}${path.extname(filename)}`;
161
+ await storage.upload({
162
+ key: storageKey,
163
+ body: bytes,
164
+ contentType: mimeType
165
+ });
166
+ try {
167
+ const enriched = await enrichImageMetadata(bytes, mimeType);
168
+ return {
169
+ success: true,
170
+ data: { item: withUrl(await repo.create({
171
+ filename,
172
+ mimeType,
173
+ size,
174
+ width: enriched.width,
175
+ height: enriched.height,
176
+ alt: input.alt,
177
+ storageKey,
178
+ contentHash,
179
+ blurhash: enriched.blurhash,
180
+ dominantColor: enriched.dominantColor,
181
+ authorId: input.authorId
182
+ })) }
183
+ };
184
+ } catch (error) {
185
+ try {
186
+ await storage.delete(storageKey);
187
+ } catch {}
188
+ throw error;
189
+ }
190
+ } catch {
191
+ return fail("UPLOAD_ERROR", "Upload failed");
192
+ }
193
+ }
194
+
195
+ //#endregion
196
+ //#region src/i18n/repair-locale-casing.ts
197
+ /** Rewrite stored locales to the exact casing used by the site configuration. */
198
+ async function repairLocaleCasing(db, configuredLocales) {
199
+ const tableNames = await listTablesLike(db, "ec_%");
200
+ for (const tableName of tableNames) {
201
+ const table = sql.ref(tableName);
202
+ for (const locale of configuredLocales) await sql`
203
+ UPDATE ${table} AS target
204
+ SET locale = ${locale}
205
+ WHERE lower(target.locale) = lower(${locale})
206
+ AND target.locale != ${locale}
207
+ AND NOT EXISTS (
208
+ SELECT 1
209
+ FROM ${table} AS existing
210
+ WHERE existing.slug = target.slug AND existing.locale = ${locale}
211
+ )
212
+ AND target.id = (
213
+ SELECT MIN(candidate.id)
214
+ FROM ${table} AS candidate
215
+ WHERE candidate.slug = target.slug
216
+ AND lower(candidate.locale) = lower(${locale})
217
+ AND candidate.locale != ${locale}
218
+ )
219
+ `.execute(db);
220
+ }
221
+ }
222
+
223
+ //#endregion
224
+ //#region src/i18n/taxonomy-locale-diagnostic.ts
225
+ const REPAIR_GUIDE = "https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches";
226
+ async function warnAboutUnconfiguredTaxonomyLocales(db, configuredLocales, definitionLocales) {
227
+ const supportedLocales = configuredLocales.length > 0 ? configuredLocales : ["en"];
228
+ const definitionRows = definitionLocales === void 0 ? await db.selectFrom("_emdash_taxonomy_defs").select("locale").distinct().where("locale", "not in", supportedLocales).execute() : [...new Set(definitionLocales)].filter((locale) => !supportedLocales.includes(locale)).map((locale) => ({ locale }));
229
+ const termRows = await db.selectFrom("taxonomies").select("locale").distinct().where("locale", "not in", supportedLocales).execute();
230
+ const mismatches = [...definitionRows.map(({ locale }) => ({
231
+ source: "definitions",
232
+ locale
233
+ })), ...termRows.map(({ locale }) => ({
234
+ source: "terms",
235
+ locale
236
+ }))].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));
237
+ if (mismatches.length === 0) return;
238
+ const details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join("; ");
239
+ console.warn(`EmDash: Taxonomy rows use locales outside the configured locales (${supportedLocales.join(", ")}): ${details}. Locale-scoped reads may not return these rows. Review and repair them explicitly: ${REPAIR_GUIDE}`);
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/plugins/sandbox/proxy.ts
244
+ const SANDBOX_REJECTION = Symbol("emdash:sandbox-save-rejection");
245
+ const VALID_METADATA_KINDS = new Set([
246
+ "meta",
247
+ "property",
248
+ "link",
249
+ "jsonld"
250
+ ]);
251
+ const VALID_LINK_REL = new Set([
252
+ "canonical",
253
+ "alternate",
254
+ "author",
255
+ "license",
256
+ "nlweb",
257
+ "site.standard.document"
258
+ ]);
259
+ function isValidMetadataContribution(value) {
260
+ if (!value || typeof value !== "object" || !("kind" in value)) return false;
261
+ const contribution = value;
262
+ if (typeof contribution.kind !== "string" || !VALID_METADATA_KINDS.has(contribution.kind)) return false;
263
+ if (contribution.kind === "meta") return typeof contribution.name === "string" && typeof contribution.content === "string";
264
+ if (contribution.kind === "property") return typeof contribution.property === "string" && typeof contribution.content === "string";
265
+ if (contribution.kind === "link") return typeof contribution.href === "string" && typeof contribution.rel === "string" && VALID_LINK_REL.has(contribution.rel);
266
+ return contribution.graph != null && typeof contribution.graph === "object";
267
+ }
268
+ function getSandboxSaveRejectionDetails(error) {
269
+ if (!(error instanceof ContentSaveRejectedError) || !(SANDBOX_REJECTION in error)) return null;
270
+ const details = Object.getOwnPropertyDescriptor(error, SANDBOX_REJECTION)?.value;
271
+ if (!details || typeof details !== "object") return null;
272
+ if (!("pluginId" in details) || typeof details.pluginId !== "string") return null;
273
+ if (!("reason" in details) || typeof details.reason !== "string") return null;
274
+ return {
275
+ pluginId: details.pluginId,
276
+ reason: details.reason
277
+ };
278
+ }
279
+ /**
280
+ * Represent an isolate-backed plugin inside the normal host hook pipeline.
281
+ * The proxy contains metadata and RPC handlers only; plugin code remains in
282
+ * the runner isolate.
283
+ */
284
+ function createSandboxedPluginProxy(manifest, instance) {
285
+ const hooks = {};
286
+ for (const entry of manifest.hooks) {
287
+ const hook = normalizeManifestHook(entry);
288
+ if (hook.name === "page:fragments") continue;
289
+ hooks[hook.name] = {
290
+ handler: async (event) => {
291
+ const result = await instance.invokeHook(hook.name, event);
292
+ if (hook.name === "page:metadata") {
293
+ if (result == null) return null;
294
+ const contributions = (Array.isArray(result) ? result : [result]).filter(isValidMetadataContribution);
295
+ return contributions.length > 0 ? contributions : null;
296
+ }
297
+ if (hook.name !== "content:beforeSave") return result;
298
+ const inspection = inspectSandboxHookResult(result);
299
+ if (inspection.kind === "error") {
300
+ const error = new ContentSaveRejectedError("Save rejected by a sandboxed plugin");
301
+ return Promise.reject(Object.assign(error, { [SANDBOX_REJECTION]: {
302
+ pluginId: manifest.id,
303
+ reason: inspection.error.reason
304
+ } }));
305
+ }
306
+ if (inspection.kind === "malformed") throw new Error(`Sandboxed plugin "${manifest.id}" returned an invalid hook result`);
307
+ return result;
308
+ },
309
+ priority: hook.priority,
310
+ timeout: hook.timeout,
311
+ dependencies: hook.dependencies,
312
+ errorPolicy: hook.errorPolicy,
313
+ exclusive: hook.exclusive
314
+ };
315
+ }
316
+ return adaptSandboxEntry({ hooks }, {
317
+ id: manifest.id,
318
+ version: manifest.version,
319
+ entrypoint: "",
320
+ format: "standard",
321
+ capabilities: manifest.capabilities,
322
+ allowedHosts: manifest.allowedHosts,
323
+ storage: manifest.storage,
324
+ adminPages: manifest.admin.pages,
325
+ adminWidgets: manifest.admin.widgets,
326
+ editorPanels: manifest.admin.editorPanels,
327
+ editorActions: manifest.admin.editorActions,
328
+ settingsSchema: manifest.admin.settingsSchema,
329
+ portableTextBlocks: manifest.admin.portableTextBlocks,
330
+ fieldWidgets: manifest.admin.fieldWidgets
331
+ });
332
+ }
333
+
334
+ //#endregion
335
+ //#region src/plugins/sandbox/runner-options.ts
336
+ /**
337
+ * Build platform sandbox options with the same normalized site context used
338
+ * by trusted plugin hooks and routes.
339
+ */
340
+ function createSandboxRunnerOptions(options, siteInfo) {
341
+ return {
342
+ ...options,
343
+ siteInfo: createSiteInfo(siteInfo ?? {})
344
+ };
345
+ }
346
+
347
+ //#endregion
348
+ //#region src/media/usage/cleanup.ts
349
+ const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;
350
+ const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;
351
+ const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;
352
+ const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1e3;
353
+ const MEDIA_USAGE_CLEANUP_LEASE_MS = 300 * 1e3;
354
+ const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 3600 * 1e3;
355
+ const MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1e3;
356
+ /**
357
+ * Reclaims a bounded window of obsolete media-usage occurrences.
358
+ *
359
+ * The persisted claim makes a cron tick single-flight across Worker isolates
360
+ * and Node processes.
361
+ */
362
+ async function cleanupMediaUsage(db) {
363
+ const startedMs = Date.now();
364
+ const canIssueStatement = () => withinBudget(startedMs);
365
+ const repo = new MediaUsageRepository(db);
366
+ const leaseToken = ulid();
367
+ const claim = await repo.claimMediaUsageCleanup({
368
+ leaseToken,
369
+ leaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1e3,
370
+ nextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1e3,
371
+ sweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1e3
372
+ });
373
+ if (!claim) return emptyResult("skipped", elapsedSince(startedMs));
374
+ let candidateRows = 0;
375
+ let deletedOrphans = 0;
376
+ let deletedStale = 0;
377
+ let deletedAbandoned = 0;
378
+ let deletedWriteLeases = 0;
379
+ let backlogLowerBound = 0;
380
+ let scanHasMore = false;
381
+ let nextCursor = claim.cursor;
382
+ let sweepComplete = false;
383
+ try {
384
+ if (canIssueStatement()) deletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT, cleanupLease(leaseToken), canIssueStatement);
385
+ const cutoff = claim.scanBeforeAt;
386
+ const candidates = await repo.findMediaUsageCleanupCandidates({
387
+ cutoff,
388
+ cursor: claim.cursor,
389
+ limit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,
390
+ cleanupLease: cleanupLease(leaseToken),
391
+ canIssueStatement
392
+ });
393
+ if (candidates) {
394
+ candidateRows = candidates.length;
395
+ scanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;
396
+ const selected = selectCleanupCandidates(candidates, claim.claimedAt);
397
+ backlogLowerBound = selected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;
398
+ const completedTargets = /* @__PURE__ */ new Set();
399
+ let canContinue = true;
400
+ if (canIssueStatement() && selected.orphanIds.length > 0) {
401
+ deletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(cutoff, selected.orphanIds.length, {
402
+ candidateIds: selected.orphanIds,
403
+ cleanupLease: cleanupLease(leaseToken),
404
+ canIssueStatement
405
+ });
406
+ canContinue = deletedOrphans === selected.orphanIds.length;
407
+ if (canContinue) completedTargets.add("orphan");
408
+ }
409
+ if (canContinue && canIssueStatement() && selected.staleIds.length > 0) {
410
+ deletedStale = await repo.deleteStaleGenerationsOlderThan(cutoff, selected.staleIds.length, {
411
+ candidateIds: selected.staleIds,
412
+ cleanupLease: cleanupLease(leaseToken),
413
+ canIssueStatement
414
+ });
415
+ canContinue = deletedStale === selected.staleIds.length;
416
+ if (canContinue) completedTargets.add("stale");
417
+ }
418
+ if (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {
419
+ deletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(cutoff, selected.abandonedIds.length, {
420
+ candidateIds: selected.abandonedIds,
421
+ cleanupLease: cleanupLease(leaseToken),
422
+ canIssueStatement
423
+ });
424
+ if (deletedAbandoned === selected.abandonedIds.length) completedTargets.add("abandoned");
425
+ }
426
+ const hasIncompleteTargets = selected.entries.some((entry) => entry.target !== null && !completedTargets.has(entry.target));
427
+ sweepComplete = !scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;
428
+ nextCursor = sweepComplete ? null : cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);
429
+ }
430
+ const durationMs = elapsedSince(startedMs);
431
+ return {
432
+ status: await repo.completeMediaUsageCleanup({
433
+ leaseToken,
434
+ nextCursor,
435
+ sweepComplete,
436
+ candidateCount: candidateRows,
437
+ deletedOrphans,
438
+ deletedStale,
439
+ deletedAbandoned,
440
+ deletedWriteLeases,
441
+ backlogLowerBound,
442
+ scanHasMore,
443
+ durationMs
444
+ }) ? "completed" : "skipped",
445
+ candidateRows,
446
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
447
+ deletedOrphans,
448
+ deletedStale,
449
+ deletedAbandoned,
450
+ deletedWriteLeases,
451
+ backlogLowerBound,
452
+ scanHasMore,
453
+ durationMs
454
+ };
455
+ } catch (error) {
456
+ const durationMs = elapsedSince(startedMs);
457
+ const failures = Math.min(claim.consecutiveFailures + 1, 5);
458
+ try {
459
+ await repo.failMediaUsageCleanup({
460
+ leaseToken,
461
+ retryDelaySeconds: failureDelayMs(failures) / 1e3,
462
+ consecutiveFailures: failures,
463
+ durationMs,
464
+ errorCode: "MEDIA_USAGE_CLEANUP_FAILED"
465
+ });
466
+ } catch (failureError) {
467
+ console.error("[media-usage-cleanup] Failed to record cleanup failure:", failureError);
468
+ }
469
+ console.error("[media-usage-cleanup] Cleanup failed:", error);
470
+ return {
471
+ ...emptyResult("failed", durationMs),
472
+ candidateRows,
473
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
474
+ deletedOrphans,
475
+ deletedStale,
476
+ deletedAbandoned,
477
+ deletedWriteLeases,
478
+ backlogLowerBound,
479
+ scanHasMore
480
+ };
481
+ }
482
+ }
483
+ function selectCleanupCandidates(candidates, activeLeaseAt) {
484
+ const orphanIds = [];
485
+ const staleIds = [];
486
+ const abandonedIds = [];
487
+ const entries = [];
488
+ for (const candidate of candidates) {
489
+ if (hasActiveWriteLease(candidate, activeLeaseAt)) {
490
+ entries.push({
491
+ candidate,
492
+ target: null
493
+ });
494
+ continue;
495
+ }
496
+ const target = cleanupTarget(candidate);
497
+ if (target === null) {
498
+ entries.push({
499
+ candidate,
500
+ target: null
501
+ });
502
+ continue;
503
+ }
504
+ if (orphanIds.length + staleIds.length + abandonedIds.length >= MEDIA_USAGE_CLEANUP_DELETE_LIMIT) break;
505
+ if (target === "orphan") orphanIds.push(candidate.id);
506
+ if (target === "stale") staleIds.push(candidate.id);
507
+ if (target === "abandoned") abandonedIds.push(candidate.id);
508
+ entries.push({
509
+ candidate,
510
+ target
511
+ });
512
+ }
513
+ return {
514
+ orphanIds,
515
+ staleIds,
516
+ abandonedIds,
517
+ entries
518
+ };
519
+ }
520
+ function cleanupTarget(candidate) {
521
+ if (candidate.currentGeneration === null) return "orphan";
522
+ if (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null) return null;
523
+ return candidate.createdAt < candidate.indexedAt ? "stale" : "abandoned";
524
+ }
525
+ function hasActiveWriteLease(candidate, activeLeaseAt) {
526
+ return candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;
527
+ }
528
+ function cursorFor(candidate) {
529
+ return {
530
+ createdAt: candidate.createdAt,
531
+ id: candidate.id
532
+ };
533
+ }
534
+ function cursorAfterCompletedCandidates(selected, completedTargets, priorCursor) {
535
+ let cursor = priorCursor;
536
+ for (const entry of selected.entries) {
537
+ if (entry.target !== null && !completedTargets.has(entry.target)) break;
538
+ cursor = cursorFor(entry.candidate);
539
+ }
540
+ return cursor;
541
+ }
542
+ function emptyResult(status, durationMs) {
543
+ return {
544
+ status,
545
+ candidateRows: 0,
546
+ deletedRows: 0,
547
+ deletedOrphans: 0,
548
+ deletedStale: 0,
549
+ deletedAbandoned: 0,
550
+ deletedWriteLeases: 0,
551
+ backlogLowerBound: 0,
552
+ scanHasMore: false,
553
+ durationMs
554
+ };
555
+ }
556
+ function withinBudget(startedMs) {
557
+ return elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;
558
+ }
559
+ function elapsedSince(startedMs) {
560
+ return Math.max(0, Date.now() - startedMs);
561
+ }
562
+ function failureDelayMs(consecutiveFailures) {
563
+ return Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;
564
+ }
565
+ function cleanupLease(leaseToken) {
566
+ return { leaseToken };
567
+ }
568
+
569
+ //#endregion
570
+ //#region src/cleanup.ts
571
+ /**
572
+ * System cleanup
573
+ *
574
+ * Runs periodic maintenance tasks that prevent unbounded accumulation of
575
+ * expired or stale data. Called from cron scheduler ticks and (for latency-
576
+ * sensitive subsystems) inline during relevant requests.
577
+ *
578
+ * Each subsystem cleanup is independent and non-fatal -- if one fails, the
579
+ * rest still run. Failures are logged but never surface to callers.
580
+ */
581
+ const REVISION_KEEP_COUNT = 50;
582
+ const REVISION_PRUNE_BATCH_SIZE = 10;
583
+ /**
584
+ * Run all system cleanup tasks.
585
+ *
586
+ * Safe to call frequently -- each subsystem tolerates repeated calls, and
587
+ * repeated calls with nothing to clean are cheap.
588
+ *
589
+ * @param db - The database instance
590
+ * @param storage - Optional storage backend for deleting orphaned files.
591
+ * When omitted, pending upload DB rows are still deleted but the
592
+ * corresponding files in object storage are not removed.
593
+ */
594
+ async function runSystemCleanup(db, storage) {
595
+ const result = {
596
+ challenges: -1,
597
+ expiredTokens: -1,
598
+ pendingUploads: -1,
599
+ pendingUploadFiles: -1,
600
+ uploadAttempts: -1,
601
+ revisionsPruned: -1,
602
+ mediaUsage: -1,
603
+ notFoundLog: -1,
604
+ transferStaging: -1
605
+ };
606
+ try {
607
+ result.challenges = await cleanupExpiredChallenges(db);
608
+ } catch (error) {
609
+ console.error("[cleanup] Failed to clean expired challenges:", error);
610
+ }
611
+ try {
612
+ await createKyselyAdapter(db).deleteExpiredTokens();
613
+ result.expiredTokens = 0;
614
+ } catch (error) {
615
+ console.error("[cleanup] Failed to clean expired tokens:", error);
616
+ }
617
+ try {
618
+ const orphanedKeys = await new MediaRepository(db).cleanupPendingUploads();
619
+ result.pendingUploads = orphanedKeys.length;
620
+ if (storage && orphanedKeys.length > 0) {
621
+ let filesDeleted = 0;
622
+ for (const key of orphanedKeys) try {
623
+ await storage.delete(key);
624
+ filesDeleted++;
625
+ } catch (error) {
626
+ console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
627
+ }
628
+ result.pendingUploadFiles = filesDeleted;
629
+ } else result.pendingUploadFiles = 0;
630
+ } catch (error) {
631
+ console.error("[cleanup] Failed to clean pending uploads:", error);
632
+ }
633
+ try {
634
+ const mediaRepo = new MediaRepository(db);
635
+ const completedAttemptsDeleted = await mediaRepo.deleteCompletedUploadAttempts();
636
+ if (!storage) result.uploadAttempts = completedAttemptsDeleted;
637
+ else {
638
+ const storageKeys = await mediaRepo.findUploadAttemptsForCleanup();
639
+ let attemptsDeleted = completedAttemptsDeleted;
640
+ for (const storageKey of storageKeys) if (await removeUploadAttempt(storage, mediaRepo, storageKey)) attemptsDeleted++;
641
+ result.uploadAttempts = attemptsDeleted;
642
+ }
643
+ } catch (error) {
644
+ console.error("[cleanup] Failed to clean media upload attempts:", error);
645
+ }
646
+ try {
647
+ result.revisionsPruned = await pruneQueuedRevisions(db);
648
+ } catch (error) {
649
+ console.error("[cleanup] Failed to prune revisions:", error);
650
+ }
651
+ try {
652
+ const mediaUsage = await cleanupMediaUsage(db);
653
+ result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
654
+ } catch (error) {
655
+ console.error("[cleanup] Failed to clean media usage:", error);
656
+ }
657
+ try {
658
+ result.notFoundLog = await new RedirectRepository(db).cleanup404Log();
659
+ } catch (error) {
660
+ console.error("[cleanup] Failed to cap the 404 log:", error);
661
+ }
662
+ if (storage) try {
663
+ const { collectTransferStaging } = await import("./gc-xowRjczI.mjs");
664
+ result.transferStaging = (await collectTransferStaging(db, storage)).collected;
665
+ } catch (error) {
666
+ console.error("[transfer] Failed to collect transfer staging:", error);
667
+ }
668
+ else result.transferStaging = 0;
669
+ return result;
670
+ }
671
+ async function pruneQueuedRevisions(db) {
672
+ const queued = await db.selectFrom("_emdash_revision_prune_queue").selectAll().orderBy("revision_id").limit(REVISION_PRUNE_BATCH_SIZE).execute();
673
+ const revisionRepo = new RevisionRepository(db);
674
+ let totalPruned = 0;
675
+ for (const row of queued) try {
676
+ totalPruned += await revisionRepo.pruneQueuedEntry(row.collection, row.entry_id, row.revision_id, REVISION_KEEP_COUNT);
677
+ } catch (error) {
678
+ console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
679
+ }
680
+ return totalPruned;
681
+ }
682
+
683
+ //#endregion
684
+ //#region src/comments/moderator.ts
685
+ /** Plugin ID for the built-in default comment moderator */
686
+ const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = "emdash-default-comment-moderator";
687
+ /**
688
+ * The comment:moderate handler for the built-in default moderator.
689
+ */
690
+ async function defaultCommentModerate(event, _ctx) {
691
+ const { comment, collectionSettings, priorApprovedCount } = event;
692
+ if (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) return {
693
+ status: "approved",
694
+ reason: "Authenticated CMS user"
695
+ };
696
+ if (collectionSettings.commentsModeration === "none") return {
697
+ status: "approved",
698
+ reason: "Moderation disabled"
699
+ };
700
+ if (collectionSettings.commentsModeration === "first_time" && priorApprovedCount > 0) return {
701
+ status: "approved",
702
+ reason: "Returning commenter"
703
+ };
704
+ return {
705
+ status: "pending",
706
+ reason: "Held for review"
707
+ };
708
+ }
709
+
710
+ //#endregion
711
+ //#region src/scheduled-publish.ts
712
+ /**
713
+ * Default cap on items promoted per collection in a single sweep. Bounds the
714
+ * publish/webhook fan-out of one tick so a large backlog can't exhaust a Worker
715
+ * invocation's CPU/subrequest budget; the remainder drains on later ticks.
716
+ */
717
+ const SCHEDULED_PUBLISH_BATCH_LIMIT = 100;
718
+ const SCHEDULED_PUBLISH_CURSOR_PREFIX = "emdash:scheduled-publish-cursor:";
719
+ function scheduledPublishCursorKey(collection) {
720
+ return `${SCHEDULED_PUBLISH_CURSOR_PREFIX}${collection}`;
721
+ }
722
+ function isScheduledPublishCursor(value) {
723
+ return typeof value === "object" && value !== null && "scheduledAt" in value && typeof value.scheduledAt === "string" && "id" in value && typeof value.id === "string";
724
+ }
725
+ /**
726
+ * Publish every content item whose `scheduled_at` is in the past.
727
+ *
728
+ * Iterates all collections, finds due items (`findReadyToPublish` returns both
729
+ * scheduled drafts and published entries with pending scheduled changes), and
730
+ * publishes each. `publish()` clears `scheduled_at`, so a second sweep is a
731
+ * no-op — safe to run on every tick.
732
+ *
733
+ * Bounded per collection by `limit` (default `SCHEDULED_PUBLISH_BATCH_LIMIT`):
734
+ * a large backlog drains across successive ticks rather than in one unbounded
735
+ * pass. After each collection's batch, `onPublished` (if given) is awaited so
736
+ * cache-tag invalidation happens incrementally, not just at the very end.
737
+ *
738
+ * Returns every item it promoted so request-less callers (the Cloudflare
739
+ * `scheduled()` handler) can also act on the full set.
740
+ */
741
+ async function publishDueContent(db, options = {}) {
742
+ const { publish, onPublished, limit = SCHEDULED_PUBLISH_BATCH_LIMIT, currentTime = /* @__PURE__ */ new Date() } = options;
743
+ const published = [];
744
+ let collections;
745
+ try {
746
+ collections = await new SchemaRegistry(db).listCollections();
747
+ } catch (error) {
748
+ console.error("[scheduled-publish] Failed to list collections:", error);
749
+ return published;
750
+ }
751
+ const repo = new ContentRepository(db);
752
+ const optionsRepo = new OptionsRepository(db);
753
+ const doPublish = publish ?? ((collection, id, opts) => handleContentPublish(db, collection, id, opts));
754
+ const batchLimit = limit > 0 ? limit : void 0;
755
+ for (const collection of collections) try {
756
+ let cursor;
757
+ let cursorRevision = null;
758
+ if (batchLimit !== void 0) try {
759
+ const stored = await optionsRepo.getVersioned(scheduledPublishCursorKey(collection.slug));
760
+ cursorRevision = stored?.revision ?? null;
761
+ if (isScheduledPublishCursor(stored?.value)) cursor = stored.value;
762
+ } catch (error) {
763
+ console.error(`[scheduled-publish] Failed to read cursor for "${collection.slug}":`, error);
764
+ }
765
+ let due = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime, cursor);
766
+ if (cursor && due.length === 0) due = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime);
767
+ const batch = [];
768
+ for (const item of due) {
769
+ const publishedAt = item.publishedAt == null ? item.scheduledAt ?? void 0 : void 0;
770
+ const result = await doPublish(collection.slug, item.id, {
771
+ publishedAt,
772
+ requireScheduledDue: true,
773
+ expectedScheduledAt: item.scheduledAt ?? void 0,
774
+ currentTime
775
+ });
776
+ if (result.success) batch.push({
777
+ collection: collection.slug,
778
+ id: item.id
779
+ });
780
+ else if (result.error?.code === "NOT_DUE" || result.error?.code === "PUBLISH_REJECTED") {} else console.error(`[scheduled-publish] Failed to publish ${collection.slug}/${item.id}:`, result.error);
781
+ }
782
+ const lastAttempted = due.at(-1);
783
+ if (batchLimit !== void 0 && lastAttempted?.scheduledAt) try {
784
+ await optionsRepo.compareAndSet(scheduledPublishCursorKey(collection.slug), cursorRevision, {
785
+ scheduledAt: lastAttempted.scheduledAt,
786
+ id: lastAttempted.id
787
+ });
788
+ } catch (error) {
789
+ console.error(`[scheduled-publish] Failed to save cursor for "${collection.slug}":`, error);
790
+ }
791
+ if (batch.length > 0) {
792
+ published.push(...batch);
793
+ if (onPublished) try {
794
+ await onPublished(batch);
795
+ } catch (error) {
796
+ console.error(`[scheduled-publish] onPublished failed after "${collection.slug}" batch:`, error);
797
+ }
798
+ }
799
+ } catch (error) {
800
+ console.error(`[scheduled-publish] Sweep failed for "${collection.slug}":`, error);
801
+ }
802
+ return published;
803
+ }
804
+
805
+ //#endregion
806
+ //#region src/emdash-runtime.ts
807
+ /**
808
+ * EmDashRuntime - Core runtime for EmDash CMS
809
+ *
810
+ * Manages database, storage, plugins (trusted + sandboxed), hooks, and
811
+ * provides handlers for content/media operations.
812
+ *
813
+ * Created once per worker lifetime, cached and reused across requests.
814
+ */
815
+ const LEADING_SLASH_PATTERN = /^\//;
816
+ const LOCALE_CASING_REPAIR_OPTION = "emdash:repair_locale_casing";
817
+ const PLUGIN_COMMENT_STATUSES = new Set([
818
+ "approved",
819
+ "pending",
820
+ "spam"
821
+ ]);
822
+ function assertPluginCommentStatus(value, name) {
823
+ if (!PLUGIN_COMMENT_STATUSES.has(value)) throw Object.assign(/* @__PURE__ */ new Error(`${name} must be one of: approved, pending, spam`), { code: "COMMENT_STATUS_INVALID" });
824
+ }
825
+ function getLocaleCasingRepairVersion(locales) {
826
+ if (locales.length === 0) return null;
827
+ return `1:${locales.toSorted().join(",")}`;
828
+ }
829
+ /**
830
+ * Parse a JSON column expected to contain an array of strings.
831
+ *
832
+ * Throws on malformed JSON rather than returning []; callers are responsible
833
+ * for deciding how to handle/log the error. Empty string / null inputs return
834
+ * [] (they represent "no value"). Non-string array entries are filtered out.
835
+ */
836
+ function parseStringArray(raw) {
837
+ if (!raw) return [];
838
+ const parsed = JSON.parse(raw);
839
+ if (!Array.isArray(parsed)) return [];
840
+ return parsed.filter((v) => typeof v === "string");
841
+ }
842
+ function isModerationDecision(value) {
843
+ if (typeof value !== "object" || value === null || !("status" in value)) return false;
844
+ if (value.status !== "pending" && value.status !== "approved" && value.status !== "spam") return false;
845
+ return !("reason" in value) || value.reason === void 0 || typeof value.reason === "string";
846
+ }
847
+ const DRAFT_ONLY_UPDATE_KEYS = new Set([
848
+ "data",
849
+ "slug",
850
+ "locale",
851
+ "skipRevision",
852
+ "references",
853
+ "actor",
854
+ "migrateBlocks",
855
+ "replaceBlocks"
856
+ ]);
857
+ /** Field types whose schema is an array, so a stored blank string can never validate. */
858
+ const ARRAY_FIELD_TYPES = new Set([
859
+ "portableText",
860
+ "multiSelect",
861
+ "repeater"
862
+ ]);
863
+ const MAX_DRAFT_STAGE_ATTEMPTS = 32;
864
+ const PLUGIN_INVOCATION_RELEASE_GRACE_MS = 6e4;
865
+ /**
866
+ * A `ContentSaveRejectedError` carries a message the plugin wrote for the
867
+ * editor; every other exception stays internal and is replaced by a generic
868
+ * message so hook internals cannot leak through the API.
869
+ */
870
+ function beforeSaveFailure(error) {
871
+ if (isContentSaveRejection(error)) {
872
+ const sandbox = getSandboxSaveRejectionDetails(error);
873
+ if (sandbox) return {
874
+ success: false,
875
+ error: {
876
+ code: ErrorCode.SAVE_REJECTED,
877
+ message: error.message,
878
+ details: sandbox
879
+ }
880
+ };
881
+ return {
882
+ success: false,
883
+ error: {
884
+ code: ErrorCode.SAVE_REJECTED,
885
+ message: error.message
886
+ }
887
+ };
888
+ }
889
+ console.error("EmDash: content:beforeSave hook failed:", error);
890
+ return {
891
+ success: false,
892
+ error: {
893
+ code: ErrorCode.CONTENT_HOOK_ERROR,
894
+ message: "A plugin hook failed while saving content"
895
+ }
896
+ };
897
+ }
898
+ /**
899
+ * Convert a ContentItem to Record<string, unknown> for hook consumption.
900
+ * Hooks receive the full item as a flat record.
901
+ */
902
+ function contentItemToRecord(item) {
903
+ return { ...item };
904
+ }
905
+ function toPluginContentItem(item) {
906
+ return {
907
+ id: item.id,
908
+ type: item.type,
909
+ slug: item.slug,
910
+ status: item.status,
911
+ locale: item.locale,
912
+ data: item.data,
913
+ createdAt: item.createdAt,
914
+ updatedAt: item.updatedAt,
915
+ publishedAt: item.publishedAt,
916
+ scheduledAt: item.scheduledAt,
917
+ authorId: item.authorId,
918
+ translationGroup: item.translationGroup,
919
+ liveRevisionId: item.liveRevisionId,
920
+ draftRevisionId: item.draftRevisionId,
921
+ version: item.version,
922
+ ...item.seo === void 0 ? {} : { seo: item.seo }
923
+ };
924
+ }
925
+ /**
926
+ * Db init lock reclaim deadline. Derived from the migration race wait so
927
+ * they can't drift apart: a healthy init can legitimately block for the
928
+ * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
929
+ * connect and migrator work, before it should be presumed dead. The outer
930
+ * runtime init lock (middleware.ts) must use a strictly larger deadline —
931
+ * it wraps create() → getDatabase() → this lock, and equal deadlines would
932
+ * let the outer reclaim while the inner is legitimately still working.
933
+ */
934
+ const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 2e4;
935
+ /**
936
+ * Db cache + its init lock live on globalThis behind a Symbol: the bundler
937
+ * can duplicate this module across SSR chunks (same reasoning as
938
+ * request-cache.ts), and a duplicated cache/lock would mean concurrent
939
+ * independent db inits — and duplicate migrators — per isolate.
940
+ */
941
+ const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
942
+ const globalSymbolStore = globalThis;
943
+ function getDbHolder() {
944
+ let holder = globalSymbolStore[DB_HOLDER_KEY];
945
+ if (!holder) {
946
+ holder = {
947
+ cache: /* @__PURE__ */ new Map(),
948
+ lock: createInitLock(),
949
+ failures: /* @__PURE__ */ new Map()
950
+ };
951
+ globalSymbolStore[DB_HOLDER_KEY] = holder;
952
+ }
953
+ holder.failures ??= /* @__PURE__ */ new Map();
954
+ return holder;
955
+ }
956
+ /**
957
+ * After a database init fails (migrations threw), skip re-attempting for
958
+ * this long. Cold isolates still get one attempt each, so a transient
959
+ * failure heals on its own; a persistently failing migration is retried at
960
+ * most once per backoff window per isolate instead of on every request.
961
+ */
962
+ const DB_INIT_FAILURE_BACKOFF_MS = 3e4;
963
+ /**
964
+ * Auto-seed runs at most once per isolate per database. Its lock + "done" set
965
+ * live on globalThis (same bundler-duplication reasoning as the db cache) so a
966
+ * reclaimed-and-rerun `create()` can't seed a second time concurrently. The
967
+ * lock polls rather than sharing a promise, so it is safe to await across a
968
+ * cancelled owner in workerd.
969
+ */
970
+ const SEED_HOLDER_KEY = Symbol.for("emdash:seed-state");
971
+ function getSeedHolder() {
972
+ let holder = globalSymbolStore[SEED_HOLDER_KEY];
973
+ if (!holder) {
974
+ holder = {
975
+ done: /* @__PURE__ */ new Set(),
976
+ lock: createInitLock()
977
+ };
978
+ globalSymbolStore[SEED_HOLDER_KEY] = holder;
979
+ }
980
+ return holder;
981
+ }
982
+ const storageCache = /* @__PURE__ */ new Map();
983
+ const sandboxedPluginCache = /* @__PURE__ */ new Map();
984
+ const sandboxedManifestCache = /* @__PURE__ */ new Map();
985
+ /**
986
+ * Per-tier sets of `${pluginId}:${version}` keys present in
987
+ * `sandboxedPluginCache`. Used during sync to know which entries belong
988
+ * to which install source so we can invalidate only what belongs to the
989
+ * tier currently being synced.
990
+ */
991
+ const marketplacePluginKeys = /* @__PURE__ */ new Set();
992
+ const registryPluginKeys = /* @__PURE__ */ new Set();
993
+ /**
994
+ * Manifest metadata for runtime-installed sandboxed plugins (marketplace
995
+ * and registry both). Keyed by `pluginId`; readers don't care which
996
+ * source the plugin came from. Named `marketplace*` for legacy reasons.
997
+ */
998
+ const marketplaceManifestCache = /* @__PURE__ */ new Map();
999
+ /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
1000
+ const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
1001
+ let sandboxRunner = null;
1002
+ function allowedBrowserImageHosts(capabilities, allowedHosts) {
1003
+ if (capabilities.includes("network:request:unrestricted") || capabilities.includes("network:fetch:any")) return ["*"];
1004
+ if (capabilities.includes("network:request") || capabilities.includes("network:fetch")) return allowedHosts;
1005
+ return [];
1006
+ }
1007
+ /**
1008
+ * EmDashRuntime - singleton per worker
1009
+ */
1010
+ var EmDashRuntime = class EmDashRuntime {
1011
+ /**
1012
+ * The singleton database instance (worker-lifetime cached).
1013
+ * Use the `db` getter instead — it checks the request context first
1014
+ * for per-request overrides (D1 read replica sessions, DO multi-site).
1015
+ */
1016
+ _db;
1017
+ storage;
1018
+ configuredPlugins;
1019
+ sandboxedPlugins;
1020
+ sandboxedPluginEntries;
1021
+ /**
1022
+ * Schema registry bound to the current request/event-scoped connection.
1023
+ * Built per access (SchemaRegistry just wraps a db) against `this.db`, the
1024
+ * ALS-aware getter — never a captured snapshot of the singleton. On a
1025
+ * connection-backed adapter (Postgres over Hyperdrive) a captured singleton
1026
+ * would query a socket opened by an earlier event and trip workerd's
1027
+ * cross-request I/O guard; the catch in handlers like handleContentUpdate
1028
+ * would then silently treat a revision-enabled collection as non-revisioned
1029
+ * and write draft edits to live columns. Same reasoning as the per-call
1030
+ * registry in _buildManifest().
1031
+ */
1032
+ get schemaRegistry() {
1033
+ return new SchemaRegistry(this.db);
1034
+ }
1035
+ _hooks;
1036
+ config;
1037
+ mediaProviders;
1038
+ mediaProviderEntries;
1039
+ cronExecutor;
1040
+ email;
1041
+ cronScheduler;
1042
+ enabledPlugins;
1043
+ pluginStates;
1044
+ activePluginContentActions = /* @__PURE__ */ new Map();
1045
+ pendingPluginAfterHooks = /* @__PURE__ */ new Map();
1046
+ activePluginInvocations = /* @__PURE__ */ new Set();
1047
+ /** Timed-out sandbox invocations whose later actions must schedule hooks without another flush. */
1048
+ releasedPluginInvocations = /* @__PURE__ */ new Set();
1049
+ pluginInvocationCacheInvalidators = /* @__PURE__ */ new Map();
1050
+ pluginContentCacheInvalidator;
1051
+ /**
1052
+ * Isolate-lifetime guard so FTS indexes are verified at most once per
1053
+ * worker rather than on every admin request. See ensureSearchHealthy().
1054
+ * Uses the poison-immune single-flight cache (never a shared awaitable
1055
+ * promise) so a cancelled first caller can't wedge later ones.
1056
+ */
1057
+ _searchHealthCache = createSingleFlightCache();
1058
+ /** Current hook pipeline. Use the `hooks` getter for external access. */
1059
+ get hooks() {
1060
+ return this._hooks;
1061
+ }
1062
+ /** All plugins eligible for the hook pipeline (includes built-in plugins).
1063
+ * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
1064
+ allPipelinePlugins;
1065
+ /** Guards the once-per-process plugin storage-index sync. */
1066
+ storageIndexesSynced = false;
1067
+ /** Factory options for the hook pipeline context factory */
1068
+ pipelineFactoryOptions;
1069
+ /** Dependencies needed for exclusive hook resolution */
1070
+ runtimeDeps;
1071
+ /** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
1072
+ pipelineRef;
1073
+ commentModerationInProgress = /* @__PURE__ */ new Set();
1074
+ /**
1075
+ * Get the database instance for the current request.
1076
+ *
1077
+ * Checks the ALS-based request context first — middleware sets a
1078
+ * per-request Kysely instance there for D1 read replica sessions
1079
+ * or DO preview databases. Falls back to the singleton instance.
1080
+ */
1081
+ get db() {
1082
+ const ctx = getRequestContext();
1083
+ if (ctx?.db) return ctx.db;
1084
+ return this._db;
1085
+ }
1086
+ /** Bind the platform cache provider used by plugin actions, including request-less cron hooks. */
1087
+ setPluginContentCacheInvalidator(invalidateContentCache) {
1088
+ this.pluginContentCacheInvalidator = invalidateContentCache;
1089
+ }
1090
+ constructor(parts) {
1091
+ this._db = parts.db;
1092
+ this.storage = parts.storage;
1093
+ this.configuredPlugins = parts.configuredPlugins;
1094
+ this.sandboxedPlugins = parts.sandboxedPlugins;
1095
+ this.sandboxedPluginEntries = parts.sandboxedPluginEntries;
1096
+ this._hooks = parts.hooks;
1097
+ this.enabledPlugins = parts.enabledPlugins;
1098
+ this.pluginStates = parts.pluginStates;
1099
+ this.config = parts.config;
1100
+ this.mediaProviders = parts.mediaProviders;
1101
+ this.mediaProviderEntries = parts.mediaProviderEntries;
1102
+ this.cronExecutor = parts.cronExecutor;
1103
+ this.cronScheduler = parts.cronScheduler;
1104
+ this.email = parts.emailPipeline;
1105
+ this.allPipelinePlugins = parts.allPipelinePlugins;
1106
+ this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
1107
+ this.runtimeDeps = parts.runtimeDeps;
1108
+ this.pipelineRef = parts.pipelineRef;
1109
+ }
1110
+ /**
1111
+ * Get the sandbox runner instance (for marketplace install/update)
1112
+ */
1113
+ getSandboxRunner() {
1114
+ return sandboxRunner;
1115
+ }
1116
+ /**
1117
+ * Whether the sandbox bypass mode (sandbox: false) is active.
1118
+ * Marketplace install/update handlers use this to skip the
1119
+ * SANDBOX_NOT_AVAILABLE gate, since the bypass path loads
1120
+ * marketplace plugins in-process via syncMarketplacePlugins().
1121
+ */
1122
+ isSandboxBypassed() {
1123
+ return this.runtimeDeps.sandboxBypassed === true;
1124
+ }
1125
+ /**
1126
+ * Publish any content whose scheduled time has passed.
1127
+ * Returns the items promoted so callers can invalidate their cache tags.
1128
+ */
1129
+ async publishScheduled() {
1130
+ return this.publishScheduledWithFence();
1131
+ }
1132
+ async publishScheduledWithFence(onPublished) {
1133
+ const recordWrite = await assertSiteWriteAllowed(this.db);
1134
+ const currentTime = this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date();
1135
+ const published = await publishDueContent(this.db, {
1136
+ publish: (collection, id, options) => this.handleContentPublish(collection, id, {
1137
+ ...options,
1138
+ origin: { source: "scheduler" }
1139
+ }),
1140
+ onPublished,
1141
+ currentTime
1142
+ });
1143
+ if (published.length > 0) await recordWrite();
1144
+ return published;
1145
+ }
1146
+ /**
1147
+ * Run the full scheduled-maintenance batch: cron tasks, scheduled
1148
+ * publishing, and system cleanup. For request-less drivers — the
1149
+ * Cloudflare `scheduled()` handler invokes this from a Cron Trigger.
1150
+ * (On Node the timer-based scheduler drives the same work itself.)
1151
+ *
1152
+ * Each step is independent and non-fatal. Returns the content promoted
1153
+ * by the publishing sweep so the caller can purge edge-cache tags.
1154
+ *
1155
+ * `onPublished` (optional) is awaited after each collection's batch so a
1156
+ * request-less driver can invalidate edge-cache tags incrementally rather
1157
+ * than only after the whole sweep — bounding stale-cache exposure if the
1158
+ * runtime is killed mid-sweep.
1159
+ */
1160
+ async runScheduledTasks(options = {}) {
1161
+ const { published } = await this.runScheduledTasksWithStats(options);
1162
+ return { published };
1163
+ }
1164
+ async runScheduledTasksWithStats(options = {}) {
1165
+ if (options.invalidateContentCache) this.pluginContentCacheInvalidator = options.invalidateContentCache;
1166
+ let processed = 0;
1167
+ if (this.cronExecutor) {
1168
+ try {
1169
+ processed = await this.cronExecutor.tick();
1170
+ } catch (error) {
1171
+ console.error("[cron] Tick failed:", error);
1172
+ }
1173
+ try {
1174
+ await this.cronExecutor.recoverStaleLocks();
1175
+ } catch (error) {
1176
+ console.error("[cron] Stale lock recovery failed:", error);
1177
+ }
1178
+ }
1179
+ let published = [];
1180
+ try {
1181
+ published = await this.publishScheduledWithFence(options.onPublished);
1182
+ } catch (error) {
1183
+ console.error("[scheduled-publish] Sweep failed:", error);
1184
+ }
1185
+ try {
1186
+ await runSystemCleanup(this.db, this.storage ?? void 0);
1187
+ } catch (error) {
1188
+ console.error("[cleanup] System cleanup failed:", error);
1189
+ }
1190
+ try {
1191
+ await this.syncPluginStorageIndexesOnce();
1192
+ } catch (error) {
1193
+ console.error("[plugins] Storage index sync failed:", error);
1194
+ }
1195
+ await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
1196
+ await recordSchedulerHeartbeatSafely(this.db);
1197
+ return {
1198
+ processed,
1199
+ published
1200
+ };
1201
+ }
1202
+ /**
1203
+ * Materialize plugin-declared storage indexes, once per process.
1204
+ *
1205
+ * Called from the scheduler path, not from request handlers — configured
1206
+ * plugins have no install handler, so the tick is their only sync moment.
1207
+ */
1208
+ async syncPluginStorageIndexesOnce() {
1209
+ if (this.storageIndexesSynced) return;
1210
+ this.storageIndexesSynced = true;
1211
+ await syncDeclaredStorageIndexes(this.db, [...this.allPipelinePlugins, ...marketplaceManifestCache.values()]);
1212
+ }
1213
+ /**
1214
+ * Stop the cron scheduler gracefully.
1215
+ * Call during worker shutdown or hot-reload.
1216
+ */
1217
+ async stopCron() {
1218
+ if (this.cronScheduler) await this.cronScheduler.stop();
1219
+ }
1220
+ /** Stop background work and discard loaded sandbox isolates. */
1221
+ async shutdown() {
1222
+ await this.stopCron();
1223
+ sandboxRunner?.setCommentModerate?.(null);
1224
+ await sandboxRunner?.terminateAll();
1225
+ sandboxRunner = null;
1226
+ sandboxedPluginCache.clear();
1227
+ sandboxedManifestCache.clear();
1228
+ sandboxedRouteMetaCache.clear();
1229
+ marketplaceManifestCache.clear();
1230
+ marketplacePluginKeys.clear();
1231
+ registryPluginKeys.clear();
1232
+ }
1233
+ /**
1234
+ * Update in-memory plugin status and rebuild the hook pipeline.
1235
+ *
1236
+ * Rebuilding the pipeline ensures disabled plugins' hooks stop firing
1237
+ * and re-enabled plugins' hooks start firing again without a restart.
1238
+ * Exclusive hook selections are re-resolved after each rebuild.
1239
+ */
1240
+ async setPluginStatus(pluginId, status) {
1241
+ this.pluginStates.set(pluginId, status);
1242
+ if (status === "active") {
1243
+ this.setSandboxedPluginActive(pluginId, true);
1244
+ this.enabledPlugins.add(pluginId);
1245
+ await this.rebuildHookPipeline();
1246
+ await this._hooks.runPluginActivate(pluginId);
1247
+ } else try {
1248
+ await this._hooks.runPluginDeactivate(pluginId);
1249
+ } finally {
1250
+ this.setSandboxedPluginActive(pluginId, false);
1251
+ this.enabledPlugins.delete(pluginId);
1252
+ await this.rebuildHookPipeline();
1253
+ }
1254
+ }
1255
+ setSandboxedPluginActive(pluginId, active) {
1256
+ for (const [key, plugin] of this.sandboxedPlugins) if (key.slice(0, key.lastIndexOf(":")) === pluginId) plugin.setActive?.(active);
1257
+ }
1258
+ async handlePluginEnable(pluginId) {
1259
+ return enableRuntimePlugin(this, pluginId);
1260
+ }
1261
+ async handlePluginDisable(pluginId) {
1262
+ return disableRuntimePlugin(this, pluginId);
1263
+ }
1264
+ /** Dispatch teardown lifecycle while the runtime-installed isolate is still loaded. */
1265
+ async runPluginUninstallLifecycle(pluginId, deleteData) {
1266
+ if (this.isPluginEnabled(pluginId)) try {
1267
+ await this._hooks.runPluginDeactivate(pluginId);
1268
+ } catch (error) {
1269
+ console.error(`EmDash: Plugin ${pluginId} deactivate hook failed during uninstall:`, error);
1270
+ }
1271
+ try {
1272
+ if ((await this._hooks.runPluginUninstall(pluginId, deleteData)).length > 0) return;
1273
+ await this.findSandboxedPlugin(pluginId)?.invokeHook("plugin:uninstall", { deleteData });
1274
+ } catch (error) {
1275
+ console.error(`EmDash: Plugin ${pluginId} uninstall hook failed:`, error);
1276
+ }
1277
+ }
1278
+ async runPluginInstallLifecycle(pluginId) {
1279
+ await this._hooks.runPluginInstall(pluginId);
1280
+ await this._hooks.runPluginActivate(pluginId);
1281
+ }
1282
+ async runPluginActivateLifecycle(pluginId) {
1283
+ await this._hooks.runPluginActivate(pluginId);
1284
+ }
1285
+ /**
1286
+ * Rebuild the hook pipeline from the current set of enabled plugins.
1287
+ *
1288
+ * Filters `allPipelinePlugins` to only those in `enabledPlugins`,
1289
+ * creates a fresh HookPipeline, re-resolves exclusive hook selections,
1290
+ * and re-wires the context factory so existing references (cron
1291
+ * callbacks, email pipeline) use the new pipeline.
1292
+ */
1293
+ async rebuildHookPipeline() {
1294
+ const newPipeline = createHookPipeline(this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id)), this.pipelineFactoryOptions);
1295
+ await EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);
1296
+ if (this.email) newPipeline.setContextFactory({ emailPipeline: this.email });
1297
+ newPipeline.setContextFactory({ cronReschedule: () => this.cronScheduler?.reschedule() });
1298
+ if (this.email) this.email.setPipeline(newPipeline);
1299
+ this.pipelineRef.current = newPipeline;
1300
+ this._hooks = newPipeline;
1301
+ }
1302
+ /**
1303
+ * Synchronize marketplace plugin runtime state with DB + storage.
1304
+ *
1305
+ * Ensures install/update/uninstall changes take effect immediately in the
1306
+ * current worker: loads newly active plugins and removes uninstalled ones.
1307
+ */
1308
+ async syncMarketplacePlugins() {
1309
+ if (!this.config.marketplace) return;
1310
+ if (this.runtimeDeps.sandboxBypassed) {
1311
+ await this.syncMarketplacePluginsBypassed();
1312
+ return;
1313
+ }
1314
+ await this.syncSandboxedSourcePlugins("marketplace");
1315
+ }
1316
+ /**
1317
+ * Synchronize registry plugin runtime state with DB + storage.
1318
+ *
1319
+ * Mirrors {@link syncMarketplacePlugins} for plugins installed via the
1320
+ * experimental decentralized plugin registry. Called after install,
1321
+ * update, and uninstall handlers complete.
1322
+ */
1323
+ async syncRegistryPlugins() {
1324
+ if (!getRegistryConfigInput(this.config.registry, this.config.experimental?.registry)) return;
1325
+ await this.syncSandboxedSourcePlugins("registry");
1326
+ }
1327
+ /**
1328
+ * Internal: reconcile in-memory sandboxed-plugin state with the
1329
+ * `_plugin_state` table for the given source tier. Shared
1330
+ * implementation behind {@link syncMarketplacePlugins} and
1331
+ * {@link syncRegistryPlugins}.
1332
+ *
1333
+ * Each source tier has its own key set in `${source}PluginKeys` so a
1334
+ * sync for one tier doesn't invalidate the other.
1335
+ */
1336
+ async syncSandboxedSourcePlugins(source) {
1337
+ if (!this.storage) return;
1338
+ if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
1339
+ const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
1340
+ let pipelineChanged = false;
1341
+ try {
1342
+ const stateRepo = new PluginStateRepository(this.db);
1343
+ const states = source === "marketplace" ? await stateRepo.getMarketplacePlugins() : await stateRepo.getRegistryPlugins();
1344
+ const desired = /* @__PURE__ */ new Map();
1345
+ for (const state of states) {
1346
+ this.pluginStates.set(state.pluginId, state.status);
1347
+ if (state.status === "active") this.enabledPlugins.add(state.pluginId);
1348
+ else this.enabledPlugins.delete(state.pluginId);
1349
+ if (state.status !== "active") continue;
1350
+ const desiredVersion = source === "marketplace" ? state.marketplaceVersion ?? state.version : state.version;
1351
+ desired.set(state.pluginId, desiredVersion);
1352
+ }
1353
+ const keysToRemove = [];
1354
+ for (const key of keySet) {
1355
+ const [pluginId] = key.split(":");
1356
+ if (!pluginId) continue;
1357
+ const desiredVersion = desired.get(pluginId);
1358
+ if (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;
1359
+ keysToRemove.push(key);
1360
+ }
1361
+ for (const key of keysToRemove) {
1362
+ const [pluginId] = key.split(":");
1363
+ if (!pluginId) continue;
1364
+ if (!desired.get(pluginId)) {
1365
+ this.pluginStates.delete(pluginId);
1366
+ this.enabledPlugins.delete(pluginId);
1367
+ }
1368
+ const existing = sandboxedPluginCache.get(key);
1369
+ if (existing) try {
1370
+ await existing.terminate();
1371
+ } catch (error) {
1372
+ console.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);
1373
+ }
1374
+ sandboxedPluginCache.delete(key);
1375
+ sandboxedManifestCache.delete(key);
1376
+ this.sandboxedPlugins.delete(key);
1377
+ this.removePluginFromLists(pluginId);
1378
+ keySet.delete(key);
1379
+ pipelineChanged = true;
1380
+ if (pluginId) {
1381
+ sandboxedRouteMetaCache.delete(pluginId);
1382
+ marketplaceManifestCache.delete(pluginId);
1383
+ }
1384
+ }
1385
+ for (const [pluginId, version] of desired) {
1386
+ const key = `${pluginId}:${version}`;
1387
+ if (sandboxedPluginCache.has(key)) {
1388
+ keySet.add(key);
1389
+ continue;
1390
+ }
1391
+ const bundle = await loadBundleFromR2(this.storage, pluginId, version, source);
1392
+ if (!bundle) {
1393
+ console.warn(`EmDash: ${source} plugin ${pluginId}@${version} not found in R2`);
1394
+ continue;
1395
+ }
1396
+ const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
1397
+ sandboxedPluginCache.set(key, loaded);
1398
+ sandboxedManifestCache.set(key, bundle.manifest);
1399
+ this.sandboxedPlugins.set(key, loaded);
1400
+ this.allPipelinePlugins.push(createSandboxedPluginProxy(bundle.manifest, loaded));
1401
+ keySet.add(key);
1402
+ pipelineChanged = true;
1403
+ marketplaceManifestCache.set(pluginId, {
1404
+ id: bundle.manifest.id,
1405
+ version: bundle.manifest.version,
1406
+ admin: bundle.manifest.admin,
1407
+ mcp: bundle.manifest.mcp,
1408
+ storage: bundle.manifest.storage,
1409
+ allowedHosts: bundle.manifest.allowedHosts,
1410
+ capabilities: bundle.manifest.capabilities
1411
+ });
1412
+ if (bundle.manifest.routes.length > 0) {
1413
+ const routeMetaMap = /* @__PURE__ */ new Map();
1414
+ for (const entry of bundle.manifest.routes) {
1415
+ const normalized = normalizeManifestRoute$1(entry);
1416
+ routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
1417
+ }
1418
+ sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
1419
+ } else sandboxedRouteMetaCache.delete(pluginId);
1420
+ }
1421
+ if (pipelineChanged) await this.rebuildHookPipeline();
1422
+ } catch (error) {
1423
+ console.error(`EmDash: Failed to sync ${source} plugins:`, error);
1424
+ }
1425
+ }
1426
+ /**
1427
+ * Remove a plugin from the in-memory pipeline lists by ID.
1428
+ * Mutates allPipelinePlugins and configuredPlugins in place.
1429
+ */
1430
+ removePluginFromLists(pluginId) {
1431
+ const allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId);
1432
+ if (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1);
1433
+ const configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId);
1434
+ if (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1);
1435
+ }
1436
+ /**
1437
+ * Sync marketplace plugin metadata in sandbox: false bypass mode.
1438
+ *
1439
+ * In bypass mode the noop runner can't load plugins, but admin pages,
1440
+ * widgets, and route metadata still need to refresh in-process when an
1441
+ * admin installs/updates/uninstalls a marketplace plugin. Otherwise the
1442
+ * admin UI shows stale data until the server restarts.
1443
+ *
1444
+ * Hooks and routes still won't execute under bypass (matches the
1445
+ * cold-start bypass behavior in loadMarketplacePluginsBypassed).
1446
+ *
1447
+ * Known limitation: bypass plugins are loaded via `import(dataUrl)`,
1448
+ * which Node's ESM cache keys on the full URL. Updates create fresh
1449
+ * module objects, but old ones remain cached for the worker's lifetime.
1450
+ * In practice this is a few KB per update — only matters for sites with
1451
+ * very frequent marketplace updates running long-lived processes. The
1452
+ * fix would be vm.SourceTextModule for explicit lifecycle management.
1453
+ */
1454
+ async syncMarketplacePluginsBypassed() {
1455
+ if (!this.storage) return;
1456
+ try {
1457
+ const marketplaceStates = await new PluginStateRepository(this.db).getMarketplacePlugins();
1458
+ const desired = /* @__PURE__ */ new Map();
1459
+ for (const state of marketplaceStates) {
1460
+ this.pluginStates.set(state.pluginId, state.status);
1461
+ if (state.status === "active") this.enabledPlugins.add(state.pluginId);
1462
+ else this.enabledPlugins.delete(state.pluginId);
1463
+ if (state.status !== "active") continue;
1464
+ desired.set(state.pluginId, state.marketplaceVersion ?? state.version);
1465
+ }
1466
+ const toRemove = [];
1467
+ for (const pluginId of marketplaceManifestCache.keys()) if (!desired.has(pluginId)) toRemove.push(pluginId);
1468
+ for (const pluginId of toRemove) {
1469
+ const resolved = this.allPipelinePlugins.find((p) => p.id === pluginId);
1470
+ if (resolved) try {
1471
+ const deactivateHook = resolved.hooks?.["plugin:deactivate"];
1472
+ if (deactivateHook) {
1473
+ const handler = typeof deactivateHook === "function" ? deactivateHook : deactivateHook.handler;
1474
+ if (typeof handler === "function") await handler({ pluginId }, {});
1475
+ }
1476
+ } catch (err) {
1477
+ console.warn(`[emdash] plugin:deactivate hook failed for ${pluginId}:`, err);
1478
+ }
1479
+ marketplaceManifestCache.delete(pluginId);
1480
+ sandboxedRouteMetaCache.delete(pluginId);
1481
+ this.removePluginFromLists(pluginId);
1482
+ this.enabledPlugins.delete(pluginId);
1483
+ }
1484
+ const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
1485
+ const newPlugins = [];
1486
+ for (const [pluginId, version] of desired) {
1487
+ const bundle = await loadBundleFromR2(this.storage, pluginId, version);
1488
+ if (!bundle) {
1489
+ console.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);
1490
+ continue;
1491
+ }
1492
+ marketplaceManifestCache.set(pluginId, {
1493
+ id: bundle.manifest.id,
1494
+ version: bundle.manifest.version,
1495
+ admin: bundle.manifest.admin,
1496
+ mcp: bundle.manifest.mcp,
1497
+ storage: bundle.manifest.storage,
1498
+ allowedHosts: bundle.manifest.allowedHosts,
1499
+ capabilities: bundle.manifest.capabilities
1500
+ });
1501
+ if (bundle.manifest.routes.length > 0) {
1502
+ const routeMetaMap = /* @__PURE__ */ new Map();
1503
+ for (const entry of bundle.manifest.routes) {
1504
+ const normalized = normalizeManifestRoute$1(entry);
1505
+ routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
1506
+ }
1507
+ sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
1508
+ } else sandboxedRouteMetaCache.delete(pluginId);
1509
+ const existing = this.allPipelinePlugins.find((p) => p.id === pluginId);
1510
+ if (existing && existing.version === bundle.manifest.version) continue;
1511
+ if (existing) this.removePluginFromLists(pluginId);
1512
+ try {
1513
+ const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`);
1514
+ const adapted = adaptSandboxEntry(pluginModule.default ?? pluginModule, {
1515
+ id: bundle.manifest.id,
1516
+ version: bundle.manifest.version,
1517
+ entrypoint: "",
1518
+ capabilities: bundle.manifest.capabilities ?? [],
1519
+ allowedHosts: bundle.manifest.allowedHosts ?? [],
1520
+ storage: bundle.manifest.storage ?? {},
1521
+ adminPages: bundle.manifest.admin?.pages,
1522
+ adminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({
1523
+ id: w.id,
1524
+ title: w.title,
1525
+ size: w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0
1526
+ })),
1527
+ settingsSchema: bundle.manifest.admin?.settingsSchema,
1528
+ editorPanels: bundle.manifest.admin?.editorPanels,
1529
+ editorActions: bundle.manifest.admin?.editorActions
1530
+ });
1531
+ newPlugins.push(adapted);
1532
+ this.allPipelinePlugins.push(adapted);
1533
+ this.configuredPlugins.push(adapted);
1534
+ this.enabledPlugins.add(adapted.id);
1535
+ } catch (error) {
1536
+ console.error(`EmDash: Failed to load marketplace plugin ${pluginId}@${version} in-process:`, error);
1537
+ }
1538
+ }
1539
+ if (toRemove.length > 0 || newPlugins.length > 0) await this.rebuildHookPipeline();
1540
+ } catch (error) {
1541
+ console.error("EmDash: Failed to sync marketplace plugins (bypass):", error);
1542
+ }
1543
+ }
1544
+ /**
1545
+ * Create and initialize the runtime
1546
+ */
1547
+ static async create(deps, timings) {
1548
+ const phase = async (name, desc, fn) => {
1549
+ if (!timings) return fn();
1550
+ const t0 = performance.now();
1551
+ try {
1552
+ return await fn();
1553
+ } finally {
1554
+ timings.push({
1555
+ name,
1556
+ dur: performance.now() - t0,
1557
+ desc
1558
+ });
1559
+ }
1560
+ };
1561
+ const db = await phase("rt.db", "DB init + migration policy", () => EmDashRuntime.getDatabase(deps));
1562
+ const resolveDb = () => {
1563
+ return getRequestContext()?.db ?? db;
1564
+ };
1565
+ await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
1566
+ const storage = EmDashRuntime.getStorage(deps);
1567
+ let pluginStates = /* @__PURE__ */ new Map();
1568
+ const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
1569
+ const localeCasingRepairVersion = getLocaleCasingRepairVersion(configuredLocales);
1570
+ let storedLocaleCasingRepairVersion;
1571
+ let siteInfo;
1572
+ let seedGate = {
1573
+ collectionCount: 1,
1574
+ setupDone: true
1575
+ };
1576
+ const reqCtx = getRequestContext();
1577
+ const ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);
1578
+ let readDb = db;
1579
+ let readDbDisposable;
1580
+ const disposeReadDb = async () => {
1581
+ const disposable = readDbDisposable;
1582
+ readDbDisposable = void 0;
1583
+ if (!disposable) return;
1584
+ try {
1585
+ await disposable.destroy();
1586
+ } catch {}
1587
+ };
1588
+ if (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) try {
1589
+ const dialect = deps.createCoalescingDialect(deps.config.database.config);
1590
+ if (dialect) {
1591
+ readDb = new Kysely({
1592
+ dialect,
1593
+ log: kyselyLogOption()
1594
+ });
1595
+ readDbDisposable = readDb;
1596
+ }
1597
+ } catch {
1598
+ readDb = db;
1599
+ }
1600
+ const optionsRepo = new OptionsRepository(readDb);
1601
+ let missingManualSchemaError;
1602
+ const captureMissingManualSchema = (error) => {
1603
+ if ((deps.migrationMode ?? "auto") === "manual" && isMissingTableError(error)) missingManualSchemaError ??= error;
1604
+ };
1605
+ const readSiteInfo = async () => {
1606
+ const siteOpts = await optionsRepo.getMany([
1607
+ "emdash:site_title",
1608
+ "emdash:site_url",
1609
+ "emdash:locale",
1610
+ LOCALE_CASING_REPAIR_OPTION
1611
+ ]);
1612
+ storedLocaleCasingRepairVersion = siteOpts.get(LOCALE_CASING_REPAIR_OPTION);
1613
+ return {
1614
+ siteName: deps.siteInfo?.name ?? siteOpts.get("emdash:site_title") ?? void 0,
1615
+ siteUrl: deps.siteInfo?.url ?? siteOpts.get("emdash:site_url") ?? void 0,
1616
+ locale: deps.siteInfo?.locale ?? siteOpts.get("emdash:locale") ?? void 0,
1617
+ trailingSlash: deps.siteInfo?.trailingSlash ?? virtualConfig?.trailingSlash
1618
+ };
1619
+ };
1620
+ const coldStartReads = [phase("rt.plugins", "Plugin states", async () => {
1621
+ try {
1622
+ const states = await readDb.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
1623
+ pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
1624
+ } catch (error) {
1625
+ captureMissingManualSchema(error);
1626
+ }
1627
+ }), phase("rt.site", "Site info options", async () => {
1628
+ try {
1629
+ siteInfo = await readSiteInfo();
1630
+ } catch (error) {
1631
+ captureMissingManualSchema(error);
1632
+ }
1633
+ })];
1634
+ if (ownsConfiguredDb) coldStartReads.push(phase("rt.seedcheck", "Auto-seed gate", async () => {
1635
+ try {
1636
+ const [collectionRows, setupOption] = await Promise.all([readDb.selectFrom("_emdash_collections").select("slug").execute(), readDb.selectFrom("options").select("value").where("name", "=", "emdash:setup_complete").executeTakeFirst()]);
1637
+ primeRegisteredCollections(collectionRows.map((row) => row.slug));
1638
+ const setupDone = (() => {
1639
+ try {
1640
+ return !!setupOption && JSON.parse(setupOption.value) === true;
1641
+ } catch {
1642
+ return false;
1643
+ }
1644
+ })();
1645
+ seedGate = {
1646
+ collectionCount: collectionRows.length,
1647
+ setupDone
1648
+ };
1649
+ } catch (error) {
1650
+ captureMissingManualSchema(error);
1651
+ }
1652
+ }));
1653
+ await Promise.all(coldStartReads);
1654
+ if (missingManualSchemaError) {
1655
+ await disposeReadDb();
1656
+ throw missingManualSchemaError;
1657
+ }
1658
+ if (localeCasingRepairVersion && (configuredLocales.some((locale) => locale.includes("-") || locale !== locale.toLowerCase()) || storedLocaleCasingRepairVersion !== void 0) && storedLocaleCasingRepairVersion !== localeCasingRepairVersion) await phase("rt.locale", "Repair locale casing", async () => {
1659
+ await repairLocaleCasing(db, configuredLocales);
1660
+ await new OptionsRepository(db).set(LOCALE_CASING_REPAIR_OPTION, localeCasingRepairVersion);
1661
+ });
1662
+ if (seedGate.collectionCount === 0 && !seedGate.setupDone) {
1663
+ try {
1664
+ if ((await activateMediaUsageCapture(db, { writersDrained: true })).outcome !== "active") throw new Error("Fresh-site media usage activation did not complete");
1665
+ } catch (error) {
1666
+ await disposeReadDb();
1667
+ throw error;
1668
+ }
1669
+ const seedKey = deps.config.database?.entrypoint ?? "default";
1670
+ const seedHolder = getSeedHolder();
1671
+ try {
1672
+ await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
1673
+ const { applySeed } = await import("./apply-Do3KNnUk.mjs").then((n) => n.r);
1674
+ const { loadSeed } = await import("./load-D99axixL.mjs").then((n) => n.r);
1675
+ const { validateSeed } = await import("./validate-CfgRpcjA.mjs").then((n) => n.r);
1676
+ const seed = await loadSeed();
1677
+ if (validateSeed(seed).valid) {
1678
+ const seedResult = await applySeed(db, seed, { onConflict: "skip" });
1679
+ console.log("Auto-seeded default collections");
1680
+ if (seedResult.taxonomies.skipped > 0) console.warn(`[auto-seed] Kept ${seedResult.taxonomies.skipped} existing taxonomy definition(s) instead of the seed's. Edit them in the admin, or run \`emdash seed <file> --on-conflict update\` to replace them (this also overwrites other seeded records).`);
1681
+ }
1682
+ seedHolder.done.add(seedKey);
1683
+ return true;
1684
+ }, {
1685
+ deadlineMs: DB_INIT_DEADLINE_MS,
1686
+ anchor: (promise) => after(() => promise)
1687
+ });
1688
+ try {
1689
+ siteInfo = await readSiteInfo();
1690
+ } catch {}
1691
+ } catch {}
1692
+ }
1693
+ await disposeReadDb();
1694
+ const enabledPlugins = /* @__PURE__ */ new Set();
1695
+ for (const plugin of deps.plugins) {
1696
+ const status = pluginStates.get(plugin.id);
1697
+ if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
1698
+ }
1699
+ const allPipelinePlugins = [...deps.plugins];
1700
+ const bypassedPluginsList = [];
1701
+ if (import.meta.env.DEV) try {
1702
+ const devConsolePlugin = definePlugin({
1703
+ id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
1704
+ version: "0.0.0",
1705
+ capabilities: ["hooks.email-transport:register"],
1706
+ hooks: { "email:deliver": {
1707
+ exclusive: true,
1708
+ handler: devConsoleEmailDeliver
1709
+ } }
1710
+ });
1711
+ allPipelinePlugins.push(devConsolePlugin);
1712
+ enabledPlugins.add(devConsolePlugin.id);
1713
+ } catch (error) {
1714
+ console.warn("[email] Failed to register dev console email provider:", error);
1715
+ }
1716
+ try {
1717
+ const defaultModeratorPlugin = definePlugin({
1718
+ id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
1719
+ version: "0.0.0",
1720
+ capabilities: ["users:read"],
1721
+ hooks: { "comment:moderate": {
1722
+ exclusive: true,
1723
+ handler: defaultCommentModerate
1724
+ } }
1725
+ });
1726
+ allPipelinePlugins.push(defaultModeratorPlugin);
1727
+ enabledPlugins.add(defaultModeratorPlugin.id);
1728
+ } catch (error) {
1729
+ console.warn("[comments] Failed to register default moderator:", error);
1730
+ }
1731
+ if (deps.sandboxBypassed && deps.sandboxedPluginEntries.length > 0) {
1732
+ if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string" && navigator.userAgent.includes("Cloudflare-Workers")) throw new Error("sandbox: false is not supported in Cloudflare Workers. Remove the sandbox: false option or use the Cloudflare sandbox runner.");
1733
+ console.info("EmDash: Sandbox disabled (sandbox: false). Sandboxed plugins will run in-process without isolation.");
1734
+ const bypassedPlugins = await EmDashRuntime.loadBypassedPlugins(deps.sandboxedPluginEntries);
1735
+ for (const plugin of bypassedPlugins) {
1736
+ allPipelinePlugins.push(plugin);
1737
+ bypassedPluginsList.push(plugin);
1738
+ const status = pluginStates.get(plugin.id);
1739
+ if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
1740
+ }
1741
+ }
1742
+ if (deps.sandboxBypassed && deps.config.marketplace && storage) {
1743
+ const marketplaceBypassed = await EmDashRuntime.loadMarketplacePluginsBypassed(db, storage);
1744
+ for (const plugin of marketplaceBypassed) {
1745
+ allPipelinePlugins.push(plugin);
1746
+ bypassedPluginsList.push(plugin);
1747
+ const status = pluginStates.get(plugin.id);
1748
+ if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
1749
+ }
1750
+ }
1751
+ const sandboxedPluginPool = await phase("rt.sandbox", "Sandboxed plugins", () => EmDashRuntime.loadSandboxedPlugins(deps, db, storage, siteInfo));
1752
+ for (const [key, plugin] of sandboxedPluginPool) {
1753
+ const pluginId = key.slice(0, key.lastIndexOf(":"));
1754
+ const status = pluginStates.get(pluginId);
1755
+ plugin.setActive?.(status === void 0 || status === "active");
1756
+ }
1757
+ const installedTierPhases = [];
1758
+ if (deps.config.marketplace && storage && !deps.sandboxBypassed) installedTierPhases.push(phase("rt.market", "Marketplace plugins", () => EmDashRuntime.loadInstalledSandboxedPlugins("marketplace", db, storage, deps, sandboxedPluginPool, siteInfo)));
1759
+ if (getRegistryConfigInput(deps.config.registry, deps.config.experimental?.registry) && storage) installedTierPhases.push(phase("rt.registry", "Registry plugins", () => EmDashRuntime.loadInstalledSandboxedPlugins("registry", db, storage, deps, sandboxedPluginPool, siteInfo)));
1760
+ if (installedTierPhases.length > 0) await Promise.all(installedTierPhases);
1761
+ const configuredSandboxIds = new Set(deps.sandboxedPluginEntries.map((entry) => entry.id));
1762
+ const sandboxedPlugins = new Map([...sandboxedPluginPool].filter(([pluginKey]) => {
1763
+ const pluginId = pluginKey.slice(0, pluginKey.lastIndexOf(":"));
1764
+ return configuredSandboxIds.has(pluginId) || pluginStates.get(pluginId) === "active";
1765
+ }));
1766
+ for (const [pluginKey, instance] of sandboxedPlugins) {
1767
+ const manifest = sandboxedManifestCache.get(pluginKey);
1768
+ if (!manifest) continue;
1769
+ allPipelinePlugins.push(createSandboxedPluginProxy(manifest, instance));
1770
+ const status = pluginStates.get(manifest.id);
1771
+ if (status === void 0 || status === "active") enabledPlugins.add(manifest.id);
1772
+ }
1773
+ const runtimeRef = { current: null };
1774
+ const requireRuntime = () => {
1775
+ if (!runtimeRef.current) throw new Error("EmDash runtime is not ready");
1776
+ return runtimeRef.current;
1777
+ };
1778
+ const contentActions = {
1779
+ begin: (pluginId, invocationId, invalidateContentCache) => requireRuntime().beginPluginInvocation(pluginId, invocationId, invalidateContentCache),
1780
+ flush: (pluginId, invocationId, final) => requireRuntime().flushPluginAfterHooks(pluginId, invocationId, final),
1781
+ getVersioned: (pluginId, collection, id) => requireRuntime().pluginGetVersioned(pluginId, collection, id),
1782
+ publish: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginPublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
1783
+ unpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginUnpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
1784
+ schedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginSchedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
1785
+ unschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginUnschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
1786
+ getTrashedVersioned: (pluginId, collection, id) => requireRuntime().pluginGetTrashedVersioned(pluginId, collection, id),
1787
+ restore: (pluginId, collection, id, options, invocationId, invalidateContentCache) => requireRuntime().pluginRestore(pluginId, collection, id, options, invocationId, invalidateContentCache)
1788
+ };
1789
+ const pipelineFactoryOptions = {
1790
+ db,
1791
+ getDb: resolveDb,
1792
+ beforeContentWrite: () => assertSiteWriteAllowed(resolveDb()),
1793
+ contentActions,
1794
+ now: deps.now,
1795
+ storage: storage ?? void 0,
1796
+ siteInfo,
1797
+ commentModerate: (pluginId, id, status, expectedStatus) => {
1798
+ const current = runtimeRef.current;
1799
+ if (!current) throw new Error("Comment moderation is unavailable during runtime startup");
1800
+ return current.handlePluginCommentModerate(pluginId, id, status, expectedStatus);
1801
+ }
1802
+ };
1803
+ const pipeline = createHookPipeline(allPipelinePlugins.filter((p) => enabledPlugins.has(p.id)), pipelineFactoryOptions);
1804
+ const mediaProviders = /* @__PURE__ */ new Map();
1805
+ const mediaProviderEntries = deps.mediaProviderEntries ?? [];
1806
+ const providerContext = {
1807
+ db,
1808
+ storage,
1809
+ getDb: resolveDb
1810
+ };
1811
+ for (const entry of mediaProviderEntries) try {
1812
+ const provider = entry.createProvider(providerContext);
1813
+ mediaProviders.set(entry.id, provider);
1814
+ } catch (error) {
1815
+ console.warn(`Failed to initialize media provider "${entry.id}":`, error);
1816
+ }
1817
+ await phase("rt.hooks", "Exclusive hook resolution", () => EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps));
1818
+ const emailPipeline = new EmailPipeline(pipeline);
1819
+ if (sandboxRunner) sandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));
1820
+ const pipelineRef = { current: pipeline };
1821
+ const invokeCronHook = async (pluginId, event) => {
1822
+ const result = await pipelineRef.current.invokeCronHook(pluginId, event);
1823
+ if (!result.success && result.error) throw result.error;
1824
+ };
1825
+ pipeline.setContextFactory({ emailPipeline });
1826
+ let cronExecutor = null;
1827
+ let cronScheduler = null;
1828
+ await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
1829
+ try {
1830
+ cronExecutor = new CronExecutor(resolveDb, invokeCronHook, deps.now);
1831
+ pipeline.setContextFactory({ cronReschedule: () => cronScheduler?.reschedule() });
1832
+ const executorForRecovery = cronExecutor;
1833
+ after(async () => {
1834
+ try {
1835
+ const recovered = await executorForRecovery.recoverStaleLocks();
1836
+ if (recovered > 0) console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
1837
+ } catch (error) {
1838
+ console.error("[cron] Failed to recover stale task locks:", error);
1839
+ }
1840
+ });
1841
+ if (deps.createScheduler) {
1842
+ const scheduler = deps.createScheduler(cronExecutor);
1843
+ cronScheduler = scheduler;
1844
+ scheduler.setSystemCleanup(async () => {
1845
+ try {
1846
+ const runtime = runtimeRef.current;
1847
+ if (runtime) await runtime.publishScheduled();
1848
+ else {
1849
+ const recordWrite = await assertSiteWriteAllowed(db);
1850
+ if ((await publishDueContent(db)).length > 0) await recordWrite();
1851
+ }
1852
+ } catch (error) {
1853
+ console.error("[scheduled-publish] Sweep failed:", error);
1854
+ }
1855
+ try {
1856
+ await runSystemCleanup(db, storage ?? void 0);
1857
+ } catch (error) {
1858
+ console.error("[cleanup] System cleanup failed:", error);
1859
+ }
1860
+ try {
1861
+ await runtimeRef.current?.syncPluginStorageIndexesOnce();
1862
+ } catch (error) {
1863
+ console.error("[plugins] Storage index sync failed:", error);
1864
+ }
1865
+ await maybeRunScheduledBackup(db, storage ?? void 0);
1866
+ await recordSchedulerHeartbeatSafely(db);
1867
+ });
1868
+ scheduler.start();
1869
+ }
1870
+ } catch (error) {
1871
+ console.warn("[cron] Failed to initialize cron system:", error);
1872
+ }
1873
+ });
1874
+ sandboxRunner?.setCronReschedule?.(() => cronScheduler?.reschedule());
1875
+ const runtime = new EmDashRuntime({
1876
+ db,
1877
+ storage,
1878
+ configuredPlugins: [...deps.plugins, ...bypassedPluginsList],
1879
+ sandboxedPlugins,
1880
+ sandboxedPluginEntries: deps.sandboxedPluginEntries,
1881
+ hooks: pipeline,
1882
+ enabledPlugins,
1883
+ pluginStates,
1884
+ config: deps.config,
1885
+ mediaProviders,
1886
+ mediaProviderEntries,
1887
+ cronExecutor,
1888
+ cronScheduler,
1889
+ emailPipeline,
1890
+ allPipelinePlugins,
1891
+ pipelineFactoryOptions,
1892
+ runtimeDeps: deps,
1893
+ pipelineRef
1894
+ });
1895
+ const contentCreate = async (_pluginId, collection, input, options) => {
1896
+ const { seo, ...data } = input;
1897
+ const result = await runtime.handleContentCreate(collection, {
1898
+ data,
1899
+ seo,
1900
+ locale: resolveContentCreateLocale(options?.locale),
1901
+ translationOf: options?.translationOf
1902
+ }, {
1903
+ skipSaveHooks: options?.sandboxOrigin ? options.originHook !== void 0 : getActiveContentSaveHookName() != null,
1904
+ excludeAfterSavePluginId: _pluginId
1905
+ });
1906
+ if (!result.success) throw Object.assign(new Error(result.error.message), {
1907
+ name: result.error.code,
1908
+ code: result.error.code
1909
+ });
1910
+ return result.data.item;
1911
+ };
1912
+ runtime.pipelineFactoryOptions.contentCreate = contentCreate;
1913
+ pipeline.setContextFactory({ contentCreate });
1914
+ sandboxRunner?.setContentCreate?.(contentCreate);
1915
+ runtimeRef.current = runtime;
1916
+ sandboxRunner?.setCommentModerate?.((pluginId, id, status, expectedStatus) => runtime.handlePluginCommentModerate(pluginId, id, status, expectedStatus));
1917
+ sandboxRunner?.setContentActions?.(contentActions);
1918
+ return runtime;
1919
+ }
1920
+ /**
1921
+ * Get a media provider by ID
1922
+ */
1923
+ getMediaProvider(providerId) {
1924
+ return this.mediaProviders.get(providerId);
1925
+ }
1926
+ /**
1927
+ * Get all media provider entries (for admin UI)
1928
+ */
1929
+ getMediaProviderList() {
1930
+ return this.mediaProviderEntries.map((e) => ({
1931
+ id: e.id,
1932
+ name: e.name,
1933
+ icon: e.icon,
1934
+ capabilities: e.capabilities
1935
+ }));
1936
+ }
1937
+ /**
1938
+ * Get or create database instance
1939
+ */
1940
+ static async getDatabase(deps) {
1941
+ const ctx = getRequestContext();
1942
+ if (ctx?.dbIsIsolated && ctx.db) return ctx.db;
1943
+ const dbConfig = deps.config.database;
1944
+ if (!dbConfig) try {
1945
+ return await getDb();
1946
+ } catch {
1947
+ throw new Error("EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts");
1948
+ }
1949
+ const cacheKey = dbConfig.entrypoint;
1950
+ const holder = getDbHolder();
1951
+ const throwIfBackingOff = () => {
1952
+ const failure = holder.failures.get(cacheKey);
1953
+ if (!failure) return;
1954
+ if (Date.now() - failure.at < DB_INIT_FAILURE_BACKOFF_MS) throw new Error(`Database initialization is backing off after a recent migration failure: ${failure.message}`);
1955
+ holder.failures.delete(cacheKey);
1956
+ };
1957
+ throwIfBackingOff();
1958
+ return initWithLock(holder.lock, () => holder.cache.get(cacheKey), async (isCurrentClaim) => {
1959
+ throwIfBackingOff();
1960
+ const db = new Kysely({
1961
+ dialect: deps.createDialect(dbConfig.config),
1962
+ log: kyselyLogOption()
1963
+ });
1964
+ try {
1965
+ await enforceRuntimeMigrationPolicy(db, deps.migrationMode ?? "auto");
1966
+ } catch (error) {
1967
+ if (!(error instanceof ConcurrentMigrationTimeoutError) && !(error instanceof PendingMigrationsError)) holder.failures.set(cacheKey, {
1968
+ at: Date.now(),
1969
+ message: error instanceof Error ? error.message : String(error)
1970
+ });
1971
+ await db.destroy().catch(() => {});
1972
+ throw error;
1973
+ }
1974
+ holder.failures.delete(cacheKey);
1975
+ if (isCurrentClaim()) holder.cache.set(cacheKey, db);
1976
+ return db;
1977
+ }, {
1978
+ deadlineMs: DB_INIT_DEADLINE_MS,
1979
+ anchor: (promise) => after(() => promise)
1980
+ });
1981
+ }
1982
+ /**
1983
+ * Get or create storage instance
1984
+ */
1985
+ static getStorage(deps) {
1986
+ const storageConfig = deps.config.storage;
1987
+ if (!storageConfig || !deps.createStorage) return null;
1988
+ const cacheKey = storageConfig.entrypoint;
1989
+ const cached = storageCache.get(cacheKey);
1990
+ if (cached) return cached;
1991
+ const storage = deps.createStorage(storageConfig.config);
1992
+ storageCache.set(cacheKey, storage);
1993
+ return storage;
1994
+ }
1995
+ /**
1996
+ * Load sandboxed plugin entries as trusted in-process plugins.
1997
+ * Used by the sandbox: false debugging escape hatch.
1998
+ *
1999
+ * Imports each plugin's bundled ESM code via a data URL, adapts it
2000
+ * with adaptSandboxEntry, and returns ResolvedPlugin objects ready
2001
+ * to be merged into the pipeline plugin list.
2002
+ */
2003
+ static async loadBypassedPlugins(entries) {
2004
+ const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
2005
+ const plugins = [];
2006
+ for (const entry of entries) try {
2007
+ const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(entry.code).toString("base64")}`);
2008
+ const pluginDef = pluginModule.default ?? pluginModule;
2009
+ const adminPages = entry.adminPages?.map((p) => ({
2010
+ path: p.path,
2011
+ label: p.label ?? p.path,
2012
+ icon: p.icon
2013
+ }));
2014
+ const adminWidgets = entry.adminWidgets?.map((w) => {
2015
+ const size = w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0;
2016
+ return {
2017
+ id: w.id,
2018
+ title: w.title,
2019
+ size
2020
+ };
2021
+ });
2022
+ const resolved = adaptSandboxEntry(pluginDef, {
2023
+ id: entry.id,
2024
+ version: entry.version,
2025
+ entrypoint: "",
2026
+ capabilities: entry.capabilities,
2027
+ allowedHosts: entry.allowedHosts,
2028
+ storage: entry.storage,
2029
+ adminPages,
2030
+ adminWidgets,
2031
+ settingsSchema: entry.settingsSchema,
2032
+ portableTextBlocks: entry.portableTextBlocks,
2033
+ fieldWidgets: entry.fieldWidgets,
2034
+ editorPanels: entry.editorPanels,
2035
+ editorActions: entry.editorActions
2036
+ });
2037
+ plugins.push(resolved);
2038
+ console.log(`EmDash: Loaded plugin ${entry.id}:${entry.version} in-process (sandbox bypassed)`);
2039
+ } catch (error) {
2040
+ console.error(`EmDash: Failed to load sandboxed plugin ${entry.id} in-process:`, error);
2041
+ }
2042
+ return plugins;
2043
+ }
2044
+ /**
2045
+ * Load sandboxed plugins using SandboxRunner
2046
+ */
2047
+ static async loadSandboxedPlugins(deps, db, mediaStorage, siteInfo) {
2048
+ if (!deps.sandboxEnabled) return sandboxedPluginCache;
2049
+ if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
2050
+ db,
2051
+ beforeContentWrite: () => assertSiteWriteAllowed(db),
2052
+ taxonomyWrite: createTaxonomyAccessWithWrite(db),
2053
+ now: deps.now,
2054
+ mediaStorage: mediaStorage ? {
2055
+ upload: (opts) => mediaStorage.upload({
2056
+ key: opts.key,
2057
+ body: opts.body,
2058
+ contentType: opts.contentType
2059
+ }),
2060
+ download: (key) => mediaStorage.download(key),
2061
+ delete: (key) => mediaStorage.delete(key)
2062
+ } : void 0
2063
+ }, siteInfo));
2064
+ if (!sandboxRunner) return sandboxedPluginCache;
2065
+ if (!sandboxRunner.isAvailable()) {
2066
+ const reason = sandboxRunner.unavailableReason?.();
2067
+ console.warn(reason ? `EmDash: Plugin sandbox is configured but not available on this platform: ${reason}. Sandboxed plugins will not be loaded.` : "EmDash: Plugin sandbox is configured but not available on this platform. Sandboxed plugins will not be loaded. If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.");
2068
+ return sandboxedPluginCache;
2069
+ }
2070
+ if (deps.sandboxedPluginEntries.length === 0) return sandboxedPluginCache;
2071
+ if (deps.sandboxBypassed) return sandboxedPluginCache;
2072
+ for (const entry of deps.sandboxedPluginEntries) {
2073
+ const pluginKey = `${entry.id}:${entry.version}`;
2074
+ if (sandboxedPluginCache.has(pluginKey)) continue;
2075
+ try {
2076
+ const adminPages = entry.adminPages?.map((page) => ({
2077
+ path: page.path,
2078
+ label: page.label ?? page.path,
2079
+ icon: page.icon
2080
+ }));
2081
+ const adminWidgets = entry.adminWidgets?.map((widget) => ({
2082
+ id: widget.id,
2083
+ title: widget.title,
2084
+ size: widget.size === "full" || widget.size === "half" || widget.size === "third" ? widget.size : void 0
2085
+ }));
2086
+ const capabilities = normalizePluginCapabilities(entry.capabilities ?? []);
2087
+ const manifest = {
2088
+ id: entry.id,
2089
+ version: entry.version,
2090
+ capabilities,
2091
+ allowedHosts: entry.allowedHosts ?? [],
2092
+ storage: entry.storage ?? {},
2093
+ hooks: entry.hooks ?? [...HOOK_NAMES],
2094
+ routes: entry.routes ?? [],
2095
+ admin: {
2096
+ pages: adminPages,
2097
+ widgets: adminWidgets,
2098
+ settingsSchema: entry.settingsSchema,
2099
+ portableTextBlocks: entry.portableTextBlocks,
2100
+ fieldWidgets: entry.fieldWidgets,
2101
+ editorPanels: entry.editorPanels,
2102
+ editorActions: entry.editorActions
2103
+ },
2104
+ mcp: entry.mcp
2105
+ };
2106
+ const plugin = await sandboxRunner.load(manifest, entry.code);
2107
+ sandboxedPluginCache.set(pluginKey, plugin);
2108
+ sandboxedManifestCache.set(pluginKey, manifest);
2109
+ console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
2110
+ if (manifest.routes.length > 0) {
2111
+ const routeMetaMap = /* @__PURE__ */ new Map();
2112
+ for (const routeEntry of manifest.routes) {
2113
+ const normalized = normalizeManifestRoute$1(routeEntry);
2114
+ routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
2115
+ }
2116
+ sandboxedRouteMetaCache.set(entry.id, routeMetaMap);
2117
+ } else sandboxedRouteMetaCache.delete(entry.id);
2118
+ } catch (error) {
2119
+ console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
2120
+ }
2121
+ }
2122
+ return sandboxedPluginCache;
2123
+ }
2124
+ /**
2125
+ * Cold-start: load marketplace-installed plugins from site-local R2 storage
2126
+ *
2127
+ * Queries _plugin_state for source='marketplace' rows, fetches each bundle
2128
+ * from R2, and loads via SandboxRunner.
2129
+ */
2130
+ /**
2131
+ * Cold-start load of all active sandboxed plugins for one install
2132
+ * tier (marketplace or registry) from site-local R2.
2133
+ *
2134
+ * Mirrors {@link syncSandboxedSourcePlugins} but runs once at runtime
2135
+ * creation, before request traffic arrives; the sync method runs on
2136
+ * demand after install / update / uninstall handlers.
2137
+ */
2138
+ static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
2139
+ if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
2140
+ db,
2141
+ beforeContentWrite: () => assertSiteWriteAllowed(db),
2142
+ taxonomyWrite: createTaxonomyAccessWithWrite(db),
2143
+ now: deps.now,
2144
+ mediaStorage: {
2145
+ upload: (opts) => storage.upload({
2146
+ key: opts.key,
2147
+ body: opts.body,
2148
+ contentType: opts.contentType
2149
+ }),
2150
+ download: (key) => storage.download(key),
2151
+ delete: (key) => storage.delete(key)
2152
+ }
2153
+ }, siteInfo));
2154
+ if (deps.sandboxBypassed) return;
2155
+ if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
2156
+ const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
2157
+ try {
2158
+ const stateRepo = new PluginStateRepository(db);
2159
+ const plugins = source === "marketplace" ? await stateRepo.getMarketplacePlugins() : await stateRepo.getRegistryPlugins();
2160
+ for (const plugin of plugins) {
2161
+ if (plugin.status !== "active") continue;
2162
+ const version = source === "marketplace" ? plugin.marketplaceVersion ?? plugin.version : plugin.version;
2163
+ const pluginKey = `${plugin.pluginId}:${version}`;
2164
+ if (cache.has(pluginKey)) continue;
2165
+ try {
2166
+ const bundle = await loadBundleFromR2(storage, plugin.pluginId, version, source);
2167
+ if (!bundle) {
2168
+ console.warn(`EmDash: ${source} plugin ${plugin.pluginId}@${version} not found in R2`);
2169
+ continue;
2170
+ }
2171
+ const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
2172
+ cache.set(pluginKey, loaded);
2173
+ sandboxedManifestCache.set(pluginKey, bundle.manifest);
2174
+ keySet.add(pluginKey);
2175
+ marketplaceManifestCache.set(plugin.pluginId, {
2176
+ id: bundle.manifest.id,
2177
+ version: bundle.manifest.version,
2178
+ admin: bundle.manifest.admin,
2179
+ mcp: bundle.manifest.mcp,
2180
+ storage: bundle.manifest.storage,
2181
+ allowedHosts: bundle.manifest.allowedHosts,
2182
+ capabilities: bundle.manifest.capabilities
2183
+ });
2184
+ if (bundle.manifest.routes.length > 0) {
2185
+ const routeMeta = /* @__PURE__ */ new Map();
2186
+ for (const entry of bundle.manifest.routes) {
2187
+ const normalized = normalizeManifestRoute$1(entry);
2188
+ routeMeta.set(normalized.name, buildRouteMeta(normalized));
2189
+ }
2190
+ sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
2191
+ }
2192
+ console.log(`EmDash: Loaded ${source} plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(", ")}]`);
2193
+ } catch (error) {
2194
+ console.error(`EmDash: Failed to load ${source} plugin ${plugin.pluginId}:`, error);
2195
+ }
2196
+ }
2197
+ } catch {}
2198
+ }
2199
+ /**
2200
+ * Cold-start: load marketplace plugins in bypass mode (sandbox: false).
2201
+ *
2202
+ * Each active marketplace bundle is read, evaluated via data URL, adapted
2203
+ * with adaptSandboxEntry, and returned as a ResolvedPlugin. The caller is
2204
+ * responsible for merging these into allPipelinePlugins / configuredPlugins
2205
+ * BEFORE the hook pipeline is created, so hooks and routes register in
2206
+ * the trusted pipeline.
2207
+ *
2208
+ * Also caches manifest and route metadata so admin UI / getManifest() work.
2209
+ *
2210
+ * Returns ResolvedPlugins to be merged into the pipeline.
2211
+ */
2212
+ static async loadMarketplacePluginsBypassed(db, storage) {
2213
+ const resolved = [];
2214
+ try {
2215
+ const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
2216
+ if (marketplacePlugins.length === 0) return resolved;
2217
+ console.info("EmDash: Sandbox disabled (sandbox: false). Marketplace plugins will run in-process without isolation.");
2218
+ const { adaptSandboxEntry } = await import("./plugins/adapt-sandbox-entry.mjs");
2219
+ for (const plugin of marketplacePlugins) {
2220
+ if (plugin.status !== "active") continue;
2221
+ const version = plugin.marketplaceVersion ?? plugin.version;
2222
+ try {
2223
+ const bundle = await loadBundleFromR2(storage, plugin.pluginId, version);
2224
+ if (!bundle) {
2225
+ console.warn(`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`);
2226
+ continue;
2227
+ }
2228
+ marketplaceManifestCache.set(plugin.pluginId, {
2229
+ id: bundle.manifest.id,
2230
+ version: bundle.manifest.version,
2231
+ admin: bundle.manifest.admin,
2232
+ mcp: bundle.manifest.mcp,
2233
+ storage: bundle.manifest.storage,
2234
+ allowedHosts: bundle.manifest.allowedHosts,
2235
+ capabilities: bundle.manifest.capabilities
2236
+ });
2237
+ if (bundle.manifest.routes.length > 0) {
2238
+ const routeMeta = /* @__PURE__ */ new Map();
2239
+ for (const entry of bundle.manifest.routes) {
2240
+ const normalized = normalizeManifestRoute$1(entry);
2241
+ routeMeta.set(normalized.name, buildRouteMeta(normalized));
2242
+ }
2243
+ sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
2244
+ }
2245
+ const pluginModule = await import(`data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`);
2246
+ const adapted = adaptSandboxEntry(pluginModule.default ?? pluginModule, {
2247
+ id: bundle.manifest.id,
2248
+ version: bundle.manifest.version,
2249
+ entrypoint: "",
2250
+ capabilities: bundle.manifest.capabilities ?? [],
2251
+ allowedHosts: bundle.manifest.allowedHosts ?? [],
2252
+ storage: bundle.manifest.storage ?? {},
2253
+ adminPages: bundle.manifest.admin?.pages,
2254
+ adminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({
2255
+ id: w.id,
2256
+ title: w.title,
2257
+ size: w.size === "full" || w.size === "half" || w.size === "third" ? w.size : void 0
2258
+ })),
2259
+ settingsSchema: bundle.manifest.admin?.settingsSchema,
2260
+ editorPanels: bundle.manifest.admin?.editorPanels,
2261
+ editorActions: bundle.manifest.admin?.editorActions
2262
+ });
2263
+ resolved.push(adapted);
2264
+ console.log(`EmDash: Loaded marketplace plugin ${plugin.pluginId}@${version} in-process (sandbox bypassed)`);
2265
+ } catch (error) {
2266
+ console.error(`EmDash: Failed to load marketplace plugin ${plugin.pluginId} in-process:`, error);
2267
+ }
2268
+ }
2269
+ } catch {}
2270
+ return resolved;
2271
+ }
2272
+ /**
2273
+ * Resolve exclusive hook selections on startup.
2274
+ *
2275
+ * Delegates to the shared resolveExclusiveHooks() in hooks.ts.
2276
+ * The runtime version considers all pipeline providers as "active" since
2277
+ * the pipeline was already built from only active/enabled plugins.
2278
+ */
2279
+ static async resolveExclusiveHooks(pipeline, db, deps) {
2280
+ if (pipeline.getRegisteredExclusiveHooks().length === 0) return;
2281
+ let optionsRepo;
2282
+ try {
2283
+ optionsRepo = new OptionsRepository(db);
2284
+ } catch {
2285
+ return;
2286
+ }
2287
+ const preferredHints = /* @__PURE__ */ new Map();
2288
+ for (const entry of deps.sandboxedPluginEntries) if (entry.preferred && entry.preferred.length > 0) preferredHints.set(entry.id, entry.preferred);
2289
+ await resolveExclusiveHooks({
2290
+ pipeline,
2291
+ isActive: () => true,
2292
+ getOption: (key) => optionsRepo.get(key),
2293
+ getOptions: (keys) => optionsRepo.getMany(keys),
2294
+ setOption: (key, value) => optionsRepo.set(key, value),
2295
+ deleteOption: async (key) => {
2296
+ await optionsRepo.delete(key);
2297
+ },
2298
+ preferredHints,
2299
+ fallbackProviders: new Set([DEFAULT_COMMENT_MODERATOR_PLUGIN_ID])
2300
+ });
2301
+ }
2302
+ /**
2303
+ * Build the admin manifest from the live database.
2304
+ *
2305
+ * Used by the admin UI (sidebar collections, content editor field
2306
+ * dispatch, manifest endpoint) and by WordPress import — it's never
2307
+ * read on a public request, so this isn't on any anonymous hot path.
2308
+ *
2309
+ * No cross-request cache. The previous worker-isolate cache produced
2310
+ * a class of cross-isolate staleness bugs (#776, #873, #876, #877)
2311
+ * because Cloudflare Workers keeps multiple warm isolates per region
2312
+ * and there's no fan-out primitive to invalidate them in step. The
2313
+ * cache existed to amortize an N+1 schema query pattern; now that
2314
+ * `listCollectionsWithFields()` does the same work in two queries,
2315
+ * the rebuild is fast enough to pay on every admin request.
2316
+ *
2317
+ * Within a single request, `requestCached` deduplicates concurrent
2318
+ * callers (the manifest endpoint and an admin SSR template, say).
2319
+ */
2320
+ getManifest() {
2321
+ return requestCached("emdash:manifest", () => this._buildManifest());
2322
+ }
2323
+ /**
2324
+ * Build the manifest from the database.
2325
+ *
2326
+ * Constant query shapes via `listCollectionsWithFields()` — one query
2327
+ * for collections, one batched query for fields (chunked at
2328
+ * `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter
2329
+ * limit). Typical sites stay well under the chunk threshold, so this
2330
+ * is two queries in practice; never N+1.
2331
+ */
2332
+ async _buildManifest() {
2333
+ const manifestCollections = await buildManifestCollections({}, this.db);
2334
+ const manifestPlugins = {};
2335
+ for (const plugin of this.configuredPlugins) {
2336
+ const status = this.pluginStates.get(plugin.id);
2337
+ const enabled = status === void 0 || status === "active";
2338
+ const hasAdminEntry = !!plugin.admin?.entry;
2339
+ const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
2340
+ const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
2341
+ const hasEditorExtensions = (plugin.admin?.editorPanels?.length ?? 0) > 0 || (plugin.admin?.editorActions?.length ?? 0) > 0;
2342
+ let adminMode = "none";
2343
+ if (hasAdminEntry) adminMode = "react";
2344
+ else if (hasAdminPages || hasWidgets || hasEditorExtensions) adminMode = "blocks";
2345
+ manifestPlugins[plugin.id] = {
2346
+ version: plugin.version,
2347
+ enabled,
2348
+ adminMode,
2349
+ adminPages: plugin.admin?.pages ?? [],
2350
+ dashboardWidgets: plugin.admin?.widgets ?? [],
2351
+ portableTextBlocks: plugin.admin?.portableTextBlocks,
2352
+ fieldWidgets: plugin.admin?.fieldWidgets,
2353
+ editorPanels: plugin.admin?.editorPanels,
2354
+ editorActions: plugin.admin?.editorActions
2355
+ };
2356
+ }
2357
+ for (const entry of this.sandboxedPluginEntries) {
2358
+ const status = this.pluginStates.get(entry.id);
2359
+ const enabled = status === void 0 || status === "active";
2360
+ const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
2361
+ const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
2362
+ const hasEditorExtensions = (entry.editorPanels?.length ?? 0) > 0 || (entry.editorActions?.length ?? 0) > 0;
2363
+ manifestPlugins[entry.id] = {
2364
+ version: entry.version,
2365
+ enabled,
2366
+ sandboxed: true,
2367
+ adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
2368
+ adminPages: entry.adminPages ?? [],
2369
+ dashboardWidgets: entry.adminWidgets ?? [],
2370
+ portableTextBlocks: entry.portableTextBlocks,
2371
+ fieldWidgets: entry.fieldWidgets,
2372
+ editorPanels: entry.editorPanels,
2373
+ editorActions: entry.editorActions
2374
+ };
2375
+ }
2376
+ for (const [pluginId, meta] of marketplaceManifestCache) {
2377
+ if (manifestPlugins[pluginId]) continue;
2378
+ const enabled = this.pluginStates.get(pluginId) === "active";
2379
+ const pages = meta.admin?.pages;
2380
+ const widgets = meta.admin?.widgets;
2381
+ const hasAdminPages = (pages?.length ?? 0) > 0;
2382
+ const hasWidgets = (widgets?.length ?? 0) > 0;
2383
+ const hasEditorExtensions = (meta.admin?.editorPanels?.length ?? 0) > 0 || (meta.admin?.editorActions?.length ?? 0) > 0;
2384
+ manifestPlugins[pluginId] = {
2385
+ version: meta.version,
2386
+ enabled,
2387
+ sandboxed: true,
2388
+ adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
2389
+ adminPages: pages ?? [],
2390
+ dashboardWidgets: widgets ?? [],
2391
+ editorPanels: meta.admin?.editorPanels,
2392
+ editorActions: meta.admin?.editorActions
2393
+ };
2394
+ }
2395
+ let manifestTaxonomies = [];
2396
+ let taxonomyDefinitionLocales = [];
2397
+ try {
2398
+ const rows = await selectTaxonomyDefs(this.db).orderBy("d.name").execute();
2399
+ taxonomyDefinitionLocales = rows.map((row) => row.locale);
2400
+ manifestTaxonomies = rows.map((row) => ({
2401
+ id: row.id,
2402
+ name: row.name,
2403
+ label: row.label,
2404
+ labelSingular: row.label_singular ?? void 0,
2405
+ hierarchical: row.hierarchical === 1,
2406
+ collections: parseStringArray(row.collections).toSorted(),
2407
+ locale: row.locale,
2408
+ translationGroup: row.translation_group ?? row.id
2409
+ }));
2410
+ } catch (error) {
2411
+ console.debug("EmDash: Could not load taxonomy definitions:", error);
2412
+ }
2413
+ try {
2414
+ const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
2415
+ await warnAboutUnconfiguredTaxonomyLocales(this.db, configuredLocales, taxonomyDefinitionLocales);
2416
+ } catch (error) {
2417
+ console.warn("[i18n] taxonomy locale diagnostic failed:", error);
2418
+ }
2419
+ const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
2420
+ const authMode = getAuthMode(this.config);
2421
+ const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
2422
+ const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
2423
+ const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
2424
+ defaultLocale: i18nConfig.defaultLocale,
2425
+ locales: i18nConfig.locales,
2426
+ prefixDefaultLocale: i18nConfig.prefixDefaultLocale
2427
+ } : void 0;
2428
+ const registryConfig = resolveRegistryConfigForSandbox({
2429
+ registry: this.config.registry,
2430
+ experimentalRegistry: this.config.experimental?.registry
2431
+ });
2432
+ const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(registryConfig.input, { fieldPrefix: registryConfig.fieldPrefix });
2433
+ if (registryConfigurationError) console.error(`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`);
2434
+ return {
2435
+ version: VERSION,
2436
+ commit: COMMIT,
2437
+ astroVersion: this.config.astroVersion,
2438
+ hash: manifestHash,
2439
+ collections: manifestCollections,
2440
+ plugins: manifestPlugins,
2441
+ taxonomies: manifestTaxonomies,
2442
+ authMode: authModeValue,
2443
+ i18n,
2444
+ contentLocale: {
2445
+ defaultLocale: i18nConfig?.defaultLocale ?? "en",
2446
+ implicit: i18nConfig === null
2447
+ },
2448
+ marketplace: !!this.config.marketplace,
2449
+ sandboxEnabled: this.runtimeDeps.sandboxEnabled && this.runtimeDeps.sandboxBypassed !== true,
2450
+ registry,
2451
+ registryConfigurationError
2452
+ };
2453
+ }
2454
+ /**
2455
+ * Verify and repair FTS indexes on demand. Runs at most once per worker
2456
+ * lifetime.
2457
+ *
2458
+ * Originally called from `EmDashRuntime.create()`, but on a busy D1 link
2459
+ * (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold
2460
+ * start for a modest-sized site — more than every other init phase
2461
+ * combined. Anonymous public reads never touch the search write path,
2462
+ * so the cost isn't paid back for the vast majority of requests.
2463
+ *
2464
+ * Instead, search endpoints call this lazily: the first request that
2465
+ * actually needs the index pays the verify cost (usually fast — no
2466
+ * rebuild needed), everyone else runs cold-free.
2467
+ *
2468
+ * Uses the runtime's singleton database (`this._db`) rather than the
2469
+ * request-scoped DB. Verify reads only, but `rebuildIndex` writes, and
2470
+ * a GET search request on D1 carries a `first-unconstrained` session
2471
+ * that's free to route at a read replica — unsafe for writes. The
2472
+ * singleton always goes through the default binding, which the D1
2473
+ * adapter will promote to `first-primary` for write statements.
2474
+ *
2475
+ * Safe to call concurrently: repeated callers share the same in-flight
2476
+ * promise. Errors are swallowed internally so callers don't need to
2477
+ * defend against FTS not existing yet (pre-setup).
2478
+ */
2479
+ async ensureSearchHealthy() {
2480
+ if (!isSqlite(this._db)) return;
2481
+ try {
2482
+ await singleFlightCached(this._searchHealthCache, async () => {
2483
+ try {
2484
+ const repaired = await new FTSManager(this._db).verifyAndRepairAll();
2485
+ if (repaired > 0) console.log(`Repaired ${repaired} corrupted FTS index(es)`);
2486
+ } catch {}
2487
+ }, {
2488
+ anchor: (promise) => after(() => promise),
2489
+ ownerTimeoutMs: 3e4
2490
+ });
2491
+ } catch {}
2492
+ }
2493
+ async handleContentList(collection, params) {
2494
+ return handleContentList(this.db, collection, params);
2495
+ }
2496
+ async handleContentAuthors(collection) {
2497
+ return handleContentAuthors(this.db, collection);
2498
+ }
2499
+ async handleContentGet(collection, id, locale, referenceOptions) {
2500
+ const result = await handleContentGet(this.db, collection, id, locale, referenceOptions);
2501
+ return this.hydrateDraftData(result);
2502
+ }
2503
+ async handleContentGetIncludingTrashed(collection, id, locale) {
2504
+ const result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);
2505
+ return this.hydrateDraftData(result);
2506
+ }
2507
+ /**
2508
+ * If the response item has a `draftRevisionId`, replace `item.data` with
2509
+ * the draft revision's data and expose the original published values as
2510
+ * `liveData`. This makes the content_get / content_update round-trip
2511
+ * intuitive — read returns the latest content the caller has saved
2512
+ * (their pending draft), with the previously-published values still
2513
+ * accessible for compare-style flows.
2514
+ *
2515
+ * No-op when no draft exists or the response is an error.
2516
+ */
2517
+ async hydrateDraftData(result, options = {}) {
2518
+ if (!result || typeof result !== "object") return result;
2519
+ const r = result;
2520
+ if (!r.success || !r.data?.item) return result;
2521
+ const item = r.data.item;
2522
+ const draftRevisionId = typeof item.draftRevisionId === "string" ? item.draftRevisionId : null;
2523
+ if (!draftRevisionId) return result;
2524
+ try {
2525
+ const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
2526
+ if (!revision) {
2527
+ if (options.strict) throw new Error(`Draft revision not found: ${draftRevisionId}`);
2528
+ return result;
2529
+ }
2530
+ const liveData = item.data && typeof item.data === "object" ? item.data : {};
2531
+ const revisionData = {};
2532
+ for (const [key, value] of Object.entries(revision.data)) if (!key.startsWith("_")) revisionData[key] = value;
2533
+ const mergedData = {
2534
+ ...liveData,
2535
+ ...revisionData
2536
+ };
2537
+ return {
2538
+ ...result,
2539
+ data: {
2540
+ ...r.data,
2541
+ item: {
2542
+ ...item,
2543
+ ...options.includeStagedSlug && typeof revision.data._slug === "string" ? { slug: revision.data._slug } : {},
2544
+ data: mergedData,
2545
+ liveData
2546
+ }
2547
+ }
2548
+ };
2549
+ } catch (error) {
2550
+ if (options.strict) throw error;
2551
+ console.error("[emdash] draft hydration failed:", error);
2552
+ return result;
2553
+ }
2554
+ }
2555
+ async handleContentCreate(collection, body, options = {}) {
2556
+ const actor = body.actor ? { ...body.actor } : void 0;
2557
+ let locale;
2558
+ try {
2559
+ locale = resolveContentCreateLocale(body.locale);
2560
+ } catch (error) {
2561
+ return {
2562
+ success: false,
2563
+ error: {
2564
+ code: "VALIDATION_ERROR",
2565
+ message: error instanceof Error ? error.message : "Invalid locale"
2566
+ }
2567
+ };
2568
+ }
2569
+ let translationSource = null;
2570
+ let sharedFieldSlugs = [];
2571
+ if (body.translationOf) {
2572
+ const repo = new ContentRepository(this.db);
2573
+ const source = await repo.findById(collection, body.translationOf);
2574
+ if (!source) return {
2575
+ success: false,
2576
+ error: {
2577
+ code: "NOT_FOUND",
2578
+ message: "Translation source content not found"
2579
+ }
2580
+ };
2581
+ translationSource = source;
2582
+ sharedFieldSlugs = (await this.db.selectFrom("_emdash_fields as field").innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id").select([
2583
+ "field.slug",
2584
+ "field.type",
2585
+ "field.validation"
2586
+ ]).where("collection.slug", "=", collection).where("field.translatable", "=", 0).execute()).filter((field) => !isStoragelessFieldRow(field)).map((field) => field.slug);
2587
+ if ((await repo.findTranslations(collection, source.translationGroup ?? source.id)).some((sibling) => sibling.locale?.toLowerCase() === locale.toLowerCase())) return {
2588
+ success: false,
2589
+ error: {
2590
+ code: "CONFLICT",
2591
+ message: `Translation already exists in locale "${locale}" for this content item`
2592
+ }
2593
+ };
2594
+ }
2595
+ let processedData = body.data;
2596
+ if (!options.skipSaveHooks && this.hooks.hasHooks("content:beforeSave")) try {
2597
+ processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true, void 0, actor)).content;
2598
+ } catch (error) {
2599
+ return beforeSaveFailure(error);
2600
+ }
2601
+ if (translationSource) {
2602
+ processedData = { ...processedData };
2603
+ for (const slug of sharedFieldSlugs) if (Object.hasOwn(translationSource.data, slug)) processedData[slug] = translationSource.data[slug];
2604
+ else delete processedData[slug];
2605
+ }
2606
+ const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null);
2607
+ const resolvedBlockTypes = collectionInfo?.fields.some((field) => field.type === "blocks") ? await resolveBlockTypes(this.db) : void 0;
2608
+ if (collectionInfo) try {
2609
+ processedData = await normalizeBlocksData(this.db, collectionInfo, processedData, translationSource?.data, {
2610
+ migrateBlocks: body.migrateBlocks,
2611
+ replaceBlocks: body.replaceBlocks
2612
+ }, false, resolvedBlockTypes);
2613
+ } catch (error) {
2614
+ if (error instanceof SchemaError) return {
2615
+ success: false,
2616
+ error: {
2617
+ code: error.code,
2618
+ message: error.message,
2619
+ details: error.details
2620
+ }
2621
+ };
2622
+ throw error;
2623
+ }
2624
+ processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, resolvedBlockTypes);
2625
+ const { validateContentData } = await import("./validation-DvYJtv4k.mjs");
2626
+ const validation = await validateContentData(this.db, collection, processedData, { partial: false });
2627
+ if (!validation.ok) return {
2628
+ success: false,
2629
+ error: validation.error
2630
+ };
2631
+ const { actor: _discardedActor, migrateBlocks: _discardedMigrateBlocks, replaceBlocks: _discardedReplaceBlocks, ...contentBody } = body;
2632
+ const result = await handleContentCreate(this.db, collection, {
2633
+ ...contentBody,
2634
+ data: processedData,
2635
+ locale,
2636
+ authorId: body.authorId,
2637
+ bylines: body.bylines
2638
+ });
2639
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
2640
+ if (!options.skipSaveHooks && result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true, actor, options.excludeAfterSavePluginId);
2641
+ return result;
2642
+ }
2643
+ async handleContentUpdate(collection, id, body) {
2644
+ const actor = body.actor ? { ...body.actor } : void 0;
2645
+ const repo = new ContentRepository(this.db);
2646
+ const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
2647
+ const resolvedId = resolvedItem?.id ?? id;
2648
+ if (body._rev) {
2649
+ if (!resolvedItem) return {
2650
+ success: false,
2651
+ error: {
2652
+ code: "NOT_FOUND",
2653
+ message: `Content item not found: ${id}`
2654
+ }
2655
+ };
2656
+ const revCheck = validateRev(body._rev, resolvedItem);
2657
+ if (!revCheck.valid) return {
2658
+ success: false,
2659
+ error: {
2660
+ code: "CONFLICT",
2661
+ message: revCheck.message
2662
+ }
2663
+ };
2664
+ }
2665
+ const { _rev: _discardedRev, actor: _discardedActor, migrateBlocks, replaceBlocks, ...bodyWithoutRev } = body;
2666
+ const blockWriteOptions = {
2667
+ migrateBlocks,
2668
+ replaceBlocks
2669
+ };
2670
+ const collectionInfo = bodyWithoutRev.data || bodyWithoutRev.references ? await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null) : null;
2671
+ const knownFieldSlugs = new Set((collectionInfo?.fields ?? []).map((f) => f.slug));
2672
+ if (bodyWithoutRev.data && collectionInfo) {
2673
+ const changed = changedStoragelessDataKeys(new Set(collectionInfo.fields.filter(isStoragelessField).map((field) => field.slug)), bodyWithoutRev.data, resolvedItem?.data ?? {});
2674
+ if (changed.length > 0) return storagelessDataKeyError(changed);
2675
+ }
2676
+ let processedData = bodyWithoutRev.data;
2677
+ let resolvedBlockTypes;
2678
+ if (bodyWithoutRev.data) {
2679
+ if (this.hooks.hasHooks("content:beforeSave")) try {
2680
+ processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false, resolvedItem?.id, actor)).content;
2681
+ } catch (error) {
2682
+ return beforeSaveFailure(error);
2683
+ }
2684
+ processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, void 0, false);
2685
+ if (collectionInfo?.fields) processedData = await this.dropUnknownKeysAlreadyStored(processedData, resolvedItem, knownFieldSlugs);
2686
+ if (collectionInfo?.fields.some((field) => field.type === "blocks" && Object.hasOwn(processedData, field.slug))) resolvedBlockTypes = await resolveBlockTypes(this.db);
2687
+ if (!collectionInfo?.supports?.includes("revisions")) {
2688
+ if (collectionInfo) try {
2689
+ processedData = await normalizeBlocksData(this.db, collectionInfo, processedData, resolvedItem?.data, blockWriteOptions, true, resolvedBlockTypes);
2690
+ processedData = await this.normalizeFieldValues(collection, processedData, collectionInfo, resolvedBlockTypes);
2691
+ } catch (error) {
2692
+ if (error instanceof SchemaError) return {
2693
+ success: false,
2694
+ error: {
2695
+ code: error.code,
2696
+ message: error.message,
2697
+ details: error.details
2698
+ }
2699
+ };
2700
+ throw error;
2701
+ }
2702
+ const { validateContentData } = await import("./validation-DvYJtv4k.mjs");
2703
+ const validation = await validateContentData(this.db, collection, processedData, { partial: true });
2704
+ if (!validation.ok) return {
2705
+ success: false,
2706
+ error: validation.error
2707
+ };
2708
+ }
2709
+ }
2710
+ let usesDraftRevisions = false;
2711
+ let draftStorageChanged = false;
2712
+ if (processedData || bodyWithoutRev.references) {
2713
+ if (collectionInfo?.supports?.includes("revisions")) {
2714
+ usesDraftRevisions = true;
2715
+ let stagedReferences;
2716
+ if (bodyWithoutRev.references) {
2717
+ stagedReferences = {};
2718
+ for (const [fieldSlug, selectedIds] of Object.entries(bodyWithoutRev.references)) {
2719
+ const resolved = await resolveReferenceSelection(this.db, collection, resolvedId, fieldSlug, selectedIds);
2720
+ if (!resolved.success) return {
2721
+ success: false,
2722
+ error: resolved.error
2723
+ };
2724
+ stagedReferences[fieldSlug] = resolved.data.groups;
2725
+ }
2726
+ }
2727
+ const revisionRepo = new RevisionRepository(this.db);
2728
+ let existing = await repo.findById(collection, resolvedId);
2729
+ for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
2730
+ let baseData;
2731
+ if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
2732
+ else baseData = existing.data;
2733
+ let attemptData = processedData ?? {};
2734
+ try {
2735
+ attemptData = await normalizeBlocksData(this.db, collectionInfo, attemptData, baseData, blockWriteOptions, true, resolvedBlockTypes);
2736
+ attemptData = await this.normalizeFieldValues(collection, attemptData, collectionInfo, resolvedBlockTypes);
2737
+ } catch (error) {
2738
+ if (error instanceof SchemaError) return {
2739
+ success: false,
2740
+ error: {
2741
+ code: error.code,
2742
+ message: error.message,
2743
+ details: error.details
2744
+ }
2745
+ };
2746
+ throw error;
2747
+ }
2748
+ const { validateContentData } = await import("./validation-DvYJtv4k.mjs");
2749
+ const validation = await validateContentData(this.db, collection, attemptData, { partial: true });
2750
+ if (!validation.ok) return {
2751
+ success: false,
2752
+ error: validation.error
2753
+ };
2754
+ const mergedData = collectionInfo?.fields ? keepKnownFields({
2755
+ ...baseData,
2756
+ ...attemptData
2757
+ }, knownFieldSlugs) : {
2758
+ ...baseData,
2759
+ ...attemptData
2760
+ };
2761
+ if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
2762
+ if (stagedReferences) mergedData[STAGED_REFERENCES_KEY] = mergeStagedReferences(baseData, stagedReferences);
2763
+ const revision = await revisionRepo.create({
2764
+ collection,
2765
+ entryId: resolvedId,
2766
+ data: mergedData,
2767
+ authorId: actor?.id
2768
+ });
2769
+ let staged;
2770
+ try {
2771
+ staged = await repo.replaceDraftRevision(collection, resolvedId, revision.id, existing);
2772
+ } catch (error) {
2773
+ try {
2774
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
2775
+ } catch (cleanupError) {
2776
+ console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
2777
+ }
2778
+ throw error;
2779
+ }
2780
+ if (!staged) {
2781
+ try {
2782
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
2783
+ } catch (cleanupError) {
2784
+ console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
2785
+ }
2786
+ if (body._rev || attempt === MAX_DRAFT_STAGE_ATTEMPTS - 1) return {
2787
+ success: false,
2788
+ error: {
2789
+ code: "CONFLICT",
2790
+ message: new ContentMutationConflictError().message
2791
+ }
2792
+ };
2793
+ existing = await repo.findById(collection, resolvedId);
2794
+ continue;
2795
+ }
2796
+ draftStorageChanged = true;
2797
+ processedData = attemptData;
2798
+ if (bodyWithoutRev.skipRevision && existing.draftRevisionId) try {
2799
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, existing.draftRevisionId);
2800
+ } catch (error) {
2801
+ console.error(`[emdash] Failed to clean up superseded revision ${existing.draftRevisionId}:`, error);
2802
+ }
2803
+ else after(async () => {
2804
+ try {
2805
+ await revisionRepo.pruneQueuedEntry(collection, resolvedId, revision.id, 50);
2806
+ } catch (error) {
2807
+ console.error(`[revisions] Failed to prune revisions for ${collection}/${resolvedId}:`, error);
2808
+ }
2809
+ });
2810
+ break;
2811
+ }
2812
+ }
2813
+ }
2814
+ const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
2815
+ const result = usesDraftRevisions && !liveMetaTouched ? await handleContentGet(this.db, collection, resolvedId) : await handleContentUpdate(this.db, collection, resolvedId, {
2816
+ ...bodyWithoutRev,
2817
+ data: usesDraftRevisions ? void 0 : processedData,
2818
+ slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
2819
+ references: usesDraftRevisions ? void 0 : bodyWithoutRev.references,
2820
+ authorId: bodyWithoutRev.authorId,
2821
+ bylines: bodyWithoutRev.bylines
2822
+ });
2823
+ const liveContentChanged = usesDraftRevisions ? liveMetaTouched : Boolean(processedData || bodyWithoutRev.slug !== void 0 || bodyWithoutRev.references || liveMetaTouched);
2824
+ const hydrated = await this.hydrateDraftData(result);
2825
+ if (hydrated.success && hydrated.data) {
2826
+ const contentIdsToRefresh = [resolvedId];
2827
+ if (!usesDraftRevisions && processedData) contentIdsToRefresh.push(...await this.findSyncedSiblingsForUsageRefresh(collection, resolvedId, hydrated.data.item.translationGroup, processedData));
2828
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);
2829
+ } else if (draftStorageChanged) try {
2830
+ await markContentMediaUsageCollectionStale(this.db, collection, "CONTENT_USAGE_STALE");
2831
+ } catch (error) {
2832
+ console.error(`[media-usage] Failed to mark ${collection} stale:`, error);
2833
+ }
2834
+ if (hydrated.success && hydrated.data) this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false, actor);
2835
+ if (hydrated.success) return {
2836
+ ...hydrated,
2837
+ liveContentChanged
2838
+ };
2839
+ return hydrated;
2840
+ }
2841
+ async handleContentDelete(collection, id) {
2842
+ if (this.hooks.hasHooks("content:beforeDelete")) {
2843
+ const { allowed } = await this.hooks.runContentBeforeDelete(id, collection);
2844
+ if (!allowed) return {
2845
+ success: false,
2846
+ error: {
2847
+ code: "DELETE_BLOCKED",
2848
+ message: "Delete blocked by plugin hook"
2849
+ }
2850
+ };
2851
+ }
2852
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
2853
+ const result = await handleContentDelete(this.db, collection, id);
2854
+ if (result.success) {
2855
+ await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
2856
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);
2857
+ }
2858
+ if (result.success) this.runAfterDeleteHooks(id, collection, false);
2859
+ return result;
2860
+ }
2861
+ async handleContentListTrashed(collection, params = {}) {
2862
+ return handleContentListTrashed(this.db, collection, params);
2863
+ }
2864
+ async handleContentRestore(collection, id, options = {}) {
2865
+ const result = await handleContentRestore(this.db, collection, id, { _rev: options._rev });
2866
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
2867
+ if (result.success) this.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
2868
+ return result;
2869
+ }
2870
+ async handleContentPermanentDelete(collection, id) {
2871
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
2872
+ const result = await handleContentPermanentDelete(this.db, collection, id);
2873
+ if (result.success) {
2874
+ await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
2875
+ await this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);
2876
+ }
2877
+ if (result.success) this.runAfterDeleteHooks(id, collection, true);
2878
+ return result;
2879
+ }
2880
+ async handleContentCountTrashed(collection, params = {}) {
2881
+ return handleContentCountTrashed(this.db, collection, params);
2882
+ }
2883
+ async handleContentDuplicate(collection, id, authorId) {
2884
+ const result = await handleContentDuplicate(this.db, collection, id, authorId);
2885
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
2886
+ return result;
2887
+ }
2888
+ pluginVersionedResult(result) {
2889
+ if (!result.success) throw Object.assign(new Error(result.error.message), { code: result.error.code });
2890
+ if (!result.data._rev) throw new Error("Content action returned no revision");
2891
+ return {
2892
+ item: toPluginContentItem(result.data.item),
2893
+ _rev: result.data._rev
2894
+ };
2895
+ }
2896
+ async runPluginContentAction(pluginId, action, collection, id, fn, invocationId, invalidateContentCache) {
2897
+ let invocationInvalidator;
2898
+ if (invocationId) {
2899
+ const invocationKey = this.pluginInvocationKey(pluginId, invocationId);
2900
+ if (!this.activePluginInvocations.has(invocationKey) && !this.releasedPluginInvocations.has(invocationKey)) throw Object.assign(/* @__PURE__ */ new Error("Plugin content action invocation has expired"), { code: "CONTENT_ACTION_INVOCATION_EXPIRED" });
2901
+ invocationInvalidator = this.pluginInvocationCacheInvalidators.get(invocationKey);
2902
+ }
2903
+ const recordWrite = await assertSiteWriteAllowed(this.db);
2904
+ const repo = new ContentRepository(this.db);
2905
+ const resolvedId = (action === "restore" ? await repo.findByIdOrSlugIncludingTrashed(collection, id) : await repo.findByIdOrSlug(collection, id))?.id ?? id;
2906
+ const key = this.pluginContentActionKey(collection, resolvedId);
2907
+ if ((this.activePluginContentActions.get(key) ?? 0) > 0) throw Object.assign(/* @__PURE__ */ new Error("Plugin content action re-entered itself"), { code: "CONTENT_ACTION_REENTRANT" });
2908
+ this.retainPluginContentAction(key);
2909
+ try {
2910
+ const result = await fn(resolvedId);
2911
+ await recordWrite();
2912
+ const invalidator = invalidateContentCache ?? invocationInvalidator ?? this.pluginContentCacheInvalidator;
2913
+ if (invalidator) try {
2914
+ await invalidator([collection, resolvedId]);
2915
+ } catch (error) {
2916
+ console.error(`[plugin-content-action] Cache invalidation failed for ${collection}/${resolvedId}:`, error);
2917
+ }
2918
+ return result;
2919
+ } finally {
2920
+ this.releasePluginContentAction(key);
2921
+ }
2922
+ }
2923
+ pluginContentActionKey(collection, id) {
2924
+ return JSON.stringify([collection, id]);
2925
+ }
2926
+ retainPluginContentAction(key) {
2927
+ this.activePluginContentActions.set(key, (this.activePluginContentActions.get(key) ?? 0) + 1);
2928
+ }
2929
+ releasePluginContentAction(key) {
2930
+ const remaining = (this.activePluginContentActions.get(key) ?? 1) - 1;
2931
+ if (remaining > 0) this.activePluginContentActions.set(key, remaining);
2932
+ else this.activePluginContentActions.delete(key);
2933
+ }
2934
+ async pluginGetVersioned(_pluginId, collection, id) {
2935
+ const result = await this.handleContentGet(collection, id);
2936
+ if (!result.success) {
2937
+ if (result.error.code === "NOT_FOUND") return null;
2938
+ throw Object.assign(new Error(result.error.message), { code: result.error.code });
2939
+ }
2940
+ if (!result.data._rev) throw new Error("Content read returned no revision");
2941
+ return {
2942
+ item: toPluginContentItem(result.data.item),
2943
+ _rev: result.data._rev
2944
+ };
2945
+ }
2946
+ async pluginGetTrashedVersioned(_pluginId, collection, id) {
2947
+ const result = await this.handleContentGetIncludingTrashed(collection, id);
2948
+ if (!result.success) {
2949
+ if (result.error.code === "NOT_FOUND") return null;
2950
+ throw Object.assign(new Error(result.error.message), { code: result.error.code });
2951
+ }
2952
+ if (!await new ContentRepository(this.db).isTrashed(collection, result.data.item.id)) return null;
2953
+ if (!result.data._rev) throw new Error("Trashed content read returned no revision");
2954
+ return {
2955
+ item: toPluginContentItem(result.data.item),
2956
+ _rev: result.data._rev
2957
+ };
2958
+ }
2959
+ pluginPublish(pluginId, collection, id, options, invocationId, invalidateContentCache) {
2960
+ return this.runPluginContentAction(pluginId, "publish", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentPublish(collection, resolvedId, {
2961
+ _rev: options._rev,
2962
+ origin: {
2963
+ source: "plugin",
2964
+ pluginId
2965
+ },
2966
+ pluginInvocationId: invocationId
2967
+ })), invocationId, invalidateContentCache);
2968
+ }
2969
+ pluginUnpublish(pluginId, collection, id, options, invocationId, invalidateContentCache) {
2970
+ return this.runPluginContentAction(pluginId, "unpublish", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentUnpublish(collection, resolvedId, {
2971
+ _rev: options._rev,
2972
+ origin: {
2973
+ source: "plugin",
2974
+ pluginId
2975
+ },
2976
+ pluginInvocationId: invocationId
2977
+ })), invocationId, invalidateContentCache);
2978
+ }
2979
+ pluginSchedule(pluginId, collection, id, options, invocationId, invalidateContentCache) {
2980
+ return this.runPluginContentAction(pluginId, "schedule", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentSchedule(collection, resolvedId, options.scheduledAt, {
2981
+ _rev: options._rev,
2982
+ origin: {
2983
+ source: "plugin",
2984
+ pluginId
2985
+ },
2986
+ pluginInvocationId: invocationId
2987
+ })), invocationId, invalidateContentCache);
2988
+ }
2989
+ pluginUnschedule(pluginId, collection, id, options, invocationId, invalidateContentCache) {
2990
+ return this.runPluginContentAction(pluginId, "unschedule", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentUnschedule(collection, resolvedId, {
2991
+ ...options,
2992
+ origin: {
2993
+ source: "plugin",
2994
+ pluginId
2995
+ },
2996
+ pluginInvocationId: invocationId
2997
+ })), invocationId, invalidateContentCache);
2998
+ }
2999
+ pluginRestore(pluginId, collection, id, options, invocationId, invalidateContentCache) {
3000
+ return this.runPluginContentAction(pluginId, "restore", collection, id, async (resolvedId) => this.pluginVersionedResult(await this.handleContentRestore(collection, resolvedId, {
3001
+ ...options,
3002
+ origin: {
3003
+ source: "plugin",
3004
+ pluginId
3005
+ },
3006
+ pluginInvocationId: invocationId
3007
+ })), invocationId, invalidateContentCache);
3008
+ }
3009
+ async checkContentPolicy(name, collection, id, options, rejectionCode, failureCode, scheduledAt) {
3010
+ let current = await handleContentGet(this.db, collection, id);
3011
+ if (!current.success) return {
3012
+ allowed: false,
3013
+ revision: options._rev ?? "",
3014
+ error: current.error
3015
+ };
3016
+ const revision = current.data._rev;
3017
+ if (!revision) return {
3018
+ allowed: false,
3019
+ revision: options._rev ?? "",
3020
+ error: {
3021
+ code: failureCode,
3022
+ message: "Failed to read content revision"
3023
+ }
3024
+ };
3025
+ if (options._rev !== void 0 && options._rev !== revision) return {
3026
+ allowed: false,
3027
+ revision,
3028
+ error: {
3029
+ code: "CONFLICT",
3030
+ message: "Revision precondition did not match"
3031
+ }
3032
+ };
3033
+ if (!this.hooks.hasHooks(name)) return {
3034
+ allowed: true,
3035
+ revision
3036
+ };
3037
+ if (name !== "content:beforeUnpublish") try {
3038
+ current = await this.hydrateDraftData(current, {
3039
+ includeStagedSlug: true,
3040
+ strict: true
3041
+ });
3042
+ } catch (error) {
3043
+ console.error(`[content-policy] ${name} draft hydration failed:`, error);
3044
+ return {
3045
+ allowed: false,
3046
+ revision,
3047
+ error: {
3048
+ code: failureCode,
3049
+ message: `Failed to read content for ${name} policy`
3050
+ }
3051
+ };
3052
+ }
3053
+ const origin = options.origin ?? { source: "system" };
3054
+ const actor = options.actor && (origin.source === "api" || origin.source === "mcp" || origin.source === "visual-editor") ? {
3055
+ ...options.actor,
3056
+ source: origin.source
3057
+ } : void 0;
3058
+ try {
3059
+ const decision = await this.hooks.runContentPolicy(name, {
3060
+ content: contentItemToRecord(current.data.item),
3061
+ collection,
3062
+ origin,
3063
+ actor,
3064
+ ...scheduledAt === void 0 ? {} : { scheduledAt }
3065
+ });
3066
+ if (decision.cancellation) return {
3067
+ allowed: false,
3068
+ revision,
3069
+ error: {
3070
+ code: rejectionCode,
3071
+ message: decision.cancellation.reason
3072
+ },
3073
+ cancellation: decision.cancellation
3074
+ };
3075
+ return {
3076
+ allowed: true,
3077
+ revision
3078
+ };
3079
+ } catch (error) {
3080
+ console.error(`[content-policy] ${name} failed:`, error);
3081
+ return {
3082
+ allowed: false,
3083
+ revision,
3084
+ error: {
3085
+ code: failureCode,
3086
+ message: `Failed to run ${name} policy`
3087
+ }
3088
+ };
3089
+ }
3090
+ }
3091
+ async getScheduledPolicyRejectionRevision(collection, id) {
3092
+ try {
3093
+ return (await new OptionsRepository(this.db).getVersioned(scheduledPolicyRejectionKey(collection, id)))?.revision;
3094
+ } catch (error) {
3095
+ console.error(`[content-policy] Failed to read scheduled rejection for ${collection}/${id}:`, error);
3096
+ return;
3097
+ }
3098
+ }
3099
+ async clearScheduledPolicyRejection(collection, id, expectedRevision) {
3100
+ if (expectedRevision === void 0) return;
3101
+ try {
3102
+ await new OptionsRepository(this.db).compareAndDelete(scheduledPolicyRejectionKey(collection, id), expectedRevision);
3103
+ } catch (error) {
3104
+ console.error(`[content-policy] Failed to clear scheduled rejection for ${collection}/${id}:`, error);
3105
+ }
3106
+ }
3107
+ createScheduledPolicyRejection(collection, id, cancellation) {
3108
+ return {
3109
+ collection,
3110
+ id,
3111
+ pluginId: cancellation.pluginId,
3112
+ reason: cancellation.reason,
3113
+ rejectedAt: (this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
3114
+ };
3115
+ }
3116
+ async handleContentPublish(collection, id, options = {}) {
3117
+ const policy = await this.checkContentPolicy("content:beforePublish", collection, id, options, "PUBLISH_REJECTED", "CONTENT_PUBLISH_ERROR");
3118
+ if (!policy.allowed) {
3119
+ if (policy.cancellation && options.origin?.source === "scheduler") {
3120
+ const unscheduled = await handleScheduledPolicyRejection(this.db, collection, id, {
3121
+ _rev: policy.revision,
3122
+ rejection: this.createScheduledPolicyRejection(collection, id, policy.cancellation)
3123
+ });
3124
+ if (!unscheduled.success) return unscheduled;
3125
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [unscheduled.data.item.id]);
3126
+ this.runAfterUnscheduleHooks(contentItemToRecord(unscheduled.data.item), collection);
3127
+ }
3128
+ return {
3129
+ success: false,
3130
+ error: policy.error
3131
+ };
3132
+ }
3133
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
3134
+ const { actor: _actor, origin: _origin, pluginInvocationId: _pluginInvocationId, ...handlerOptions } = options;
3135
+ const result = await handleContentPublish(this.db, collection, id, {
3136
+ ...handlerOptions,
3137
+ _rev: policy.revision
3138
+ });
3139
+ if (result.success && result.data) {
3140
+ await this.clearScheduledPolicyRejection(collection, result.data.item.id, policyRejectionRevision);
3141
+ const { item } = result.data;
3142
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [item.id, ...await this.findSyncedSiblingsForUsageRefresh(collection, item.id, item.translationGroup, item.data, { absentAsCleared: true })]);
3143
+ }
3144
+ if (result.success && result.data) this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
3145
+ return result;
3146
+ }
3147
+ async handleContentUnpublish(collection, id, options = {}) {
3148
+ const policy = await this.checkContentPolicy("content:beforeUnpublish", collection, id, options, "UNPUBLISH_REJECTED", "CONTENT_UNPUBLISH_ERROR");
3149
+ if (!policy.allowed) return {
3150
+ success: false,
3151
+ error: policy.error
3152
+ };
3153
+ const result = await handleContentUnpublish(this.db, collection, id, { _rev: policy.revision });
3154
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3155
+ if (result.success && result.data) this.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
3156
+ return result;
3157
+ }
3158
+ async handleContentSchedule(collection, id, scheduledAt, options = {}) {
3159
+ const policy = await this.checkContentPolicy("content:beforeSchedule", collection, id, options, "SCHEDULE_REJECTED", "CONTENT_SCHEDULE_ERROR", scheduledAt);
3160
+ if (!policy.allowed) return {
3161
+ success: false,
3162
+ error: policy.error
3163
+ };
3164
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
3165
+ const result = await handleContentSchedule(this.db, collection, id, scheduledAt, this.runtimeDeps.now?.() ?? /* @__PURE__ */ new Date(), policy.revision);
3166
+ if (result.success && result.data) {
3167
+ await this.clearScheduledPolicyRejection(collection, result.data.item.id, policyRejectionRevision);
3168
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3169
+ }
3170
+ if (result.success && result.data) this.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
3171
+ return result;
3172
+ }
3173
+ async handleContentUnschedule(collection, id, options = {}) {
3174
+ const result = await handleContentUnschedule(this.db, collection, id, { _rev: options._rev });
3175
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3176
+ if (result.success && result.data) this.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection, options.origin?.source === "plugin" ? options.origin.pluginId : void 0, options.pluginInvocationId);
3177
+ return result;
3178
+ }
3179
+ async handleContentCountScheduled(collection) {
3180
+ return handleContentCountScheduled(this.db, collection);
3181
+ }
3182
+ async handleContentDiscardDraft(collection, id, options = {}) {
3183
+ const result = await handleContentDiscardDraft(this.db, collection, id, options);
3184
+ if (result.success && result.data) await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3185
+ return result;
3186
+ }
3187
+ async handleContentCompare(collection, id) {
3188
+ return handleContentCompare(this.db, collection, id);
3189
+ }
3190
+ async handleContentTranslations(collection, id) {
3191
+ return handleContentTranslations(this.db, collection, id);
3192
+ }
3193
+ async handleMediaList(params) {
3194
+ return handleMediaList(this.db, params);
3195
+ }
3196
+ async handleMediaGet(id) {
3197
+ return handleMediaGet(this.db, id);
3198
+ }
3199
+ async handleMediaUpload(input) {
3200
+ if (!this.storage) return {
3201
+ success: false,
3202
+ error: {
3203
+ code: "NO_STORAGE",
3204
+ message: "Storage not configured"
3205
+ }
3206
+ };
3207
+ const result = await handleMediaUpload(this.db, this.storage, input, { beforeUpload: async (file) => {
3208
+ if (!this.hooks.hasHooks("media:beforeUpload")) return file;
3209
+ return (await this.hooks.runMediaBeforeUpload(file)).file;
3210
+ } });
3211
+ if (result.success && !result.data.deduplicated && this.hooks.hasHooks("media:afterUpload")) {
3212
+ const item = result.data.item;
3213
+ await this.hooks.runMediaAfterUpload({
3214
+ id: item.id,
3215
+ filename: item.filename,
3216
+ mimeType: item.mimeType,
3217
+ size: item.size,
3218
+ url: item.url,
3219
+ createdAt: item.createdAt
3220
+ });
3221
+ }
3222
+ return result;
3223
+ }
3224
+ async handleMediaRegisterUpload(input) {
3225
+ if (!this.storage) return {
3226
+ success: false,
3227
+ error: {
3228
+ code: "NO_STORAGE",
3229
+ message: "Storage not configured"
3230
+ }
3231
+ };
3232
+ const result = await handleMediaRegisterUpload(this.db, this.storage, input);
3233
+ if (result.success && this.hooks.hasHooks("media:afterUpload")) {
3234
+ const item = result.data.item;
3235
+ const mediaItem = {
3236
+ id: item.id,
3237
+ filename: item.filename,
3238
+ mimeType: item.mimeType,
3239
+ size: item.size,
3240
+ url: `/media/${item.id}/${item.filename}`,
3241
+ createdAt: item.createdAt
3242
+ };
3243
+ this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
3244
+ }
3245
+ return result;
3246
+ }
3247
+ async handleMediaCreate(input) {
3248
+ let processedInput = input;
3249
+ if (this.hooks.hasHooks("media:beforeUpload")) {
3250
+ const hookResult = await this.hooks.runMediaBeforeUpload({
3251
+ name: input.filename,
3252
+ type: input.mimeType,
3253
+ size: input.size || 0
3254
+ });
3255
+ processedInput = {
3256
+ ...input,
3257
+ filename: hookResult.file.name,
3258
+ mimeType: hookResult.file.type,
3259
+ size: hookResult.file.size
3260
+ };
3261
+ }
3262
+ const result = await handleMediaCreate(this.db, processedInput);
3263
+ if (result.success && this.hooks.hasHooks("media:afterUpload")) {
3264
+ const item = result.data.item;
3265
+ const mediaItem = {
3266
+ id: item.id,
3267
+ filename: item.filename,
3268
+ mimeType: item.mimeType,
3269
+ size: item.size,
3270
+ url: `/media/${item.id}/${item.filename}`,
3271
+ createdAt: item.createdAt
3272
+ };
3273
+ this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
3274
+ }
3275
+ return result;
3276
+ }
3277
+ async handleMediaUpdate(id, input) {
3278
+ const result = await handleMediaUpdate(this.db, id, input);
3279
+ if (result.success) invalidateSiteSettingsCache();
3280
+ return result;
3281
+ }
3282
+ async handlePluginMediaMetadataUpdate(id, patch) {
3283
+ return updatePluginMediaMetadata(this.db, id, patch);
3284
+ }
3285
+ async handleMediaReplaceMetadata(id, expectedStorageKey, input) {
3286
+ const result = await handleMediaReplaceMetadata(this.db, id, expectedStorageKey, input);
3287
+ if (result.success) invalidateSiteSettingsCache();
3288
+ return result;
3289
+ }
3290
+ async handleMediaDelete(id) {
3291
+ const result = await handleMediaDelete(this.db, id, this.storage);
3292
+ if (result.success) invalidateSiteSettingsCache();
3293
+ return result;
3294
+ }
3295
+ commentHooks() {
3296
+ return {
3297
+ runBeforeCreate: (event) => this.hooks.runCommentBeforeCreate(event),
3298
+ runModerate: async (event) => {
3299
+ const result = await this.hooks.invokeExclusiveHook("comment:moderate", event);
3300
+ if (!result) return {
3301
+ status: "pending",
3302
+ reason: "No moderator configured"
3303
+ };
3304
+ if (result.error) {
3305
+ console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
3306
+ return {
3307
+ status: "pending",
3308
+ reason: "Moderation error"
3309
+ };
3310
+ }
3311
+ if (!isModerationDecision(result.result)) return {
3312
+ status: "pending",
3313
+ reason: "Invalid moderation result"
3314
+ };
3315
+ return result.result;
3316
+ },
3317
+ fireAfterCreate: (event) => {
3318
+ this.hooks.runCommentAfterCreate(event).catch((error) => console.error("[comments] afterCreate error:", error instanceof Error ? error.message : error));
3319
+ },
3320
+ fireAfterModerate: (event) => {
3321
+ return this.hooks.runCommentAfterModerate(event).catch((error) => console.error("[comments] afterModerate error:", error instanceof Error ? error.message : error));
3322
+ }
3323
+ };
3324
+ }
3325
+ async handleCommentCreate(input, settings, contentInfo) {
3326
+ return createComment(this.db, input, settings, this.commentHooks(), contentInfo);
3327
+ }
3328
+ async handlePublicCommentSubmission(collection, contentId, request, user) {
3329
+ return submitPublicComment(this, collection, contentId, request, user);
3330
+ }
3331
+ async sendCommentApproval(comment, request) {
3332
+ if (!comment || !this.email) return;
3333
+ try {
3334
+ const adminBaseUrl = await getSiteBaseUrl(this.db, request ?? new Request("https://plugin.invalid/"), this.config);
3335
+ const content = await lookupContentAuthor(this.db, comment.collection, comment.contentId);
3336
+ if (content?.author) await sendCommentNotification({
3337
+ email: this.email,
3338
+ comment,
3339
+ contentAuthor: content.author,
3340
+ adminBaseUrl
3341
+ });
3342
+ } catch (error) {
3343
+ console.error("[comments] notification error:", error instanceof Error ? error.message : error);
3344
+ }
3345
+ }
3346
+ async moderateCommentWithOrigin(id, status, expectedStatus, origin, request) {
3347
+ if (this.commentModerationInProgress.has(id)) {
3348
+ const current = await new CommentRepository(this.db).findById(id);
3349
+ if (current && current.status !== expectedStatus) throw new CommentStatusConflictError(current.status);
3350
+ throw Object.assign(/* @__PURE__ */ new Error("Comment moderation is already in progress"), { code: "COMMENT_MODERATION_IN_PROGRESS" });
3351
+ }
3352
+ this.commentModerationInProgress.add(id);
3353
+ try {
3354
+ return await moderateComment(this.db, id, status, expectedStatus, origin, this.commentHooks(), (comment) => this.sendCommentApproval(comment, request));
3355
+ } finally {
3356
+ this.commentModerationInProgress.delete(id);
3357
+ }
3358
+ }
3359
+ async handleCommentModerate(id, status, moderator, expectedStatus, request) {
3360
+ let expected = expectedStatus;
3361
+ if (!expected) {
3362
+ const current = await new CommentRepository(this.db).findById(id);
3363
+ if (!current || current.status === "trash") return null;
3364
+ expected = current.status;
3365
+ }
3366
+ return this.moderateCommentWithOrigin(id, status, expected, {
3367
+ source: "admin",
3368
+ userId: moderator.id,
3369
+ name: moderator.name
3370
+ }, request);
3371
+ }
3372
+ async handlePluginCommentModerate(pluginId, id, status, expectedStatus) {
3373
+ assertPluginCommentStatus(status, "status");
3374
+ assertPluginCommentStatus(expectedStatus, "expectedStatus");
3375
+ if (!await this.moderateCommentWithOrigin(id, status, expectedStatus, {
3376
+ source: "plugin",
3377
+ pluginId
3378
+ })) throw new Error(`Comment not found: ${id}`);
3379
+ const result = await createCommentAccess(this.db).get(id);
3380
+ if (!result) throw new Error(`Comment not found: ${id}`);
3381
+ return result;
3382
+ }
3383
+ async handleRevisionList(collection, entryId, params = {}) {
3384
+ return handleRevisionList(this.db, collection, entryId, params);
3385
+ }
3386
+ async handleRevisionGet(revisionId) {
3387
+ return handleRevisionGet(this.db, revisionId);
3388
+ }
3389
+ async handleRevisionRestore(revisionId, callerUserId) {
3390
+ const revisionRepo = new RevisionRepository(this.db);
3391
+ const revision = await revisionRepo.findById(revisionId);
3392
+ if (!revision) return {
3393
+ success: false,
3394
+ error: {
3395
+ code: "NOT_FOUND",
3396
+ message: `Revision not found: ${revisionId}`
3397
+ }
3398
+ };
3399
+ if (!((await this.schemaRegistry.getCollectionWithFields(revision.collection))?.supports?.includes("revisions") ?? false)) {
3400
+ const result = await handleRevisionRestore(this.db, revisionId, callerUserId);
3401
+ if (result.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
3402
+ return this.hydrateDraftData(result);
3403
+ }
3404
+ try {
3405
+ const newDraftId = await new ContentRepository(this.db).restoreDraftRevision(revision.collection, revision.entryId, revision.data, callerUserId);
3406
+ if (!newDraftId) return {
3407
+ success: false,
3408
+ error: {
3409
+ code: "NOT_FOUND",
3410
+ message: `Content item not found: ${revision.entryId}`
3411
+ }
3412
+ };
3413
+ after(async () => {
3414
+ try {
3415
+ await revisionRepo.pruneQueuedEntry(revision.collection, revision.entryId, newDraftId, 50);
3416
+ } catch (error) {
3417
+ console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
3418
+ }
3419
+ });
3420
+ const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
3421
+ const hydrated = await this.hydrateDraftData(refetched);
3422
+ if (hydrated.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
3423
+ return hydrated;
3424
+ } catch (error) {
3425
+ if (error instanceof ContentMutationConflictError) return {
3426
+ success: false,
3427
+ error: {
3428
+ code: "CONFLICT",
3429
+ message: error.message
3430
+ }
3431
+ };
3432
+ console.error("[emdash] revision restore failed:", error);
3433
+ return {
3434
+ success: false,
3435
+ error: {
3436
+ code: "REVISION_RESTORE_ERROR",
3437
+ message: "Failed to restore revision"
3438
+ }
3439
+ };
3440
+ }
3441
+ }
3442
+ async findSyncedSiblingsForUsageRefresh(collection, contentId, translationGroup, data, options = {}) {
3443
+ try {
3444
+ return await findNonTranslatableSiblingContentIds(this.db, collection, contentId, translationGroup, data, options);
3445
+ } catch (error) {
3446
+ console.error(`[media-usage] Failed to discover synced i18n siblings for ${collection}/${contentId}:`, error);
3447
+ try {
3448
+ await markContentMediaUsageCollectionStale(this.db, collection, "CONTENT_USAGE_REFRESH_ERROR");
3449
+ } catch (staleError) {
3450
+ console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
3451
+ }
3452
+ return [];
3453
+ }
3454
+ }
3455
+ async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
3456
+ for (const contentId of new Set(contentIds)) try {
3457
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") continue;
3458
+ await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
3459
+ } catch (error) {
3460
+ console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
3461
+ continue;
3462
+ }
3463
+ }
3464
+ async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
3465
+ try {
3466
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
3467
+ const result = await deleteContentMediaUsage(this.db, collection, contentId);
3468
+ if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
3469
+ } catch (error) {
3470
+ console.error(`[media-usage] Failed after permanent content delete ${collection}/${contentId}:`, error);
3471
+ }
3472
+ }
3473
+ getSandboxedAdminDefinition(pluginId) {
3474
+ const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
3475
+ if (entry) {
3476
+ const pages = (entry.adminPages ?? []).map((page) => normalizePluginPagePath(page.path));
3477
+ const imageHosts = allowedBrowserImageHosts(entry.capabilities, entry.allowedHosts);
3478
+ return {
3479
+ pages,
3480
+ widgets: (entry.adminWidgets ?? []).map((widget) => widget.id),
3481
+ policy: {
3482
+ pluginPagePaths: pages,
3483
+ allowedImageHosts: imageHosts
3484
+ }
3485
+ };
3486
+ }
3487
+ const manifest = marketplaceManifestCache.get(pluginId);
3488
+ if (!manifest) return null;
3489
+ const pages = (manifest.admin?.pages ?? []).map((page) => normalizePluginPagePath(page.path));
3490
+ const imageHosts = allowedBrowserImageHosts(manifest.capabilities ?? [], manifest.allowedHosts ?? []);
3491
+ return {
3492
+ pages,
3493
+ widgets: (manifest.admin?.widgets ?? []).map((widget) => widget.id),
3494
+ policy: {
3495
+ pluginPagePaths: pages,
3496
+ allowedImageHosts: imageHosts
3497
+ }
3498
+ };
3499
+ }
3500
+ resolvePluginUiContext(definition, body, request) {
3501
+ if (typeof body !== "object" || body === null || !("page" in body)) return {};
3502
+ const page = body.page;
3503
+ if (typeof page !== "string") return { error: {
3504
+ code: "INVALID_PLUGIN_UI_CONTEXT",
3505
+ message: "Plugin UI page must be a string"
3506
+ } };
3507
+ let surface;
3508
+ if (page.startsWith("widget:")) {
3509
+ if (!definition.widgets.includes(page.slice(7))) return { error: {
3510
+ code: "INVALID_PLUGIN_UI_CONTEXT",
3511
+ message: "Plugin dashboard widget is not declared"
3512
+ } };
3513
+ surface = "dashboard-widget";
3514
+ } else {
3515
+ if (!definition.pages.includes(normalizePluginPagePath(page))) return { error: {
3516
+ code: "INVALID_PLUGIN_UI_CONTEXT",
3517
+ message: "Plugin admin page is not declared"
3518
+ } };
3519
+ surface = "admin-page";
3520
+ }
3521
+ const locale = resolveLocale(request);
3522
+ return { context: {
3523
+ surface,
3524
+ locale,
3525
+ direction: getLocaleDir(locale)
3526
+ } };
3527
+ }
3528
+ validateSandboxedAdminResponse(pluginId, definition, result) {
3529
+ if (!result.success) return result;
3530
+ const validation = validateBlockResponse(result.data, definition.policy);
3531
+ if (validation.valid) return result;
3532
+ console.error(`EmDash: Sandboxed plugin ${pluginId} returned invalid Block Kit content:`, validation.errors);
3533
+ return {
3534
+ success: false,
3535
+ status: 502,
3536
+ error: {
3537
+ code: "INVALID_BLOCK_RESPONSE",
3538
+ message: "Plugin returned invalid Block Kit content"
3539
+ }
3540
+ };
3541
+ }
3542
+ getPluginEditorExtension(pluginId, kind, extensionId, collection) {
3543
+ if (!this.isPluginEnabled(pluginId)) return null;
3544
+ let panels;
3545
+ let actions;
3546
+ let pages = [];
3547
+ let capabilities = [];
3548
+ let allowedHosts = [];
3549
+ const configured = this.configuredPlugins.find((plugin) => plugin.id === pluginId);
3550
+ if (configured) {
3551
+ if (configured.admin.entry) return null;
3552
+ panels = configured.admin.editorPanels;
3553
+ actions = configured.admin.editorActions;
3554
+ pages = (configured.admin.pages ?? []).map((page) => page.path);
3555
+ capabilities = configured.capabilities;
3556
+ allowedHosts = configured.allowedHosts;
3557
+ } else {
3558
+ const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
3559
+ if (entry) {
3560
+ panels = entry.editorPanels;
3561
+ actions = entry.editorActions;
3562
+ pages = (entry.adminPages ?? []).map((page) => page.path);
3563
+ capabilities = entry.capabilities;
3564
+ allowedHosts = entry.allowedHosts;
3565
+ } else {
3566
+ const manifest = marketplaceManifestCache.get(pluginId);
3567
+ if (!manifest) return null;
3568
+ panels = manifest.admin?.editorPanels;
3569
+ actions = manifest.admin?.editorActions;
3570
+ pages = (manifest.admin?.pages ?? []).map((page) => page.path);
3571
+ capabilities = manifest.capabilities ?? [];
3572
+ allowedHosts = manifest.allowedHosts ?? [];
3573
+ }
3574
+ }
3575
+ const normalizedCapabilities = normalizePluginCapabilities(capabilities);
3576
+ const policy = {
3577
+ pluginPagePaths: pages,
3578
+ allowedImageHosts: allowedBrowserImageHosts(capabilities, allowedHosts)
3579
+ };
3580
+ if (kind === "panel") {
3581
+ const matches = panels?.filter((panel) => panel.id === extensionId) ?? [];
3582
+ const extension = matches[0];
3583
+ if (matches.length !== 1 || !extension || extension.collections && !extension.collections.includes(collection)) return null;
3584
+ return {
3585
+ kind,
3586
+ extension,
3587
+ policy,
3588
+ capabilities: normalizedCapabilities
3589
+ };
3590
+ }
3591
+ const matches = actions?.filter((action) => action.id === extensionId) ?? [];
3592
+ const extension = matches[0];
3593
+ if (matches.length !== 1 || !extension || extension.collections && !extension.collections.includes(collection) || extension.style === "danger" && !extension.confirm) return null;
3594
+ return {
3595
+ kind,
3596
+ extension,
3597
+ policy,
3598
+ capabilities: normalizedCapabilities
3599
+ };
3600
+ }
3601
+ async getPluginEditorDraftSchema(collection) {
3602
+ return this.schemaRegistry.getCollectionWithFields(collection);
3603
+ }
3604
+ validatePluginEditorExtensionResponse(pluginId, dispatch, result) {
3605
+ if (!result.success) return result;
3606
+ const validation = dispatch.kind === "panel" ? validateBlockResponse(result.data, dispatch.policy) : validateContentEditorActionResponse(result.data, dispatch.policy);
3607
+ if (validation.valid) return result;
3608
+ console.error(`EmDash: Plugin ${pluginId} returned an invalid content editor ${dispatch.kind} response:`, validation.errors);
3609
+ return {
3610
+ success: false,
3611
+ status: 502,
3612
+ error: {
3613
+ code: dispatch.kind === "panel" ? "INVALID_BLOCK_RESPONSE" : "INVALID_EDITOR_ACTION_RESPONSE",
3614
+ message: "Plugin returned an invalid editor extension response"
3615
+ }
3616
+ };
3617
+ }
3618
+ /**
3619
+ * Get route metadata for a plugin route without invoking the handler.
3620
+ * Used by the catch-all route to decide auth before dispatch.
3621
+ * Returns null if the plugin or route doesn't exist.
3622
+ */
3623
+ getPluginRouteMeta(pluginId, path) {
3624
+ if (!this.isPluginEnabled(pluginId)) return null;
3625
+ const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
3626
+ const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
3627
+ if (trustedPlugin) {
3628
+ const route = trustedPlugin.routes[routeKey];
3629
+ if (!route) return null;
3630
+ return buildRouteMeta(route);
3631
+ }
3632
+ const meta = sandboxedRouteMetaCache.get(pluginId);
3633
+ if (meta) {
3634
+ const routeMeta = meta.get(routeKey);
3635
+ if (routeMeta) return routeMeta;
3636
+ }
3637
+ if (routeKey === "admin") {
3638
+ const manifestMeta = marketplaceManifestCache.get(pluginId);
3639
+ if (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) return { public: false };
3640
+ const entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);
3641
+ if (entry?.adminPages?.length || entry?.adminWidgets?.length) return { public: false };
3642
+ }
3643
+ if (this.findSandboxedPlugin(pluginId)) return { public: false };
3644
+ return null;
3645
+ }
3646
+ /**
3647
+ * Resolve the settings schema for a runtime-installed (marketplace or
3648
+ * registry) plugin from its cached manifest. Returns `{}` for a known
3649
+ * plugin without a schema and `null` for unknown plugins, matching the
3650
+ * contract of `getPluginSettingsSchema` for build-time plugins.
3651
+ */
3652
+ getRuntimePluginSettingsSchema(pluginId) {
3653
+ const meta = marketplaceManifestCache.get(pluginId);
3654
+ if (!meta) return null;
3655
+ return meta.admin?.settingsSchema ?? {};
3656
+ }
3657
+ async handlePluginApiRoute(pluginId, method, path, request, user, invalidateContentCache, editorDispatch) {
3658
+ if (!this.isPluginEnabled(pluginId)) return {
3659
+ success: false,
3660
+ error: {
3661
+ code: "NOT_FOUND",
3662
+ message: `Plugin not enabled: ${pluginId}`
3663
+ }
3664
+ };
3665
+ const normalizedMethod = method.toUpperCase();
3666
+ const routeMeta = this.getPluginRouteMeta(pluginId, path);
3667
+ if (routeMeta?.methods && !routeMeta.methods.some((allowed) => allowed === normalizedMethod)) return {
3668
+ success: false,
3669
+ status: 405,
3670
+ error: {
3671
+ code: "METHOD_NOT_ALLOWED",
3672
+ message: "Method not allowed"
3673
+ }
3674
+ };
3675
+ const caller = user ? toRouteCallerInfo(user) : void 0;
3676
+ let body;
3677
+ try {
3678
+ body = await parseRouteInput(request, routeMeta?.request);
3679
+ } catch (error) {
3680
+ if (error instanceof PluginRouteRequestError) return {
3681
+ success: false,
3682
+ status: error.status,
3683
+ error: {
3684
+ code: "INVALID_PLUGIN_REQUEST",
3685
+ message: error.message
3686
+ }
3687
+ };
3688
+ throw error;
3689
+ }
3690
+ const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
3691
+ const adminDefinition = !editorDispatch && routeKey === "admin" ? this.getSandboxedAdminDefinition(pluginId) : null;
3692
+ const uiResult = adminDefinition ? this.resolvePluginUiContext(adminDefinition, body, request) : {};
3693
+ if (uiResult.error) return {
3694
+ success: false,
3695
+ status: 400,
3696
+ error: uiResult.error
3697
+ };
3698
+ const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
3699
+ if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
3700
+ const routeRegistry = new PluginRouteRegistry({
3701
+ ...this.pipelineFactoryOptions,
3702
+ contentActions: this.contentActionsWithInvalidator(invalidateContentCache),
3703
+ emailPipeline: this.email ?? void 0,
3704
+ cronReschedule: () => this.cronScheduler?.reschedule(),
3705
+ trustedProxyHeaders: getTrustedProxyHeaders(this.config)
3706
+ });
3707
+ routeRegistry.register(trustedPlugin);
3708
+ const result = await routeRegistry.invoke(pluginId, routeKey, {
3709
+ request,
3710
+ body,
3711
+ user: caller,
3712
+ ui: editorDispatch?.ui ?? uiResult.context
3713
+ });
3714
+ return editorDispatch ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result) : adminDefinition ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result) : result;
3715
+ }
3716
+ const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
3717
+ if (sandboxedPlugin) {
3718
+ const result = await this.handleSandboxedRoute(sandboxedPlugin, path, request, body, routeMeta ?? { public: false }, caller, editorDispatch?.ui ?? uiResult.context, invalidateContentCache);
3719
+ return editorDispatch ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result) : adminDefinition ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result) : result;
3720
+ }
3721
+ return {
3722
+ success: false,
3723
+ error: {
3724
+ code: "NOT_FOUND",
3725
+ message: `Plugin not found: ${pluginId}`
3726
+ }
3727
+ };
3728
+ }
3729
+ contentActionsWithInvalidator(invalidateContentCache) {
3730
+ const actions = this.pipelineFactoryOptions.contentActions;
3731
+ if (!actions || !invalidateContentCache) return actions;
3732
+ return {
3733
+ ...actions,
3734
+ begin: (pluginId, invocationId, invocationInvalidator) => actions.begin?.(pluginId, invocationId, invocationInvalidator ?? invalidateContentCache),
3735
+ publish: (pluginId, collection, id, options, invocationId) => actions.publish(pluginId, collection, id, options, invocationId, invalidateContentCache),
3736
+ unpublish: (pluginId, collection, id, options, invocationId) => actions.unpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
3737
+ schedule: (pluginId, collection, id, options, invocationId) => actions.schedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
3738
+ unschedule: (pluginId, collection, id, options, invocationId) => actions.unschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
3739
+ restore: (pluginId, collection, id, options, invocationId) => actions.restore(pluginId, collection, id, options, invocationId, invalidateContentCache)
3740
+ };
3741
+ }
3742
+ async getPluginMcpTools(pluginId) {
3743
+ const tools = [];
3744
+ const seen = /* @__PURE__ */ new Set();
3745
+ for (const plugin of this.configuredPlugins) {
3746
+ if (pluginId && plugin.id !== pluginId) continue;
3747
+ for (const [name, tool] of Object.entries(plugin.mcp?.tools ?? {})) {
3748
+ const route = plugin.routes[tool.route];
3749
+ if (!route || route.public || route.response === "raw" || !isJsonPostRouteContract(route) || !route.permission || !Object.hasOwn(Permissions, route.permission)) continue;
3750
+ const key = `${plugin.id}__${name}`;
3751
+ if (seen.has(key)) continue;
3752
+ seen.add(key);
3753
+ tools.push({
3754
+ pluginId: plugin.id,
3755
+ name,
3756
+ description: tool.description,
3757
+ route: tool.route,
3758
+ permission: route.permission,
3759
+ destructive: tool.destructive ?? false,
3760
+ inputSchema: tool.input,
3761
+ outputSchema: tool.output
3762
+ });
3763
+ }
3764
+ }
3765
+ const addManifestTools = (id, mcp) => {
3766
+ if (pluginId && id !== pluginId) return;
3767
+ for (const tool of mcp?.tools ?? []) {
3768
+ const key = `${id}__${tool.name}`;
3769
+ const routeMeta = this.getPluginRouteMeta(id, tool.route);
3770
+ if (seen.has(key) || !routeMeta || routeMeta.public || routeMeta.response === "raw" || !isJsonPostRouteContract(routeMeta) || routeMeta.permission !== tool.permission || !Object.hasOwn(Permissions, tool.permission)) continue;
3771
+ seen.add(key);
3772
+ tools.push({
3773
+ pluginId: id,
3774
+ name: tool.name,
3775
+ description: tool.description,
3776
+ route: tool.route,
3777
+ permission: tool.permission,
3778
+ destructive: tool.destructive,
3779
+ inputSchema: z.fromJSONSchema({ ...tool.inputSchema }),
3780
+ outputSchema: tool.outputSchema ? z.fromJSONSchema({ ...tool.outputSchema }) : void 0
3781
+ });
3782
+ }
3783
+ };
3784
+ for (const entry of this.sandboxedPluginEntries) addManifestTools(entry.id, entry.mcp);
3785
+ for (const [id, manifest] of marketplaceManifestCache) addManifestTools(id, manifest.mcp);
3786
+ return tools;
3787
+ }
3788
+ async getEnabledPluginMcpTools() {
3789
+ const [tools, states] = await Promise.all([this.getPluginMcpTools(), new PluginStateRepository(this.db).getAll()]);
3790
+ const stateByPlugin = new Map(states.map((state) => [state.pluginId, state]));
3791
+ return tools.filter((tool) => {
3792
+ const state = stateByPlugin.get(tool.pluginId);
3793
+ if (!state?.mcpToolsEnabled || state.status !== "active" || !this.isPluginEnabled(tool.pluginId)) return false;
3794
+ return state.mcpToolsConsent === this.serializePluginMcpConsent(tools, tool.pluginId);
3795
+ });
3796
+ }
3797
+ serializePluginMcpConsent(tools, pluginId) {
3798
+ return JSON.stringify(tools.filter((tool) => tool.pluginId === pluginId).map((tool) => ({
3799
+ name: tool.name,
3800
+ description: tool.description,
3801
+ route: tool.route,
3802
+ permission: tool.permission,
3803
+ destructive: tool.destructive,
3804
+ inputSchema: z.toJSONSchema(tool.inputSchema, { target: "draft-7" }),
3805
+ ...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
3806
+ })).toSorted((a, b) => a.name.localeCompare(b.name)));
3807
+ }
3808
+ async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller, invalidateContentCache) {
3809
+ const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
3810
+ const audit = new AuditRepository(this.db);
3811
+ const headers = new Headers(request.headers);
3812
+ headers.delete("content-length");
3813
+ headers.delete("content-encoding");
3814
+ const internalRequest = new Request(request.url, {
3815
+ method: "POST",
3816
+ headers,
3817
+ body: JSON.stringify(input)
3818
+ });
3819
+ const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller, invalidateContentCache);
3820
+ await audit.log({
3821
+ actorId,
3822
+ actorIp: requestMeta.ip ?? void 0,
3823
+ action: "plugin_tool_invoke",
3824
+ resourceType: "plugin_mcp_tool",
3825
+ resourceId: `${pluginId}__${toolName}`,
3826
+ details: {
3827
+ pluginId,
3828
+ tool: toolName,
3829
+ route
3830
+ },
3831
+ status: result.success ? "success" : "failure"
3832
+ });
3833
+ return result;
3834
+ }
3835
+ async handlePluginMcpDenied(pluginId, toolName, route, actorId, request, reason) {
3836
+ const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
3837
+ await new AuditRepository(this.db).log({
3838
+ actorId,
3839
+ actorIp: requestMeta.ip ?? void 0,
3840
+ action: "plugin_tool_invoke",
3841
+ resourceType: "plugin_mcp_tool",
3842
+ resourceId: `${pluginId}__${toolName}`,
3843
+ details: {
3844
+ pluginId,
3845
+ tool: toolName,
3846
+ route,
3847
+ reason
3848
+ },
3849
+ status: "denied"
3850
+ });
3851
+ }
3852
+ findSandboxedPlugin(pluginId) {
3853
+ for (const [key, plugin] of this.sandboxedPlugins) if (key.startsWith(pluginId + ":")) return plugin;
3854
+ }
3855
+ /**
3856
+ * Drop incoming keys that have no matching collection field when the entry
3857
+ * already stores them in its live data or current draft revision.
3858
+ *
3859
+ * Draft revisions keep the full `data` JSON, so deleting a field can strand
3860
+ * the old value; this lets a read-then-write save succeed without allowing
3861
+ * genuinely unknown keys.
3862
+ */
3863
+ async dropUnknownKeysAlreadyStored(data, existing, knownFieldSlugs) {
3864
+ if (!existing) return data;
3865
+ let stored = existing.data ?? {};
3866
+ if (existing.draftRevisionId) {
3867
+ const draft = await new RevisionRepository(this.db).findById(existing.draftRevisionId).catch(() => null);
3868
+ if (draft?.data) stored = draft.data;
3869
+ }
3870
+ const stale = staleStoredKeys(data, stored, knownFieldSlugs);
3871
+ if (stale.length === 0) return data;
3872
+ const result = { ...data };
3873
+ for (const key of stale) delete result[key];
3874
+ return result;
3875
+ }
3876
+ /**
3877
+ * Normalize field values in content data before validation.
3878
+ * Turns a blank string in an array-valued field into `null`, and fills
3879
+ * missing image/file dimensions, storageKey, mimeType, and filename from providers.
3880
+ */
3881
+ async normalizeFieldValues(collection, data, preloaded, preloadedBlockTypes, includeBlocks = true) {
3882
+ let collectionInfo = preloaded;
3883
+ if (collectionInfo === void 0) try {
3884
+ collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
3885
+ } catch {
3886
+ return data;
3887
+ }
3888
+ if (!collectionInfo?.fields) return data;
3889
+ const result = { ...data };
3890
+ for (const field of collectionInfo.fields) {
3891
+ const value = result[field.slug];
3892
+ if (ARRAY_FIELD_TYPES.has(field.type) && typeof value === "string" && !value.trim()) result[field.slug] = null;
3893
+ }
3894
+ const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
3895
+ const repeaterFields = collectionInfo.fields.filter((f) => f.type === "repeater" && Array.isArray(f.validation?.subFields));
3896
+ const blockFields = includeBlocks ? collectionInfo.fields.filter((field) => field.type === "blocks") : [];
3897
+ if (imageFields.length === 0 && repeaterFields.length === 0 && blockFields.length === 0) return result;
3898
+ const getProvider = (id) => this.getMediaProvider(id);
3899
+ for (const field of imageFields) {
3900
+ const value = result[field.slug];
3901
+ if (value == null) continue;
3902
+ try {
3903
+ const normalized = field.type === "image" ? await normalizeImageValue(value, getProvider) : await normalizeMediaValue(value, getProvider);
3904
+ if (normalized) result[field.slug] = normalized;
3905
+ } catch {}
3906
+ }
3907
+ for (const field of repeaterFields) {
3908
+ const value = result[field.slug];
3909
+ if (!Array.isArray(value)) continue;
3910
+ const mediaSubFieldSlugs = (field.validation?.subFields ?? []).filter((sub) => sub.type === "image").map((sub) => sub.slug);
3911
+ if (mediaSubFieldSlugs.length === 0) continue;
3912
+ const items = value;
3913
+ result[field.slug] = await Promise.all(items.map(async (item) => {
3914
+ if (!isRecord(item)) return item;
3915
+ const normalizedItem = { ...item };
3916
+ for (const slug of mediaSubFieldSlugs) {
3917
+ const subValue = normalizedItem[slug];
3918
+ if (subValue == null) continue;
3919
+ try {
3920
+ const normalized = await normalizeImageValue(subValue, getProvider);
3921
+ if (normalized) normalizedItem[slug] = normalized;
3922
+ } catch {}
3923
+ }
3924
+ return normalizedItem;
3925
+ }));
3926
+ }
3927
+ if (blockFields.length > 0) {
3928
+ const blockTypes = preloadedBlockTypes ?? await resolveBlockTypes(this.db);
3929
+ for (const field of blockFields) {
3930
+ const value = result[field.slug];
3931
+ if (!Array.isArray(value)) continue;
3932
+ result[field.slug] = await Promise.all(value.map(async (block) => {
3933
+ if (!isRecord(block) || typeof block._type !== "string") return block;
3934
+ const version = blockTypes.get(block._type)?.versions.find((candidate) => candidate.version === block._version);
3935
+ if (!version || version.unsupportedTypes?.length) return block;
3936
+ const normalizedBlock = { ...block };
3937
+ for (const nestedField of version.fields) {
3938
+ const nestedValue = normalizedBlock[nestedField.slug];
3939
+ if (nestedValue == null) continue;
3940
+ try {
3941
+ if (nestedField.type === "image") {
3942
+ const normalized = await normalizeImageValue(nestedValue, getProvider);
3943
+ if (normalized) normalizedBlock[nestedField.slug] = normalized;
3944
+ } else if (nestedField.type === "file") {
3945
+ const normalized = await normalizeMediaValue(nestedValue, getProvider);
3946
+ if (normalized) normalizedBlock[nestedField.slug] = normalized;
3947
+ } else if (nestedField.type === "repeater" && Array.isArray(nestedValue)) {
3948
+ const imageSlugs = (nestedField.validation?.subFields ?? []).filter((subField) => subField.type === "image").map((subField) => subField.slug);
3949
+ normalizedBlock[nestedField.slug] = await Promise.all(nestedValue.map(async (item) => {
3950
+ if (!isRecord(item)) return item;
3951
+ const normalizedItem = { ...item };
3952
+ for (const slug of imageSlugs) try {
3953
+ const normalized = await normalizeImageValue(normalizedItem[slug], getProvider);
3954
+ if (normalized) normalizedItem[slug] = normalized;
3955
+ } catch {
3956
+ continue;
3957
+ }
3958
+ return normalizedItem;
3959
+ }));
3960
+ }
3961
+ } catch {
3962
+ continue;
3963
+ }
3964
+ }
3965
+ return normalizedBlock;
3966
+ }));
3967
+ }
3968
+ }
3969
+ return result;
3970
+ }
3971
+ runAfterSaveHooks(content, collection, isNew, actor, excludePluginId) {
3972
+ after(async () => {
3973
+ if (this.hooks.hasHooks("content:afterSave")) try {
3974
+ await this.hooks.runContentAfterSave(content, collection, isNew, actor, excludePluginId);
3975
+ } catch (err) {
3976
+ console.error("EmDash afterSave hook error:", err);
3977
+ }
3978
+ });
3979
+ }
3980
+ runAfterDeleteHooks(id, collection, permanent) {
3981
+ after(async () => {
3982
+ if (this.hooks.hasHooks("content:afterDelete")) try {
3983
+ await this.hooks.runContentAfterDelete(id, collection, permanent);
3984
+ } catch (err) {
3985
+ console.error("EmDash afterDelete hook error:", err);
3986
+ }
3987
+ });
3988
+ }
3989
+ runDeferredContentHook(name, content, collection, afterPluginId, pluginInvocationId) {
3990
+ const label = name.slice(8);
3991
+ const invoke = async () => {
3992
+ if (this.hooks.hasHooks(name)) try {
3993
+ switch (name) {
3994
+ case "content:afterPublish":
3995
+ await this.hooks.runContentAfterPublish(content, collection);
3996
+ break;
3997
+ case "content:afterUnpublish":
3998
+ await this.hooks.runContentAfterUnpublish(content, collection);
3999
+ break;
4000
+ case "content:afterRestore":
4001
+ await this.hooks.runContentAfterRestore(content, collection);
4002
+ break;
4003
+ case "content:afterSchedule":
4004
+ await this.hooks.runContentAfterSchedule(content, collection);
4005
+ break;
4006
+ case "content:afterUnschedule":
4007
+ await this.hooks.runContentAfterUnschedule(content, collection);
4008
+ break;
4009
+ }
4010
+ } catch (err) {
4011
+ console.error(`EmDash ${label} hook error:`, err);
4012
+ }
4013
+ };
4014
+ const contentId = typeof content.id === "string" ? content.id : void 0;
4015
+ const guardKey = afterPluginId && contentId ? this.pluginContentActionKey(collection, contentId) : void 0;
4016
+ const invokeWithGuard = async () => {
4017
+ if (guardKey) this.retainPluginContentAction(guardKey);
4018
+ try {
4019
+ await invoke();
4020
+ } finally {
4021
+ if (guardKey) this.releasePluginContentAction(guardKey);
4022
+ }
4023
+ };
4024
+ if (afterPluginId && pluginInvocationId && this.findSandboxedPlugin(afterPluginId)) {
4025
+ const key = this.pluginInvocationKey(afterPluginId, pluginInvocationId);
4026
+ if (!this.activePluginInvocations.has(key) || this.releasedPluginInvocations.has(key)) {
4027
+ after(invokeWithGuard);
4028
+ return;
4029
+ }
4030
+ const pending = this.pendingPluginAfterHooks.get(key) ?? [];
4031
+ pending.push(invokeWithGuard);
4032
+ this.pendingPluginAfterHooks.set(key, pending);
4033
+ return;
4034
+ }
4035
+ after(invokeWithGuard);
4036
+ }
4037
+ pluginInvocationKey(pluginId, invocationId) {
4038
+ return JSON.stringify([pluginId, invocationId]);
4039
+ }
4040
+ beginPluginInvocation(pluginId, invocationId, invalidateContentCache) {
4041
+ const key = this.pluginInvocationKey(pluginId, invocationId);
4042
+ this.pendingPluginAfterHooks.delete(key);
4043
+ this.releasedPluginInvocations.delete(key);
4044
+ if (invalidateContentCache) this.pluginInvocationCacheInvalidators.set(key, invalidateContentCache);
4045
+ else this.pluginInvocationCacheInvalidators.delete(key);
4046
+ this.activePluginInvocations.add(key);
4047
+ }
4048
+ flushPluginAfterHooks(pluginId, invocationId, final = true) {
4049
+ if (!invocationId) return Promise.resolve();
4050
+ const key = this.pluginInvocationKey(pluginId, invocationId);
4051
+ this.releasedPluginInvocations.add(key);
4052
+ const pending = this.pendingPluginAfterHooks.get(key) ?? [];
4053
+ this.pendingPluginAfterHooks.delete(key);
4054
+ for (const invoke of pending) after(invoke);
4055
+ if (final) {
4056
+ this.releasedPluginInvocations.delete(key);
4057
+ this.activePluginInvocations.delete(key);
4058
+ this.pluginInvocationCacheInvalidators.delete(key);
4059
+ } else {
4060
+ const timer = setTimeout(() => {
4061
+ this.releasedPluginInvocations.delete(key);
4062
+ this.activePluginInvocations.delete(key);
4063
+ this.pluginInvocationCacheInvalidators.delete(key);
4064
+ }, PLUGIN_INVOCATION_RELEASE_GRACE_MS);
4065
+ if (typeof timer === "object") timer.unref();
4066
+ }
4067
+ return Promise.resolve();
4068
+ }
4069
+ runAfterPublishHooks(content, collection, afterPluginId, pluginInvocationId) {
4070
+ this.runDeferredContentHook("content:afterPublish", content, collection, afterPluginId, pluginInvocationId);
4071
+ }
4072
+ runAfterUnpublishHooks(content, collection, afterPluginId, pluginInvocationId) {
4073
+ this.runDeferredContentHook("content:afterUnpublish", content, collection, afterPluginId, pluginInvocationId);
4074
+ }
4075
+ runAfterRestoreHooks(content, collection, afterPluginId, pluginInvocationId) {
4076
+ this.runDeferredContentHook("content:afterRestore", content, collection, afterPluginId, pluginInvocationId);
4077
+ }
4078
+ runAfterScheduleHooks(content, collection, afterPluginId, pluginInvocationId) {
4079
+ this.runDeferredContentHook("content:afterSchedule", content, collection, afterPluginId, pluginInvocationId);
4080
+ }
4081
+ runAfterUnscheduleHooks(content, collection, afterPluginId, pluginInvocationId) {
4082
+ this.runDeferredContentHook("content:afterUnschedule", content, collection, afterPluginId, pluginInvocationId);
4083
+ }
4084
+ async handleSandboxedRoute(plugin, path, request, body, routeMeta, user, ui, invalidateContentCache) {
4085
+ const routeName = path.replace(LEADING_SLASH_PATTERN, "");
4086
+ try {
4087
+ const declaredHeaders = routeMeta.request ? routeMeta.request.headers ?? [] : void 0;
4088
+ const headers = sanitizeHeadersForSandbox(request.headers, declaredHeaders);
4089
+ const meta = extractRequestMeta(request, this.config);
4090
+ return {
4091
+ success: true,
4092
+ data: await plugin.invokeRoute(routeName, body, {
4093
+ url: request.url,
4094
+ method: request.method,
4095
+ headers,
4096
+ meta,
4097
+ user,
4098
+ ui
4099
+ }, { invalidateContentCache })
4100
+ };
4101
+ } catch (error) {
4102
+ if (error instanceof PluginRouteRequestError) return {
4103
+ success: false,
4104
+ status: error.status,
4105
+ error: {
4106
+ code: "INVALID_PLUGIN_REQUEST",
4107
+ message: error.message
4108
+ }
4109
+ };
4110
+ console.error(`EmDash: Sandboxed plugin route error:`, error);
4111
+ const sandboxRouteError = getSandboxRouteErrorDetails(error);
4112
+ if (sandboxRouteError) return {
4113
+ success: false,
4114
+ status: sandboxRouteError.status,
4115
+ error: {
4116
+ code: sandboxRouteError.code,
4117
+ message: sandboxRouteError.message
4118
+ }
4119
+ };
4120
+ return {
4121
+ success: false,
4122
+ error: {
4123
+ code: "ROUTE_ERROR",
4124
+ message: error instanceof Error ? error.message : String(error)
4125
+ }
4126
+ };
4127
+ }
4128
+ }
4129
+ /**
4130
+ * Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext
4131
+ * object so results are collected once per page context per request, even when
4132
+ * multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)
4133
+ * request contributions from the same page.
4134
+ */
4135
+ pageContributionCache = /* @__PURE__ */ new WeakMap();
4136
+ /**
4137
+ * Collect all page contributions (metadata + fragments) in a single pass.
4138
+ * Results are cached by page context object identity.
4139
+ */
4140
+ async collectPageContributions(page) {
4141
+ const cached = this.pageContributionCache.get(page);
4142
+ if (cached) return cached;
4143
+ const promise = this.doCollectPageContributions(page);
4144
+ this.pageContributionCache.set(page, promise);
4145
+ return promise;
4146
+ }
4147
+ async doCollectPageContributions(page) {
4148
+ const metadata = [];
4149
+ const fragments = [];
4150
+ if (this.hooks.hasHooks("page:metadata")) {
4151
+ const results = await this.hooks.runPageMetadata({ page });
4152
+ for (const r of results) metadata.push(...r.contributions);
4153
+ }
4154
+ if (this.hooks.hasHooks("page:fragments")) {
4155
+ const results = await this.hooks.runPageFragments({ page });
4156
+ for (const r of results) fragments.push(...r.contributions);
4157
+ }
4158
+ return {
4159
+ metadata,
4160
+ fragments
4161
+ };
4162
+ }
4163
+ /**
4164
+ * Collect page metadata contributions from trusted and sandboxed plugins.
4165
+ * Delegates to the single-pass collector and returns the metadata portion.
4166
+ */
4167
+ async collectPageMetadata(page) {
4168
+ const { metadata } = await this.collectPageContributions(page);
4169
+ return metadata;
4170
+ }
4171
+ /**
4172
+ * Collect page fragment contributions from trusted plugins only.
4173
+ * Delegates to the single-pass collector and returns the fragments portion.
4174
+ */
4175
+ async collectPageFragments(page) {
4176
+ const { fragments } = await this.collectPageContributions(page);
4177
+ return fragments;
4178
+ }
4179
+ isPluginEnabled(pluginId) {
4180
+ const status = this.pluginStates.get(pluginId);
4181
+ return status === void 0 || status === "active";
4182
+ }
4183
+ };
4184
+ /**
4185
+ * Normalize an image field value together with its `darkVariant`. The media
4186
+ * normalizer only keeps the media item's own keys, so the variant is normalized
4187
+ * separately and reattached.
4188
+ */
4189
+ async function normalizeImageValue(value, getProvider) {
4190
+ const primary = await normalizePrimaryImageValue(value, getProvider);
4191
+ if (!primary || !isRecord(value) || value.darkVariant == null) return primary;
4192
+ const darkVariant = await normalizeMediaValue(value.darkVariant, getProvider);
4193
+ return darkVariant ? {
4194
+ ...primary,
4195
+ darkVariant
4196
+ } : primary;
4197
+ }
4198
+ /**
4199
+ * Normalize the primary image of an image field value.
4200
+ *
4201
+ * A legacy string URL that the admin upgraded to `{ id: "", src: url }` so it
4202
+ * can carry a dark variant still has to normalize as a string: as an object it
4203
+ * counts as local media, which strips `src` and leaves nothing behind. The
4204
+ * upgrade outlives the variant — an editor can add one and remove it again —
4205
+ * so the shape decides, not the presence of `darkVariant`.
4206
+ */
4207
+ async function normalizePrimaryImageValue(value, getProvider) {
4208
+ if (isRecord(value) && !value.id && typeof value.src === "string" && !value.provider) return normalizeMediaValue(value.src, getProvider);
4209
+ return normalizeMediaValue(value, getProvider);
4210
+ }
4211
+
4212
+ //#endregion
4213
+ export { EmDashRuntime as n, DB_INIT_DEADLINE_MS as t };
4214
+ //# sourceMappingURL=emdash-runtime-CrzxJFmF.mjs.map