emdash 0.37.0 → 0.39.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 (1445) hide show
  1. package/dist/action-token-On1sot_c.mjs +26 -0
  2. package/dist/action-token-On1sot_c.mjs.map +1 -0
  3. package/dist/activation-CaMfdBt1.mjs +382 -0
  4. package/dist/activation-CaMfdBt1.mjs.map +1 -0
  5. package/dist/adapters-Cq10K4_c.d.mts +171 -0
  6. package/dist/adapters-Cq10K4_c.d.mts.map +1 -0
  7. package/dist/after-PcEvMY_6.mjs +96 -0
  8. package/dist/after-PcEvMY_6.mjs.map +1 -0
  9. package/dist/allowed-origins-1Sa-2wZK.mjs +116 -0
  10. package/dist/allowed-origins-1Sa-2wZK.mjs.map +1 -0
  11. package/dist/api/route-utils.d.mts +4 -3
  12. package/dist/api/route-utils.d.mts.map +1 -1
  13. package/dist/api/route-utils.mjs +45 -29
  14. package/dist/api/route-utils.mjs.map +1 -1
  15. package/dist/api/schemas/index.d.mts +2 -2
  16. package/dist/api/schemas/index.mjs +7 -6
  17. package/dist/api-DmjKxPCY.mjs +4831 -0
  18. package/dist/api-DmjKxPCY.mjs.map +1 -0
  19. package/dist/api-tokens-BIXItFKV.mjs +163 -0
  20. package/dist/api-tokens-BIXItFKV.mjs.map +1 -0
  21. package/dist/apply-drpfmqrR.mjs +1005 -0
  22. package/dist/apply-drpfmqrR.mjs.map +1 -0
  23. package/dist/artifact-fetch-COiMhVgk.mjs +813 -0
  24. package/dist/artifact-fetch-COiMhVgk.mjs.map +1 -0
  25. package/dist/astro/index.d.mts +14 -12
  26. package/dist/astro/index.d.mts.map +1 -1
  27. package/dist/astro/index.mjs +123 -13
  28. package/dist/astro/index.mjs.map +1 -1
  29. package/dist/astro/middleware/auth.d.mts +12 -10
  30. package/dist/astro/middleware/auth.d.mts.map +1 -1
  31. package/dist/astro/middleware/auth.mjs +20 -11
  32. package/dist/astro/middleware/auth.mjs.map +1 -1
  33. package/dist/astro/middleware/media-usage-write-fence.mjs +5 -5
  34. package/dist/astro/middleware/redirect.mjs +11 -9
  35. package/dist/astro/middleware/redirect.mjs.map +1 -1
  36. package/dist/astro/middleware/request-context.d.mts +1 -1
  37. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  38. package/dist/astro/middleware/request-context.mjs +154 -28
  39. package/dist/astro/middleware/request-context.mjs.map +1 -1
  40. package/dist/astro/middleware/setup.mjs +1 -1
  41. package/dist/astro/middleware.d.mts +13 -10
  42. package/dist/astro/middleware.d.mts.map +1 -1
  43. package/dist/astro/middleware.mjs +122 -3041
  44. package/dist/astro/middleware.mjs.map +1 -1
  45. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +10 -9
  46. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  47. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +10 -9
  48. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  50. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  51. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  52. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +13 -12
  53. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  54. package/dist/astro/routes/api/admin/byline-fields/index.mjs +13 -12
  55. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  56. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +13 -12
  57. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  58. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +22 -21
  59. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  60. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +22 -21
  61. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
  62. package/dist/astro/routes/api/admin/bylines/index.mjs +22 -21
  63. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  64. package/dist/astro/routes/api/admin/comments/_id_/status.d.mts.map +1 -1
  65. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +21 -54
  66. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
  68. package/dist/astro/routes/api/admin/comments/bulk.mjs +15 -14
  69. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
  71. package/dist/astro/routes/api/admin/comments/index.mjs +15 -14
  72. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
  74. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
  75. package/dist/astro/routes/api/admin/media-usage/activation.mjs +17 -16
  76. package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +19 -18
  78. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
  79. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +19 -18
  80. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
  81. package/dist/astro/routes/api/admin/media-usage/progress.mjs +17 -17
  82. package/dist/astro/routes/api/admin/media-usage/repair.mjs +21 -20
  83. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +19 -18
  85. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +19 -18
  87. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  89. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  90. package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +9 -66
  92. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
  94. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +9 -69
  95. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +66 -59
  97. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
  99. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +65 -59
  100. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  101. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +70 -61
  102. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  104. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +87 -63
  105. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/index.mjs +66 -59
  107. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  108. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  109. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +65 -59
  110. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  112. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +85 -63
  113. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  114. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +65 -59
  115. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  116. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +70 -61
  117. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  118. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  119. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +90 -66
  120. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  121. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
  122. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +9 -7
  123. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  124. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  125. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +77 -63
  126. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  127. package/dist/astro/routes/api/admin/plugins/registry/verify.d.mts.map +1 -1
  128. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +67 -61
  129. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
  130. package/dist/astro/routes/api/admin/plugins/updates.d.mts.map +1 -1
  131. package/dist/astro/routes/api/admin/plugins/updates.mjs +66 -60
  132. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  133. package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.d.mts +8 -0
  134. package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.d.mts.map +1 -0
  135. package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs +27 -0
  136. package/dist/astro/routes/api/admin/scheduled-policy-rejections/_collection_/_id_.mjs.map +1 -0
  137. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +65 -59
  138. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  139. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  140. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +65 -59
  141. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  142. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
  143. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  144. package/dist/astro/routes/api/admin/users/_id_/index.mjs +11 -10
  145. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  146. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +5 -5
  147. package/dist/astro/routes/api/admin/users/index.mjs +10 -9
  148. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  149. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
  150. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  151. package/dist/astro/routes/api/auth/invite/complete.mjs +15 -14
  152. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  153. package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
  154. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  155. package/dist/astro/routes/api/auth/invite/register-options.mjs +14 -13
  156. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  157. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  158. package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
  159. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  160. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  161. package/dist/astro/routes/api/auth/me.mjs +11 -10
  162. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  164. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  165. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  166. package/dist/astro/routes/api/auth/passkey/_id_.mjs +10 -9
  167. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  168. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  169. package/dist/astro/routes/api/auth/passkey/options.mjs +16 -15
  170. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  171. package/dist/astro/routes/api/auth/passkey/register/options.mjs +14 -13
  172. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  173. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +15 -14
  174. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  175. package/dist/astro/routes/api/auth/passkey/verify.mjs +15 -14
  176. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  177. package/dist/astro/routes/api/auth/signup/complete.mjs +15 -14
  178. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  179. package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
  180. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  181. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  182. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.d.mts.map +1 -1
  183. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +15 -211
  184. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  185. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +18 -17
  186. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  187. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  188. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts.map +1 -1
  189. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +17 -11
  190. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  191. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  192. package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts +10 -0
  193. package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts.map +1 -0
  194. package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +67 -0
  195. package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs.map +1 -0
  196. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  197. package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.d.mts +8 -0
  198. package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.d.mts.map +1 -0
  199. package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs +35 -0
  200. package/dist/astro/routes/api/content/_collection_/_id_/plugin-extensions/_pluginId_/_kind_/_extensionId_.mjs.map +1 -0
  201. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +16 -15
  202. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -1
  203. package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts +9 -2
  204. package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts.map +1 -1
  205. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +28 -16
  206. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  207. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +27 -23
  208. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  209. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +27 -23
  210. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  211. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  212. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  213. package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
  214. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +27 -12
  215. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  216. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +23 -20
  217. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  218. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  219. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts.map +1 -1
  220. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +25 -12
  221. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  222. package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
  223. package/dist/astro/routes/api/content/_collection_/_id_.mjs +27 -14
  224. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  225. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  226. package/dist/astro/routes/api/content/_collection_/index.mjs +18 -12
  227. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/content/_collection_/trash.mjs +11 -10
  229. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  230. package/dist/astro/routes/api/dashboard.mjs +14 -12
  231. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  232. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  233. package/dist/astro/routes/api/health.d.mts +8 -0
  234. package/dist/astro/routes/api/health.d.mts.map +1 -0
  235. package/dist/astro/routes/api/health.mjs +21 -0
  236. package/dist/astro/routes/api/health.mjs.map +1 -0
  237. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  238. package/dist/astro/routes/api/import/probe.mjs +25 -23
  239. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  240. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  241. package/dist/astro/routes/api/import/wordpress/execute.d.mts +13 -11
  242. package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
  243. package/dist/astro/routes/api/import/wordpress/execute.mjs +20 -19
  244. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  245. package/dist/astro/routes/api/import/wordpress/media.d.mts +12 -10
  246. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  247. package/dist/astro/routes/api/import/wordpress/media.mjs +15 -14
  248. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  249. package/dist/astro/routes/api/import/wordpress/prepare.mjs +14 -13
  250. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  251. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +13 -11
  252. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
  253. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +18 -17
  254. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  255. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  256. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +25 -23
  257. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  258. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  259. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +13 -11
  260. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -1
  261. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +36 -33
  262. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  263. package/dist/astro/routes/api/manifest.mjs +17 -9
  264. package/dist/astro/routes/api/manifest.mjs.map +1 -1
  265. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  266. package/dist/astro/routes/api/mcp.mjs +192 -110
  267. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  268. package/dist/astro/routes/api/media/_id_/confirm.mjs +15 -14
  269. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  270. package/dist/astro/routes/api/media/_id_/replace.mjs +12 -11
  271. package/dist/astro/routes/api/media/_id_/replace.mjs.map +1 -1
  272. package/dist/astro/routes/api/media/_id_/upload.mjs +8 -8
  273. package/dist/astro/routes/api/media/_id_/usage.mjs +23 -22
  274. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  275. package/dist/astro/routes/api/media/_id_.mjs +22 -21
  276. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  277. package/dist/astro/routes/api/media/asset/_id_/_filename_.d.mts +8 -0
  278. package/dist/astro/routes/api/media/asset/_id_/_filename_.d.mts.map +1 -0
  279. package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +52 -0
  280. package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs.map +1 -0
  281. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  282. package/dist/astro/routes/api/media/folders/_id_.mjs +12 -11
  283. package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -1
  284. package/dist/astro/routes/api/media/folders/index.mjs +12 -11
  285. package/dist/astro/routes/api/media/folders/index.mjs.map +1 -1
  286. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  287. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  288. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  289. package/dist/astro/routes/api/media/upload-url.mjs +14 -13
  290. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  291. package/dist/astro/routes/api/media.mjs +26 -25
  292. package/dist/astro/routes/api/media.mjs.map +1 -1
  293. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +15 -14
  294. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
  295. package/dist/astro/routes/api/menus/_name_/items.mjs +15 -14
  296. package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
  297. package/dist/astro/routes/api/menus/_name_/reorder.mjs +15 -14
  298. package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
  299. package/dist/astro/routes/api/menus/_name_/translations.mjs +15 -14
  300. package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
  301. package/dist/astro/routes/api/menus/_name_.mjs +15 -14
  302. package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
  303. package/dist/astro/routes/api/menus/index.mjs +15 -14
  304. package/dist/astro/routes/api/menus/index.mjs.map +1 -1
  305. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  306. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  307. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  308. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  309. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  310. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  311. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  312. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  313. package/dist/astro/routes/api/openapi.json.mjs +95 -24
  314. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  315. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts +2 -1
  316. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  317. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +22 -40
  318. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  319. package/dist/astro/routes/api/redirects/404s/index.mjs +18 -14
  320. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  321. package/dist/astro/routes/api/redirects/404s/summary.mjs +18 -14
  322. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  323. package/dist/astro/routes/api/redirects/_id_.d.mts.map +1 -1
  324. package/dist/astro/routes/api/redirects/_id_.mjs +20 -22
  325. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  326. package/dist/astro/routes/api/redirects/index.d.mts.map +1 -1
  327. package/dist/astro/routes/api/redirects/index.mjs +19 -19
  328. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  329. package/dist/astro/routes/api/relations/_id_/index.mjs +27 -23
  330. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  331. package/dist/astro/routes/api/relations/_id_/translations.mjs +21 -18
  332. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  333. package/dist/astro/routes/api/relations/index.mjs +27 -23
  334. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  335. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  336. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  337. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +65 -59
  338. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  339. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +65 -59
  340. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  341. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +65 -59
  342. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  343. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +65 -59
  344. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  345. package/dist/astro/routes/api/schema/collections/index.mjs +65 -59
  346. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  347. package/dist/astro/routes/api/schema/collections/reorder.mjs +65 -59
  348. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  349. package/dist/astro/routes/api/schema/index.mjs +16 -14
  350. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  351. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +65 -59
  352. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  353. package/dist/astro/routes/api/schema/orphans/index.mjs +65 -59
  354. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  355. package/dist/astro/routes/api/search/enable.mjs +16 -14
  356. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  357. package/dist/astro/routes/api/search/index.mjs +17 -14
  358. package/dist/astro/routes/api/search/index.mjs.map +1 -1
  359. package/dist/astro/routes/api/search/rebuild.mjs +16 -14
  360. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  361. package/dist/astro/routes/api/search/stats.mjs +10 -9
  362. package/dist/astro/routes/api/search/stats.mjs.map +1 -1
  363. package/dist/astro/routes/api/search/suggest.mjs +15 -13
  364. package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
  365. package/dist/astro/routes/api/sections/_slug_.mjs +15 -13
  366. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  367. package/dist/astro/routes/api/sections/index.mjs +15 -13
  368. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  369. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +9 -8
  370. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs.map +1 -1
  371. package/dist/astro/routes/api/settings/backups/archives/index.mjs +8 -7
  372. package/dist/astro/routes/api/settings/backups/archives/index.mjs.map +1 -1
  373. package/dist/astro/routes/api/settings/backups/export.mjs +9 -8
  374. package/dist/astro/routes/api/settings/backups/export.mjs.map +1 -1
  375. package/dist/astro/routes/api/settings/backups/index.mjs +10 -9
  376. package/dist/astro/routes/api/settings/backups/index.mjs.map +1 -1
  377. package/dist/astro/routes/api/settings/email.mjs +6 -6
  378. package/dist/astro/routes/api/settings.mjs +22 -20
  379. package/dist/astro/routes/api/settings.mjs.map +1 -1
  380. package/dist/astro/routes/api/setup/admin-verify.mjs +16 -15
  381. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  382. package/dist/astro/routes/api/setup/admin.mjs +15 -14
  383. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  384. package/dist/astro/routes/api/setup/dev-bypass.mjs +42 -38
  385. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  386. package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
  387. package/dist/astro/routes/api/setup/index.mjs +43 -38
  388. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  389. package/dist/astro/routes/api/setup/status.mjs +5 -5
  390. package/dist/astro/routes/api/snapshot.mjs +11 -10
  391. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  392. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +23 -21
  393. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
  394. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +23 -21
  395. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  396. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +23 -21
  397. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  398. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +23 -21
  399. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  400. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +22 -20
  401. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -1
  402. package/dist/astro/routes/api/taxonomies/_name_.mjs +23 -21
  403. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -1
  404. package/dist/astro/routes/api/taxonomies/index.mjs +23 -21
  405. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  406. package/dist/astro/routes/api/themes/preview.mjs +8 -8
  407. package/dist/astro/routes/api/typegen.mjs +6 -6
  408. package/dist/astro/routes/api/visual-editing/action-token.d.mts +8 -0
  409. package/dist/astro/routes/api/visual-editing/action-token.d.mts.map +1 -0
  410. package/dist/astro/routes/api/visual-editing/action-token.mjs +25 -0
  411. package/dist/astro/routes/api/visual-editing/action-token.mjs.map +1 -0
  412. package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.d.mts +8 -0
  413. package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.d.mts.map +1 -0
  414. package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs +37 -0
  415. package/dist/astro/routes/api/visual-editing/content/_collection_/_id_/publish.mjs.map +1 -0
  416. package/dist/astro/routes/api/visual-editing/toolbar-labels.d.mts +8 -0
  417. package/dist/astro/routes/api/visual-editing/toolbar-labels.d.mts.map +1 -0
  418. package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs +20 -0
  419. package/dist/astro/routes/api/visual-editing/toolbar-labels.mjs.map +1 -0
  420. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  421. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  422. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  423. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +12 -11
  424. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  425. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +16 -15
  426. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  427. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +16 -15
  428. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  429. package/dist/astro/routes/api/widget-areas/_name_.mjs +10 -10
  430. package/dist/astro/routes/api/widget-areas/index.mjs +16 -15
  431. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  432. package/dist/astro/routes/api/widget-components.mjs +4 -4
  433. package/dist/astro/routes/robots.txt.mjs +13 -12
  434. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  435. package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
  436. package/dist/astro/routes/sitemap-_collection_.xml.mjs +24 -19
  437. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  438. package/dist/astro/routes/sitemap.xml.mjs +14 -13
  439. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  440. package/dist/astro/types.d.mts +79 -26
  441. package/dist/astro/types.d.mts.map +1 -1
  442. package/dist/auth/providers/github.d.mts +1 -1
  443. package/dist/auth/providers/google.d.mts +1 -1
  444. package/dist/authoritative-records-DbqshfKD.mjs +5172 -0
  445. package/dist/authoritative-records-DbqshfKD.mjs.map +1 -0
  446. package/dist/authorize-BXpfMpp0.mjs +40 -0
  447. package/dist/authorize-BXpfMpp0.mjs.map +1 -0
  448. package/dist/backup-BsPQdwnS.mjs +246 -0
  449. package/dist/backup-BsPQdwnS.mjs.map +1 -0
  450. package/dist/base64-DvnNgP7e.mjs +63 -0
  451. package/dist/base64-DvnNgP7e.mjs.map +1 -0
  452. package/dist/byline-Doh-wt99.mjs +906 -0
  453. package/dist/byline-Doh-wt99.mjs.map +1 -0
  454. package/dist/byline-fields-CsrdvaLV.mjs +123 -0
  455. package/dist/byline-fields-CsrdvaLV.mjs.map +1 -0
  456. package/dist/byline-registry-D6lWzXk6.mjs +406 -0
  457. package/dist/byline-registry-D6lWzXk6.mjs.map +1 -0
  458. package/dist/bylines-Cop0hkoy.mjs +214 -0
  459. package/dist/bylines-Cop0hkoy.mjs.map +1 -0
  460. package/dist/bylines-kpBzHPr8.mjs +232 -0
  461. package/dist/bylines-kpBzHPr8.mjs.map +1 -0
  462. package/dist/cache-B0pOlXdM.mjs +108 -0
  463. package/dist/cache-B0pOlXdM.mjs.map +1 -0
  464. package/dist/capture-triggers-gxQxnxrJ.mjs +362 -0
  465. package/dist/capture-triggers-gxQxnxrJ.mjs.map +1 -0
  466. package/dist/challenge-store-CkRdSdd8.mjs +51 -0
  467. package/dist/challenge-store-CkRdSdd8.mjs.map +1 -0
  468. package/dist/checksum-Bsi2D9M1.mjs +162 -0
  469. package/dist/checksum-Bsi2D9M1.mjs.map +1 -0
  470. package/dist/chrome-tags-D7LGB91U.mjs +24 -0
  471. package/dist/chrome-tags-D7LGB91U.mjs.map +1 -0
  472. package/dist/chunks-BmWyou79.mjs +25 -0
  473. package/dist/chunks-BmWyou79.mjs.map +1 -0
  474. package/dist/cli/index.mjs +921 -106
  475. package/dist/cli/index.mjs.map +1 -1
  476. package/dist/client/cf-access.d.mts +1 -1
  477. package/dist/client/index.d.mts +17 -7
  478. package/dist/client/index.d.mts.map +1 -1
  479. package/dist/client/index.mjs +17 -12
  480. package/dist/client/index.mjs.map +1 -1
  481. package/dist/collection-deletion-C3Xixmkx.mjs +436 -0
  482. package/dist/collection-deletion-C3Xixmkx.mjs.map +1 -0
  483. package/dist/collection-slugs-cache-D9auiKmI.mjs +85 -0
  484. package/dist/collection-slugs-cache-D9auiKmI.mjs.map +1 -0
  485. package/dist/comment-DcT8qdPK.mjs +313 -0
  486. package/dist/comment-DcT8qdPK.mjs.map +1 -0
  487. package/dist/comment-reaction-DY5-y8Y5.mjs +85 -0
  488. package/dist/comment-reaction-DY5-y8Y5.mjs.map +1 -0
  489. package/dist/comments-B67VRNGw.mjs +204 -0
  490. package/dist/comments-B67VRNGw.mjs.map +1 -0
  491. package/dist/comments-BVzzt6x5.mjs +99 -0
  492. package/dist/comments-BVzzt6x5.mjs.map +1 -0
  493. package/dist/components-DIl9Qzh6.mjs +108 -0
  494. package/dist/components-DIl9Qzh6.mjs.map +1 -0
  495. package/dist/config-B2acLeC5.mjs +242 -0
  496. package/dist/config-B2acLeC5.mjs.map +1 -0
  497. package/dist/config-CGVeT8Hc.mjs +73 -0
  498. package/dist/config-CGVeT8Hc.mjs.map +1 -0
  499. package/dist/config-D3HhLON3.d.mts +39 -0
  500. package/dist/config-D3HhLON3.d.mts.map +1 -0
  501. package/dist/config-DS7e4EZ9.d.mts +6387 -0
  502. package/dist/config-DS7e4EZ9.d.mts.map +1 -0
  503. package/dist/content-DZ3B5qtA.mjs +2216 -0
  504. package/dist/content-DZ3B5qtA.mjs.map +1 -0
  505. package/dist/content-list-query-CbPdzsna.d.mts +26 -0
  506. package/dist/content-list-query-CbPdzsna.d.mts.map +1 -0
  507. package/dist/content-policy-CM4skrNi.mjs +46 -0
  508. package/dist/content-policy-CM4skrNi.mjs.map +1 -0
  509. package/dist/content-refresh-BHLHJgyt.mjs +3599 -0
  510. package/dist/content-refresh-BHLHJgyt.mjs.map +1 -0
  511. package/dist/content-repair-BVll5WNl.mjs +440 -0
  512. package/dist/content-repair-BVll5WNl.mjs.map +1 -0
  513. package/dist/context-B195kjqZ.mjs +1835 -0
  514. package/dist/context-B195kjqZ.mjs.map +1 -0
  515. package/dist/cron-BcR3RN4K.mjs +282 -0
  516. package/dist/cron-BcR3RN4K.mjs.map +1 -0
  517. package/dist/dashboard-CZD4G7k_.mjs +151 -0
  518. package/dist/dashboard-CZD4G7k_.mjs.map +1 -0
  519. package/dist/database/migration-lock.d.mts +37 -0
  520. package/dist/database/migration-lock.d.mts.map +1 -0
  521. package/dist/database/migration-lock.mjs +76 -0
  522. package/dist/database/migration-lock.mjs.map +1 -0
  523. package/dist/database/pg-migration-lock.d.mts +1 -1
  524. package/dist/database/pg-migration-lock.mjs +1 -1
  525. package/dist/database/pg-migration-lock.mjs.map +1 -1
  526. package/dist/datetime-normalization-PeiEdcRY.mjs +187 -0
  527. package/dist/datetime-normalization-PeiEdcRY.mjs.map +1 -0
  528. package/dist/db/index.d.mts +3 -3
  529. package/dist/db/index.mjs +4 -3
  530. package/dist/db/index.mjs.map +1 -1
  531. package/dist/db/libsql-migrations.d.mts +1 -1
  532. package/dist/db/libsql-migrations.mjs +7 -6
  533. package/dist/db/libsql-migrations.mjs.map +1 -1
  534. package/dist/db/libsql.d.mts +1 -1
  535. package/dist/db/postgres-migrations.d.mts +1 -1
  536. package/dist/db/postgres-migrations.mjs +7 -6
  537. package/dist/db/postgres-migrations.mjs.map +1 -1
  538. package/dist/db/postgres.d.mts +1 -1
  539. package/dist/db/sqlite-migrations.d.mts +1 -1
  540. package/dist/db/sqlite-migrations.mjs +7 -6
  541. package/dist/db/sqlite-migrations.mjs.map +1 -1
  542. package/dist/db/sqlite.d.mts +1 -1
  543. package/dist/db/sqlite.mjs +1 -1
  544. package/dist/db-errors-CBD5yMro.mjs +55 -0
  545. package/dist/db-errors-CBD5yMro.mjs.map +1 -0
  546. package/dist/default-BtlxWgh9.mjs +81 -0
  547. package/dist/default-BtlxWgh9.mjs.map +1 -0
  548. package/dist/deferred-tasks-Ba9fphlE.d.mts +9 -0
  549. package/dist/deferred-tasks-Ba9fphlE.d.mts.map +1 -0
  550. package/dist/dev-typegen-CZaPLl3V.mjs +29 -0
  551. package/dist/dev-typegen-CZaPLl3V.mjs.map +1 -0
  552. package/dist/device-flow-3E5l0fpH.mjs +466 -0
  553. package/dist/device-flow-3E5l0fpH.mjs.map +1 -0
  554. package/dist/dialect-helpers-CtYdsX4W.mjs +307 -0
  555. package/dist/dialect-helpers-CtYdsX4W.mjs.map +1 -0
  556. package/dist/direct-executor-ZI8E01Yt.mjs +104 -0
  557. package/dist/direct-executor-ZI8E01Yt.mjs.map +1 -0
  558. package/dist/dist-2hDLsLeq.mjs +810 -0
  559. package/dist/dist-2hDLsLeq.mjs.map +1 -0
  560. package/dist/email-console-gc-E3Eqj.mjs +50 -0
  561. package/dist/email-console-gc-E3Eqj.mjs.map +1 -0
  562. package/dist/emdash-runtime-DZGinqTL.mjs +4041 -0
  563. package/dist/emdash-runtime-DZGinqTL.mjs.map +1 -0
  564. package/dist/enrich-C2Mk0lwE.mjs +194 -0
  565. package/dist/enrich-C2Mk0lwE.mjs.map +1 -0
  566. package/dist/entry-lock-D3C99MXS.mjs +1520 -0
  567. package/dist/entry-lock-D3C99MXS.mjs.map +1 -0
  568. package/dist/entry-lock-D9rnj2Lu.mjs +163 -0
  569. package/dist/entry-lock-D9rnj2Lu.mjs.map +1 -0
  570. package/dist/entry-lock-DX7QkqEg.d.mts +3503 -0
  571. package/dist/entry-lock-DX7QkqEg.d.mts.map +1 -0
  572. package/dist/entry-locks-CO_KaoU5.mjs +167 -0
  573. package/dist/entry-locks-CO_KaoU5.mjs.map +1 -0
  574. package/dist/error-CCTCskie.mjs +90 -0
  575. package/dist/error-CCTCskie.mjs.map +1 -0
  576. package/dist/errors-W3PA8HqB.mjs +447 -0
  577. package/dist/errors-W3PA8HqB.mjs.map +1 -0
  578. package/dist/escape-DN4Qmjsl.mjs +9 -0
  579. package/dist/escape-DN4Qmjsl.mjs.map +1 -0
  580. package/dist/field-defs-cache-B337z8UJ.mjs +77 -0
  581. package/dist/field-defs-cache-B337z8UJ.mjs.map +1 -0
  582. package/dist/focal-point-Gr34o-4o.mjs +23 -0
  583. package/dist/focal-point-Gr34o-4o.mjs.map +1 -0
  584. package/dist/fts-manager-DMb0sbIQ.mjs +413 -0
  585. package/dist/fts-manager-DMb0sbIQ.mjs.map +1 -0
  586. package/dist/hash-CLVFzWvx.mjs +37 -0
  587. package/dist/hash-CLVFzWvx.mjs.map +1 -0
  588. package/dist/hook-result-KY1jq21o.d.mts +31 -0
  589. package/dist/hook-result-KY1jq21o.d.mts.map +1 -0
  590. package/dist/hreflang-BgS2FqhH.d.mts +50 -0
  591. package/dist/hreflang-BgS2FqhH.d.mts.map +1 -0
  592. package/dist/hreflang-CrJwqoeb.mjs +105 -0
  593. package/dist/hreflang-CrJwqoeb.mjs.map +1 -0
  594. package/dist/http-route-dispatch-DAKlanXb.mjs +339 -0
  595. package/dist/http-route-dispatch-DAKlanXb.mjs.map +1 -0
  596. package/dist/identity-DRGSHEEr.mjs +35 -0
  597. package/dist/identity-DRGSHEEr.mjs.map +1 -0
  598. package/dist/import-DFr5B_hC.mjs +2020 -0
  599. package/dist/import-DFr5B_hC.mjs.map +1 -0
  600. package/dist/index-CDwKDkTq.d.mts +623 -0
  601. package/dist/index-CDwKDkTq.d.mts.map +1 -0
  602. package/dist/index.d.mts +27 -22
  603. package/dist/index.mjs +101 -86
  604. package/dist/init-lock-4eRRB4Le.mjs +56 -0
  605. package/dist/init-lock-4eRRB4Le.mjs.map +1 -0
  606. package/dist/install-finalization-5Os-QZ-2.mjs +58 -0
  607. package/dist/install-finalization-5Os-QZ-2.mjs.map +1 -0
  608. package/dist/lifecycle-7e-Lt_kX.mjs +25 -0
  609. package/dist/lifecycle-7e-Lt_kX.mjs.map +1 -0
  610. package/dist/load-Cu5bvCZh.mjs +28 -0
  611. package/dist/load-Cu5bvCZh.mjs.map +1 -0
  612. package/dist/loader-t4TxaFiQ.mjs +1004 -0
  613. package/dist/loader-t4TxaFiQ.mjs.map +1 -0
  614. package/dist/manifest-builder-D31UoJCu.mjs +189 -0
  615. package/dist/manifest-builder-D31UoJCu.mjs.map +1 -0
  616. package/dist/manifest-schema-Bkh8SeHd.mjs +491 -0
  617. package/dist/manifest-schema-Bkh8SeHd.mjs.map +1 -0
  618. package/dist/media/image-endpoint.mjs +1 -1
  619. package/dist/media/index.d.mts +1 -1
  620. package/dist/media/index.mjs +4 -4
  621. package/dist/media/local-runtime.d.mts +14 -12
  622. package/dist/media/local-runtime.d.mts.map +1 -1
  623. package/dist/media/local-runtime.mjs +12 -11
  624. package/dist/media/local-runtime.mjs.map +1 -1
  625. package/dist/media-TBlKCwo7.mjs +334 -0
  626. package/dist/media-TBlKCwo7.mjs.map +1 -0
  627. package/dist/media-allowlist-s60eAIf6.mjs +44 -0
  628. package/dist/media-allowlist-s60eAIf6.mjs.map +1 -0
  629. package/dist/media-folders-ClIRLUQv.mjs +210 -0
  630. package/dist/media-folders-ClIRLUQv.mjs.map +1 -0
  631. package/dist/media-url-DY_ijO3B.mjs +43 -0
  632. package/dist/media-url-DY_ijO3B.mjs.map +1 -0
  633. package/dist/media-usage-C4NolTpQ.mjs +1833 -0
  634. package/dist/media-usage-C4NolTpQ.mjs.map +1 -0
  635. package/dist/media-usage-activation-CAhmb14O.mjs +78 -0
  636. package/dist/media-usage-activation-CAhmb14O.mjs.map +1 -0
  637. package/dist/media-usage-work-Cu5DqewW.mjs +586 -0
  638. package/dist/media-usage-work-Cu5DqewW.mjs.map +1 -0
  639. package/dist/media-usage-work-DJr3rOEN.mjs +152 -0
  640. package/dist/media-usage-work-DJr3rOEN.mjs.map +1 -0
  641. package/dist/media-usage-write-fence-DW5oHT7q.mjs +42 -0
  642. package/dist/media-usage-write-fence-DW5oHT7q.mjs.map +1 -0
  643. package/dist/menus-DbgU9z5Q.mjs +3732 -0
  644. package/dist/menus-DbgU9z5Q.mjs.map +1 -0
  645. package/dist/menus-Wy8m5NcH.mjs +753 -0
  646. package/dist/menus-Wy8m5NcH.mjs.map +1 -0
  647. package/dist/migrations/index.d.mts +2 -2
  648. package/dist/migrations/index.d.mts.map +1 -1
  649. package/dist/migrations/index.mjs +6 -5
  650. package/dist/mime-CCYRC4r1.mjs +36 -0
  651. package/dist/mime-CCYRC4r1.mjs.map +1 -0
  652. package/dist/mode-CkmOhFqj.mjs +23 -0
  653. package/dist/mode-CkmOhFqj.mjs.map +1 -0
  654. package/dist/node-sqlite-compat-vpVaILnb.mjs +46 -0
  655. package/dist/node-sqlite-compat-vpVaILnb.mjs.map +1 -0
  656. package/dist/normalize-dChXQjwS.mjs +191 -0
  657. package/dist/normalize-dChXQjwS.mjs.map +1 -0
  658. package/dist/oauth-authorization-DZ-R-hmx.mjs +274 -0
  659. package/dist/oauth-authorization-DZ-R-hmx.mjs.map +1 -0
  660. package/dist/oauth-clients-BMPA8vjl.mjs +266 -0
  661. package/dist/oauth-clients-BMPA8vjl.mjs.map +1 -0
  662. package/dist/oauth-state-store-0gIdb48-.mjs +50 -0
  663. package/dist/oauth-state-store-0gIdb48-.mjs.map +1 -0
  664. package/dist/oauth-user-lookup-ChVW4UWu.mjs +26 -0
  665. package/dist/oauth-user-lookup-ChVW4UWu.mjs.map +1 -0
  666. package/dist/object-cache/memory.d.mts +1 -1
  667. package/dist/object-cache-DlSVbn5Y.mjs +534 -0
  668. package/dist/object-cache-DlSVbn5Y.mjs.map +1 -0
  669. package/dist/options-D5WcorD5.mjs +200 -0
  670. package/dist/options-D5WcorD5.mjs.map +1 -0
  671. package/dist/options-DAknChnw.d.mts +77 -0
  672. package/dist/options-DAknChnw.d.mts.map +1 -0
  673. package/dist/page/index.d.mts +2 -2
  674. package/dist/page/index.d.mts.map +1 -1
  675. package/dist/page/index.mjs +13 -0
  676. package/dist/page/index.mjs.map +1 -1
  677. package/dist/parse-Cr379Av9.mjs +81 -0
  678. package/dist/parse-Cr379Av9.mjs.map +1 -0
  679. package/dist/parse-tj9XLNO_.d.mts +165 -0
  680. package/dist/parse-tj9XLNO_.d.mts.map +1 -0
  681. package/dist/passkey-config-BD-wMaCf.mjs +46 -0
  682. package/dist/passkey-config-BD-wMaCf.mjs.map +1 -0
  683. package/dist/patterns-Do3iqi7N.mjs +145 -0
  684. package/dist/patterns-Do3iqi7N.mjs.map +1 -0
  685. package/dist/placeholder-CubPFn_L.d.mts +306 -0
  686. package/dist/placeholder-CubPFn_L.d.mts.map +1 -0
  687. package/dist/plugin-test-runtime.d.mts +97 -0
  688. package/dist/plugin-test-runtime.d.mts.map +1 -0
  689. package/dist/plugin-test-runtime.mjs +105 -0
  690. package/dist/plugin-types.d.mts +53 -15
  691. package/dist/plugin-types.d.mts.map +1 -1
  692. package/dist/plugin-types.mjs +18 -1
  693. package/dist/plugin-types.mjs.map +1 -0
  694. package/dist/plugin-utils.d.mts +12 -10
  695. package/dist/plugin-utils.d.mts.map +1 -1
  696. package/dist/plugins/adapt-sandbox-entry.d.mts +12 -10
  697. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  698. package/dist/plugins/adapt-sandbox-entry.mjs +18 -13
  699. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  700. package/dist/plugins/host.d.mts +7 -0
  701. package/dist/plugins/host.mjs +45 -0
  702. package/dist/plugins/http-wire.d.mts +25 -0
  703. package/dist/plugins/http-wire.d.mts.map +1 -0
  704. package/dist/plugins/http-wire.mjs +148 -0
  705. package/dist/plugins/http-wire.mjs.map +1 -0
  706. package/dist/plugins/secret-redactor.d.mts +9 -0
  707. package/dist/plugins/secret-redactor.d.mts.map +1 -0
  708. package/dist/plugins/secret-redactor.mjs +51 -0
  709. package/dist/plugins/secret-redactor.mjs.map +1 -0
  710. package/dist/plugins-DHurtdXQ.mjs +277 -0
  711. package/dist/plugins-DHurtdXQ.mjs.map +1 -0
  712. package/dist/policy-lyZnjpwz.mjs +45 -0
  713. package/dist/policy-lyZnjpwz.mjs.map +1 -0
  714. package/dist/portable-text-oAwodbPS.mjs +286 -0
  715. package/dist/portable-text-oAwodbPS.mjs.map +1 -0
  716. package/dist/preview-Cup2bj8c.mjs +107 -0
  717. package/dist/preview-Cup2bj8c.mjs.map +1 -0
  718. package/dist/protocol-C1CNiKX4.d.mts +61 -0
  719. package/dist/protocol-C1CNiKX4.d.mts.map +1 -0
  720. package/dist/public-submission-Cs-pFMQK.mjs +245 -0
  721. package/dist/public-submission-Cs-pFMQK.mjs.map +1 -0
  722. package/dist/public-url-BWVHsfQv.mjs +101 -0
  723. package/dist/public-url-BWVHsfQv.mjs.map +1 -0
  724. package/dist/query-D-lxiOgn.mjs +890 -0
  725. package/dist/query-D-lxiOgn.mjs.map +1 -0
  726. package/dist/query-D7kHKOws.mjs +41 -0
  727. package/dist/query-D7kHKOws.mjs.map +1 -0
  728. package/dist/rate-limit-CqJn-w4h.mjs +120 -0
  729. package/dist/rate-limit-CqJn-w4h.mjs.map +1 -0
  730. package/dist/redirect-BrChFPIu.mjs +17 -0
  731. package/dist/redirect-BrChFPIu.mjs.map +1 -0
  732. package/dist/redirect-D2Yo-b6S.mjs +415 -0
  733. package/dist/redirect-D2Yo-b6S.mjs.map +1 -0
  734. package/dist/redirects-BCiTaIQ-.mjs +146 -0
  735. package/dist/redirects-BCiTaIQ-.mjs.map +1 -0
  736. package/dist/redirects-ByItliuu.mjs +644 -0
  737. package/dist/redirects-ByItliuu.mjs.map +1 -0
  738. package/dist/registry/testing.d.mts +14 -0
  739. package/dist/registry/testing.d.mts.map +1 -0
  740. package/dist/registry/testing.mjs +255 -0
  741. package/dist/registry/testing.mjs.map +1 -0
  742. package/dist/registry-CkuPoZs7.mjs +1222 -0
  743. package/dist/registry-CkuPoZs7.mjs.map +1 -0
  744. package/dist/relation-XTAtOpJO.mjs +334 -0
  745. package/dist/relation-XTAtOpJO.mjs.map +1 -0
  746. package/dist/relations-B4w-G5U9.mjs +309 -0
  747. package/dist/relations-B4w-G5U9.mjs.map +1 -0
  748. package/dist/relations-O63JWBRj.mjs +403 -0
  749. package/dist/relations-O63JWBRj.mjs.map +1 -0
  750. package/dist/request-cache-DaMhSQdx.mjs +104 -0
  751. package/dist/request-cache-DaMhSQdx.mjs.map +1 -0
  752. package/dist/request-context.d.mts +1 -1
  753. package/dist/request-meta-C8yl_sIs.mjs +141 -0
  754. package/dist/request-meta-C8yl_sIs.mjs.map +1 -0
  755. package/dist/resolve-BJIS8wKr.mjs +199 -0
  756. package/dist/resolve-BJIS8wKr.mjs.map +1 -0
  757. package/dist/route-wire-D66L2oaZ.mjs +197 -0
  758. package/dist/route-wire-D66L2oaZ.mjs.map +1 -0
  759. package/dist/routes-DYsTe-oR.mjs +321 -0
  760. package/dist/routes-DYsTe-oR.mjs.map +1 -0
  761. package/dist/runner-3gfxRsJG.d.mts +68 -0
  762. package/dist/runner-3gfxRsJG.d.mts.map +1 -0
  763. package/dist/runner-ClrleR8A.mjs +5502 -0
  764. package/dist/runner-ClrleR8A.mjs.map +1 -0
  765. package/dist/runtime.d.mts +13 -11
  766. package/dist/runtime.d.mts.map +1 -1
  767. package/dist/runtime.mjs +6 -5
  768. package/dist/runtime.mjs.map +1 -1
  769. package/dist/schema-CPhPM9Do.mjs +420 -0
  770. package/dist/schema-CPhPM9Do.mjs.map +1 -0
  771. package/dist/schema-DEERRrKS.mjs +9 -0
  772. package/dist/schema-DEERRrKS.mjs.map +1 -0
  773. package/dist/scopes-Bn8_rCSt.mjs +27 -0
  774. package/dist/scopes-Bn8_rCSt.mjs.map +1 -0
  775. package/dist/search-lami_Ota.mjs +448 -0
  776. package/dist/search-lami_Ota.mjs.map +1 -0
  777. package/dist/secrets-DNgVgz_e.mjs +341 -0
  778. package/dist/secrets-DNgVgz_e.mjs.map +1 -0
  779. package/dist/sections-BIpd4Cxf.mjs +346 -0
  780. package/dist/sections-BIpd4Cxf.mjs.map +1 -0
  781. package/dist/seed/index.d.mts +2 -2
  782. package/dist/seed/index.mjs +35 -31
  783. package/dist/seo/index.d.mts +3 -3
  784. package/dist/seo/index.d.mts.map +1 -1
  785. package/dist/seo/index.mjs +27 -26
  786. package/dist/seo/index.mjs.map +1 -1
  787. package/dist/seo-BtU70b7l.mjs +114 -0
  788. package/dist/seo-BtU70b7l.mjs.map +1 -0
  789. package/dist/seo-_4qmazxz.mjs +133 -0
  790. package/dist/seo-_4qmazxz.mjs.map +1 -0
  791. package/dist/service-Bpy5iTX0.mjs +150 -0
  792. package/dist/service-Bpy5iTX0.mjs.map +1 -0
  793. package/dist/session-user-BQeS2Mrb.mjs +51 -0
  794. package/dist/session-user-BQeS2Mrb.mjs.map +1 -0
  795. package/dist/settings-DmroR53o.mjs +420 -0
  796. package/dist/settings-DmroR53o.mjs.map +1 -0
  797. package/dist/settings-g2F2a3Ub.mjs +51 -0
  798. package/dist/settings-g2F2a3Ub.mjs.map +1 -0
  799. package/dist/setup-complete-BrZaXpVV.mjs +26 -0
  800. package/dist/setup-complete-BrZaXpVV.mjs.map +1 -0
  801. package/dist/setup-nonce-Cb9-884O.mjs +25 -0
  802. package/dist/setup-nonce-Cb9-884O.mjs.map +1 -0
  803. package/dist/single-flight-cache-B5O2qhO1.mjs +104 -0
  804. package/dist/single-flight-cache-B5O2qhO1.mjs.map +1 -0
  805. package/dist/site-url-DjhYCaep.mjs +16 -0
  806. package/dist/site-url-DjhYCaep.mjs.map +1 -0
  807. package/dist/slugify-T5GHH42D.mjs +17 -0
  808. package/dist/slugify-T5GHH42D.mjs.map +1 -0
  809. package/dist/snapshot-C32Xuv3o.mjs +233 -0
  810. package/dist/snapshot-C32Xuv3o.mjs.map +1 -0
  811. package/dist/ssrf-Co1GGFZz.mjs +406 -0
  812. package/dist/ssrf-Co1GGFZz.mjs.map +1 -0
  813. package/dist/state-C7gCoiHr.mjs +153 -0
  814. package/dist/state-C7gCoiHr.mjs.map +1 -0
  815. package/dist/status-DLoBmcDz.mjs +30 -0
  816. package/dist/status-DLoBmcDz.mjs.map +1 -0
  817. package/dist/storage/local.d.mts +1 -1
  818. package/dist/storage/local.mjs +1 -1
  819. package/dist/storage/s3.d.mts +1 -1
  820. package/dist/storage/s3.mjs +1 -1
  821. package/dist/storage-query-CPctdMKX.d.mts +1519 -0
  822. package/dist/storage-query-CPctdMKX.d.mts.map +1 -0
  823. package/dist/target-CUKlGrXb.mjs +27 -0
  824. package/dist/target-CUKlGrXb.mjs.map +1 -0
  825. package/dist/taxonomies-BpqpXJUE.mjs +707 -0
  826. package/dist/taxonomies-BpqpXJUE.mjs.map +1 -0
  827. package/dist/taxonomies-ClCo4IGO.mjs +899 -0
  828. package/dist/taxonomies-ClCo4IGO.mjs.map +1 -0
  829. package/dist/taxonomy-Dj1vpw5E.mjs +508 -0
  830. package/dist/taxonomy-Dj1vpw5E.mjs.map +1 -0
  831. package/dist/tokens-Ns02GAma.mjs +171 -0
  832. package/dist/tokens-Ns02GAma.mjs.map +1 -0
  833. package/dist/transaction-BcJO4Tmc.mjs +29 -0
  834. package/dist/transaction-BcJO4Tmc.mjs.map +1 -0
  835. package/dist/transport-B8P1xsqk.mjs +136 -0
  836. package/dist/transport-B8P1xsqk.mjs.map +1 -0
  837. package/dist/transport-BLHZZcD_.d.mts +42 -0
  838. package/dist/transport-BLHZZcD_.d.mts.map +1 -0
  839. package/dist/trusted-proxy-Cg7SuD15.mjs +51 -0
  840. package/dist/trusted-proxy-Cg7SuD15.mjs.map +1 -0
  841. package/dist/turnstile-DWCSh_Qc.mjs +53 -0
  842. package/dist/turnstile-DWCSh_Qc.mjs.map +1 -0
  843. package/dist/types-B3RkqvDW.mjs +156 -0
  844. package/dist/types-B3RkqvDW.mjs.map +1 -0
  845. package/dist/types-BkUg2yHx.mjs +92 -0
  846. package/dist/types-BkUg2yHx.mjs.map +1 -0
  847. package/dist/types-BtuppOTV.mjs +16 -0
  848. package/dist/types-BtuppOTV.mjs.map +1 -0
  849. package/dist/types-CCR3ivXE.d.mts +363 -0
  850. package/dist/types-CCR3ivXE.d.mts.map +1 -0
  851. package/dist/types-CSHgU_-_.mjs +18 -0
  852. package/dist/types-CSHgU_-_.mjs.map +1 -0
  853. package/dist/types-CbwqGOXm.d.mts +766 -0
  854. package/dist/types-CbwqGOXm.d.mts.map +1 -0
  855. package/dist/types-D4RGIc2I.d.mts +198 -0
  856. package/dist/types-D4RGIc2I.d.mts.map +1 -0
  857. package/dist/types-DEHidMEk.d.mts +1637 -0
  858. package/dist/types-DEHidMEk.d.mts.map +1 -0
  859. package/dist/types-DQeU2SR7.d.mts +279 -0
  860. package/dist/types-DQeU2SR7.d.mts.map +1 -0
  861. package/dist/types-DxQWoQLA.d.mts +264 -0
  862. package/dist/types-DxQWoQLA.d.mts.map +1 -0
  863. package/dist/types-FBBnGZ2Y.d.mts +198 -0
  864. package/dist/types-FBBnGZ2Y.d.mts.map +1 -0
  865. package/dist/types-ZCnlropY.mjs +69 -0
  866. package/dist/types-ZCnlropY.mjs.map +1 -0
  867. package/dist/types-_Qtyawiy.d.mts +124 -0
  868. package/dist/types-_Qtyawiy.d.mts.map +1 -0
  869. package/dist/types-gC6FAoa_.d.mts +75 -0
  870. package/dist/types-gC6FAoa_.d.mts.map +1 -0
  871. package/dist/types-k--LneIF.mjs +22 -0
  872. package/dist/types-k--LneIF.mjs.map +1 -0
  873. package/dist/upload-attempts-2xdnoKGe.mjs +27 -0
  874. package/dist/upload-attempts-2xdnoKGe.mjs.map +1 -0
  875. package/dist/url-pattern-3m3reMzr.mjs +33 -0
  876. package/dist/url-pattern-3m3reMzr.mjs.map +1 -0
  877. package/dist/user-DroRntkx.mjs +171 -0
  878. package/dist/user-DroRntkx.mjs.map +1 -0
  879. package/dist/utils-CC2oNGV8.mjs +435 -0
  880. package/dist/utils-CC2oNGV8.mjs.map +1 -0
  881. package/dist/validate-BCP9HNDn.mjs +384 -0
  882. package/dist/validate-BCP9HNDn.mjs.map +1 -0
  883. package/dist/validate-CyQ7I3xF.d.mts +410 -0
  884. package/dist/validate-CyQ7I3xF.d.mts.map +1 -0
  885. package/dist/validate-DANX1-Qi.mjs +120 -0
  886. package/dist/validate-DANX1-Qi.mjs.map +1 -0
  887. package/dist/validation-DBy6WF94.mjs +216 -0
  888. package/dist/validation-DBy6WF94.mjs.map +1 -0
  889. package/dist/version-B57E4Uo7.mjs +7 -0
  890. package/dist/version-B57E4Uo7.mjs.map +1 -0
  891. package/dist/widgets-wQ7Xcgq8.mjs +126 -0
  892. package/dist/widgets-wQ7Xcgq8.mjs.map +1 -0
  893. package/dist/wxr-taxonomies-DIHfvEDt.mjs +406 -0
  894. package/dist/wxr-taxonomies-DIHfvEDt.mjs.map +1 -0
  895. package/dist/wxr-taxonomies-DU2ZSiGc.d.mts +34 -0
  896. package/dist/wxr-taxonomies-DU2ZSiGc.d.mts.map +1 -0
  897. package/dist/zod-generator-D_yHIWfs.mjs +329 -0
  898. package/dist/zod-generator-D_yHIWfs.mjs.map +1 -0
  899. package/package.json +39 -12
  900. package/src/api/errors.ts +23 -0
  901. package/src/api/handlers/backup.ts +11 -5
  902. package/src/api/handlers/content.ts +226 -85
  903. package/src/api/handlers/dashboard.ts +31 -1
  904. package/src/api/handlers/entry-lock.ts +206 -0
  905. package/src/api/handlers/index.ts +3 -0
  906. package/src/api/handlers/manifest.ts +5 -6
  907. package/src/api/handlers/marketplace.ts +126 -34
  908. package/src/api/handlers/media-upload.ts +31 -4
  909. package/src/api/handlers/menus.ts +9 -0
  910. package/src/api/handlers/plugin-settings.ts +35 -3
  911. package/src/api/handlers/redirects.ts +221 -138
  912. package/src/api/handlers/registry.ts +75 -29
  913. package/src/api/handlers/revision.ts +16 -27
  914. package/src/api/handlers/seo.ts +10 -1
  915. package/src/api/handlers/snapshot.ts +87 -72
  916. package/src/api/handlers/taxonomies.ts +79 -5
  917. package/src/api/handlers/validation.ts +120 -19
  918. package/src/api/openapi/document.ts +133 -11
  919. package/src/api/schemas/content.ts +32 -12
  920. package/src/api/schemas/entry-lock.ts +57 -0
  921. package/src/api/schemas/index.ts +1 -0
  922. package/src/api/schemas/schema.ts +10 -1
  923. package/src/api/schemas/search.ts +4 -0
  924. package/src/api/types.ts +4 -0
  925. package/src/astro/integration/index.ts +22 -6
  926. package/src/astro/integration/routes.ts +89 -0
  927. package/src/astro/integration/runtime.ts +40 -24
  928. package/src/astro/integration/virtual-modules.ts +4 -0
  929. package/src/astro/integration/vite-config.ts +31 -0
  930. package/src/astro/middleware/auth.ts +25 -4
  931. package/src/astro/middleware/request-context.ts +28 -10
  932. package/src/astro/middleware.ts +33 -1
  933. package/src/astro/prefetch.ts +2 -2
  934. package/src/astro/routes/api/admin/comments/[id]/status.ts +22 -58
  935. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +2 -15
  936. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +2 -25
  937. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +1 -0
  938. package/src/astro/routes/api/admin/plugins/[id]/update.ts +23 -4
  939. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +12 -2
  940. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +1 -0
  941. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +29 -10
  942. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +8 -2
  943. package/src/astro/routes/api/admin/plugins/registry/install.ts +15 -4
  944. package/src/astro/routes/api/admin/plugins/registry/verify.ts +2 -1
  945. package/src/astro/routes/api/admin/plugins/updates.ts +6 -1
  946. package/src/astro/routes/api/admin/scheduled-policy-rejections/[collection]/[id].ts +34 -0
  947. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +4 -259
  948. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +10 -0
  949. package/src/astro/routes/api/content/[collection]/[id]/lock.ts +133 -0
  950. package/src/astro/routes/api/content/[collection]/[id]/plugin-extensions/[pluginId]/[kind]/[extensionId].ts +29 -0
  951. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +21 -4
  952. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +30 -1
  953. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +15 -1
  954. package/src/astro/routes/api/content/[collection]/[id].ts +27 -2
  955. package/src/astro/routes/api/content/[collection]/index.ts +3 -0
  956. package/src/astro/routes/api/health.ts +23 -0
  957. package/src/astro/routes/api/manifest.ts +14 -4
  958. package/src/astro/routes/api/mcp.ts +2 -1
  959. package/src/astro/routes/api/media/asset/[id]/[filename].ts +50 -0
  960. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +13 -81
  961. package/src/astro/routes/api/redirects/[id].ts +0 -3
  962. package/src/astro/routes/api/redirects/index.ts +0 -2
  963. package/src/astro/routes/api/search/index.ts +1 -0
  964. package/src/astro/routes/api/visual-editing/action-token.ts +36 -0
  965. package/src/astro/routes/api/visual-editing/content/[collection]/[id]/publish.ts +32 -0
  966. package/src/astro/routes/api/visual-editing/toolbar-labels.ts +16 -0
  967. package/src/astro/routes/sitemap-[collection].xml.ts +11 -4
  968. package/src/astro/types.ts +83 -12
  969. package/src/cli/commands/bundle-utils.ts +19 -19
  970. package/src/cli/commands/bundle.ts +9 -6
  971. package/src/cli/commands/content.ts +32 -6
  972. package/src/cli/commands/doctor.ts +25 -0
  973. package/src/cli/commands/export-seed.ts +195 -56
  974. package/src/cli/commands/migrate.ts +71 -23
  975. package/src/cli/commands/publish.ts +2 -2
  976. package/src/client/index.ts +34 -7
  977. package/src/comments/public-submission.ts +246 -0
  978. package/src/comments/service.ts +59 -12
  979. package/src/components/EmDashBodyEnd.astro +4 -1
  980. package/src/components/EmDashBodyStart.astro +4 -1
  981. package/src/components/EmDashHead.astro +36 -16
  982. package/src/components/Gallery.astro +18 -4
  983. package/src/components/InlineEditor.astro +3 -1
  984. package/src/components/InlinePortableTextEditor.tsx +106 -11
  985. package/src/components/PortableText.astro +4 -0
  986. package/src/components/Table.astro +147 -62
  987. package/src/components/widgets/Archives.astro +6 -5
  988. package/src/config/errors.ts +13 -0
  989. package/src/config/secrets.ts +25 -11
  990. package/src/content/converters/portable-text-to-prosemirror.ts +26 -2
  991. package/src/content/converters/prosemirror-to-portable-text.ts +35 -6
  992. package/src/content/converters/types.ts +10 -0
  993. package/src/content/known-fields.ts +44 -0
  994. package/src/database/connection.ts +5 -1
  995. package/src/database/content-datetime.ts +123 -0
  996. package/src/database/datetime-storage.ts +452 -0
  997. package/src/database/dialect-helpers.ts +161 -1
  998. package/src/database/migration-lock.ts +140 -0
  999. package/src/database/migrations/075_entry_edit_locks.ts +44 -0
  1000. package/src/database/migrations/076_collection_nav_group.ts +22 -0
  1001. package/src/database/migrations/077_plugin_storage_revisions.ts +89 -0
  1002. package/src/database/migrations/078_menu_item_translation_groups.ts +15 -0
  1003. package/src/database/migrations/079_datetime_normalization.ts +13 -0
  1004. package/src/database/migrations/080_content_translation_locale_unique.ts +88 -0
  1005. package/src/database/migrations/081_redirect_write_guards.ts +257 -0
  1006. package/src/database/migrations/082_taxonomy_translation_locale_unique.ts +49 -0
  1007. package/src/database/migrations/runner.ts +63 -15
  1008. package/src/database/pg-migration-lock.ts +2 -2
  1009. package/src/database/repositories/byline.ts +9 -0
  1010. package/src/database/repositories/comment.ts +86 -0
  1011. package/src/database/repositories/content.ts +560 -69
  1012. package/src/database/repositories/entry-locks.ts +257 -0
  1013. package/src/database/repositories/media.ts +35 -4
  1014. package/src/database/repositories/menu.ts +1 -0
  1015. package/src/database/repositories/options.ts +113 -7
  1016. package/src/database/repositories/plugin-storage.ts +159 -0
  1017. package/src/database/repositories/redirect.ts +262 -56
  1018. package/src/database/repositories/revision.ts +116 -17
  1019. package/src/database/repositories/taxonomy.ts +42 -0
  1020. package/src/database/repositories/types.ts +5 -0
  1021. package/src/database/types.ts +26 -1
  1022. package/src/datetime-normalization.ts +311 -0
  1023. package/src/db/node-sqlite-compat.ts +3 -2
  1024. package/src/emdash-runtime.ts +1994 -424
  1025. package/src/fields/image.ts +2 -0
  1026. package/src/i18n/config.ts +6 -6
  1027. package/src/i18n/resolve.ts +98 -15
  1028. package/src/index.ts +101 -0
  1029. package/src/loader.ts +49 -4
  1030. package/src/mcp/server.ts +216 -68
  1031. package/src/media/responsive.ts +23 -0
  1032. package/src/media/usage/content-refresh.ts +2 -1
  1033. package/src/media/usage/extractor.ts +50 -15
  1034. package/src/media/usage/types.ts +1 -1
  1035. package/src/menus/index.ts +64 -35
  1036. package/src/migrations/direct-executor.ts +40 -2
  1037. package/src/migrations/index.ts +1 -0
  1038. package/src/migrations/protocol.ts +11 -1
  1039. package/src/page/seo-contributions.ts +78 -1
  1040. package/src/page/seo-panel.ts +47 -0
  1041. package/src/plugin-test-runtime.ts +17 -0
  1042. package/src/plugin-types.ts +128 -21
  1043. package/src/plugins/adapt-sandbox-entry.ts +34 -27
  1044. package/src/plugins/conditional-storage.ts +30 -0
  1045. package/src/plugins/content-access.ts +170 -0
  1046. package/src/plugins/content-policy.ts +74 -0
  1047. package/src/plugins/context.ts +790 -166
  1048. package/src/plugins/cron.ts +18 -10
  1049. package/src/plugins/define-plugin.ts +55 -54
  1050. package/src/plugins/editor-draft.ts +257 -0
  1051. package/src/plugins/hooks.ts +136 -3
  1052. package/src/plugins/host.ts +29 -0
  1053. package/src/plugins/http-route-dispatch.ts +389 -0
  1054. package/src/plugins/http-wire.ts +180 -0
  1055. package/src/plugins/index.ts +97 -3
  1056. package/src/plugins/install-finalization.ts +77 -0
  1057. package/src/plugins/lifecycle.ts +47 -0
  1058. package/src/plugins/manager.ts +5 -2
  1059. package/src/plugins/manifest-schema.ts +314 -58
  1060. package/src/plugins/media.ts +166 -0
  1061. package/src/plugins/request-meta.ts +12 -2
  1062. package/src/plugins/route-wire.ts +304 -0
  1063. package/src/plugins/routes.ts +38 -5
  1064. package/src/plugins/sandbox/index.ts +4 -0
  1065. package/src/plugins/sandbox/proxy.ts +129 -0
  1066. package/src/plugins/sandbox/types.ts +83 -7
  1067. package/src/plugins/secret-redactor.ts +65 -0
  1068. package/src/plugins/settings.ts +303 -0
  1069. package/src/plugins/state.ts +26 -0
  1070. package/src/plugins/storage-query.ts +45 -0
  1071. package/src/plugins/storage-update.ts +96 -0
  1072. package/src/plugins/types.ts +602 -15
  1073. package/src/query.ts +84 -4
  1074. package/src/registry/authoritative-records.ts +26 -0
  1075. package/src/registry/config.ts +182 -31
  1076. package/src/registry/testing.ts +100 -0
  1077. package/src/registry/types.ts +12 -70
  1078. package/src/scheduled-publish.ts +70 -3
  1079. package/src/schema/collection-slugs-cache.ts +157 -0
  1080. package/src/schema/index.ts +1 -0
  1081. package/src/schema/registry.ts +64 -1
  1082. package/src/schema/types.ts +32 -6
  1083. package/src/schema/zod-generator.ts +5 -13
  1084. package/src/search/fts-manager.ts +3 -3
  1085. package/src/search/query.ts +32 -5
  1086. package/src/search/types.ts +8 -0
  1087. package/src/security/ssrf.ts +167 -141
  1088. package/src/seed/apply.ts +41 -3
  1089. package/src/seed/types.ts +13 -3
  1090. package/src/seed/validate.ts +18 -1
  1091. package/src/seo/hreflang.ts +10 -6
  1092. package/src/seo/index.ts +5 -10
  1093. package/src/seo/media-url.ts +26 -0
  1094. package/src/settings/index.ts +53 -11
  1095. package/src/taxonomies/term-counts.ts +22 -5
  1096. package/src/visual-editing/action-token.ts +31 -0
  1097. package/src/visual-editing/toolbar-bootstrap.ts +31 -6
  1098. package/src/visual-editing/toolbar.ts +103 -12
  1099. package/src/widgets/archives.ts +10 -9
  1100. package/src/widgets/index.ts +11 -1
  1101. package/dist/activation-K0kGBWuJ.mjs +0 -382
  1102. package/dist/activation-K0kGBWuJ.mjs.map +0 -1
  1103. package/dist/adapters-BDQwg236.d.mts +0 -171
  1104. package/dist/adapters-BDQwg236.d.mts.map +0 -1
  1105. package/dist/after-CBU2vkoO.mjs +0 -96
  1106. package/dist/after-CBU2vkoO.mjs.map +0 -1
  1107. package/dist/allowed-origins-BGglilcs.mjs +0 -116
  1108. package/dist/allowed-origins-BGglilcs.mjs.map +0 -1
  1109. package/dist/api-Dz6rgoa3.mjs +0 -10040
  1110. package/dist/api-Dz6rgoa3.mjs.map +0 -1
  1111. package/dist/api-tokens-BSsQACNe.mjs +0 -163
  1112. package/dist/api-tokens-BSsQACNe.mjs.map +0 -1
  1113. package/dist/apply-B4SIOxR1.mjs +0 -980
  1114. package/dist/apply-B4SIOxR1.mjs.map +0 -1
  1115. package/dist/authorize-Bkf_UHhD.mjs +0 -40
  1116. package/dist/authorize-Bkf_UHhD.mjs.map +0 -1
  1117. package/dist/backup-rwVAl9NI.mjs +0 -241
  1118. package/dist/backup-rwVAl9NI.mjs.map +0 -1
  1119. package/dist/base64-CvLhUKQv.mjs +0 -63
  1120. package/dist/base64-CvLhUKQv.mjs.map +0 -1
  1121. package/dist/bundle-C8S2Zl6U.mjs +0 -663
  1122. package/dist/bundle-C8S2Zl6U.mjs.map +0 -1
  1123. package/dist/byline-DuhF7NWl.mjs +0 -902
  1124. package/dist/byline-DuhF7NWl.mjs.map +0 -1
  1125. package/dist/byline-fields-BovY4KYM.mjs +0 -123
  1126. package/dist/byline-fields-BovY4KYM.mjs.map +0 -1
  1127. package/dist/byline-registry-Bpu9-MVs.mjs +0 -406
  1128. package/dist/byline-registry-Bpu9-MVs.mjs.map +0 -1
  1129. package/dist/bylines-CJ5nd6Lw.mjs +0 -232
  1130. package/dist/bylines-CJ5nd6Lw.mjs.map +0 -1
  1131. package/dist/bylines-RwBAeOF1.mjs +0 -214
  1132. package/dist/bylines-RwBAeOF1.mjs.map +0 -1
  1133. package/dist/cache-BF5Kvn59.mjs +0 -108
  1134. package/dist/cache-BF5Kvn59.mjs.map +0 -1
  1135. package/dist/capture-triggers-CNW1q3HK.mjs +0 -362
  1136. package/dist/capture-triggers-CNW1q3HK.mjs.map +0 -1
  1137. package/dist/challenge-store-DQN_Yrck.mjs +0 -51
  1138. package/dist/challenge-store-DQN_Yrck.mjs.map +0 -1
  1139. package/dist/chrome-tags-D8fiZG94.mjs +0 -24
  1140. package/dist/chrome-tags-D8fiZG94.mjs.map +0 -1
  1141. package/dist/chunks-C4szrD8o.mjs +0 -25
  1142. package/dist/chunks-C4szrD8o.mjs.map +0 -1
  1143. package/dist/collection-deletion-Bj07NHbj.mjs +0 -436
  1144. package/dist/collection-deletion-Bj07NHbj.mjs.map +0 -1
  1145. package/dist/comment-reaction-BhrvSHml.mjs +0 -85
  1146. package/dist/comment-reaction-BhrvSHml.mjs.map +0 -1
  1147. package/dist/comment-vxOzWbZN.mjs +0 -255
  1148. package/dist/comment-vxOzWbZN.mjs.map +0 -1
  1149. package/dist/comments-Ct4ensnf.mjs +0 -204
  1150. package/dist/comments-Ct4ensnf.mjs.map +0 -1
  1151. package/dist/components-DJbo1z0S.mjs +0 -108
  1152. package/dist/components-DJbo1z0S.mjs.map +0 -1
  1153. package/dist/config-B9eZ340f.d.mts +0 -34
  1154. package/dist/config-B9eZ340f.d.mts.map +0 -1
  1155. package/dist/config-C3EBstHi.mjs +0 -72
  1156. package/dist/config-C3EBstHi.mjs.map +0 -1
  1157. package/dist/config-it29yDLA.mjs +0 -1163
  1158. package/dist/config-it29yDLA.mjs.map +0 -1
  1159. package/dist/content-DjvM0T92.mjs +0 -1725
  1160. package/dist/content-DjvM0T92.mjs.map +0 -1
  1161. package/dist/content-list-query-Btqd4uV2.d.mts +0 -26
  1162. package/dist/content-list-query-Btqd4uV2.d.mts.map +0 -1
  1163. package/dist/content-refresh-Bwp-8yGy.mjs +0 -3588
  1164. package/dist/content-refresh-Bwp-8yGy.mjs.map +0 -1
  1165. package/dist/content-repair-Czdp7jz5.mjs +0 -440
  1166. package/dist/content-repair-Czdp7jz5.mjs.map +0 -1
  1167. package/dist/context-CX6gxOLW.mjs +0 -1068
  1168. package/dist/context-CX6gxOLW.mjs.map +0 -1
  1169. package/dist/cron-C6o7VppJ.mjs +0 -277
  1170. package/dist/cron-C6o7VppJ.mjs.map +0 -1
  1171. package/dist/dashboard-C2kTeeRI.mjs +0 -140
  1172. package/dist/dashboard-C2kTeeRI.mjs.map +0 -1
  1173. package/dist/db-errors-Cl-sKYTm.mjs +0 -55
  1174. package/dist/db-errors-Cl-sKYTm.mjs.map +0 -1
  1175. package/dist/default-Clra32mw.mjs +0 -81
  1176. package/dist/default-Clra32mw.mjs.map +0 -1
  1177. package/dist/deferred-tasks-CvDETTJO.d.mts +0 -9
  1178. package/dist/deferred-tasks-CvDETTJO.d.mts.map +0 -1
  1179. package/dist/dev-typegen-DzOPfsNj.mjs +0 -29
  1180. package/dist/dev-typegen-DzOPfsNj.mjs.map +0 -1
  1181. package/dist/device-flow-D2oCWvLJ.mjs +0 -466
  1182. package/dist/device-flow-D2oCWvLJ.mjs.map +0 -1
  1183. package/dist/dialect-helpers-BSbfaO3F.mjs +0 -202
  1184. package/dist/dialect-helpers-BSbfaO3F.mjs.map +0 -1
  1185. package/dist/direct-executor-Bfa5_ToE.mjs +0 -85
  1186. package/dist/direct-executor-Bfa5_ToE.mjs.map +0 -1
  1187. package/dist/dist-89xCrGqi.mjs +0 -810
  1188. package/dist/dist-89xCrGqi.mjs.map +0 -1
  1189. package/dist/email-console-Dzb6uGYa.mjs +0 -50
  1190. package/dist/email-console-Dzb6uGYa.mjs.map +0 -1
  1191. package/dist/enrich-BX0IDp4z.mjs +0 -194
  1192. package/dist/enrich-BX0IDp4z.mjs.map +0 -1
  1193. package/dist/error-DFrQjppn.mjs +0 -90
  1194. package/dist/error-DFrQjppn.mjs.map +0 -1
  1195. package/dist/errors-DsVoDUjJ.mjs +0 -427
  1196. package/dist/errors-DsVoDUjJ.mjs.map +0 -1
  1197. package/dist/escape-HmHTC1Is.mjs +0 -9
  1198. package/dist/escape-HmHTC1Is.mjs.map +0 -1
  1199. package/dist/field-defs-cache-BM4SIRqH.mjs +0 -77
  1200. package/dist/field-defs-cache-BM4SIRqH.mjs.map +0 -1
  1201. package/dist/focal-point-BeWFo6Xx.mjs +0 -23
  1202. package/dist/focal-point-BeWFo6Xx.mjs.map +0 -1
  1203. package/dist/fts-manager-Do1BlW2r.mjs +0 -413
  1204. package/dist/fts-manager-Do1BlW2r.mjs.map +0 -1
  1205. package/dist/hash-3JJPlHF2.mjs +0 -37
  1206. package/dist/hash-3JJPlHF2.mjs.map +0 -1
  1207. package/dist/hook-result-DCh2WUpe.d.mts +0 -31
  1208. package/dist/hook-result-DCh2WUpe.d.mts.map +0 -1
  1209. package/dist/hreflang-B6hbAuuO.d.mts +0 -48
  1210. package/dist/hreflang-B6hbAuuO.d.mts.map +0 -1
  1211. package/dist/hreflang-DQbPZliA.mjs +0 -102
  1212. package/dist/hreflang-DQbPZliA.mjs.map +0 -1
  1213. package/dist/identity-DksvEazf.mjs +0 -35
  1214. package/dist/identity-DksvEazf.mjs.map +0 -1
  1215. package/dist/import-CJpaBBBT.mjs +0 -2020
  1216. package/dist/import-CJpaBBBT.mjs.map +0 -1
  1217. package/dist/index-BJNMCWmu.d.mts +0 -6562
  1218. package/dist/index-BJNMCWmu.d.mts.map +0 -1
  1219. package/dist/index-Du8K7bWa.d.mts +0 -278
  1220. package/dist/index-Du8K7bWa.d.mts.map +0 -1
  1221. package/dist/init-lock-DJkX6Hto.mjs +0 -56
  1222. package/dist/init-lock-DJkX6Hto.mjs.map +0 -1
  1223. package/dist/load-CawHjTOH.mjs +0 -28
  1224. package/dist/load-CawHjTOH.mjs.map +0 -1
  1225. package/dist/loader-Z8vl7zTq.mjs +0 -966
  1226. package/dist/loader-Z8vl7zTq.mjs.map +0 -1
  1227. package/dist/manifest-builder-COtzuca2.mjs +0 -189
  1228. package/dist/manifest-builder-COtzuca2.mjs.map +0 -1
  1229. package/dist/manifest-schema-BNWwR8Vd.mjs +0 -301
  1230. package/dist/manifest-schema-BNWwR8Vd.mjs.map +0 -1
  1231. package/dist/media-BBm8Wg3f.mjs +0 -318
  1232. package/dist/media-BBm8Wg3f.mjs.map +0 -1
  1233. package/dist/media-BHjus86Y.mjs +0 -464
  1234. package/dist/media-BHjus86Y.mjs.map +0 -1
  1235. package/dist/media-allowlist-CoTBWoDp.mjs +0 -44
  1236. package/dist/media-allowlist-CoTBWoDp.mjs.map +0 -1
  1237. package/dist/media-folders-BGkATs8n.mjs +0 -210
  1238. package/dist/media-folders-BGkATs8n.mjs.map +0 -1
  1239. package/dist/media-upload-BCidR-EY.mjs +0 -147
  1240. package/dist/media-upload-BCidR-EY.mjs.map +0 -1
  1241. package/dist/media-url-DHoPr0hq.mjs +0 -26
  1242. package/dist/media-url-DHoPr0hq.mjs.map +0 -1
  1243. package/dist/media-usage-CdY5jhSR.mjs +0 -1833
  1244. package/dist/media-usage-CdY5jhSR.mjs.map +0 -1
  1245. package/dist/media-usage-activation-BgeMtK5C.mjs +0 -78
  1246. package/dist/media-usage-activation-BgeMtK5C.mjs.map +0 -1
  1247. package/dist/media-usage-work-CnGCdHxx.mjs +0 -152
  1248. package/dist/media-usage-work-CnGCdHxx.mjs.map +0 -1
  1249. package/dist/media-usage-work-D6D9sMCA.mjs +0 -586
  1250. package/dist/media-usage-work-D6D9sMCA.mjs.map +0 -1
  1251. package/dist/media-usage-write-fence-hjeD4u7s.mjs +0 -42
  1252. package/dist/media-usage-write-fence-hjeD4u7s.mjs.map +0 -1
  1253. package/dist/menus-6qSwTNxG.mjs +0 -4010
  1254. package/dist/menus-6qSwTNxG.mjs.map +0 -1
  1255. package/dist/menus-uvPLXMZf.mjs +0 -744
  1256. package/dist/menus-uvPLXMZf.mjs.map +0 -1
  1257. package/dist/mime-CUIG6x84.mjs +0 -36
  1258. package/dist/mime-CUIG6x84.mjs.map +0 -1
  1259. package/dist/mode-DN_WneRx.mjs +0 -23
  1260. package/dist/mode-DN_WneRx.mjs.map +0 -1
  1261. package/dist/node-sqlite-compat-BuJXIeMz.mjs +0 -46
  1262. package/dist/node-sqlite-compat-BuJXIeMz.mjs.map +0 -1
  1263. package/dist/normalize-C2xNjTSG.mjs +0 -191
  1264. package/dist/normalize-C2xNjTSG.mjs.map +0 -1
  1265. package/dist/oauth-authorization-Dj6Kwc21.mjs +0 -274
  1266. package/dist/oauth-authorization-Dj6Kwc21.mjs.map +0 -1
  1267. package/dist/oauth-clients-FD7I0J8m.mjs +0 -266
  1268. package/dist/oauth-clients-FD7I0J8m.mjs.map +0 -1
  1269. package/dist/oauth-state-store-DkmDOmHs.mjs +0 -50
  1270. package/dist/oauth-state-store-DkmDOmHs.mjs.map +0 -1
  1271. package/dist/oauth-user-lookup-CvIwGpYd.mjs +0 -26
  1272. package/dist/oauth-user-lookup-CvIwGpYd.mjs.map +0 -1
  1273. package/dist/object-cache-DVKnyOfy.mjs +0 -534
  1274. package/dist/object-cache-DVKnyOfy.mjs.map +0 -1
  1275. package/dist/options-BHPeIbBd.mjs +0 -117
  1276. package/dist/options-BHPeIbBd.mjs.map +0 -1
  1277. package/dist/options-CYb4y7hM.d.mts +0 -221
  1278. package/dist/options-CYb4y7hM.d.mts.map +0 -1
  1279. package/dist/parse-Ckpg-Qfp.mjs +0 -81
  1280. package/dist/parse-Ckpg-Qfp.mjs.map +0 -1
  1281. package/dist/passkey-config-O_lj3y0k.mjs +0 -46
  1282. package/dist/passkey-config-O_lj3y0k.mjs.map +0 -1
  1283. package/dist/patterns-D9iDO3j8.mjs +0 -145
  1284. package/dist/patterns-D9iDO3j8.mjs.map +0 -1
  1285. package/dist/placeholder-4X9BrRrp.d.mts +0 -306
  1286. package/dist/placeholder-4X9BrRrp.d.mts.map +0 -1
  1287. package/dist/policy-Dhtwntrt.mjs +0 -45
  1288. package/dist/policy-Dhtwntrt.mjs.map +0 -1
  1289. package/dist/portable-text-U85hOafW.mjs +0 -286
  1290. package/dist/portable-text-U85hOafW.mjs.map +0 -1
  1291. package/dist/preview-B_vJLvf8.mjs +0 -107
  1292. package/dist/preview-B_vJLvf8.mjs.map +0 -1
  1293. package/dist/protocol-JdD2YhY9.d.mts +0 -52
  1294. package/dist/protocol-JdD2YhY9.d.mts.map +0 -1
  1295. package/dist/public-url-CarhZJgf.mjs +0 -101
  1296. package/dist/public-url-CarhZJgf.mjs.map +0 -1
  1297. package/dist/query-BZKZcG5b.mjs +0 -881
  1298. package/dist/query-BZKZcG5b.mjs.map +0 -1
  1299. package/dist/query-BndaFxxp.mjs +0 -41
  1300. package/dist/query-BndaFxxp.mjs.map +0 -1
  1301. package/dist/rate-limit-BZmATC6W.mjs +0 -120
  1302. package/dist/rate-limit-BZmATC6W.mjs.map +0 -1
  1303. package/dist/redirect-CZkbozu9.mjs +0 -313
  1304. package/dist/redirect-CZkbozu9.mjs.map +0 -1
  1305. package/dist/redirect-CzqnLxK-.mjs +0 -17
  1306. package/dist/redirect-CzqnLxK-.mjs.map +0 -1
  1307. package/dist/redirects-BLNOWQTn.mjs +0 -1238
  1308. package/dist/redirects-BLNOWQTn.mjs.map +0 -1
  1309. package/dist/redirects-CqzNM0KM.mjs +0 -579
  1310. package/dist/redirects-CqzNM0KM.mjs.map +0 -1
  1311. package/dist/registry-AZAsRsor.mjs +0 -1179
  1312. package/dist/registry-AZAsRsor.mjs.map +0 -1
  1313. package/dist/relation-04DfKRTO.mjs +0 -334
  1314. package/dist/relation-04DfKRTO.mjs.map +0 -1
  1315. package/dist/relations-C4htk5sb.mjs +0 -309
  1316. package/dist/relations-C4htk5sb.mjs.map +0 -1
  1317. package/dist/relations-CcFwDJdJ.mjs +0 -403
  1318. package/dist/relations-CcFwDJdJ.mjs.map +0 -1
  1319. package/dist/relations-DnRK4YIT.d.mts +0 -3354
  1320. package/dist/relations-DnRK4YIT.d.mts.map +0 -1
  1321. package/dist/request-cache-DH9zdCOD.mjs +0 -104
  1322. package/dist/request-cache-DH9zdCOD.mjs.map +0 -1
  1323. package/dist/request-meta-DYxLlnmz.mjs +0 -140
  1324. package/dist/request-meta-DYxLlnmz.mjs.map +0 -1
  1325. package/dist/resolve-prZ3mP7l.mjs +0 -143
  1326. package/dist/resolve-prZ3mP7l.mjs.map +0 -1
  1327. package/dist/runner-BLIDKpmA.mjs +0 -4676
  1328. package/dist/runner-BLIDKpmA.mjs.map +0 -1
  1329. package/dist/runner-YKfIMnaF.d.mts +0 -67
  1330. package/dist/runner-YKfIMnaF.d.mts.map +0 -1
  1331. package/dist/schema-CGnBeLiW.mjs +0 -9
  1332. package/dist/schema-CGnBeLiW.mjs.map +0 -1
  1333. package/dist/schema-DYr-Vd-L.mjs +0 -420
  1334. package/dist/schema-DYr-Vd-L.mjs.map +0 -1
  1335. package/dist/scopes-BhWvBavj.mjs +0 -27
  1336. package/dist/scopes-BhWvBavj.mjs.map +0 -1
  1337. package/dist/search-C-qDk4kx.mjs +0 -439
  1338. package/dist/search-C-qDk4kx.mjs.map +0 -1
  1339. package/dist/secrets-rEtHt-6z.mjs +0 -330
  1340. package/dist/secrets-rEtHt-6z.mjs.map +0 -1
  1341. package/dist/sections-B_nYvlwn.mjs +0 -346
  1342. package/dist/sections-B_nYvlwn.mjs.map +0 -1
  1343. package/dist/seo-926NRIgt.mjs +0 -133
  1344. package/dist/seo-926NRIgt.mjs.map +0 -1
  1345. package/dist/seo-CVWkdv6x.mjs +0 -113
  1346. package/dist/seo-CVWkdv6x.mjs.map +0 -1
  1347. package/dist/service-CJl3ZAYs.mjs +0 -195
  1348. package/dist/service-CJl3ZAYs.mjs.map +0 -1
  1349. package/dist/session-user-BlMgqCHS.mjs +0 -51
  1350. package/dist/session-user-BlMgqCHS.mjs.map +0 -1
  1351. package/dist/settings--wobrkkC.mjs +0 -258
  1352. package/dist/settings--wobrkkC.mjs.map +0 -1
  1353. package/dist/settings-Cwm6o2jY.mjs +0 -51
  1354. package/dist/settings-Cwm6o2jY.mjs.map +0 -1
  1355. package/dist/setup-complete-DQbClIN2.mjs +0 -26
  1356. package/dist/setup-complete-DQbClIN2.mjs.map +0 -1
  1357. package/dist/setup-nonce-DZCFS2sn.mjs +0 -25
  1358. package/dist/setup-nonce-DZCFS2sn.mjs.map +0 -1
  1359. package/dist/single-flight-cache-BPuGPdcN.mjs +0 -104
  1360. package/dist/single-flight-cache-BPuGPdcN.mjs.map +0 -1
  1361. package/dist/site-url-BrxA_E9H.mjs +0 -16
  1362. package/dist/site-url-BrxA_E9H.mjs.map +0 -1
  1363. package/dist/slugify-B994zkXC.mjs +0 -17
  1364. package/dist/slugify-B994zkXC.mjs.map +0 -1
  1365. package/dist/snapshot--OCDTqdb.mjs +0 -216
  1366. package/dist/snapshot--OCDTqdb.mjs.map +0 -1
  1367. package/dist/ssrf-5sIM5jzl.mjs +0 -349
  1368. package/dist/ssrf-5sIM5jzl.mjs.map +0 -1
  1369. package/dist/state-BpFVsJZB.mjs +0 -136
  1370. package/dist/state-BpFVsJZB.mjs.map +0 -1
  1371. package/dist/status-B777-dXz.mjs +0 -30
  1372. package/dist/status-B777-dXz.mjs.map +0 -1
  1373. package/dist/target-BmR0iTwz.mjs +0 -27
  1374. package/dist/target-BmR0iTwz.mjs.map +0 -1
  1375. package/dist/taxonomies-DkAkMupr.mjs +0 -837
  1376. package/dist/taxonomies-DkAkMupr.mjs.map +0 -1
  1377. package/dist/taxonomies-Wfy_Lg12.mjs +0 -698
  1378. package/dist/taxonomies-Wfy_Lg12.mjs.map +0 -1
  1379. package/dist/taxonomy-tJepplLL.mjs +0 -484
  1380. package/dist/taxonomy-tJepplLL.mjs.map +0 -1
  1381. package/dist/tokens-BCO1oruF.mjs +0 -171
  1382. package/dist/tokens-BCO1oruF.mjs.map +0 -1
  1383. package/dist/transaction-0KOpOyMu.mjs +0 -29
  1384. package/dist/transaction-0KOpOyMu.mjs.map +0 -1
  1385. package/dist/transport-Dr3qtwXC.mjs +0 -136
  1386. package/dist/transport-Dr3qtwXC.mjs.map +0 -1
  1387. package/dist/transport-xh6YKW2G.d.mts +0 -42
  1388. package/dist/transport-xh6YKW2G.d.mts.map +0 -1
  1389. package/dist/trusted-proxy-BR5F1Otp.mjs +0 -51
  1390. package/dist/trusted-proxy-BR5F1Otp.mjs.map +0 -1
  1391. package/dist/types-3qNFPlci.mjs +0 -16
  1392. package/dist/types-3qNFPlci.mjs.map +0 -1
  1393. package/dist/types-75KIqzL9.mjs +0 -142
  1394. package/dist/types-75KIqzL9.mjs.map +0 -1
  1395. package/dist/types-B6GrPic4.d.mts +0 -1335
  1396. package/dist/types-B6GrPic4.d.mts.map +0 -1
  1397. package/dist/types-BuHW4xDE.d.mts +0 -259
  1398. package/dist/types-BuHW4xDE.d.mts.map +0 -1
  1399. package/dist/types-C00oyCZp.d.mts +0 -198
  1400. package/dist/types-C00oyCZp.d.mts.map +0 -1
  1401. package/dist/types-CDtkNSFQ.d.mts +0 -75
  1402. package/dist/types-CDtkNSFQ.d.mts.map +0 -1
  1403. package/dist/types-CPeXo1wF.d.mts +0 -124
  1404. package/dist/types-CPeXo1wF.d.mts.map +0 -1
  1405. package/dist/types-CQPWhPxE.mjs +0 -92
  1406. package/dist/types-CQPWhPxE.mjs.map +0 -1
  1407. package/dist/types-CXFkwEGC.d.mts +0 -198
  1408. package/dist/types-CXFkwEGC.d.mts.map +0 -1
  1409. package/dist/types-DM3vioBY.mjs +0 -18
  1410. package/dist/types-DM3vioBY.mjs.map +0 -1
  1411. package/dist/types-DczELMop.d.mts +0 -363
  1412. package/dist/types-DczELMop.d.mts.map +0 -1
  1413. package/dist/types-DfuTR6dL.mjs +0 -3
  1414. package/dist/types-FV23hEoW.d.mts +0 -743
  1415. package/dist/types-FV23hEoW.d.mts.map +0 -1
  1416. package/dist/types-xDMtUFnO.d.mts +0 -257
  1417. package/dist/types-xDMtUFnO.d.mts.map +0 -1
  1418. package/dist/upload-attempts-BsqoQ1fn.mjs +0 -27
  1419. package/dist/upload-attempts-BsqoQ1fn.mjs.map +0 -1
  1420. package/dist/url-pattern-D4c2czpJ.mjs +0 -33
  1421. package/dist/url-pattern-D4c2czpJ.mjs.map +0 -1
  1422. package/dist/user-BTZ40aq6.mjs +0 -171
  1423. package/dist/user-BTZ40aq6.mjs.map +0 -1
  1424. package/dist/utils-BKvNjjhY.mjs +0 -435
  1425. package/dist/utils-BKvNjjhY.mjs.map +0 -1
  1426. package/dist/validate-BpwoDOgt.mjs +0 -120
  1427. package/dist/validate-BpwoDOgt.mjs.map +0 -1
  1428. package/dist/validate-DE_LRwo9.mjs +0 -379
  1429. package/dist/validate-DE_LRwo9.mjs.map +0 -1
  1430. package/dist/validate-DUP27OYD.d.mts +0 -400
  1431. package/dist/validate-DUP27OYD.d.mts.map +0 -1
  1432. package/dist/validation-BMImFJ11.mjs +0 -153
  1433. package/dist/validation-BMImFJ11.mjs.map +0 -1
  1434. package/dist/version-Bii3suaO.mjs +0 -7
  1435. package/dist/version-Bii3suaO.mjs.map +0 -1
  1436. package/dist/widgets-D3gJ10xU.mjs +0 -122
  1437. package/dist/widgets-D3gJ10xU.mjs.map +0 -1
  1438. package/dist/wxr-taxonomies-B2Og_6rU.d.mts +0 -34
  1439. package/dist/wxr-taxonomies-B2Og_6rU.d.mts.map +0 -1
  1440. package/dist/wxr-taxonomies-Cxngsjwo.mjs +0 -406
  1441. package/dist/wxr-taxonomies-Cxngsjwo.mjs.map +0 -1
  1442. package/dist/zod-generator-BuYSQJB7.mjs +0 -331
  1443. package/dist/zod-generator-BuYSQJB7.mjs.map +0 -1
  1444. /package/dist/{api-tokens-BfYts-sq.mjs → api-tokens-DS1VzN7_.mjs} +0 -0
  1445. /package/dist/{ssrf-Dg3cIT-b.mjs → ssrf-BRpTDMV6.mjs} +0 -0
@@ -7,16 +7,30 @@
7
7
  * Created once per worker lifetime, cached and reused across requests.
8
8
  */
9
9
 
10
+ import { getLocaleDir, resolveLocale } from "@emdash-cms/admin/locales";
10
11
  import { Permissions } from "@emdash-cms/auth";
11
12
  import type { Element } from "@emdash-cms/blocks";
13
+ import {
14
+ normalizePluginPagePath,
15
+ validateBlockResponse,
16
+ validateContentEditorActionResponse,
17
+ type BlockValidationPolicy,
18
+ type PluginUiContext,
19
+ } from "@emdash-cms/blocks/server";
20
+ import { isJsonPostRouteContract } from "@emdash-cms/plugin-types";
12
21
  import { Kysely, type Dialect } from "kysely";
13
22
  import virtualConfig from "virtual:emdash/config";
14
23
  import { z } from "zod";
15
24
 
16
25
  import { ErrorCode } from "./api/errors.js";
17
26
  import { buildManifestCollections } from "./api/handlers/manifest.js";
27
+ import {
28
+ handleMediaUpload as uploadMedia,
29
+ type MediaUploadInput,
30
+ } from "./api/handlers/media-upload.js";
18
31
  import { assertMediaUsageActivationWriteAllowed } from "./api/media-usage-write-fence.js";
19
32
  import { validateRev } from "./api/rev.js";
33
+ import { getSiteBaseUrl } from "./api/site-url.js";
20
34
  import type {
21
35
  EmDashConfig,
22
36
  PluginAdminPage,
@@ -25,7 +39,9 @@ import type {
25
39
  import type { EmDashManifest } from "./astro/types.js";
26
40
  import { getAuthMode } from "./auth/mode.js";
27
41
  import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
42
+ import { lookupContentAuthor, sendCommentNotification } from "./comments/notifications.js";
28
43
  import type { ContentFieldFilters } from "./content-list-query.js";
44
+ import { keepKnownFields, staleStoredKeys } from "./content/known-fields.js";
29
45
  import { isSqlite } from "./database/dialect-helpers.js";
30
46
  import { kyselyLogOption } from "./database/instrumentation.js";
31
47
  import {
@@ -38,6 +54,8 @@ import {
38
54
  MIGRATION_RACE_WAIT_MS,
39
55
  } from "./database/migrations/runner.js";
40
56
  import { AuditRepository } from "./database/repositories/audit.js";
57
+ import { CommentRepository } from "./database/repositories/comment.js";
58
+ import type { CommentStatus } from "./database/repositories/comment.js";
41
59
  import { ContentRepository } from "./database/repositories/content.js";
42
60
  import { RevisionRepository } from "./database/repositories/revision.js";
43
61
  import { ContentMutationConflictError } from "./database/repositories/types.js";
@@ -46,7 +64,7 @@ import type {
46
64
  ContentDateField,
47
65
  } from "./database/repositories/types.js";
48
66
  import type { ImageValue } from "./fields/types.js";
49
- import { getI18nConfig } from "./i18n/config.js";
67
+ import { getI18nConfig, resolveContentCreateLocale } from "./i18n/config.js";
50
68
  import { repairLocaleCasing } from "./i18n/repair-locale-casing.js";
51
69
  import { warnAboutUnconfiguredTaxonomyLocales } from "./i18n/taxonomy-locale-diagnostic.js";
52
70
  import { normalizeMediaValue } from "./media/normalize.js";
@@ -59,7 +77,17 @@ import {
59
77
  refreshContentMediaUsageAfterWrite,
60
78
  } from "./media/usage/content-refresh.js";
61
79
  import { processMediaUsageWorkAfterWrite } from "./media/usage/work-processor.js";
62
- import { inspectSandboxHookResult } from "./plugins/sandbox/hook-result.js";
80
+ import {
81
+ scheduledPolicyRejectionKey,
82
+ type ScheduledPolicyRejection,
83
+ } from "./plugins/content-policy.js";
84
+ import { createCommentAccess, createTaxonomyAccessWithWrite } from "./plugins/context.js";
85
+ import type { ContentActionCallbacks } from "./plugins/context.js";
86
+ import type { PluginContentCacheInvalidator } from "./plugins/routes.js";
87
+ import {
88
+ createSandboxedPluginProxy,
89
+ getSandboxSaveRejectionDetails,
90
+ } from "./plugins/sandbox/proxy.js";
63
91
  import { createSandboxRunnerOptions } from "./plugins/sandbox/runner-options.js";
64
92
  import { getSandboxRouteErrorDetails } from "./plugins/sandbox/types.js";
65
93
  import type {
@@ -68,13 +96,18 @@ import type {
68
96
  SandboxRunnerFactory,
69
97
  } from "./plugins/sandbox/types.js";
70
98
  import type {
71
- ContentHookEvent,
99
+ ActorInfo,
100
+ ContentActionOrigin,
101
+ ContentItem as PluginContentItem,
72
102
  ResolvedPlugin,
73
103
  MediaItem,
104
+ MediaMetadataPatch,
74
105
  PluginManifest,
75
106
  PluginCapability,
76
107
  PluginStorageConfig,
77
108
  PluginMcpManifestConfig,
109
+ PluginEditorAction,
110
+ PluginEditorPanel,
78
111
  PublicPageContext,
79
112
  PageMetadataContribution,
80
113
  PageFragmentContribution,
@@ -82,8 +115,18 @@ import type {
82
115
  FieldWidgetConfig,
83
116
  SettingField,
84
117
  UserInfo,
118
+ CollectionCommentSettings,
119
+ ModerationDecision,
120
+ PluginComment,
121
+ PluginCommentStatus,
122
+ ContentWriteInput,
123
+ PluginContentCreateCallback,
124
+ VersionedContentItem,
85
125
  } from "./plugins/types.js";
126
+ import { normalizePluginCapabilities } from "./plugins/types.js";
86
127
  import { recordSchedulerHeartbeatSafely } from "./scheduler-health.js";
128
+ import { primeRegisteredCollections } from "./schema/collection-slugs-cache.js";
129
+ import type { CollectionWithFields } from "./schema/types.js";
87
130
  import { isMissingTableError } from "./utils/db-errors.js";
88
131
  import { hashString } from "./utils/hash.js";
89
132
  import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
@@ -93,6 +136,19 @@ import { COMMIT, VERSION } from "./version.js";
93
136
  const LEADING_SLASH_PATTERN = /^\//;
94
137
  const LOCALE_CASING_REPAIR_OPTION = "emdash:repair_locale_casing";
95
138
 
139
+ const PLUGIN_COMMENT_STATUSES = new Set<string>(["approved", "pending", "spam"]);
140
+
141
+ function assertPluginCommentStatus(
142
+ value: string,
143
+ name: "status" | "expectedStatus",
144
+ ): asserts value is PluginCommentStatus {
145
+ if (!PLUGIN_COMMENT_STATUSES.has(value)) {
146
+ throw Object.assign(new Error(`${name} must be one of: approved, pending, spam`), {
147
+ code: "COMMENT_STATUS_INVALID",
148
+ });
149
+ }
150
+ }
151
+
96
152
  function getLocaleCasingRepairVersion(locales: readonly string[]): string | null {
97
153
  if (locales.length === 0) return null;
98
154
  return `1:${locales.toSorted().join(",")}`;
@@ -112,50 +168,20 @@ function parseStringArray(raw: string | null | undefined): string[] {
112
168
  return parsed.filter((v): v is string => typeof v === "string");
113
169
  }
114
170
 
171
+ function isModerationDecision(value: unknown): value is ModerationDecision {
172
+ if (typeof value !== "object" || value === null || !("status" in value)) return false;
173
+ if (value.status !== "pending" && value.status !== "approved" && value.status !== "spam") {
174
+ return false;
175
+ }
176
+ return !("reason" in value) || value.reason === undefined || typeof value.reason === "string";
177
+ }
178
+
115
179
  /** Combined result from a single-pass page contribution collection */
116
180
  interface PageContributions {
117
181
  metadata: PageMetadataContribution[];
118
182
  fragments: PageFragmentContribution[];
119
183
  }
120
184
 
121
- const VALID_METADATA_KINDS = new Set(["meta", "property", "link", "jsonld"]);
122
-
123
- /** Security-critical allowlist for link rel values from sandboxed plugins */
124
- const VALID_LINK_REL = new Set([
125
- "canonical",
126
- "alternate",
127
- "author",
128
- "license",
129
- "nlweb",
130
- "site.standard.document",
131
- ]);
132
-
133
- /**
134
- * Runtime validation for sandboxed plugin metadata contributions.
135
- * Sandboxed plugins return `unknown` across the RPC boundary — we must
136
- * verify the shape before passing to the metadata collector.
137
- */
138
- function isValidMetadataContribution(c: unknown): c is PageMetadataContribution {
139
- if (!c || typeof c !== "object" || !("kind" in c)) return false;
140
- const obj = c as Record<string, unknown>;
141
- if (typeof obj.kind !== "string" || !VALID_METADATA_KINDS.has(obj.kind)) return false;
142
-
143
- switch (obj.kind) {
144
- case "meta":
145
- return typeof obj.name === "string" && typeof obj.content === "string";
146
- case "property":
147
- return typeof obj.property === "string" && typeof obj.content === "string";
148
- case "link":
149
- return (
150
- typeof obj.href === "string" && typeof obj.rel === "string" && VALID_LINK_REL.has(obj.rel)
151
- );
152
- case "jsonld":
153
- return obj.graph != null && typeof obj.graph === "object";
154
- default:
155
- return false;
156
- }
157
- }
158
-
159
185
  import { after } from "./after.js";
160
186
  import { maybeRunScheduledBackup } from "./api/handlers/backup.js";
161
187
  import { loadBundleFromR2 } from "./api/handlers/marketplace.js";
@@ -164,6 +190,15 @@ import {
164
190
  DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
165
191
  defaultCommentModerate,
166
192
  } from "./comments/moderator.js";
193
+ import { submitPublicComment } from "./comments/public-submission.js";
194
+ import {
195
+ CommentStatusConflictError,
196
+ createComment,
197
+ moderateComment,
198
+ type CommentCreateInput,
199
+ type CommentCreateResult,
200
+ type CommentHookRunner,
201
+ } from "./comments/service.js";
167
202
  import { validateEncryptionKeyAtStartup } from "./config/secrets.js";
168
203
  import { OptionsRepository } from "./database/repositories/options.js";
169
204
  import {
@@ -183,6 +218,7 @@ import {
183
218
  handleContentUnpublish,
184
219
  handleContentSchedule,
185
220
  handleContentUnschedule,
221
+ handleScheduledPolicyRejection,
186
222
  handleContentCountScheduled,
187
223
  handleContentDiscardDraft,
188
224
  handleContentCompare,
@@ -208,11 +244,15 @@ import { DEV_CONSOLE_EMAIL_PLUGIN_ID, devConsoleEmailDeliver } from "./plugins/e
208
244
  import { EmailPipeline } from "./plugins/email.js";
209
245
  import {
210
246
  createHookPipeline,
247
+ getActiveContentSaveHookName,
211
248
  resolveExclusiveHooks as resolveExclusiveHooksShared,
212
249
  type HookPipeline,
213
250
  } from "./plugins/hooks.js";
214
- import { normalizeManifestRoute } from "./plugins/manifest-schema.js";
251
+ import { disableRuntimePlugin, enableRuntimePlugin } from "./plugins/lifecycle.js";
252
+ import { HOOK_NAMES, normalizeManifestRoute } from "./plugins/manifest-schema.js";
253
+ import { updatePluginMediaMetadata } from "./plugins/media.js";
215
254
  import { extractRequestMeta, sanitizeHeadersForSandbox } from "./plugins/request-meta.js";
255
+ import { PluginRouteRequestError } from "./plugins/route-wire.js";
216
256
  import {
217
257
  buildRouteMeta,
218
258
  parseRouteInput,
@@ -225,15 +265,41 @@ import { isContentSaveRejection } from "./plugins/save-rejection.js";
225
265
  import type { CronScheduler } from "./plugins/scheduler/types.js";
226
266
  import { PluginStateRepository } from "./plugins/state.js";
227
267
  import { syncDeclaredStorageIndexes } from "./plugins/storage-indexes.js";
228
- import { normalizeRegistryConfig } from "./registry/config.js";
268
+ import {
269
+ getRegistryConfigInput,
270
+ resolveManifestRegistryConfig,
271
+ resolveRegistryConfigForSandbox,
272
+ } from "./registry/config.js";
229
273
  import { requestCached } from "./request-cache.js";
230
274
  import { getRequestContext } from "./request-context.js";
231
275
  import { publishDueContent, type PublishedRef } from "./scheduled-publish.js";
232
276
  import { FTSManager } from "./search/fts-manager.js";
233
277
  import { invalidateSiteSettingsCache } from "./settings/index.js";
234
278
 
235
- const DRAFT_ONLY_UPDATE_KEYS = new Set(["data", "slug", "locale", "skipRevision"]);
279
+ const DRAFT_ONLY_UPDATE_KEYS = new Set(["data", "slug", "locale", "skipRevision", "actor"]);
236
280
  const MAX_DRAFT_STAGE_ATTEMPTS = 32;
281
+ const PLUGIN_INVOCATION_RELEASE_GRACE_MS = 60_000;
282
+
283
+ type ContentPolicyHookName =
284
+ | "content:beforePublish"
285
+ | "content:beforeSchedule"
286
+ | "content:beforeUnpublish";
287
+
288
+ interface ContentPolicyMutationOptions {
289
+ _rev?: string;
290
+ actor?: ActorInfo;
291
+ origin?: ContentActionOrigin;
292
+ pluginInvocationId?: string;
293
+ }
294
+
295
+ type ContentPolicyCheck =
296
+ | { allowed: true; revision: string }
297
+ | {
298
+ allowed: false;
299
+ revision: string;
300
+ error: { code: string; message: string };
301
+ cancellation?: { pluginId: string; reason: string };
302
+ };
237
303
 
238
304
  /**
239
305
  * Sandboxed plugin entry from virtual module
@@ -259,6 +325,10 @@ export interface SandboxedPluginEntry {
259
325
  adminPages?: Array<{ path: string; label?: string; icon?: string }>;
260
326
  /** Dashboard widgets */
261
327
  adminWidgets?: Array<{ id: string; title?: string; size?: string }>;
328
+ /** Saved-entry Block Kit panels. */
329
+ editorPanels?: PluginManifest["admin"]["editorPanels"];
330
+ /** Saved-entry host-rendered actions. */
331
+ editorActions?: PluginManifest["admin"]["editorActions"];
262
332
  /** Settings schema for the auto-generated admin settings form */
263
333
  settingsSchema?: Record<string, SettingField>;
264
334
  /** Portable Text block types contributed to the editor (declarative Block Kit) */
@@ -274,6 +344,26 @@ export interface SandboxedPluginEntry {
274
344
  preferred?: string[];
275
345
  }
276
346
 
347
+ export type ResolvedPluginEditorExtension =
348
+ | {
349
+ kind: "panel";
350
+ extension: PluginEditorPanel;
351
+ policy: BlockValidationPolicy;
352
+ capabilities: readonly PluginCapability[];
353
+ }
354
+ | {
355
+ kind: "action";
356
+ extension: PluginEditorAction;
357
+ policy: BlockValidationPolicy;
358
+ capabilities: readonly PluginCapability[];
359
+ };
360
+
361
+ export interface PluginEditorExtensionDispatch {
362
+ ui: PluginUiContext;
363
+ kind: "panel" | "action";
364
+ policy: BlockValidationPolicy;
365
+ }
366
+
277
367
  /**
278
368
  * Media provider entry from virtual module
279
369
  */
@@ -350,6 +440,15 @@ export interface RuntimeDependencies {
350
440
  sandboxedPluginEntries: SandboxedPluginEntry[];
351
441
  /** Factory function supplied by the active platform adapter. */
352
442
  createSandboxRunner: SandboxRunnerFactory | null;
443
+ /** Clock used by cron scheduling, task execution, and scheduled publishing. */
444
+ now?: () => Date;
445
+ /** Site values supplied by hosts that do not load Astro's virtual config. */
446
+ siteInfo?: {
447
+ name?: string;
448
+ url?: string;
449
+ locale?: string;
450
+ trailingSlash?: "always" | "never" | "ignore";
451
+ };
353
452
  }
354
453
 
355
454
  /**
@@ -385,7 +484,16 @@ export interface EmDashRuntimeParts {
385
484
  db: Kysely<Database>;
386
485
  getDb?: () => Kysely<Database>;
387
486
  beforeContentWrite?: () => Promise<void>;
487
+ contentCreate?: PluginContentCreateCallback;
488
+ contentActions?: ContentActionCallbacks;
489
+ now?: () => Date;
388
490
  storage?: Storage;
491
+ commentModerate?: (
492
+ pluginId: string,
493
+ id: string,
494
+ status: PluginCommentStatus,
495
+ expectedStatus: PluginCommentStatus,
496
+ ) => Promise<PluginComment>;
389
497
  siteInfo?: {
390
498
  siteName?: string;
391
499
  siteUrl?: string;
@@ -404,6 +512,17 @@ export interface EmDashRuntimeParts {
404
512
  */
405
513
  function beforeSaveFailure(error: unknown) {
406
514
  if (isContentSaveRejection(error)) {
515
+ const sandbox = getSandboxSaveRejectionDetails(error);
516
+ if (sandbox) {
517
+ return {
518
+ success: false as const,
519
+ error: {
520
+ code: ErrorCode.SAVE_REJECTED,
521
+ message: error.message,
522
+ details: sandbox,
523
+ },
524
+ };
525
+ }
407
526
  return {
408
527
  success: false as const,
409
528
  error: { code: ErrorCode.SAVE_REJECTED, message: error.message },
@@ -427,6 +546,27 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
427
546
  return { ...item };
428
547
  }
429
548
 
549
+ function toPluginContentItem(item: ContentItemInternal): PluginContentItem {
550
+ return {
551
+ id: item.id,
552
+ type: item.type,
553
+ slug: item.slug,
554
+ status: item.status,
555
+ locale: item.locale,
556
+ data: item.data,
557
+ createdAt: item.createdAt,
558
+ updatedAt: item.updatedAt,
559
+ publishedAt: item.publishedAt,
560
+ scheduledAt: item.scheduledAt,
561
+ authorId: item.authorId,
562
+ translationGroup: item.translationGroup,
563
+ liveRevisionId: item.liveRevisionId,
564
+ draftRevisionId: item.draftRevisionId,
565
+ version: item.version,
566
+ ...(item.seo === undefined ? {} : { seo: item.seo }),
567
+ };
568
+ }
569
+
430
570
  /**
431
571
  * Db init lock reclaim deadline. Derived from the migration race wait so
432
572
  * they can't drift apart: a healthy init can legitimately block for the
@@ -507,6 +647,7 @@ function getSeedHolder(): SeedHolder {
507
647
  }
508
648
  const storageCache = new Map<string, Storage>();
509
649
  const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
650
+ const sandboxedManifestCache = new Map<string, PluginManifest>();
510
651
  /**
511
652
  * Per-tier sets of `${pluginId}:${version}` keys present in
512
653
  * `sandboxedPluginCache`. Used during sync to know which entries belong
@@ -529,15 +670,35 @@ const marketplaceManifestCache = new Map<
529
670
  pages?: PluginAdminPage[];
530
671
  widgets?: PluginDashboardWidget[];
531
672
  settingsSchema?: Record<string, SettingField>;
673
+ editorPanels?: PluginManifest["admin"]["editorPanels"];
674
+ editorActions?: PluginManifest["admin"]["editorActions"];
532
675
  };
533
676
  mcp?: PluginMcpManifestConfig;
534
677
  storage?: PluginManifest["storage"];
678
+ allowedHosts?: string[];
679
+ capabilities?: PluginCapability[];
535
680
  }
536
681
  >();
537
682
  /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
538
683
  const sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();
539
684
  let sandboxRunner: SandboxRunner | null = null;
540
685
 
686
+ function allowedBrowserImageHosts(
687
+ capabilities: readonly PluginCapability[],
688
+ allowedHosts: readonly string[],
689
+ ): readonly string[] {
690
+ if (
691
+ capabilities.includes("network:request:unrestricted") ||
692
+ capabilities.includes("network:fetch:any")
693
+ ) {
694
+ return ["*"];
695
+ }
696
+ if (capabilities.includes("network:request") || capabilities.includes("network:fetch")) {
697
+ return allowedHosts;
698
+ }
699
+ return [];
700
+ }
701
+
541
702
  /**
542
703
  * EmDashRuntime - singleton per worker
543
704
  */
@@ -576,6 +737,16 @@ export class EmDashRuntime {
576
737
  private cronScheduler: CronScheduler | null;
577
738
  private enabledPlugins: Set<string>;
578
739
  private pluginStates: Map<string, string>;
740
+ private readonly activePluginContentActions = new Map<string, number>();
741
+ private readonly pendingPluginAfterHooks = new Map<string, Array<() => Promise<void>>>();
742
+ private readonly activePluginInvocations = new Set<string>();
743
+ /** Timed-out sandbox invocations whose later actions must schedule hooks without another flush. */
744
+ private readonly releasedPluginInvocations = new Set<string>();
745
+ private readonly pluginInvocationCacheInvalidators = new Map<
746
+ string,
747
+ PluginContentCacheInvalidator
748
+ >();
749
+ private pluginContentCacheInvalidator?: PluginContentCacheInvalidator;
579
750
 
580
751
  /**
581
752
  * Isolate-lifetime guard so FTS indexes are verified at most once per
@@ -600,7 +771,16 @@ export class EmDashRuntime {
600
771
  db: Kysely<Database>;
601
772
  getDb?: () => Kysely<Database>;
602
773
  beforeContentWrite?: () => Promise<void>;
774
+ contentCreate?: PluginContentCreateCallback;
775
+ contentActions?: ContentActionCallbacks;
776
+ now?: () => Date;
603
777
  storage?: Storage;
778
+ commentModerate?: (
779
+ pluginId: string,
780
+ id: string,
781
+ status: PluginCommentStatus,
782
+ expectedStatus: PluginCommentStatus,
783
+ ) => Promise<PluginComment>;
604
784
  siteInfo?: {
605
785
  siteName?: string;
606
786
  siteUrl?: string;
@@ -612,6 +792,7 @@ export class EmDashRuntime {
612
792
  private runtimeDeps: RuntimeDependencies;
613
793
  /** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
614
794
  private pipelineRef!: { current: HookPipeline };
795
+ private readonly commentModerationInProgress = new Set<string>();
615
796
 
616
797
  /**
617
798
  * Get the database instance for the current request.
@@ -629,6 +810,13 @@ export class EmDashRuntime {
629
810
  return this._db;
630
811
  }
631
812
 
813
+ /** Bind the platform cache provider used by plugin actions, including request-less cron hooks. */
814
+ setPluginContentCacheInvalidator(
815
+ invalidateContentCache: PluginContentCacheInvalidator | undefined,
816
+ ): void {
817
+ this.pluginContentCacheInvalidator = invalidateContentCache;
818
+ }
819
+
632
820
  constructor(parts: EmDashRuntimeParts) {
633
821
  this._db = parts.db;
634
822
  this.storage = parts.storage;
@@ -679,9 +867,15 @@ export class EmDashRuntime {
679
867
  onPublished?: (refs: PublishedRef[]) => Promise<void>,
680
868
  ): Promise<PublishedRef[]> {
681
869
  await assertMediaUsageActivationWriteAllowed(this.db);
870
+ const currentTime = this.runtimeDeps.now?.() ?? new Date();
682
871
  return publishDueContent(this.db, {
683
- publish: (collection, id, options) => this.handleContentPublish(collection, id, options),
872
+ publish: (collection, id, options) =>
873
+ this.handleContentPublish(collection, id, {
874
+ ...options,
875
+ origin: { source: "scheduler" },
876
+ }),
684
877
  onPublished,
878
+ currentTime,
685
879
  });
686
880
  }
687
881
 
@@ -702,11 +896,26 @@ export class EmDashRuntime {
702
896
  async runScheduledTasks(
703
897
  options: {
704
898
  onPublished?: (refs: PublishedRef[]) => Promise<void>;
899
+ invalidateContentCache?: PluginContentCacheInvalidator;
705
900
  } = {},
706
901
  ): Promise<{ published: PublishedRef[] }> {
902
+ const { published } = await this.runScheduledTasksWithStats(options);
903
+ return { published };
904
+ }
905
+
906
+ async runScheduledTasksWithStats(
907
+ options: {
908
+ onPublished?: (refs: PublishedRef[]) => Promise<void>;
909
+ invalidateContentCache?: PluginContentCacheInvalidator;
910
+ } = {},
911
+ ): Promise<{ processed: number; published: PublishedRef[] }> {
912
+ if (options.invalidateContentCache) {
913
+ this.pluginContentCacheInvalidator = options.invalidateContentCache;
914
+ }
915
+ let processed = 0;
707
916
  if (this.cronExecutor) {
708
917
  try {
709
- await this.cronExecutor.tick();
918
+ processed = await this.cronExecutor.tick();
710
919
  } catch (error) {
711
920
  console.error("[cron] Tick failed:", error);
712
921
  }
@@ -740,7 +949,7 @@ export class EmDashRuntime {
740
949
  await maybeRunScheduledBackup(this.db, this.storage ?? undefined);
741
950
  await recordSchedulerHeartbeatSafely(this.db);
742
951
 
743
- return { published };
952
+ return { processed, published };
744
953
  }
745
954
 
746
955
  /**
@@ -771,6 +980,20 @@ export class EmDashRuntime {
771
980
  }
772
981
  }
773
982
 
983
+ /** Stop background work and discard loaded sandbox isolates. */
984
+ async shutdown(): Promise<void> {
985
+ await this.stopCron();
986
+ sandboxRunner?.setCommentModerate?.(null);
987
+ await sandboxRunner?.terminateAll();
988
+ sandboxRunner = null;
989
+ sandboxedPluginCache.clear();
990
+ sandboxedManifestCache.clear();
991
+ sandboxedRouteMetaCache.clear();
992
+ marketplaceManifestCache.clear();
993
+ marketplacePluginKeys.clear();
994
+ registryPluginKeys.clear();
995
+ }
996
+
774
997
  /**
775
998
  * Update in-memory plugin status and rebuild the hook pipeline.
776
999
  *
@@ -792,6 +1015,43 @@ export class EmDashRuntime {
792
1015
  }
793
1016
  }
794
1017
 
1018
+ async handlePluginEnable(pluginId: string) {
1019
+ return enableRuntimePlugin(this, pluginId);
1020
+ }
1021
+
1022
+ async handlePluginDisable(pluginId: string) {
1023
+ return disableRuntimePlugin(this, pluginId);
1024
+ }
1025
+
1026
+ /** Dispatch teardown lifecycle while the runtime-installed isolate is still loaded. */
1027
+ async runPluginUninstallLifecycle(pluginId: string, deleteData: boolean): Promise<void> {
1028
+ const active = this.isPluginEnabled(pluginId);
1029
+ if (active) {
1030
+ try {
1031
+ await this._hooks.runPluginDeactivate(pluginId);
1032
+ } catch (error) {
1033
+ console.error(`EmDash: Plugin ${pluginId} deactivate hook failed during uninstall:`, error);
1034
+ }
1035
+ }
1036
+
1037
+ try {
1038
+ const results = await this._hooks.runPluginUninstall(pluginId, deleteData);
1039
+ if (results.length > 0) return;
1040
+ await this.findSandboxedPlugin(pluginId)?.invokeHook("plugin:uninstall", { deleteData });
1041
+ } catch (error) {
1042
+ console.error(`EmDash: Plugin ${pluginId} uninstall hook failed:`, error);
1043
+ }
1044
+ }
1045
+
1046
+ async runPluginInstallLifecycle(pluginId: string): Promise<void> {
1047
+ await this._hooks.runPluginInstall(pluginId);
1048
+ await this._hooks.runPluginActivate(pluginId);
1049
+ }
1050
+
1051
+ async runPluginActivateLifecycle(pluginId: string): Promise<void> {
1052
+ await this._hooks.runPluginActivate(pluginId);
1053
+ }
1054
+
795
1055
  /**
796
1056
  * Rebuild the hook pipeline from the current set of enabled plugins.
797
1057
  *
@@ -864,7 +1124,7 @@ export class EmDashRuntime {
864
1124
  * update, and uninstall handlers complete.
865
1125
  */
866
1126
  async syncRegistryPlugins(): Promise<void> {
867
- if (!this.config.experimental?.registry) return;
1127
+ if (!getRegistryConfigInput(this.config.registry, this.config.experimental?.registry)) return;
868
1128
  await this.syncSandboxedSourcePlugins("registry");
869
1129
  }
870
1130
 
@@ -882,6 +1142,7 @@ export class EmDashRuntime {
882
1142
  if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
883
1143
 
884
1144
  const keySet = source === "marketplace" ? marketplacePluginKeys : registryPluginKeys;
1145
+ let pipelineChanged = false;
885
1146
 
886
1147
  try {
887
1148
  const stateRepo = new PluginStateRepository(this.db);
@@ -935,8 +1196,11 @@ export class EmDashRuntime {
935
1196
  }
936
1197
 
937
1198
  sandboxedPluginCache.delete(key);
1199
+ sandboxedManifestCache.delete(key);
938
1200
  this.sandboxedPlugins.delete(key);
1201
+ this.removePluginFromLists(pluginId);
939
1202
  keySet.delete(key);
1203
+ pipelineChanged = true;
940
1204
  if (pluginId) {
941
1205
  sandboxedRouteMetaCache.delete(pluginId);
942
1206
  marketplaceManifestCache.delete(pluginId);
@@ -959,8 +1223,11 @@ export class EmDashRuntime {
959
1223
 
960
1224
  const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
961
1225
  sandboxedPluginCache.set(key, loaded);
1226
+ sandboxedManifestCache.set(key, bundle.manifest);
962
1227
  this.sandboxedPlugins.set(key, loaded);
1228
+ this.allPipelinePlugins.push(createSandboxedPluginProxy(bundle.manifest, loaded));
963
1229
  keySet.add(key);
1230
+ pipelineChanged = true;
964
1231
 
965
1232
  // Cache manifest admin config for getManifest()
966
1233
  marketplaceManifestCache.set(pluginId, {
@@ -969,6 +1236,8 @@ export class EmDashRuntime {
969
1236
  admin: bundle.manifest.admin,
970
1237
  mcp: bundle.manifest.mcp,
971
1238
  storage: bundle.manifest.storage,
1239
+ allowedHosts: bundle.manifest.allowedHosts,
1240
+ capabilities: bundle.manifest.capabilities,
972
1241
  });
973
1242
 
974
1243
  // Cache route metadata from manifest for auth decisions
@@ -983,6 +1252,10 @@ export class EmDashRuntime {
983
1252
  sandboxedRouteMetaCache.delete(pluginId);
984
1253
  }
985
1254
  }
1255
+
1256
+ if (pipelineChanged) {
1257
+ await this.rebuildHookPipeline();
1258
+ }
986
1259
  } catch (error) {
987
1260
  console.error(`EmDash: Failed to sync ${source} plugins:`, error);
988
1261
  }
@@ -1085,6 +1358,8 @@ export class EmDashRuntime {
1085
1358
  admin: bundle.manifest.admin,
1086
1359
  mcp: bundle.manifest.mcp,
1087
1360
  storage: bundle.manifest.storage,
1361
+ allowedHosts: bundle.manifest.allowedHosts,
1362
+ capabilities: bundle.manifest.capabilities,
1088
1363
  });
1089
1364
  if (bundle.manifest.routes.length > 0) {
1090
1365
  const routeMetaMap = new Map<string, RouteMeta>();
@@ -1135,6 +1410,8 @@ export class EmDashRuntime {
1135
1410
  w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
1136
1411
  })),
1137
1412
  settingsSchema: bundle.manifest.admin?.settingsSchema,
1413
+ editorPanels: bundle.manifest.admin?.editorPanels,
1414
+ editorActions: bundle.manifest.admin?.editorActions,
1138
1415
  });
1139
1416
  newPlugins.push(adapted);
1140
1417
  this.allPipelinePlugins.push(adapted);
@@ -1201,10 +1478,9 @@ export class EmDashRuntime {
1201
1478
  };
1202
1479
 
1203
1480
  // Validate EMDASH_ENCRYPTION_KEY once here so a malformed value
1204
- // surfaces in startup logs instead of as request-time 500s. The key
1205
- // itself is not yet consumed (a follow-up PR adds plugin-secret
1206
- // encryption); validating early just guards against silent
1207
- // misconfiguration.
1481
+ // surfaces in startup logs. Plugin secret-setting operations re-read
1482
+ // the key and fail closed at their own boundary, so validation here
1483
+ // does not block unrelated request paths.
1208
1484
  await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
1209
1485
 
1210
1486
  // FTS verify/repair is deferred off the cold-start hot path.
@@ -1283,13 +1559,13 @@ export class EmDashRuntime {
1283
1559
  ]);
1284
1560
  storedLocaleCasingRepairVersion = siteOpts.get(LOCALE_CASING_REPAIR_OPTION);
1285
1561
  return {
1286
- siteName: siteOpts.get("emdash:site_title") ?? undefined,
1287
- siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
1288
- locale: siteOpts.get("emdash:locale") ?? undefined,
1562
+ siteName: deps.siteInfo?.name ?? siteOpts.get("emdash:site_title") ?? undefined,
1563
+ siteUrl: deps.siteInfo?.url ?? siteOpts.get("emdash:site_url") ?? undefined,
1564
+ locale: deps.siteInfo?.locale ?? siteOpts.get("emdash:locale") ?? undefined,
1289
1565
  // trailingSlash is a build-time Astro routing decision, not a
1290
1566
  // user-editable setting, so it comes from the Astro config
1291
1567
  // (virtual:emdash/config), not the options table.
1292
- trailingSlash: virtualConfig?.trailingSlash,
1568
+ trailingSlash: deps.siteInfo?.trailingSlash ?? virtualConfig?.trailingSlash,
1293
1569
  };
1294
1570
  };
1295
1571
 
@@ -1320,17 +1596,18 @@ export class EmDashRuntime {
1320
1596
  coldStartReads.push(
1321
1597
  phase("rt.seedcheck", "Auto-seed gate", async () => {
1322
1598
  try {
1323
- const [collectionCount, setupOption] = await Promise.all([
1324
- readDb
1325
- .selectFrom("_emdash_collections")
1326
- .select((eb) => eb.fn.countAll<number>().as("count"))
1327
- .executeTakeFirstOrThrow(),
1599
+ // Selecting the slugs instead of COUNT(*) costs the same
1600
+ // round trip and primes the registered-collections cache,
1601
+ // so the first render on this isolate skips its own lookup.
1602
+ const [collectionRows, setupOption] = await Promise.all([
1603
+ readDb.selectFrom("_emdash_collections").select("slug").execute(),
1328
1604
  readDb
1329
1605
  .selectFrom("options")
1330
1606
  .select("value")
1331
1607
  .where("name", "=", "emdash:setup_complete")
1332
1608
  .executeTakeFirst(),
1333
1609
  ]);
1610
+ primeRegisteredCollections(collectionRows.map((row) => row.slug));
1334
1611
  const setupDone = (() => {
1335
1612
  try {
1336
1613
  return !!setupOption && JSON.parse(setupOption.value) === true;
@@ -1338,7 +1615,7 @@ export class EmDashRuntime {
1338
1615
  return false;
1339
1616
  }
1340
1617
  })();
1341
- seedGate = { collectionCount: collectionCount.count, setupDone };
1618
+ seedGate = { collectionCount: collectionRows.length, setupDone };
1342
1619
  } catch (error) {
1343
1620
  captureMissingManualSchema(error);
1344
1621
  // Leave the "already set up" default so a read failure never
@@ -1532,26 +1809,8 @@ export class EmDashRuntime {
1532
1809
  }
1533
1810
  }
1534
1811
 
1535
- // Filter to currently enabled plugins for the initial pipeline
1536
- const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
1537
-
1538
- // Create hook pipeline. getDb travels here (not just via the email
1539
- // setContextFactory call below) so it survives rebuildHookPipeline(),
1540
- // which reconstructs the factory from pipelineFactoryOptions. Without it,
1541
- // toggling a plugin on an email-less deployment would silently revert
1542
- // plugin contexts to the singleton db — re-breaking connection-backed
1543
- // adapters. See #1622.
1544
- const pipelineFactoryOptions = {
1545
- db,
1546
- getDb: resolveDb,
1547
- beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
1548
- storage: storage ?? undefined,
1549
- siteInfo,
1550
- };
1551
- const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
1552
-
1553
1812
  // Load sandboxed plugins (build-time, sandbox runner path)
1554
- const sandboxedPlugins = await phase("rt.sandbox", "Sandboxed plugins", () =>
1813
+ const sandboxedPluginPool = await phase("rt.sandbox", "Sandboxed plugins", () =>
1555
1814
  EmDashRuntime.loadSandboxedPlugins(deps, db, storage, siteInfo),
1556
1815
  );
1557
1816
 
@@ -1569,7 +1828,7 @@ export class EmDashRuntime {
1569
1828
  db,
1570
1829
  storage,
1571
1830
  deps,
1572
- sandboxedPlugins,
1831
+ sandboxedPluginPool,
1573
1832
  siteInfo,
1574
1833
  ),
1575
1834
  ),
@@ -1577,7 +1836,10 @@ export class EmDashRuntime {
1577
1836
  }
1578
1837
 
1579
1838
  // Cold-start: load registry-installed plugins from site R2
1580
- if (deps.config.experimental?.registry && storage) {
1839
+ if (
1840
+ getRegistryConfigInput(deps.config.registry, deps.config.experimental?.registry) &&
1841
+ storage
1842
+ ) {
1581
1843
  installedTierPhases.push(
1582
1844
  phase("rt.registry", "Registry plugins", () =>
1583
1845
  EmDashRuntime.loadInstalledSandboxedPlugins(
@@ -1585,7 +1847,7 @@ export class EmDashRuntime {
1585
1847
  db,
1586
1848
  storage,
1587
1849
  deps,
1588
- sandboxedPlugins,
1850
+ sandboxedPluginPool,
1589
1851
  siteInfo,
1590
1852
  ),
1591
1853
  ),
@@ -1595,6 +1857,113 @@ export class EmDashRuntime {
1595
1857
  await Promise.all(installedTierPhases);
1596
1858
  }
1597
1859
 
1860
+ const configuredSandboxIds = new Set(deps.sandboxedPluginEntries.map((entry) => entry.id));
1861
+ const sandboxedPlugins = new Map(
1862
+ [...sandboxedPluginPool].filter(([pluginKey]) => {
1863
+ const pluginId = pluginKey.slice(0, pluginKey.lastIndexOf(":"));
1864
+ return configuredSandboxIds.has(pluginId) || pluginStates.get(pluginId) === "active";
1865
+ }),
1866
+ );
1867
+
1868
+ // Add isolate-backed proxies to the same host pipeline as trusted
1869
+ // plugins. The proxy carries only manifest metadata and RPC handlers;
1870
+ // plugin code remains inside the configured runner.
1871
+ for (const [pluginKey, instance] of sandboxedPlugins) {
1872
+ const manifest = sandboxedManifestCache.get(pluginKey);
1873
+ if (!manifest) continue;
1874
+ allPipelinePlugins.push(createSandboxedPluginProxy(manifest, instance));
1875
+ const status = pluginStates.get(manifest.id);
1876
+ if (status === undefined || status === "active") enabledPlugins.add(manifest.id);
1877
+ }
1878
+
1879
+ // Create hook pipeline. getDb travels here (not just via the email
1880
+ // setContextFactory call below) so it survives rebuildHookPipeline(),
1881
+ // which reconstructs the factory from pipelineFactoryOptions. Without it,
1882
+ // toggling a plugin on an email-less deployment would silently revert
1883
+ // plugin contexts to the singleton db — re-breaking connection-backed
1884
+ // adapters. See #1622.
1885
+ const runtimeRef: { current: EmDashRuntime | null } = { current: null };
1886
+ const requireRuntime = (): EmDashRuntime => {
1887
+ if (!runtimeRef.current) throw new Error("EmDash runtime is not ready");
1888
+ return runtimeRef.current;
1889
+ };
1890
+ const contentActions: ContentActionCallbacks = {
1891
+ begin: (pluginId, invocationId, invalidateContentCache) =>
1892
+ requireRuntime().beginPluginInvocation(pluginId, invocationId, invalidateContentCache),
1893
+ flush: (pluginId, invocationId, final) =>
1894
+ requireRuntime().flushPluginAfterHooks(pluginId, invocationId, final),
1895
+ getVersioned: (pluginId, collection, id) =>
1896
+ requireRuntime().pluginGetVersioned(pluginId, collection, id),
1897
+ publish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
1898
+ requireRuntime().pluginPublish(
1899
+ pluginId,
1900
+ collection,
1901
+ id,
1902
+ options,
1903
+ invocationId,
1904
+ invalidateContentCache,
1905
+ ),
1906
+ unpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
1907
+ requireRuntime().pluginUnpublish(
1908
+ pluginId,
1909
+ collection,
1910
+ id,
1911
+ options,
1912
+ invocationId,
1913
+ invalidateContentCache,
1914
+ ),
1915
+ schedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
1916
+ requireRuntime().pluginSchedule(
1917
+ pluginId,
1918
+ collection,
1919
+ id,
1920
+ options,
1921
+ invocationId,
1922
+ invalidateContentCache,
1923
+ ),
1924
+ unschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
1925
+ requireRuntime().pluginUnschedule(
1926
+ pluginId,
1927
+ collection,
1928
+ id,
1929
+ options,
1930
+ invocationId,
1931
+ invalidateContentCache,
1932
+ ),
1933
+ getTrashedVersioned: (pluginId, collection, id) =>
1934
+ requireRuntime().pluginGetTrashedVersioned(pluginId, collection, id),
1935
+ restore: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>
1936
+ requireRuntime().pluginRestore(
1937
+ pluginId,
1938
+ collection,
1939
+ id,
1940
+ options,
1941
+ invocationId,
1942
+ invalidateContentCache,
1943
+ ),
1944
+ };
1945
+ const pipelineFactoryOptions = {
1946
+ db,
1947
+ getDb: resolveDb,
1948
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
1949
+ contentActions,
1950
+ now: deps.now,
1951
+ storage: storage ?? undefined,
1952
+ siteInfo,
1953
+ commentModerate: (
1954
+ pluginId: string,
1955
+ id: string,
1956
+ status: PluginCommentStatus,
1957
+ expectedStatus: PluginCommentStatus,
1958
+ ) => {
1959
+ const current = runtimeRef.current;
1960
+ if (!current) throw new Error("Comment moderation is unavailable during runtime startup");
1961
+ return current.handlePluginCommentModerate(pluginId, id, status, expectedStatus);
1962
+ },
1963
+ };
1964
+ const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
1965
+ const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
1966
+
1598
1967
  // Initialize media providers
1599
1968
  const mediaProviders = new Map<string, MediaProvider>();
1600
1969
  const mediaProviderEntries = deps.mediaProviderEntries ?? [];
@@ -1650,11 +2019,9 @@ export class EmDashRuntime {
1650
2019
  // so the timer scheduler's cleanup can route scheduled publishing through
1651
2020
  // the runtime wrapper (firing content:afterPublish hooks). The first tick
1652
2021
  // is ≥1s out, well after the synchronous assignment below.
1653
- const runtimeRef: { current: EmDashRuntime | null } = { current: null };
1654
-
1655
2022
  await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
1656
2023
  try {
1657
- cronExecutor = new CronExecutor(resolveDb, invokeCronHook);
2024
+ cronExecutor = new CronExecutor(resolveDb, invokeCronHook, deps.now);
1658
2025
  // Plugin schedules are always database-backed. On long-lived runtimes this
1659
2026
  // callback also wakes the timer; on Cloudflare the external Cron Trigger
1660
2027
  // drives execution, so rescheduling is intentionally a no-op.
@@ -1733,6 +2100,7 @@ export class EmDashRuntime {
1733
2100
  // Non-fatal — CMS works without cron
1734
2101
  }
1735
2102
  });
2103
+ sandboxRunner?.setCronReschedule?.(() => cronScheduler?.reschedule());
1736
2104
 
1737
2105
  const runtime = new EmDashRuntime({
1738
2106
  db,
@@ -1757,9 +2125,46 @@ export class EmDashRuntime {
1757
2125
  runtimeDeps: deps,
1758
2126
  pipelineRef,
1759
2127
  });
2128
+ const contentCreate: PluginContentCreateCallback = async (
2129
+ _pluginId,
2130
+ collection,
2131
+ input,
2132
+ options,
2133
+ ) => {
2134
+ const { seo, ...data } = input;
2135
+ const result = await runtime.handleContentCreate(
2136
+ collection,
2137
+ {
2138
+ data,
2139
+ seo,
2140
+ locale: resolveContentCreateLocale(options?.locale),
2141
+ translationOf: options?.translationOf,
2142
+ },
2143
+ {
2144
+ skipSaveHooks: options?.sandboxOrigin
2145
+ ? options.originHook !== undefined
2146
+ : getActiveContentSaveHookName() != null,
2147
+ excludeAfterSavePluginId: _pluginId,
2148
+ },
2149
+ );
2150
+ if (!result.success) {
2151
+ throw Object.assign(new Error(result.error.message), {
2152
+ name: result.error.code,
2153
+ code: result.error.code,
2154
+ });
2155
+ }
2156
+ return result.data.item;
2157
+ };
2158
+ runtime.pipelineFactoryOptions.contentCreate = contentCreate;
2159
+ pipeline.setContextFactory({ contentCreate });
2160
+ sandboxRunner?.setContentCreate?.(contentCreate);
1760
2161
  // Hand the constructed instance to the scheduler-cleanup closure so the
1761
2162
  // timer-driven sweep can fire publish hooks (see runtimeRef above).
1762
2163
  runtimeRef.current = runtime;
2164
+ sandboxRunner?.setCommentModerate?.((pluginId, id, status, expectedStatus) =>
2165
+ runtime.handlePluginCommentModerate(pluginId, id, status, expectedStatus),
2166
+ );
2167
+ sandboxRunner?.setContentActions?.(contentActions);
1763
2168
  return runtime;
1764
2169
  }
1765
2170
 
@@ -1987,6 +2392,8 @@ export class EmDashRuntime {
1987
2392
  settingsSchema: entry.settingsSchema,
1988
2393
  portableTextBlocks: entry.portableTextBlocks,
1989
2394
  fieldWidgets: entry.fieldWidgets,
2395
+ editorPanels: entry.editorPanels,
2396
+ editorActions: entry.editorActions,
1990
2397
  });
1991
2398
  plugins.push(resolved);
1992
2399
  console.log(
@@ -2013,11 +2420,6 @@ export class EmDashRuntime {
2013
2420
  trailingSlash?: "always" | "never" | "ignore";
2014
2421
  },
2015
2422
  ): Promise<Map<string, SandboxedPluginInstance>> {
2016
- // Return cached plugins if already loaded
2017
- if (sandboxedPluginCache.size > 0) {
2018
- return sandboxedPluginCache;
2019
- }
2020
-
2021
2423
  // Check if sandboxing is enabled
2022
2424
  if (!deps.sandboxEnabled) {
2023
2425
  return sandboxedPluginCache;
@@ -2030,6 +2432,8 @@ export class EmDashRuntime {
2030
2432
  {
2031
2433
  db,
2032
2434
  beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
2435
+ taxonomyWrite: createTaxonomyAccessWithWrite(db),
2436
+ now: deps.now,
2033
2437
  mediaStorage: mediaStorage
2034
2438
  ? {
2035
2439
  upload: (opts) =>
@@ -2038,6 +2442,7 @@ export class EmDashRuntime {
2038
2442
  body: opts.body,
2039
2443
  contentType: opts.contentType,
2040
2444
  }),
2445
+ download: (key) => mediaStorage.download(key),
2041
2446
  delete: (key) => mediaStorage.delete(key),
2042
2447
  }
2043
2448
  : undefined,
@@ -2055,10 +2460,14 @@ export class EmDashRuntime {
2055
2460
  // Warn regardless of whether there are plugins to load, so operators
2056
2461
  // see the issue even if no marketplace plugins are installed yet.
2057
2462
  if (!sandboxRunner.isAvailable()) {
2463
+ const reason = sandboxRunner.unavailableReason?.();
2058
2464
  console.warn(
2059
- "EmDash: Plugin sandbox is configured but not available on this platform. " +
2060
- "Sandboxed plugins will not be loaded. " +
2061
- "If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.",
2465
+ reason
2466
+ ? `EmDash: Plugin sandbox is configured but not available on this platform: ${reason}. ` +
2467
+ "Sandboxed plugins will not be loaded."
2468
+ : "EmDash: Plugin sandbox is configured but not available on this platform. " +
2469
+ "Sandboxed plugins will not be loaded. " +
2470
+ "If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.",
2062
2471
  );
2063
2472
  return sandboxedPluginCache;
2064
2473
  }
@@ -2082,21 +2491,49 @@ export class EmDashRuntime {
2082
2491
  }
2083
2492
 
2084
2493
  try {
2494
+ const adminPages = entry.adminPages?.map((page) => ({
2495
+ path: page.path,
2496
+ label: page.label ?? page.path,
2497
+ icon: page.icon,
2498
+ }));
2499
+ const adminWidgets: PluginDashboardWidget[] | undefined = entry.adminWidgets?.map(
2500
+ (widget) => ({
2501
+ id: widget.id,
2502
+ title: widget.title,
2503
+ size:
2504
+ widget.size === "full" || widget.size === "half" || widget.size === "third"
2505
+ ? widget.size
2506
+ : undefined,
2507
+ }),
2508
+ );
2509
+ const capabilities = normalizePluginCapabilities(entry.capabilities ?? []);
2510
+
2085
2511
  // Build manifest from entry's declared config
2086
2512
  const manifest: PluginManifest = {
2087
2513
  id: entry.id,
2088
2514
  version: entry.version,
2089
- capabilities: entry.capabilities ?? [],
2515
+ capabilities,
2090
2516
  allowedHosts: entry.allowedHosts ?? [],
2091
2517
  storage: entry.storage ?? {},
2092
- hooks: entry.hooks ?? [],
2518
+ // Descriptors built before hook metadata was emitted must keep
2519
+ // working. Invoking an absent hook is a no-op in both runners.
2520
+ hooks: entry.hooks ?? [...HOOK_NAMES],
2093
2521
  routes: entry.routes ?? [],
2094
- admin: {},
2522
+ admin: {
2523
+ pages: adminPages,
2524
+ widgets: adminWidgets,
2525
+ settingsSchema: entry.settingsSchema,
2526
+ portableTextBlocks: entry.portableTextBlocks,
2527
+ fieldWidgets: entry.fieldWidgets,
2528
+ editorPanels: entry.editorPanels,
2529
+ editorActions: entry.editorActions,
2530
+ },
2095
2531
  mcp: entry.mcp,
2096
2532
  };
2097
2533
 
2098
2534
  const plugin = await sandboxRunner.load(manifest, entry.code);
2099
2535
  sandboxedPluginCache.set(pluginKey, plugin);
2536
+ sandboxedManifestCache.set(pluginKey, manifest);
2100
2537
  console.log(
2101
2538
  `EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`,
2102
2539
  );
@@ -2154,6 +2591,8 @@ export class EmDashRuntime {
2154
2591
  {
2155
2592
  db,
2156
2593
  beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
2594
+ taxonomyWrite: createTaxonomyAccessWithWrite(db),
2595
+ now: deps.now,
2157
2596
  mediaStorage: {
2158
2597
  upload: (opts) =>
2159
2598
  storage.upload({
@@ -2161,6 +2600,7 @@ export class EmDashRuntime {
2161
2600
  body: opts.body,
2162
2601
  contentType: opts.contentType,
2163
2602
  }),
2603
+ download: (key) => storage.download(key),
2164
2604
  delete: (key) => storage.delete(key),
2165
2605
  },
2166
2606
  },
@@ -2206,6 +2646,7 @@ export class EmDashRuntime {
2206
2646
 
2207
2647
  const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
2208
2648
  cache.set(pluginKey, loaded);
2649
+ sandboxedManifestCache.set(pluginKey, bundle.manifest);
2209
2650
  keySet.add(pluginKey);
2210
2651
 
2211
2652
  // Cache manifest admin config for getManifest()
@@ -2215,6 +2656,8 @@ export class EmDashRuntime {
2215
2656
  admin: bundle.manifest.admin,
2216
2657
  mcp: bundle.manifest.mcp,
2217
2658
  storage: bundle.manifest.storage,
2659
+ allowedHosts: bundle.manifest.allowedHosts,
2660
+ capabilities: bundle.manifest.capabilities,
2218
2661
  });
2219
2662
 
2220
2663
  // Cache route metadata from manifest for auth decisions
@@ -2288,6 +2731,8 @@ export class EmDashRuntime {
2288
2731
  admin: bundle.manifest.admin,
2289
2732
  mcp: bundle.manifest.mcp,
2290
2733
  storage: bundle.manifest.storage,
2734
+ allowedHosts: bundle.manifest.allowedHosts,
2735
+ capabilities: bundle.manifest.capabilities,
2291
2736
  });
2292
2737
  if (bundle.manifest.routes.length > 0) {
2293
2738
  const routeMeta = new Map<string, RouteMeta>();
@@ -2324,6 +2769,8 @@ export class EmDashRuntime {
2324
2769
  w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
2325
2770
  })),
2326
2771
  settingsSchema: bundle.manifest.admin?.settingsSchema,
2772
+ editorPanels: bundle.manifest.admin?.editorPanels,
2773
+ editorActions: bundle.manifest.admin?.editorActions,
2327
2774
  });
2328
2775
  resolved.push(adapted);
2329
2776
  console.log(
@@ -2457,6 +2904,8 @@ export class EmDashRuntime {
2457
2904
  fieldTypes: string[];
2458
2905
  elements?: Element[];
2459
2906
  }>;
2907
+ editorPanels?: PluginManifest["admin"]["editorPanels"];
2908
+ editorActions?: PluginManifest["admin"]["editorActions"];
2460
2909
  }
2461
2910
  > = {};
2462
2911
 
@@ -2468,10 +2917,13 @@ export class EmDashRuntime {
2468
2917
  const hasAdminEntry = !!plugin.admin?.entry;
2469
2918
  const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
2470
2919
  const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
2920
+ const hasEditorExtensions =
2921
+ (plugin.admin?.editorPanels?.length ?? 0) > 0 ||
2922
+ (plugin.admin?.editorActions?.length ?? 0) > 0;
2471
2923
  let adminMode: "react" | "blocks" | "none" = "none";
2472
2924
  if (hasAdminEntry) {
2473
2925
  adminMode = "react";
2474
- } else if (hasAdminPages || hasWidgets) {
2926
+ } else if (hasAdminPages || hasWidgets || hasEditorExtensions) {
2475
2927
  adminMode = "blocks";
2476
2928
  }
2477
2929
 
@@ -2483,6 +2935,8 @@ export class EmDashRuntime {
2483
2935
  dashboardWidgets: plugin.admin?.widgets ?? [],
2484
2936
  portableTextBlocks: plugin.admin?.portableTextBlocks,
2485
2937
  fieldWidgets: plugin.admin?.fieldWidgets,
2938
+ editorPanels: plugin.admin?.editorPanels,
2939
+ editorActions: plugin.admin?.editorActions,
2486
2940
  };
2487
2941
  }
2488
2942
 
@@ -2493,6 +2947,8 @@ export class EmDashRuntime {
2493
2947
 
2494
2948
  const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
2495
2949
  const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
2950
+ const hasEditorExtensions =
2951
+ (entry.editorPanels?.length ?? 0) > 0 || (entry.editorActions?.length ?? 0) > 0;
2496
2952
 
2497
2953
  manifestPlugins[entry.id] = {
2498
2954
  version: entry.version,
@@ -2502,11 +2958,13 @@ export class EmDashRuntime {
2502
2958
  // contribute portableTextBlocks/fieldWidgets with adminMode "none" —
2503
2959
  // the admin reads those from the manifest regardless, so don't gate
2504
2960
  // admin contributions on `adminMode`.
2505
- adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
2961
+ adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
2506
2962
  adminPages: entry.adminPages ?? [],
2507
2963
  dashboardWidgets: entry.adminWidgets ?? [],
2508
2964
  portableTextBlocks: entry.portableTextBlocks,
2509
2965
  fieldWidgets: entry.fieldWidgets,
2966
+ editorPanels: entry.editorPanels,
2967
+ editorActions: entry.editorActions,
2510
2968
  };
2511
2969
  }
2512
2970
 
@@ -2522,14 +2980,18 @@ export class EmDashRuntime {
2522
2980
  const widgets = meta.admin?.widgets;
2523
2981
  const hasAdminPages = (pages?.length ?? 0) > 0;
2524
2982
  const hasWidgets = (widgets?.length ?? 0) > 0;
2983
+ const hasEditorExtensions =
2984
+ (meta.admin?.editorPanels?.length ?? 0) > 0 || (meta.admin?.editorActions?.length ?? 0) > 0;
2525
2985
 
2526
2986
  manifestPlugins[pluginId] = {
2527
2987
  version: meta.version,
2528
2988
  enabled,
2529
2989
  sandboxed: true,
2530
- adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
2990
+ adminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? "blocks" : "none",
2531
2991
  adminPages: pages ?? [],
2532
2992
  dashboardWidgets: widgets ?? [],
2993
+ editorPanels: meta.admin?.editorPanels,
2994
+ editorActions: meta.admin?.editorActions,
2533
2995
  };
2534
2996
  }
2535
2997
 
@@ -2599,11 +3061,19 @@ export class EmDashRuntime {
2599
3061
  }
2600
3062
  : undefined;
2601
3063
 
2602
- // Normalize the experimental registry config for browser consumption.
2603
- // Validation errors here surface as 500s from the manifest endpoint
2604
- // rather than being silently dropped -- a misconfigured registry
2605
- // should be loud, not invisible.
2606
- const registry = normalizeRegistryConfig(this.config.experimental?.registry) ?? undefined;
3064
+ const registryConfig = resolveRegistryConfigForSandbox({
3065
+ registry: this.config.registry,
3066
+ experimentalRegistry: this.config.experimental?.registry,
3067
+ });
3068
+ const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(
3069
+ registryConfig.input,
3070
+ { fieldPrefix: registryConfig.fieldPrefix },
3071
+ );
3072
+ if (registryConfigurationError) {
3073
+ console.error(
3074
+ `EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`,
3075
+ );
3076
+ }
2607
3077
 
2608
3078
  return {
2609
3079
  version: VERSION,
@@ -2621,6 +3091,7 @@ export class EmDashRuntime {
2621
3091
  },
2622
3092
  marketplace: !!this.config.marketplace,
2623
3093
  registry,
3094
+ registryConfigurationError,
2624
3095
  };
2625
3096
  }
2626
3097
 
@@ -2729,7 +3200,10 @@ export class EmDashRuntime {
2729
3200
  *
2730
3201
  * No-op when no draft exists or the response is an error.
2731
3202
  */
2732
- private async hydrateDraftData<T>(result: T): Promise<T> {
3203
+ private async hydrateDraftData<T>(
3204
+ result: T,
3205
+ options: { includeStagedSlug?: boolean; strict?: boolean } = {},
3206
+ ): Promise<T> {
2733
3207
  if (!result || typeof result !== "object") return result;
2734
3208
  // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below
2735
3209
  const r = result as {
@@ -2742,7 +3216,10 @@ export class EmDashRuntime {
2742
3216
  if (!draftRevisionId) return result;
2743
3217
  try {
2744
3218
  const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
2745
- if (!revision) return result;
3219
+ if (!revision) {
3220
+ if (options.strict) throw new Error(`Draft revision not found: ${draftRevisionId}`);
3221
+ return result;
3222
+ }
2746
3223
  const liveData =
2747
3224
  item.data && typeof item.data === "object"
2748
3225
  ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above
@@ -2769,10 +3246,18 @@ export class EmDashRuntime {
2769
3246
  // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope
2770
3247
  data: {
2771
3248
  ...r.data,
2772
- item: { ...item, data: mergedData, liveData },
3249
+ item: {
3250
+ ...item,
3251
+ ...(options.includeStagedSlug && typeof revision.data._slug === "string"
3252
+ ? { slug: revision.data._slug }
3253
+ : {}),
3254
+ data: mergedData,
3255
+ liveData,
3256
+ },
2773
3257
  },
2774
3258
  };
2775
3259
  } catch (error) {
3260
+ if (options.strict) throw error;
2776
3261
  // Non-fatal — fall back to the unhydrated response. Log so the
2777
3262
  // failure isn't completely silent (the response will look stale
2778
3263
  // to the caller but no error is raised).
@@ -2785,6 +3270,7 @@ export class EmDashRuntime {
2785
3270
  collection: string,
2786
3271
  body: {
2787
3272
  data: Record<string, unknown>;
3273
+ seo?: ContentWriteInput["seo"];
2788
3274
  slug?: string | null;
2789
3275
  status?: string;
2790
3276
  authorId?: string;
@@ -2792,23 +3278,87 @@ export class EmDashRuntime {
2792
3278
  locale?: string;
2793
3279
  translationOf?: string;
2794
3280
  taxonomies?: Record<string, string[]>;
3281
+ actor?: ActorInfo;
2795
3282
  },
3283
+ options: { skipSaveHooks?: boolean; excludeAfterSavePluginId?: string } = {},
2796
3284
  ) {
3285
+ const actor = body.actor ? { ...body.actor } : undefined;
3286
+ let locale: string;
3287
+ try {
3288
+ locale = resolveContentCreateLocale(body.locale);
3289
+ } catch (error) {
3290
+ return {
3291
+ success: false as const,
3292
+ error: {
3293
+ code: "VALIDATION_ERROR",
3294
+ message: error instanceof Error ? error.message : "Invalid locale",
3295
+ },
3296
+ };
3297
+ }
3298
+
3299
+ let translationSource: ContentItemInternal | null = null;
3300
+ let sharedFieldSlugs: string[] = [];
3301
+ if (body.translationOf) {
3302
+ const repo = new ContentRepository(this.db);
3303
+ const source = await repo.findById(collection, body.translationOf);
3304
+ if (!source) {
3305
+ return {
3306
+ success: false as const,
3307
+ error: { code: "NOT_FOUND", message: "Translation source content not found" },
3308
+ };
3309
+ }
3310
+ translationSource = source;
3311
+ sharedFieldSlugs = (
3312
+ await this.db
3313
+ .selectFrom("_emdash_fields as field")
3314
+ .innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id")
3315
+ .select("field.slug")
3316
+ .where("collection.slug", "=", collection)
3317
+ .where("field.translatable", "=", 0)
3318
+ .execute()
3319
+ ).map((field) => field.slug);
3320
+ const siblings = await repo.findTranslations(
3321
+ collection,
3322
+ source.translationGroup ?? source.id,
3323
+ );
3324
+ if (siblings.some((sibling) => sibling.locale?.toLowerCase() === locale.toLowerCase())) {
3325
+ return {
3326
+ success: false as const,
3327
+ error: {
3328
+ code: "CONFLICT",
3329
+ message: `Translation already exists in locale "${locale}" for this content item`,
3330
+ },
3331
+ };
3332
+ }
3333
+ }
3334
+
2797
3335
  // Run beforeSave hooks (trusted plugins)
2798
3336
  let processedData = body.data;
2799
- if (this.hooks.hasHooks("content:beforeSave")) {
3337
+ if (!options.skipSaveHooks && this.hooks.hasHooks("content:beforeSave")) {
2800
3338
  try {
2801
- const hookResult = await this.hooks.runContentBeforeSave(body.data, collection, true);
3339
+ const hookResult = await this.hooks.runContentBeforeSave(
3340
+ body.data,
3341
+ collection,
3342
+ true,
3343
+ undefined,
3344
+ actor,
3345
+ );
2802
3346
  processedData = hookResult.content;
2803
3347
  } catch (error) {
2804
3348
  return beforeSaveFailure(error);
2805
3349
  }
2806
3350
  }
2807
3351
 
2808
- // Run beforeSave hooks (sandboxed plugins)
2809
- const sandboxResult = await this.runSandboxedBeforeSave(processedData, collection, true);
2810
- if (!sandboxResult.success) return sandboxResult;
2811
- processedData = sandboxResult.data;
3352
+ if (translationSource) {
3353
+ processedData = { ...processedData };
3354
+ for (const slug of sharedFieldSlugs) {
3355
+ if (Object.hasOwn(translationSource.data, slug)) {
3356
+ processedData[slug] = translationSource.data[slug];
3357
+ } else {
3358
+ delete processedData[slug];
3359
+ }
3360
+ }
3361
+ }
2812
3362
 
2813
3363
  // Normalize media fields (fill dimensions, storageKey, etc.)
2814
3364
  processedData = await this.normalizeMediaFields(collection, processedData);
@@ -2831,6 +3381,7 @@ export class EmDashRuntime {
2831
3381
  const result = await handleContentCreate(this.db, collection, {
2832
3382
  ...body,
2833
3383
  data: processedData,
3384
+ locale,
2834
3385
  authorId: body.authorId,
2835
3386
  bylines: body.bylines,
2836
3387
  });
@@ -2839,8 +3390,14 @@ export class EmDashRuntime {
2839
3390
  }
2840
3391
 
2841
3392
  // Run afterSave hooks (fire-and-forget)
2842
- if (result.success && result.data) {
2843
- this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);
3393
+ if (!options.skipSaveHooks && result.success && result.data) {
3394
+ this.runAfterSaveHooks(
3395
+ contentItemToRecord(result.data.item),
3396
+ collection,
3397
+ true,
3398
+ actor,
3399
+ options.excludeAfterSavePluginId,
3400
+ );
2844
3401
  }
2845
3402
 
2846
3403
  return result;
@@ -2868,8 +3425,15 @@ export class EmDashRuntime {
2868
3425
  /** Replace the previous autosave revision after staging this save. */
2869
3426
  skipRevision?: boolean;
2870
3427
  _rev?: string;
3428
+ /**
3429
+ * Acting user for this save. Used for revision attribution and
3430
+ * passed to content hooks; never changes entry ownership.
3431
+ */
3432
+ actor?: ActorInfo;
2871
3433
  },
2872
3434
  ) {
3435
+ const actor = body.actor ? { ...body.actor } : undefined;
3436
+
2873
3437
  // Resolve slug → ID if needed (before any lookups)
2874
3438
  const repo = new ContentRepository(this.db);
2875
3439
  const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
@@ -2893,7 +3457,14 @@ export class EmDashRuntime {
2893
3457
  };
2894
3458
  }
2895
3459
  }
2896
- const { _rev: _discardedRev, ...bodyWithoutRev } = body;
3460
+ const { _rev: _discardedRev, actor: _discardedActor, ...bodyWithoutRev } = body;
3461
+
3462
+ // Loaded once and threaded through normalization, the stale-key drop and the draft
3463
+ // merge below: each of those needs the field list and the registry does not cache.
3464
+ const collectionInfo = bodyWithoutRev.data
3465
+ ? await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null)
3466
+ : null;
3467
+ const knownFieldSlugs = new Set((collectionInfo?.fields ?? []).map((f) => f.slug));
2897
3468
 
2898
3469
  // Run beforeSave hooks if data is provided
2899
3470
  let processedData = bodyWithoutRev.data;
@@ -2905,6 +3476,7 @@ export class EmDashRuntime {
2905
3476
  collection,
2906
3477
  false,
2907
3478
  resolvedItem?.id,
3479
+ actor,
2908
3480
  );
2909
3481
  processedData = hookResult.content;
2910
3482
  } catch (error) {
@@ -2912,18 +3484,19 @@ export class EmDashRuntime {
2912
3484
  }
2913
3485
  }
2914
3486
 
2915
- // Run sandboxed beforeSave hooks
2916
- const sandboxResult = await this.runSandboxedBeforeSave(
2917
- processedData!,
2918
- collection,
2919
- false,
2920
- resolvedItem?.id,
2921
- );
2922
- if (!sandboxResult.success) return sandboxResult;
2923
- processedData = sandboxResult.data;
2924
-
2925
3487
  // Normalize media fields (fill dimensions, storageKey, etc.)
2926
- processedData = await this.normalizeMediaFields(collection, processedData);
3488
+ processedData = await this.normalizeMediaFields(collection, processedData!, collectionInfo);
3489
+
3490
+ // Drop unknown field keys the entry already stores (e.g. a deleted field
3491
+ // stranded in a draft revision) before validation, while still rejecting
3492
+ // unknown keys the entry has never stored.
3493
+ if (collectionInfo?.fields) {
3494
+ processedData = await this.dropUnknownKeysAlreadyStored(
3495
+ processedData,
3496
+ resolvedItem,
3497
+ knownFieldSlugs,
3498
+ );
3499
+ }
2927
3500
 
2928
3501
  // Validate field-level shape BEFORE the draft-revision write so
2929
3502
  // invalid updates can't silently land in revision history.
@@ -2945,7 +3518,6 @@ export class EmDashRuntime {
2945
3518
  let usesDraftRevisions = false;
2946
3519
  let draftStorageChanged = false;
2947
3520
  if (processedData) {
2948
- const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
2949
3521
  if (collectionInfo?.supports?.includes("revisions")) {
2950
3522
  usesDraftRevisions = true;
2951
3523
  const revisionRepo = new RevisionRepository(this.db);
@@ -2960,7 +3532,12 @@ export class EmDashRuntime {
2960
3532
  baseData = existing.data;
2961
3533
  }
2962
3534
 
2963
- const mergedData = { ...baseData, ...processedData };
3535
+ // Written without the keys the collection has no field for, so an entry
3536
+ // carrying a deleted field's value sheds it on its next save instead of
3537
+ // carrying it through every revision that follows.
3538
+ const mergedData = collectionInfo?.fields
3539
+ ? keepKnownFields({ ...baseData, ...processedData }, knownFieldSlugs)
3540
+ : { ...baseData, ...processedData };
2964
3541
  if (bodyWithoutRev.slug !== undefined) {
2965
3542
  mergedData._slug = bodyWithoutRev.slug;
2966
3543
  }
@@ -2969,7 +3546,7 @@ export class EmDashRuntime {
2969
3546
  collection,
2970
3547
  entryId: resolvedId,
2971
3548
  data: mergedData,
2972
- authorId: bodyWithoutRev.authorId ?? undefined,
3549
+ authorId: actor?.id,
2973
3550
  });
2974
3551
 
2975
3552
  let staged: boolean;
@@ -3070,31 +3647,14 @@ export class EmDashRuntime {
3070
3647
  if (hydrated.success && hydrated.data) {
3071
3648
  const contentIdsToRefresh = [resolvedId];
3072
3649
  if (!usesDraftRevisions && processedData) {
3073
- try {
3074
- contentIdsToRefresh.push(
3075
- ...(await findNonTranslatableSiblingContentIds(
3076
- this.db,
3077
- collection,
3078
- resolvedId,
3079
- hydrated.data.item.translationGroup,
3080
- processedData,
3081
- )),
3082
- );
3083
- } catch (error) {
3084
- console.error(
3085
- `[media-usage] Failed to discover synced i18n siblings for ${collection}/${resolvedId}:`,
3086
- error,
3087
- );
3088
- try {
3089
- await markContentMediaUsageCollectionStale(
3090
- this.db,
3091
- collection,
3092
- "CONTENT_USAGE_REFRESH_ERROR",
3093
- );
3094
- } catch (staleError) {
3095
- console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
3096
- }
3097
- }
3650
+ contentIdsToRefresh.push(
3651
+ ...(await this.findSyncedSiblingsForUsageRefresh(
3652
+ collection,
3653
+ resolvedId,
3654
+ hydrated.data.item.translationGroup,
3655
+ processedData,
3656
+ )),
3657
+ );
3098
3658
  }
3099
3659
  await this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);
3100
3660
  } else if (draftStorageChanged) {
@@ -3107,7 +3667,7 @@ export class EmDashRuntime {
3107
3667
 
3108
3668
  // Run afterSave hooks (fire-and-forget)
3109
3669
  if (hydrated.success && hydrated.data) {
3110
- this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);
3670
+ this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false, actor);
3111
3671
  }
3112
3672
 
3113
3673
  if (hydrated.success) {
@@ -3131,21 +3691,12 @@ export class EmDashRuntime {
3131
3691
  }
3132
3692
  }
3133
3693
 
3134
- // Run sandboxed beforeDelete hooks
3135
- const sandboxAllowed = await this.runSandboxedBeforeDelete(id, collection);
3136
- if (!sandboxAllowed) {
3137
- return {
3138
- success: false,
3139
- error: {
3140
- code: "DELETE_BLOCKED",
3141
- message: "Delete blocked by sandboxed plugin hook",
3142
- },
3143
- };
3144
- }
3694
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
3145
3695
 
3146
3696
  // Delete the content
3147
3697
  const result = await handleContentDelete(this.db, collection, id);
3148
3698
  if (result.success) {
3699
+ await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
3149
3700
  await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);
3150
3701
  }
3151
3702
 
@@ -3168,23 +3719,38 @@ export class EmDashRuntime {
3168
3719
  return handleContentListTrashed(this.db, collection, params);
3169
3720
  }
3170
3721
 
3171
- async handleContentRestore(collection: string, id: string) {
3172
- const result = await handleContentRestore(this.db, collection, id);
3722
+ async handleContentRestore(
3723
+ collection: string,
3724
+ id: string,
3725
+ options: {
3726
+ _rev?: string;
3727
+ origin?: ContentActionOrigin;
3728
+ pluginInvocationId?: string;
3729
+ } = {},
3730
+ ) {
3731
+ const result = await handleContentRestore(this.db, collection, id, { _rev: options._rev });
3173
3732
  if (result.success && result.data) {
3174
3733
  await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3175
3734
  }
3176
3735
 
3177
3736
  // Run afterRestore hooks (fire-and-forget)
3178
3737
  if (result.success) {
3179
- this.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection);
3738
+ this.runAfterRestoreHooks(
3739
+ contentItemToRecord(result.data.item),
3740
+ collection,
3741
+ options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
3742
+ options.pluginInvocationId,
3743
+ );
3180
3744
  }
3181
3745
 
3182
3746
  return result;
3183
3747
  }
3184
3748
 
3185
3749
  async handleContentPermanentDelete(collection: string, id: string) {
3750
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
3186
3751
  const result = await handleContentPermanentDelete(this.db, collection, id);
3187
3752
  if (result.success) {
3753
+ await this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);
3188
3754
  await this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);
3189
3755
  }
3190
3756
 
@@ -3212,66 +3778,565 @@ export class EmDashRuntime {
3212
3778
  // Publishing & Scheduling Handlers
3213
3779
  // =========================================================================
3214
3780
 
3215
- async handleContentPublish(
3781
+ private pluginVersionedResult(
3782
+ result:
3783
+ | { success: true; data: { item: ContentItemInternal; _rev?: string } }
3784
+ | { success: false; error: { code: string; message: string } },
3785
+ ): VersionedContentItem {
3786
+ if (!result.success) {
3787
+ throw Object.assign(new Error(result.error.message), { code: result.error.code });
3788
+ }
3789
+ if (!result.data._rev) throw new Error("Content action returned no revision");
3790
+ return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
3791
+ }
3792
+
3793
+ private async runPluginContentAction<T>(
3794
+ pluginId: string,
3795
+ action: string,
3216
3796
  collection: string,
3217
3797
  id: string,
3218
- options: {
3219
- publishedAt?: string;
3220
- requireScheduledDue?: boolean;
3221
- expectedScheduledAt?: string;
3222
- _rev?: string;
3223
- } = {},
3224
- ) {
3225
- const result = await handleContentPublish(this.db, collection, id, options);
3226
- if (result.success && result.data) {
3227
- await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3798
+ fn: (resolvedId: string) => Promise<T>,
3799
+ invocationId?: string,
3800
+ invalidateContentCache?: PluginContentCacheInvalidator,
3801
+ ): Promise<T> {
3802
+ let invocationInvalidator: PluginContentCacheInvalidator | undefined;
3803
+ if (invocationId) {
3804
+ const invocationKey = this.pluginInvocationKey(pluginId, invocationId);
3805
+ if (
3806
+ !this.activePluginInvocations.has(invocationKey) &&
3807
+ !this.releasedPluginInvocations.has(invocationKey)
3808
+ ) {
3809
+ throw Object.assign(new Error("Plugin content action invocation has expired"), {
3810
+ code: "CONTENT_ACTION_INVOCATION_EXPIRED",
3811
+ });
3812
+ }
3813
+ invocationInvalidator = this.pluginInvocationCacheInvalidators.get(invocationKey);
3228
3814
  }
3229
-
3230
- // Run afterPublish hooks (fire-and-forget)
3231
- if (result.success && result.data) {
3232
- this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);
3815
+ await assertMediaUsageActivationWriteAllowed(this.db);
3816
+ const repo = new ContentRepository(this.db);
3817
+ const item =
3818
+ action === "restore"
3819
+ ? await repo.findByIdOrSlugIncludingTrashed(collection, id)
3820
+ : await repo.findByIdOrSlug(collection, id);
3821
+ const resolvedId = item?.id ?? id;
3822
+ const key = this.pluginContentActionKey(collection, resolvedId);
3823
+ if ((this.activePluginContentActions.get(key) ?? 0) > 0) {
3824
+ throw Object.assign(new Error("Plugin content action re-entered itself"), {
3825
+ code: "CONTENT_ACTION_REENTRANT",
3826
+ });
3233
3827
  }
3234
-
3235
- return result;
3828
+ this.retainPluginContentAction(key);
3829
+ try {
3830
+ const result = await fn(resolvedId);
3831
+ const invalidator =
3832
+ invalidateContentCache ?? invocationInvalidator ?? this.pluginContentCacheInvalidator;
3833
+ if (invalidator) {
3834
+ try {
3835
+ await invalidator([collection, resolvedId]);
3836
+ } catch (error) {
3837
+ console.error(
3838
+ `[plugin-content-action] Cache invalidation failed for ${collection}/${resolvedId}:`,
3839
+ error,
3840
+ );
3841
+ }
3842
+ }
3843
+ return result;
3844
+ } finally {
3845
+ this.releasePluginContentAction(key);
3846
+ }
3847
+ }
3848
+
3849
+ private pluginContentActionKey(collection: string, id: string): string {
3850
+ return JSON.stringify([collection, id]);
3851
+ }
3852
+
3853
+ private retainPluginContentAction(key: string): void {
3854
+ this.activePluginContentActions.set(key, (this.activePluginContentActions.get(key) ?? 0) + 1);
3855
+ }
3856
+
3857
+ private releasePluginContentAction(key: string): void {
3858
+ const remaining = (this.activePluginContentActions.get(key) ?? 1) - 1;
3859
+ if (remaining > 0) this.activePluginContentActions.set(key, remaining);
3860
+ else this.activePluginContentActions.delete(key);
3236
3861
  }
3237
3862
 
3238
- async handleContentUnpublish(collection: string, id: string, options: { _rev?: string } = {}) {
3239
- const result = await handleContentUnpublish(this.db, collection, id, options);
3863
+ private async pluginGetVersioned(
3864
+ _pluginId: string,
3865
+ collection: string,
3866
+ id: string,
3867
+ ): Promise<VersionedContentItem | null> {
3868
+ const result = await this.handleContentGet(collection, id);
3869
+ if (!result.success) {
3870
+ if (result.error.code === "NOT_FOUND") return null;
3871
+ throw Object.assign(new Error(result.error.message), { code: result.error.code });
3872
+ }
3873
+ if (!result.data._rev) throw new Error("Content read returned no revision");
3874
+ return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
3875
+ }
3876
+
3877
+ private async pluginGetTrashedVersioned(
3878
+ _pluginId: string,
3879
+ collection: string,
3880
+ id: string,
3881
+ ): Promise<VersionedContentItem | null> {
3882
+ const result = await this.handleContentGetIncludingTrashed(collection, id);
3883
+ if (!result.success) {
3884
+ if (result.error.code === "NOT_FOUND") return null;
3885
+ throw Object.assign(new Error(result.error.message), { code: result.error.code });
3886
+ }
3887
+ if (!(await new ContentRepository(this.db).isTrashed(collection, result.data.item.id))) {
3888
+ return null;
3889
+ }
3890
+ if (!result.data._rev) throw new Error("Trashed content read returned no revision");
3891
+ return { item: toPluginContentItem(result.data.item), _rev: result.data._rev };
3892
+ }
3893
+
3894
+ private pluginPublish(
3895
+ pluginId: string,
3896
+ collection: string,
3897
+ id: string,
3898
+ options: { _rev: string },
3899
+ invocationId?: string,
3900
+ invalidateContentCache?: PluginContentCacheInvalidator,
3901
+ ): Promise<VersionedContentItem> {
3902
+ return this.runPluginContentAction(
3903
+ pluginId,
3904
+ "publish",
3905
+ collection,
3906
+ id,
3907
+ async (resolvedId) =>
3908
+ this.pluginVersionedResult(
3909
+ await this.handleContentPublish(collection, resolvedId, {
3910
+ _rev: options._rev,
3911
+ origin: { source: "plugin", pluginId },
3912
+ pluginInvocationId: invocationId,
3913
+ }),
3914
+ ),
3915
+ invocationId,
3916
+ invalidateContentCache,
3917
+ );
3918
+ }
3919
+
3920
+ private pluginUnpublish(
3921
+ pluginId: string,
3922
+ collection: string,
3923
+ id: string,
3924
+ options: { _rev: string },
3925
+ invocationId?: string,
3926
+ invalidateContentCache?: PluginContentCacheInvalidator,
3927
+ ): Promise<VersionedContentItem> {
3928
+ return this.runPluginContentAction(
3929
+ pluginId,
3930
+ "unpublish",
3931
+ collection,
3932
+ id,
3933
+ async (resolvedId) =>
3934
+ this.pluginVersionedResult(
3935
+ await this.handleContentUnpublish(collection, resolvedId, {
3936
+ _rev: options._rev,
3937
+ origin: { source: "plugin", pluginId },
3938
+ pluginInvocationId: invocationId,
3939
+ }),
3940
+ ),
3941
+ invocationId,
3942
+ invalidateContentCache,
3943
+ );
3944
+ }
3945
+
3946
+ private pluginSchedule(
3947
+ pluginId: string,
3948
+ collection: string,
3949
+ id: string,
3950
+ options: { scheduledAt: string; _rev: string },
3951
+ invocationId?: string,
3952
+ invalidateContentCache?: PluginContentCacheInvalidator,
3953
+ ): Promise<VersionedContentItem> {
3954
+ return this.runPluginContentAction(
3955
+ pluginId,
3956
+ "schedule",
3957
+ collection,
3958
+ id,
3959
+ async (resolvedId) =>
3960
+ this.pluginVersionedResult(
3961
+ await this.handleContentSchedule(collection, resolvedId, options.scheduledAt, {
3962
+ _rev: options._rev,
3963
+ origin: { source: "plugin", pluginId },
3964
+ pluginInvocationId: invocationId,
3965
+ }),
3966
+ ),
3967
+ invocationId,
3968
+ invalidateContentCache,
3969
+ );
3970
+ }
3971
+
3972
+ private pluginUnschedule(
3973
+ pluginId: string,
3974
+ collection: string,
3975
+ id: string,
3976
+ options: { _rev: string },
3977
+ invocationId?: string,
3978
+ invalidateContentCache?: PluginContentCacheInvalidator,
3979
+ ): Promise<VersionedContentItem> {
3980
+ return this.runPluginContentAction(
3981
+ pluginId,
3982
+ "unschedule",
3983
+ collection,
3984
+ id,
3985
+ async (resolvedId) =>
3986
+ this.pluginVersionedResult(
3987
+ await this.handleContentUnschedule(collection, resolvedId, {
3988
+ ...options,
3989
+ origin: { source: "plugin", pluginId },
3990
+ pluginInvocationId: invocationId,
3991
+ }),
3992
+ ),
3993
+ invocationId,
3994
+ invalidateContentCache,
3995
+ );
3996
+ }
3997
+
3998
+ private pluginRestore(
3999
+ pluginId: string,
4000
+ collection: string,
4001
+ id: string,
4002
+ options: { _rev: string },
4003
+ invocationId?: string,
4004
+ invalidateContentCache?: PluginContentCacheInvalidator,
4005
+ ): Promise<VersionedContentItem> {
4006
+ return this.runPluginContentAction(
4007
+ pluginId,
4008
+ "restore",
4009
+ collection,
4010
+ id,
4011
+ async (resolvedId) =>
4012
+ this.pluginVersionedResult(
4013
+ await this.handleContentRestore(collection, resolvedId, {
4014
+ ...options,
4015
+ origin: { source: "plugin", pluginId },
4016
+ pluginInvocationId: invocationId,
4017
+ }),
4018
+ ),
4019
+ invocationId,
4020
+ invalidateContentCache,
4021
+ );
4022
+ }
4023
+
4024
+ private async checkContentPolicy(
4025
+ name: ContentPolicyHookName,
4026
+ collection: string,
4027
+ id: string,
4028
+ options: ContentPolicyMutationOptions,
4029
+ rejectionCode: "PUBLISH_REJECTED" | "SCHEDULE_REJECTED" | "UNPUBLISH_REJECTED",
4030
+ failureCode: "CONTENT_PUBLISH_ERROR" | "CONTENT_SCHEDULE_ERROR" | "CONTENT_UNPUBLISH_ERROR",
4031
+ scheduledAt?: string,
4032
+ ): Promise<ContentPolicyCheck> {
4033
+ let current = await handleContentGet(this.db, collection, id);
4034
+ if (!current.success) {
4035
+ return {
4036
+ allowed: false,
4037
+ revision: options._rev ?? "",
4038
+ error: current.error,
4039
+ };
4040
+ }
4041
+
4042
+ const revision = current.data._rev;
4043
+ if (!revision) {
4044
+ return {
4045
+ allowed: false,
4046
+ revision: options._rev ?? "",
4047
+ error: { code: failureCode, message: "Failed to read content revision" },
4048
+ };
4049
+ }
4050
+ if (options._rev !== undefined && options._rev !== revision) {
4051
+ return {
4052
+ allowed: false,
4053
+ revision,
4054
+ error: { code: "CONFLICT", message: "Revision precondition did not match" },
4055
+ };
4056
+ }
4057
+
4058
+ if (!this.hooks.hasHooks(name)) return { allowed: true, revision };
4059
+ if (name !== "content:beforeUnpublish") {
4060
+ try {
4061
+ current = await this.hydrateDraftData(current, {
4062
+ includeStagedSlug: true,
4063
+ strict: true,
4064
+ });
4065
+ } catch (error) {
4066
+ console.error(`[content-policy] ${name} draft hydration failed:`, error);
4067
+ return {
4068
+ allowed: false,
4069
+ revision,
4070
+ error: { code: failureCode, message: `Failed to read content for ${name} policy` },
4071
+ };
4072
+ }
4073
+ }
4074
+
4075
+ const origin = options.origin ?? { source: "system" };
4076
+ const actor =
4077
+ options.actor &&
4078
+ (origin.source === "api" || origin.source === "mcp" || origin.source === "visual-editor")
4079
+ ? { ...options.actor, source: origin.source }
4080
+ : undefined;
4081
+ try {
4082
+ const decision = await this.hooks.runContentPolicy(name, {
4083
+ content: contentItemToRecord(current.data.item),
4084
+ collection,
4085
+ origin,
4086
+ actor,
4087
+ ...(scheduledAt === undefined ? {} : { scheduledAt }),
4088
+ });
4089
+ if (decision.cancellation) {
4090
+ return {
4091
+ allowed: false,
4092
+ revision,
4093
+ error: { code: rejectionCode, message: decision.cancellation.reason },
4094
+ cancellation: decision.cancellation,
4095
+ };
4096
+ }
4097
+ return { allowed: true, revision };
4098
+ } catch (error) {
4099
+ console.error(`[content-policy] ${name} failed:`, error);
4100
+ return {
4101
+ allowed: false,
4102
+ revision,
4103
+ error: { code: failureCode, message: `Failed to run ${name} policy` },
4104
+ };
4105
+ }
4106
+ }
4107
+
4108
+ private async getScheduledPolicyRejectionRevision(
4109
+ collection: string,
4110
+ id: string,
4111
+ ): Promise<string | undefined> {
4112
+ try {
4113
+ return (
4114
+ await new OptionsRepository(this.db).getVersioned(
4115
+ scheduledPolicyRejectionKey(collection, id),
4116
+ )
4117
+ )?.revision;
4118
+ } catch (error) {
4119
+ console.error(
4120
+ `[content-policy] Failed to read scheduled rejection for ${collection}/${id}:`,
4121
+ error,
4122
+ );
4123
+ return undefined;
4124
+ }
4125
+ }
4126
+
4127
+ private async clearScheduledPolicyRejection(
4128
+ collection: string,
4129
+ id: string,
4130
+ expectedRevision: string | undefined,
4131
+ ): Promise<void> {
4132
+ if (expectedRevision === undefined) return;
4133
+ try {
4134
+ await new OptionsRepository(this.db).compareAndDelete(
4135
+ scheduledPolicyRejectionKey(collection, id),
4136
+ expectedRevision,
4137
+ );
4138
+ } catch (error) {
4139
+ console.error(
4140
+ `[content-policy] Failed to clear scheduled rejection for ${collection}/${id}:`,
4141
+ error,
4142
+ );
4143
+ }
4144
+ }
4145
+
4146
+ private createScheduledPolicyRejection(
4147
+ collection: string,
4148
+ id: string,
4149
+ cancellation: { pluginId: string; reason: string },
4150
+ ): ScheduledPolicyRejection {
4151
+ return {
4152
+ collection,
4153
+ id,
4154
+ pluginId: cancellation.pluginId,
4155
+ reason: cancellation.reason,
4156
+ rejectedAt: (this.runtimeDeps.now?.() ?? new Date()).toISOString(),
4157
+ };
4158
+ }
4159
+
4160
+ async handleContentPublish(
4161
+ collection: string,
4162
+ id: string,
4163
+ options: {
4164
+ publishedAt?: string;
4165
+ requireScheduledDue?: boolean;
4166
+ expectedScheduledAt?: string;
4167
+ _rev?: string;
4168
+ currentTime?: Date;
4169
+ actor?: ActorInfo;
4170
+ origin?: ContentActionOrigin;
4171
+ pluginInvocationId?: string;
4172
+ } = {},
4173
+ ) {
4174
+ const policy = await this.checkContentPolicy(
4175
+ "content:beforePublish",
4176
+ collection,
4177
+ id,
4178
+ options,
4179
+ "PUBLISH_REJECTED",
4180
+ "CONTENT_PUBLISH_ERROR",
4181
+ );
4182
+ if (!policy.allowed) {
4183
+ if (policy.cancellation && options.origin?.source === "scheduler") {
4184
+ const unscheduled = await handleScheduledPolicyRejection(this.db, collection, id, {
4185
+ _rev: policy.revision,
4186
+ rejection: this.createScheduledPolicyRejection(collection, id, policy.cancellation),
4187
+ });
4188
+ if (!unscheduled.success) return unscheduled;
4189
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [unscheduled.data.item.id]);
4190
+ this.runAfterUnscheduleHooks(contentItemToRecord(unscheduled.data.item), collection);
4191
+ }
4192
+ return { success: false as const, error: policy.error };
4193
+ }
4194
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
4195
+ const {
4196
+ actor: _actor,
4197
+ origin: _origin,
4198
+ pluginInvocationId: _pluginInvocationId,
4199
+ ...handlerOptions
4200
+ } = options;
4201
+ const result = await handleContentPublish(this.db, collection, id, {
4202
+ ...handlerOptions,
4203
+ _rev: policy.revision,
4204
+ });
4205
+ if (result.success && result.data) {
4206
+ await this.clearScheduledPolicyRejection(
4207
+ collection,
4208
+ result.data.item.id,
4209
+ policyRejectionRevision,
4210
+ );
4211
+ const { item } = result.data;
4212
+ await this.refreshContentUsageAfterSuccessfulWrite(collection, [
4213
+ item.id,
4214
+ ...(await this.findSyncedSiblingsForUsageRefresh(
4215
+ collection,
4216
+ item.id,
4217
+ item.translationGroup,
4218
+ item.data,
4219
+ { absentAsCleared: true },
4220
+ )),
4221
+ ]);
4222
+ }
4223
+
4224
+ // Run afterPublish hooks (fire-and-forget)
4225
+ if (result.success && result.data) {
4226
+ this.runAfterPublishHooks(
4227
+ contentItemToRecord(result.data.item),
4228
+ collection,
4229
+ options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
4230
+ options.pluginInvocationId,
4231
+ );
4232
+ }
4233
+
4234
+ return result;
4235
+ }
4236
+
4237
+ async handleContentUnpublish(
4238
+ collection: string,
4239
+ id: string,
4240
+ options: ContentPolicyMutationOptions = {},
4241
+ ) {
4242
+ const policy = await this.checkContentPolicy(
4243
+ "content:beforeUnpublish",
4244
+ collection,
4245
+ id,
4246
+ options,
4247
+ "UNPUBLISH_REJECTED",
4248
+ "CONTENT_UNPUBLISH_ERROR",
4249
+ );
4250
+ if (!policy.allowed) return { success: false as const, error: policy.error };
4251
+ const result = await handleContentUnpublish(this.db, collection, id, {
4252
+ _rev: policy.revision,
4253
+ });
3240
4254
  if (result.success && result.data) {
3241
4255
  await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3242
4256
  }
3243
4257
 
3244
4258
  // Run afterUnpublish hooks (deferred past the response via after())
3245
4259
  if (result.success && result.data) {
3246
- this.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection);
4260
+ this.runAfterUnpublishHooks(
4261
+ contentItemToRecord(result.data.item),
4262
+ collection,
4263
+ options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
4264
+ options.pluginInvocationId,
4265
+ );
3247
4266
  }
3248
4267
 
3249
4268
  return result;
3250
4269
  }
3251
4270
 
3252
- async handleContentSchedule(collection: string, id: string, scheduledAt: string) {
3253
- const result = await handleContentSchedule(this.db, collection, id, scheduledAt);
4271
+ async handleContentSchedule(
4272
+ collection: string,
4273
+ id: string,
4274
+ scheduledAt: string,
4275
+ options: ContentPolicyMutationOptions = {},
4276
+ ) {
4277
+ const policy = await this.checkContentPolicy(
4278
+ "content:beforeSchedule",
4279
+ collection,
4280
+ id,
4281
+ options,
4282
+ "SCHEDULE_REJECTED",
4283
+ "CONTENT_SCHEDULE_ERROR",
4284
+ scheduledAt,
4285
+ );
4286
+ if (!policy.allowed) return { success: false as const, error: policy.error };
4287
+ const policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);
4288
+ const result = await handleContentSchedule(
4289
+ this.db,
4290
+ collection,
4291
+ id,
4292
+ scheduledAt,
4293
+ this.runtimeDeps.now?.() ?? new Date(),
4294
+ policy.revision,
4295
+ );
3254
4296
  if (result.success && result.data) {
4297
+ await this.clearScheduledPolicyRejection(
4298
+ collection,
4299
+ result.data.item.id,
4300
+ policyRejectionRevision,
4301
+ );
3255
4302
  await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3256
4303
  }
3257
4304
 
3258
4305
  // Run afterSchedule hooks (fire-and-forget)
3259
4306
  if (result.success && result.data) {
3260
- this.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection);
4307
+ this.runAfterScheduleHooks(
4308
+ contentItemToRecord(result.data.item),
4309
+ collection,
4310
+ options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
4311
+ options.pluginInvocationId,
4312
+ );
3261
4313
  }
3262
4314
 
3263
4315
  return result;
3264
4316
  }
3265
4317
 
3266
- async handleContentUnschedule(collection: string, id: string) {
3267
- const result = await handleContentUnschedule(this.db, collection, id);
4318
+ async handleContentUnschedule(
4319
+ collection: string,
4320
+ id: string,
4321
+ options: {
4322
+ _rev?: string;
4323
+ origin?: ContentActionOrigin;
4324
+ pluginInvocationId?: string;
4325
+ } = {},
4326
+ ) {
4327
+ const result = await handleContentUnschedule(this.db, collection, id, { _rev: options._rev });
3268
4328
  if (result.success && result.data) {
3269
4329
  await this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);
3270
4330
  }
3271
4331
 
3272
4332
  // Run afterUnschedule hooks (fire-and-forget)
3273
4333
  if (result.success && result.data) {
3274
- this.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection);
4334
+ this.runAfterUnscheduleHooks(
4335
+ contentItemToRecord(result.data.item),
4336
+ collection,
4337
+ options.origin?.source === "plugin" ? options.origin.pluginId : undefined,
4338
+ options.pluginInvocationId,
4339
+ );
3275
4340
  }
3276
4341
 
3277
4342
  return result;
@@ -3316,6 +4381,33 @@ export class EmDashRuntime {
3316
4381
  return handleMediaGet(this.db, id);
3317
4382
  }
3318
4383
 
4384
+ async handleMediaUpload(input: MediaUploadInput) {
4385
+ if (!this.storage) {
4386
+ return {
4387
+ success: false as const,
4388
+ error: { code: "NO_STORAGE", message: "Storage not configured" },
4389
+ };
4390
+ }
4391
+ const result = await uploadMedia(this.db, this.storage, input, {
4392
+ beforeUpload: async (file) => {
4393
+ if (!this.hooks.hasHooks("media:beforeUpload")) return file;
4394
+ return (await this.hooks.runMediaBeforeUpload(file)).file;
4395
+ },
4396
+ });
4397
+ if (result.success && !result.data.deduplicated && this.hooks.hasHooks("media:afterUpload")) {
4398
+ const item = result.data.item;
4399
+ await this.hooks.runMediaAfterUpload({
4400
+ id: item.id,
4401
+ filename: item.filename,
4402
+ mimeType: item.mimeType,
4403
+ size: item.size,
4404
+ url: item.url,
4405
+ createdAt: item.createdAt,
4406
+ });
4407
+ }
4408
+ return result;
4409
+ }
4410
+
3319
4411
  async handleMediaCreate(input: {
3320
4412
  filename: string;
3321
4413
  mimeType: string;
@@ -3392,6 +4484,10 @@ export class EmDashRuntime {
3392
4484
  return result;
3393
4485
  }
3394
4486
 
4487
+ async handlePluginMediaMetadataUpdate(id: string, patch: MediaMetadataPatch) {
4488
+ return updatePluginMediaMetadata(this.db, id, patch);
4489
+ }
4490
+
3395
4491
  async handleMediaReplaceMetadata(
3396
4492
  id: string,
3397
4493
  expectedStorageKey: string,
@@ -3416,6 +4512,163 @@ export class EmDashRuntime {
3416
4512
  return result;
3417
4513
  }
3418
4514
 
4515
+ private commentHooks(): CommentHookRunner {
4516
+ return {
4517
+ runBeforeCreate: (event) => this.hooks.runCommentBeforeCreate(event),
4518
+ runModerate: async (event) => {
4519
+ const result = await this.hooks.invokeExclusiveHook("comment:moderate", event);
4520
+ if (!result) return { status: "pending", reason: "No moderator configured" };
4521
+ if (result.error) {
4522
+ console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
4523
+ return { status: "pending", reason: "Moderation error" };
4524
+ }
4525
+ if (!isModerationDecision(result.result)) {
4526
+ return { status: "pending", reason: "Invalid moderation result" };
4527
+ }
4528
+ return result.result;
4529
+ },
4530
+ fireAfterCreate: (event) => {
4531
+ void this.hooks
4532
+ .runCommentAfterCreate(event)
4533
+ .catch((error) =>
4534
+ console.error(
4535
+ "[comments] afterCreate error:",
4536
+ error instanceof Error ? error.message : error,
4537
+ ),
4538
+ );
4539
+ },
4540
+ fireAfterModerate: (event) => {
4541
+ return this.hooks
4542
+ .runCommentAfterModerate(event)
4543
+ .catch((error) =>
4544
+ console.error(
4545
+ "[comments] afterModerate error:",
4546
+ error instanceof Error ? error.message : error,
4547
+ ),
4548
+ );
4549
+ },
4550
+ };
4551
+ }
4552
+
4553
+ async handleCommentCreate(
4554
+ input: CommentCreateInput,
4555
+ settings: CollectionCommentSettings,
4556
+ contentInfo?: Parameters<typeof createComment>[4],
4557
+ ): Promise<CommentCreateResult | null> {
4558
+ return createComment(this.db, input, settings, this.commentHooks(), contentInfo);
4559
+ }
4560
+
4561
+ async handlePublicCommentSubmission(
4562
+ collection: string,
4563
+ contentId: string,
4564
+ request: Request,
4565
+ user?: UserInfo,
4566
+ ): Promise<Response> {
4567
+ return submitPublicComment(this, collection, contentId, request, user);
4568
+ }
4569
+
4570
+ private async sendCommentApproval(
4571
+ comment: Awaited<ReturnType<CommentRepository["findById"]>>,
4572
+ request?: Request,
4573
+ ) {
4574
+ if (!comment || !this.email) return;
4575
+ try {
4576
+ const adminBaseUrl = await getSiteBaseUrl(
4577
+ this.db,
4578
+ request ?? new Request("https://plugin.invalid/"),
4579
+ this.config,
4580
+ );
4581
+ const content = await lookupContentAuthor(this.db, comment.collection, comment.contentId);
4582
+ if (content?.author) {
4583
+ await sendCommentNotification({
4584
+ email: this.email,
4585
+ comment,
4586
+ contentAuthor: content.author,
4587
+ adminBaseUrl,
4588
+ });
4589
+ }
4590
+ } catch (error) {
4591
+ console.error(
4592
+ "[comments] notification error:",
4593
+ error instanceof Error ? error.message : error,
4594
+ );
4595
+ }
4596
+ }
4597
+
4598
+ private async moderateCommentWithOrigin(
4599
+ id: string,
4600
+ status: CommentStatus,
4601
+ expectedStatus: CommentStatus,
4602
+ origin:
4603
+ | { source: "admin"; userId: string; name: string | null }
4604
+ | { source: "plugin"; pluginId: string },
4605
+ request?: Request,
4606
+ ) {
4607
+ if (this.commentModerationInProgress.has(id)) {
4608
+ const current = await new CommentRepository(this.db).findById(id);
4609
+ if (current && current.status !== expectedStatus) {
4610
+ throw new CommentStatusConflictError(current.status);
4611
+ }
4612
+ throw Object.assign(new Error("Comment moderation is already in progress"), {
4613
+ code: "COMMENT_MODERATION_IN_PROGRESS",
4614
+ });
4615
+ }
4616
+ this.commentModerationInProgress.add(id);
4617
+ try {
4618
+ return await moderateComment(
4619
+ this.db,
4620
+ id,
4621
+ status,
4622
+ expectedStatus,
4623
+ origin,
4624
+ this.commentHooks(),
4625
+ (comment) => this.sendCommentApproval(comment, request),
4626
+ );
4627
+ } finally {
4628
+ this.commentModerationInProgress.delete(id);
4629
+ }
4630
+ }
4631
+
4632
+ async handleCommentModerate(
4633
+ id: string,
4634
+ status: CommentStatus,
4635
+ moderator: { id: string; name: string | null },
4636
+ expectedStatus?: CommentStatus,
4637
+ request?: Request,
4638
+ ) {
4639
+ let expected = expectedStatus;
4640
+ if (!expected) {
4641
+ const current = await new CommentRepository(this.db).findById(id);
4642
+ if (!current || current.status === "trash") return null;
4643
+ expected = current.status;
4644
+ }
4645
+ return this.moderateCommentWithOrigin(
4646
+ id,
4647
+ status,
4648
+ expected,
4649
+ { source: "admin", userId: moderator.id, name: moderator.name },
4650
+ request,
4651
+ );
4652
+ }
4653
+
4654
+ async handlePluginCommentModerate(
4655
+ pluginId: string,
4656
+ id: string,
4657
+ status: PluginCommentStatus,
4658
+ expectedStatus: PluginCommentStatus,
4659
+ ): Promise<PluginComment> {
4660
+ assertPluginCommentStatus(status, "status");
4661
+ assertPluginCommentStatus(expectedStatus, "expectedStatus");
4662
+ const updated = await this.moderateCommentWithOrigin(id, status, expectedStatus, {
4663
+ source: "plugin",
4664
+ pluginId,
4665
+ });
4666
+ if (!updated) throw new Error(`Comment not found: ${id}`);
4667
+ const result = await createCommentAccess(this.db).get(id);
4668
+ if (!result) throw new Error(`Comment not found: ${id}`);
4669
+ return result;
4670
+ }
4671
+
3419
4672
  // =========================================================================
3420
4673
  // Revision Handlers
3421
4674
  // =========================================================================
@@ -3467,8 +4720,13 @@ export class EmDashRuntime {
3467
4720
  // live, matching the documented tool contract.
3468
4721
  try {
3469
4722
  const contentRepo = new ContentRepository(this.db);
3470
- const existing = await contentRepo.findById(revision.collection, revision.entryId);
3471
- if (!existing) {
4723
+ const newDraftId = await contentRepo.restoreDraftRevision(
4724
+ revision.collection,
4725
+ revision.entryId,
4726
+ revision.data,
4727
+ callerUserId,
4728
+ );
4729
+ if (!newDraftId) {
3472
4730
  return {
3473
4731
  success: false as const,
3474
4732
  error: {
@@ -3478,43 +4736,12 @@ export class EmDashRuntime {
3478
4736
  };
3479
4737
  }
3480
4738
 
3481
- const newDraft = await revisionRepo.create({
3482
- collection: revision.collection,
3483
- entryId: revision.entryId,
3484
- data: revision.data,
3485
- authorId: callerUserId,
3486
- });
3487
-
3488
- try {
3489
- const staged = await contentRepo.replaceDraftRevision(
3490
- revision.collection,
3491
- revision.entryId,
3492
- newDraft.id,
3493
- existing,
3494
- );
3495
- if (!staged) throw new ContentMutationConflictError();
3496
- } catch (error) {
3497
- try {
3498
- await revisionRepo.deleteIfUnreferenced(
3499
- revision.collection,
3500
- revision.entryId,
3501
- newDraft.id,
3502
- );
3503
- } catch (cleanupError) {
3504
- console.error(
3505
- `[emdash] Failed to clean up unrestored revision ${newDraft.id}:`,
3506
- cleanupError,
3507
- );
3508
- }
3509
- throw error;
3510
- }
3511
-
3512
4739
  after(async () => {
3513
4740
  try {
3514
4741
  await revisionRepo.pruneQueuedEntry(
3515
4742
  revision.collection,
3516
4743
  revision.entryId,
3517
- newDraft.id,
4744
+ newDraftId,
3518
4745
  50,
3519
4746
  );
3520
4747
  } catch (error) {
@@ -3553,6 +4780,40 @@ export class EmDashRuntime {
3553
4780
  }
3554
4781
  }
3555
4782
 
4783
+ private async findSyncedSiblingsForUsageRefresh(
4784
+ collection: string,
4785
+ contentId: string,
4786
+ translationGroup: string | null | undefined,
4787
+ data: Record<string, unknown>,
4788
+ options: { absentAsCleared?: boolean } = {},
4789
+ ): Promise<string[]> {
4790
+ try {
4791
+ return await findNonTranslatableSiblingContentIds(
4792
+ this.db,
4793
+ collection,
4794
+ contentId,
4795
+ translationGroup,
4796
+ data,
4797
+ options,
4798
+ );
4799
+ } catch (error) {
4800
+ console.error(
4801
+ `[media-usage] Failed to discover synced i18n siblings for ${collection}/${contentId}:`,
4802
+ error,
4803
+ );
4804
+ try {
4805
+ await markContentMediaUsageCollectionStale(
4806
+ this.db,
4807
+ collection,
4808
+ "CONTENT_USAGE_REFRESH_ERROR",
4809
+ );
4810
+ } catch (staleError) {
4811
+ console.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);
4812
+ }
4813
+ return [];
4814
+ }
4815
+ }
4816
+
3556
4817
  private async refreshContentUsageAfterSuccessfulWrite(
3557
4818
  collection: string,
3558
4819
  contentIds: readonly string[],
@@ -3597,6 +4858,211 @@ export class EmDashRuntime {
3597
4858
  // Plugin Routes
3598
4859
  // =========================================================================
3599
4860
 
4861
+ private getSandboxedAdminDefinition(pluginId: string): {
4862
+ pages: string[];
4863
+ widgets: string[];
4864
+ policy: BlockValidationPolicy;
4865
+ } | null {
4866
+ const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
4867
+ if (entry) {
4868
+ const pages = (entry.adminPages ?? []).map((page) => normalizePluginPagePath(page.path));
4869
+ const imageHosts = allowedBrowserImageHosts(entry.capabilities, entry.allowedHosts);
4870
+ return {
4871
+ pages,
4872
+ widgets: (entry.adminWidgets ?? []).map((widget) => widget.id),
4873
+ policy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },
4874
+ };
4875
+ }
4876
+
4877
+ const manifest = marketplaceManifestCache.get(pluginId);
4878
+ if (!manifest) return null;
4879
+ const pages = (manifest.admin?.pages ?? []).map((page) => normalizePluginPagePath(page.path));
4880
+ const imageHosts = allowedBrowserImageHosts(
4881
+ manifest.capabilities ?? [],
4882
+ manifest.allowedHosts ?? [],
4883
+ );
4884
+ return {
4885
+ pages,
4886
+ widgets: (manifest.admin?.widgets ?? []).map((widget) => widget.id),
4887
+ policy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },
4888
+ };
4889
+ }
4890
+
4891
+ private resolvePluginUiContext(
4892
+ definition: { pages: string[]; widgets: string[] },
4893
+ body: unknown,
4894
+ request: Request,
4895
+ ): { context?: PluginUiContext; error?: { code: string; message: string } } {
4896
+ if (typeof body !== "object" || body === null || !("page" in body)) return {};
4897
+ const page = body.page;
4898
+ if (typeof page !== "string") {
4899
+ return {
4900
+ error: { code: "INVALID_PLUGIN_UI_CONTEXT", message: "Plugin UI page must be a string" },
4901
+ };
4902
+ }
4903
+
4904
+ let surface: PluginUiContext["surface"];
4905
+ if (page.startsWith("widget:")) {
4906
+ if (!definition.widgets.includes(page.slice("widget:".length))) {
4907
+ return {
4908
+ error: {
4909
+ code: "INVALID_PLUGIN_UI_CONTEXT",
4910
+ message: "Plugin dashboard widget is not declared",
4911
+ },
4912
+ };
4913
+ }
4914
+ surface = "dashboard-widget";
4915
+ } else {
4916
+ if (!definition.pages.includes(normalizePluginPagePath(page))) {
4917
+ return {
4918
+ error: {
4919
+ code: "INVALID_PLUGIN_UI_CONTEXT",
4920
+ message: "Plugin admin page is not declared",
4921
+ },
4922
+ };
4923
+ }
4924
+ surface = "admin-page";
4925
+ }
4926
+
4927
+ const locale = resolveLocale(request);
4928
+ return { context: { surface, locale, direction: getLocaleDir(locale) } };
4929
+ }
4930
+
4931
+ private validateSandboxedAdminResponse(
4932
+ pluginId: string,
4933
+ definition: { policy: BlockValidationPolicy },
4934
+ result: {
4935
+ success: boolean;
4936
+ data?: unknown;
4937
+ error?: { code: string; message: string };
4938
+ status?: number;
4939
+ },
4940
+ ) {
4941
+ if (!result.success) return result;
4942
+ const validation = validateBlockResponse(result.data, definition.policy);
4943
+ if (validation.valid) return result;
4944
+
4945
+ console.error(
4946
+ `EmDash: Sandboxed plugin ${pluginId} returned invalid Block Kit content:`,
4947
+ validation.errors,
4948
+ );
4949
+ return {
4950
+ success: false,
4951
+ status: 502,
4952
+ error: {
4953
+ code: "INVALID_BLOCK_RESPONSE",
4954
+ message: "Plugin returned invalid Block Kit content",
4955
+ },
4956
+ };
4957
+ }
4958
+
4959
+ getPluginEditorExtension(
4960
+ pluginId: string,
4961
+ kind: "panel" | "action",
4962
+ extensionId: string,
4963
+ collection: string,
4964
+ ): ResolvedPluginEditorExtension | null {
4965
+ if (!this.isPluginEnabled(pluginId)) return null;
4966
+
4967
+ let panels: PluginEditorPanel[] | undefined;
4968
+ let actions: PluginEditorAction[] | undefined;
4969
+ let pages: string[] = [];
4970
+ let capabilities: readonly PluginCapability[] = [];
4971
+ let allowedHosts: readonly string[] = [];
4972
+
4973
+ const configured = this.configuredPlugins.find((plugin) => plugin.id === pluginId);
4974
+ if (configured) {
4975
+ if (configured.admin.entry) return null;
4976
+ panels = configured.admin.editorPanels;
4977
+ actions = configured.admin.editorActions;
4978
+ pages = (configured.admin.pages ?? []).map((page) => page.path);
4979
+ capabilities = configured.capabilities;
4980
+ allowedHosts = configured.allowedHosts;
4981
+ } else {
4982
+ const entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);
4983
+ if (entry) {
4984
+ panels = entry.editorPanels;
4985
+ actions = entry.editorActions;
4986
+ pages = (entry.adminPages ?? []).map((page) => page.path);
4987
+ capabilities = entry.capabilities;
4988
+ allowedHosts = entry.allowedHosts;
4989
+ } else {
4990
+ const manifest = marketplaceManifestCache.get(pluginId);
4991
+ if (!manifest) return null;
4992
+ panels = manifest.admin?.editorPanels;
4993
+ actions = manifest.admin?.editorActions;
4994
+ pages = (manifest.admin?.pages ?? []).map((page) => page.path);
4995
+ capabilities = manifest.capabilities ?? [];
4996
+ allowedHosts = manifest.allowedHosts ?? [];
4997
+ }
4998
+ }
4999
+
5000
+ const normalizedCapabilities = normalizePluginCapabilities(capabilities);
5001
+ const policy = {
5002
+ pluginPagePaths: pages,
5003
+ allowedImageHosts: allowedBrowserImageHosts(capabilities, allowedHosts),
5004
+ };
5005
+ if (kind === "panel") {
5006
+ const matches = panels?.filter((panel) => panel.id === extensionId) ?? [];
5007
+ const extension = matches[0];
5008
+ if (
5009
+ matches.length !== 1 ||
5010
+ !extension ||
5011
+ (extension.collections && !extension.collections.includes(collection))
5012
+ ) {
5013
+ return null;
5014
+ }
5015
+ return { kind, extension, policy, capabilities: normalizedCapabilities };
5016
+ }
5017
+ const matches = actions?.filter((action) => action.id === extensionId) ?? [];
5018
+ const extension = matches[0];
5019
+ if (
5020
+ matches.length !== 1 ||
5021
+ !extension ||
5022
+ (extension.collections && !extension.collections.includes(collection)) ||
5023
+ (extension.style === "danger" && !extension.confirm)
5024
+ ) {
5025
+ return null;
5026
+ }
5027
+ return { kind, extension, policy, capabilities: normalizedCapabilities };
5028
+ }
5029
+
5030
+ async getPluginEditorDraftSchema(collection: string) {
5031
+ return this.schemaRegistry.getCollectionWithFields(collection);
5032
+ }
5033
+
5034
+ private validatePluginEditorExtensionResponse(
5035
+ pluginId: string,
5036
+ dispatch: PluginEditorExtensionDispatch,
5037
+ result: {
5038
+ success: boolean;
5039
+ data?: unknown;
5040
+ error?: { code: string; message: string };
5041
+ status?: number;
5042
+ },
5043
+ ) {
5044
+ if (!result.success) return result;
5045
+ const validation =
5046
+ dispatch.kind === "panel"
5047
+ ? validateBlockResponse(result.data, dispatch.policy)
5048
+ : validateContentEditorActionResponse(result.data, dispatch.policy);
5049
+ if (validation.valid) return result;
5050
+
5051
+ console.error(
5052
+ `EmDash: Plugin ${pluginId} returned an invalid content editor ${dispatch.kind} response:`,
5053
+ validation.errors,
5054
+ );
5055
+ return {
5056
+ success: false,
5057
+ status: 502,
5058
+ error: {
5059
+ code:
5060
+ dispatch.kind === "panel" ? "INVALID_BLOCK_RESPONSE" : "INVALID_EDITOR_ACTION_RESPONSE",
5061
+ message: "Plugin returned an invalid editor extension response",
5062
+ },
5063
+ };
5064
+ }
5065
+
3600
5066
  /**
3601
5067
  * Get route metadata for a plugin route without invoking the handler.
3602
5068
  * Used by the catch-all route to decide auth before dispatch.
@@ -3660,10 +5126,12 @@ export class EmDashRuntime {
3660
5126
 
3661
5127
  async handlePluginApiRoute(
3662
5128
  pluginId: string,
3663
- _method: string,
5129
+ method: string,
3664
5130
  path: string,
3665
5131
  request: Request,
3666
5132
  user?: RouteCallerInput | null,
5133
+ invalidateContentCache?: PluginContentCacheInvalidator,
5134
+ editorDispatch?: PluginEditorExtensionDispatch,
3667
5135
  ) {
3668
5136
  if (!this.isPluginEnabled(pluginId)) {
3669
5137
  return {
@@ -3671,11 +5139,40 @@ export class EmDashRuntime {
3671
5139
  error: { code: "NOT_FOUND", message: `Plugin not enabled: ${pluginId}` },
3672
5140
  };
3673
5141
  }
5142
+ const normalizedMethod = method.toUpperCase();
5143
+ const routeMeta = this.getPluginRouteMeta(pluginId, path);
5144
+ if (routeMeta?.methods && !routeMeta.methods.some((allowed) => allowed === normalizedMethod)) {
5145
+ return {
5146
+ success: false,
5147
+ status: 405,
5148
+ error: { code: "METHOD_NOT_ALLOWED", message: "Method not allowed" },
5149
+ };
5150
+ }
3674
5151
 
3675
5152
  // Authenticated caller for `ctx.user`. Undefined for public routes
3676
5153
  // (the catch-all only forwards the caller after private-route auth)
3677
5154
  // and for machine tokens with no bound user.
3678
5155
  const caller = user ? toRouteCallerInfo(user) : undefined;
5156
+ let body: unknown;
5157
+ try {
5158
+ body = await parseRouteInput(request, routeMeta?.request);
5159
+ } catch (error) {
5160
+ if (error instanceof PluginRouteRequestError) {
5161
+ return {
5162
+ success: false,
5163
+ status: error.status,
5164
+ error: { code: "INVALID_PLUGIN_REQUEST", message: error.message },
5165
+ };
5166
+ }
5167
+ throw error;
5168
+ }
5169
+ const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
5170
+ const adminDefinition =
5171
+ !editorDispatch && routeKey === "admin" ? this.getSandboxedAdminDefinition(pluginId) : null;
5172
+ const uiResult = adminDefinition
5173
+ ? this.resolvePluginUiContext(adminDefinition, body, request)
5174
+ : {};
5175
+ if (uiResult.error) return { success: false, status: 400, error: uiResult.error };
3679
5176
 
3680
5177
  // Check trusted (configured) plugins first — this must match the
3681
5178
  // resolution order in getPluginRouteMeta to avoid auth/execution mismatches.
@@ -3683,24 +5180,44 @@ export class EmDashRuntime {
3683
5180
  if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
3684
5181
  const routeRegistry = new PluginRouteRegistry({
3685
5182
  ...this.pipelineFactoryOptions,
5183
+ contentActions: this.contentActionsWithInvalidator(invalidateContentCache),
3686
5184
  emailPipeline: this.email ?? undefined,
3687
5185
  cronReschedule: () => this.cronScheduler?.reschedule(),
3688
5186
  trustedProxyHeaders: getTrustedProxyHeaders(this.config),
3689
5187
  });
3690
5188
  routeRegistry.register(trustedPlugin);
3691
5189
 
3692
- const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
3693
-
3694
- // Body methods parse JSON; GET/HEAD/DELETE parse the query string (#2146).
3695
- const body = await parseRouteInput(request);
3696
-
3697
- return routeRegistry.invoke(pluginId, routeKey, { request, body, user: caller });
5190
+ const result = await routeRegistry.invoke(pluginId, routeKey, {
5191
+ request,
5192
+ body,
5193
+ user: caller,
5194
+ ui: editorDispatch?.ui ?? uiResult.context,
5195
+ });
5196
+ return editorDispatch
5197
+ ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)
5198
+ : adminDefinition
5199
+ ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)
5200
+ : result;
3698
5201
  }
3699
5202
 
3700
5203
  // Check sandboxed (marketplace) plugins second
3701
5204
  const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
3702
5205
  if (sandboxedPlugin) {
3703
- return this.handleSandboxedRoute(sandboxedPlugin, path, request, caller);
5206
+ const result = await this.handleSandboxedRoute(
5207
+ sandboxedPlugin,
5208
+ path,
5209
+ request,
5210
+ body,
5211
+ routeMeta ?? { public: false },
5212
+ caller,
5213
+ editorDispatch?.ui ?? uiResult.context,
5214
+ invalidateContentCache,
5215
+ );
5216
+ return editorDispatch
5217
+ ? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)
5218
+ : adminDefinition
5219
+ ? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)
5220
+ : result;
3704
5221
  }
3705
5222
 
3706
5223
  return {
@@ -3709,6 +5226,28 @@ export class EmDashRuntime {
3709
5226
  };
3710
5227
  }
3711
5228
 
5229
+ private contentActionsWithInvalidator(
5230
+ invalidateContentCache?: PluginContentCacheInvalidator,
5231
+ ): ContentActionCallbacks | undefined {
5232
+ const actions = this.pipelineFactoryOptions.contentActions;
5233
+ if (!actions || !invalidateContentCache) return actions;
5234
+ return {
5235
+ ...actions,
5236
+ begin: (pluginId, invocationId, invocationInvalidator) =>
5237
+ actions.begin?.(pluginId, invocationId, invocationInvalidator ?? invalidateContentCache),
5238
+ publish: (pluginId, collection, id, options, invocationId) =>
5239
+ actions.publish(pluginId, collection, id, options, invocationId, invalidateContentCache),
5240
+ unpublish: (pluginId, collection, id, options, invocationId) =>
5241
+ actions.unpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),
5242
+ schedule: (pluginId, collection, id, options, invocationId) =>
5243
+ actions.schedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
5244
+ unschedule: (pluginId, collection, id, options, invocationId) =>
5245
+ actions.unschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),
5246
+ restore: (pluginId, collection, id, options, invocationId) =>
5247
+ actions.restore(pluginId, collection, id, options, invocationId, invalidateContentCache),
5248
+ };
5249
+ }
5250
+
3712
5251
  async getPluginMcpTools(pluginId?: string) {
3713
5252
  const tools: Array<{
3714
5253
  pluginId: string;
@@ -3726,7 +5265,14 @@ export class EmDashRuntime {
3726
5265
  if (pluginId && plugin.id !== pluginId) continue;
3727
5266
  for (const [name, tool] of Object.entries(plugin.mcp?.tools ?? {})) {
3728
5267
  const route = plugin.routes[tool.route];
3729
- if (!route || route.public || !route.permission || !(route.permission in Permissions))
5268
+ if (
5269
+ !route ||
5270
+ route.public ||
5271
+ route.response === "raw" ||
5272
+ !isJsonPostRouteContract(route) ||
5273
+ !route.permission ||
5274
+ !Object.hasOwn(Permissions, route.permission)
5275
+ )
3730
5276
  continue;
3731
5277
  const key = `${plugin.id}__${name}`;
3732
5278
  if (seen.has(key)) continue;
@@ -3753,8 +5299,10 @@ export class EmDashRuntime {
3753
5299
  seen.has(key) ||
3754
5300
  !routeMeta ||
3755
5301
  routeMeta.public ||
5302
+ routeMeta.response === "raw" ||
5303
+ !isJsonPostRouteContract(routeMeta) ||
3756
5304
  routeMeta.permission !== tool.permission ||
3757
- !(tool.permission in Permissions)
5305
+ !Object.hasOwn(Permissions, tool.permission)
3758
5306
  ) {
3759
5307
  continue;
3760
5308
  }
@@ -3828,6 +5376,7 @@ export class EmDashRuntime {
3828
5376
  actorId: string,
3829
5377
  request: Request,
3830
5378
  caller?: RouteCallerInput | null,
5379
+ invalidateContentCache?: PluginContentCacheInvalidator,
3831
5380
  ) {
3832
5381
  const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
3833
5382
  const audit = new AuditRepository(this.db);
@@ -3845,6 +5394,7 @@ export class EmDashRuntime {
3845
5394
  route,
3846
5395
  internalRequest,
3847
5396
  caller,
5397
+ invalidateContentCache,
3848
5398
  );
3849
5399
  await audit.log({
3850
5400
  actorId,
@@ -3891,6 +5441,37 @@ export class EmDashRuntime {
3891
5441
  return undefined;
3892
5442
  }
3893
5443
 
5444
+ /**
5445
+ * Drop incoming keys that have no matching collection field when the entry
5446
+ * already stores them in its live data or current draft revision.
5447
+ *
5448
+ * Draft revisions keep the full `data` JSON, so deleting a field can strand
5449
+ * the old value; this lets a read-then-write save succeed without allowing
5450
+ * genuinely unknown keys.
5451
+ */
5452
+ private async dropUnknownKeysAlreadyStored(
5453
+ data: Record<string, unknown>,
5454
+ existing: { data: Record<string, unknown>; draftRevisionId?: string | null } | null,
5455
+ knownFieldSlugs: ReadonlySet<string>,
5456
+ ): Promise<Record<string, unknown>> {
5457
+ if (!existing) return data;
5458
+
5459
+ let stored: Record<string, unknown> = existing.data ?? {};
5460
+ if (existing.draftRevisionId) {
5461
+ const draft = await new RevisionRepository(this.db)
5462
+ .findById(existing.draftRevisionId)
5463
+ .catch(() => null);
5464
+ if (draft?.data) stored = draft.data;
5465
+ }
5466
+
5467
+ const stale = staleStoredKeys(data, stored, knownFieldSlugs);
5468
+ if (stale.length === 0) return data;
5469
+
5470
+ const result = { ...data };
5471
+ for (const key of stale) delete result[key];
5472
+ return result;
5473
+ }
5474
+
3894
5475
  /**
3895
5476
  * Normalize image/file fields in content data.
3896
5477
  * Fills missing dimensions, storageKey, mimeType, and filename from providers.
@@ -3898,12 +5479,15 @@ export class EmDashRuntime {
3898
5479
  private async normalizeMediaFields(
3899
5480
  collection: string,
3900
5481
  data: Record<string, unknown>,
5482
+ preloaded?: CollectionWithFields | null,
3901
5483
  ): Promise<Record<string, unknown>> {
3902
- let collectionInfo;
3903
- try {
3904
- collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
3905
- } catch {
3906
- return data;
5484
+ let collectionInfo = preloaded;
5485
+ if (collectionInfo === undefined) {
5486
+ try {
5487
+ collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
5488
+ } catch {
5489
+ return data;
5490
+ }
3907
5491
  }
3908
5492
  if (!collectionInfo?.fields) return data;
3909
5493
 
@@ -3973,119 +5557,22 @@ export class EmDashRuntime {
3973
5557
  return result;
3974
5558
  }
3975
5559
 
3976
- private async runSandboxedBeforeSave(
3977
- content: Record<string, unknown>,
3978
- collection: string,
3979
- isNew: boolean,
3980
- contentId?: string,
3981
- ) {
3982
- let result = content;
3983
-
3984
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
3985
- const [id] = pluginKey.split(":");
3986
- if (!id || !this.isPluginEnabled(id)) continue;
3987
-
3988
- try {
3989
- const event: ContentHookEvent = { content: result, collection, isNew };
3990
- if (contentId !== undefined) event.id = contentId;
3991
- const hookResult = await plugin.invokeHook("content:beforeSave", event);
3992
- const inspection = inspectSandboxHookResult(hookResult);
3993
- if (inspection.kind === "error") {
3994
- return {
3995
- success: false as const,
3996
- error: {
3997
- code: ErrorCode.SAVE_REJECTED,
3998
- message: "Save rejected by a sandboxed plugin",
3999
- details: { pluginId: id, reason: inspection.error.reason },
4000
- },
4001
- };
4002
- }
4003
- if (inspection.kind === "malformed") {
4004
- console.error(`EmDash: Sandboxed plugin ${id} returned an invalid hook result`);
4005
- return {
4006
- success: false as const,
4007
- error: {
4008
- code: ErrorCode.CONTENT_HOOK_ERROR,
4009
- message: "A plugin hook failed while saving content",
4010
- },
4011
- };
4012
- }
4013
- if (hookResult && typeof hookResult === "object" && !Array.isArray(hookResult)) {
4014
- // Sandbox returns unknown; convert to record by iterating own properties
4015
- const record: Record<string, unknown> = {};
4016
- for (const [k, v] of Object.entries(hookResult)) {
4017
- record[k] = v;
4018
- }
4019
- result = record;
4020
- }
4021
- } catch (error) {
4022
- console.error(`EmDash: Sandboxed plugin ${id} beforeSave hook failed:`, error);
4023
- return {
4024
- success: false as const,
4025
- error: {
4026
- code: ErrorCode.CONTENT_HOOK_ERROR,
4027
- message: "A plugin hook failed while saving content",
4028
- },
4029
- };
4030
- }
4031
- }
4032
-
4033
- return { success: true as const, data: result };
4034
- }
4035
-
4036
- private async runSandboxedBeforeDelete(id: string, collection: string): Promise<boolean> {
4037
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
4038
- const [pluginId] = pluginKey.split(":");
4039
- if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
4040
-
4041
- try {
4042
- const result = await plugin.invokeHook("content:beforeDelete", {
4043
- id,
4044
- collection,
4045
- });
4046
- if (result === false) {
4047
- return false;
4048
- }
4049
- } catch (error) {
4050
- console.error(`EmDash: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);
4051
- }
4052
- }
4053
-
4054
- return true;
4055
- }
4056
-
4057
5560
  private runAfterSaveHooks(
4058
5561
  content: Record<string, unknown>,
4059
5562
  collection: string,
4060
5563
  isNew: boolean,
5564
+ actor?: ActorInfo,
5565
+ excludePluginId?: string,
4061
5566
  ): void {
4062
5567
  after(async () => {
4063
5568
  // Trusted plugins
4064
5569
  if (this.hooks.hasHooks("content:afterSave")) {
4065
5570
  try {
4066
- await this.hooks.runContentAfterSave(content, collection, isNew);
5571
+ await this.hooks.runContentAfterSave(content, collection, isNew, actor, excludePluginId);
4067
5572
  } catch (err) {
4068
5573
  console.error("EmDash afterSave hook error:", err);
4069
5574
  }
4070
5575
  }
4071
-
4072
- // Sandboxed plugins
4073
- const tasks: Promise<void>[] = [];
4074
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
4075
- const [id] = pluginKey.split(":");
4076
- if (!id || !this.isPluginEnabled(id)) continue;
4077
-
4078
- tasks.push(
4079
- (async () => {
4080
- try {
4081
- await plugin.invokeHook("content:afterSave", { content, collection, isNew });
4082
- } catch (err) {
4083
- console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);
4084
- }
4085
- })(),
4086
- );
4087
- }
4088
- await Promise.allSettled(tasks);
4089
5576
  });
4090
5577
  }
4091
5578
 
@@ -4099,24 +5586,6 @@ export class EmDashRuntime {
4099
5586
  console.error("EmDash afterDelete hook error:", err);
4100
5587
  }
4101
5588
  }
4102
-
4103
- // Sandboxed plugins
4104
- const tasks: Promise<void>[] = [];
4105
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
4106
- const [pluginId] = pluginKey.split(":");
4107
- if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
4108
-
4109
- tasks.push(
4110
- (async () => {
4111
- try {
4112
- await plugin.invokeHook("content:afterDelete", { id, collection, permanent });
4113
- } catch (err) {
4114
- console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err);
4115
- }
4116
- })(),
4117
- );
4118
- }
4119
- await Promise.allSettled(tasks);
4120
5589
  });
4121
5590
  }
4122
5591
 
@@ -4129,10 +5598,12 @@ export class EmDashRuntime {
4129
5598
  | "content:afterUnschedule",
4130
5599
  content: Record<string, unknown>,
4131
5600
  collection: string,
5601
+ afterPluginId?: string,
5602
+ pluginInvocationId?: string,
4132
5603
  ): void {
4133
5604
  const label = name.slice("content:".length);
4134
5605
 
4135
- after(async () => {
5606
+ const invoke = async () => {
4136
5607
  // Trusted plugins
4137
5608
  if (this.hooks.hasHooks(name)) {
4138
5609
  try {
@@ -4157,52 +5628,160 @@ export class EmDashRuntime {
4157
5628
  console.error(`EmDash ${label} hook error:`, err);
4158
5629
  }
4159
5630
  }
5631
+ };
5632
+ const contentId = typeof content.id === "string" ? content.id : undefined;
5633
+ const guardKey =
5634
+ afterPluginId && contentId ? this.pluginContentActionKey(collection, contentId) : undefined;
5635
+ const invokeWithGuard = async () => {
5636
+ if (guardKey) this.retainPluginContentAction(guardKey);
5637
+ try {
5638
+ await invoke();
5639
+ } finally {
5640
+ if (guardKey) this.releasePluginContentAction(guardKey);
5641
+ }
5642
+ };
5643
+ if (afterPluginId && pluginInvocationId && this.findSandboxedPlugin(afterPluginId)) {
5644
+ const key = this.pluginInvocationKey(afterPluginId, pluginInvocationId);
5645
+ if (!this.activePluginInvocations.has(key) || this.releasedPluginInvocations.has(key)) {
5646
+ after(invokeWithGuard);
5647
+ return;
5648
+ }
5649
+ const pending = this.pendingPluginAfterHooks.get(key) ?? [];
5650
+ pending.push(invokeWithGuard);
5651
+ this.pendingPluginAfterHooks.set(key, pending);
5652
+ return;
5653
+ }
5654
+ after(invokeWithGuard);
5655
+ }
4160
5656
 
4161
- // Sandboxed plugins
4162
- const tasks: Promise<void>[] = [];
4163
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
4164
- const [pluginId] = pluginKey.split(":");
4165
- if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
5657
+ private pluginInvocationKey(pluginId: string, invocationId: string): string {
5658
+ return JSON.stringify([pluginId, invocationId]);
5659
+ }
4166
5660
 
4167
- tasks.push(
4168
- (async () => {
4169
- try {
4170
- await plugin.invokeHook(name, { content, collection });
4171
- } catch (err) {
4172
- console.error(`EmDash: Sandboxed plugin ${pluginId} ${label} error:`, err);
4173
- }
4174
- })(),
4175
- );
4176
- }
4177
- await Promise.allSettled(tasks);
4178
- });
5661
+ private beginPluginInvocation(
5662
+ pluginId: string,
5663
+ invocationId: string,
5664
+ invalidateContentCache?: PluginContentCacheInvalidator,
5665
+ ): void {
5666
+ const key = this.pluginInvocationKey(pluginId, invocationId);
5667
+ this.pendingPluginAfterHooks.delete(key);
5668
+ this.releasedPluginInvocations.delete(key);
5669
+ if (invalidateContentCache)
5670
+ this.pluginInvocationCacheInvalidators.set(key, invalidateContentCache);
5671
+ else this.pluginInvocationCacheInvalidators.delete(key);
5672
+ this.activePluginInvocations.add(key);
5673
+ }
5674
+
5675
+ private flushPluginAfterHooks(
5676
+ pluginId: string,
5677
+ invocationId?: string,
5678
+ final = true,
5679
+ ): Promise<void> {
5680
+ if (!invocationId) return Promise.resolve();
5681
+ const key = this.pluginInvocationKey(pluginId, invocationId);
5682
+ this.releasedPluginInvocations.add(key);
5683
+ const pending = this.pendingPluginAfterHooks.get(key) ?? [];
5684
+ this.pendingPluginAfterHooks.delete(key);
5685
+ for (const invoke of pending) after(invoke);
5686
+ if (final) {
5687
+ this.releasedPluginInvocations.delete(key);
5688
+ this.activePluginInvocations.delete(key);
5689
+ this.pluginInvocationCacheInvalidators.delete(key);
5690
+ } else {
5691
+ const timer = setTimeout(() => {
5692
+ this.releasedPluginInvocations.delete(key);
5693
+ this.activePluginInvocations.delete(key);
5694
+ this.pluginInvocationCacheInvalidators.delete(key);
5695
+ }, PLUGIN_INVOCATION_RELEASE_GRACE_MS);
5696
+ if (typeof timer === "object") timer.unref();
5697
+ }
5698
+ return Promise.resolve();
4179
5699
  }
4180
5700
 
4181
- private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
4182
- this.runDeferredContentHook("content:afterPublish", content, collection);
5701
+ private runAfterPublishHooks(
5702
+ content: Record<string, unknown>,
5703
+ collection: string,
5704
+ afterPluginId?: string,
5705
+ pluginInvocationId?: string,
5706
+ ): void {
5707
+ this.runDeferredContentHook(
5708
+ "content:afterPublish",
5709
+ content,
5710
+ collection,
5711
+ afterPluginId,
5712
+ pluginInvocationId,
5713
+ );
4183
5714
  }
4184
5715
 
4185
- private runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {
4186
- this.runDeferredContentHook("content:afterUnpublish", content, collection);
5716
+ private runAfterUnpublishHooks(
5717
+ content: Record<string, unknown>,
5718
+ collection: string,
5719
+ afterPluginId?: string,
5720
+ pluginInvocationId?: string,
5721
+ ): void {
5722
+ this.runDeferredContentHook(
5723
+ "content:afterUnpublish",
5724
+ content,
5725
+ collection,
5726
+ afterPluginId,
5727
+ pluginInvocationId,
5728
+ );
4187
5729
  }
4188
5730
 
4189
- private runAfterRestoreHooks(content: Record<string, unknown>, collection: string): void {
4190
- this.runDeferredContentHook("content:afterRestore", content, collection);
5731
+ private runAfterRestoreHooks(
5732
+ content: Record<string, unknown>,
5733
+ collection: string,
5734
+ afterPluginId?: string,
5735
+ pluginInvocationId?: string,
5736
+ ): void {
5737
+ this.runDeferredContentHook(
5738
+ "content:afterRestore",
5739
+ content,
5740
+ collection,
5741
+ afterPluginId,
5742
+ pluginInvocationId,
5743
+ );
4191
5744
  }
4192
5745
 
4193
- private runAfterScheduleHooks(content: Record<string, unknown>, collection: string): void {
4194
- this.runDeferredContentHook("content:afterSchedule", content, collection);
5746
+ private runAfterScheduleHooks(
5747
+ content: Record<string, unknown>,
5748
+ collection: string,
5749
+ afterPluginId?: string,
5750
+ pluginInvocationId?: string,
5751
+ ): void {
5752
+ this.runDeferredContentHook(
5753
+ "content:afterSchedule",
5754
+ content,
5755
+ collection,
5756
+ afterPluginId,
5757
+ pluginInvocationId,
5758
+ );
4195
5759
  }
4196
5760
 
4197
- private runAfterUnscheduleHooks(content: Record<string, unknown>, collection: string): void {
4198
- this.runDeferredContentHook("content:afterUnschedule", content, collection);
5761
+ private runAfterUnscheduleHooks(
5762
+ content: Record<string, unknown>,
5763
+ collection: string,
5764
+ afterPluginId?: string,
5765
+ pluginInvocationId?: string,
5766
+ ): void {
5767
+ this.runDeferredContentHook(
5768
+ "content:afterUnschedule",
5769
+ content,
5770
+ collection,
5771
+ afterPluginId,
5772
+ pluginInvocationId,
5773
+ );
4199
5774
  }
4200
5775
 
4201
5776
  private async handleSandboxedRoute(
4202
5777
  plugin: SandboxedPluginInstance,
4203
5778
  path: string,
4204
5779
  request: Request,
5780
+ body: unknown,
5781
+ routeMeta: RouteMeta,
4205
5782
  user?: UserInfo,
5783
+ ui?: PluginUiContext,
5784
+ invalidateContentCache?: PluginContentCacheInvalidator,
4206
5785
  ): Promise<{
4207
5786
  success: boolean;
4208
5787
  data?: unknown;
@@ -4211,21 +5790,32 @@ export class EmDashRuntime {
4211
5790
  }> {
4212
5791
  const routeName = path.replace(LEADING_SLASH_PATTERN, "");
4213
5792
 
4214
- // Body methods parse JSON; GET/HEAD/DELETE parse the query string (#2146).
4215
- const body = await parseRouteInput(request);
4216
-
4217
5793
  try {
4218
- const headers = sanitizeHeadersForSandbox(request.headers);
5794
+ const declaredHeaders = routeMeta.request ? (routeMeta.request.headers ?? []) : undefined;
5795
+ const headers = sanitizeHeadersForSandbox(request.headers, declaredHeaders);
4219
5796
  const meta = extractRequestMeta(request, this.config);
4220
- const result = await plugin.invokeRoute(routeName, body, {
4221
- url: request.url,
4222
- method: request.method,
4223
- headers,
4224
- meta,
4225
- user,
4226
- });
5797
+ const result = await plugin.invokeRoute(
5798
+ routeName,
5799
+ body,
5800
+ {
5801
+ url: request.url,
5802
+ method: request.method,
5803
+ headers,
5804
+ meta,
5805
+ user,
5806
+ ui,
5807
+ },
5808
+ { invalidateContentCache },
5809
+ );
4227
5810
  return { success: true, data: result };
4228
5811
  } catch (error) {
5812
+ if (error instanceof PluginRouteRequestError) {
5813
+ return {
5814
+ success: false,
5815
+ status: error.status,
5816
+ error: { code: "INVALID_PLUGIN_REQUEST", message: error.message },
5817
+ };
5818
+ }
4229
5819
  console.error(`EmDash: Sandboxed plugin route error:`, error);
4230
5820
  const sandboxRouteError = getSandboxRouteErrorDetails(error);
4231
5821
  if (sandboxRouteError) {
@@ -4292,26 +5882,6 @@ export class EmDashRuntime {
4292
5882
  }
4293
5883
  }
4294
5884
 
4295
- // Sandboxed plugins — metadata only, never fragments
4296
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
4297
- const [id] = pluginKey.split(":");
4298
- if (!id || !this.isPluginEnabled(id)) continue;
4299
-
4300
- try {
4301
- const result = await plugin.invokeHook("page:metadata", { page });
4302
- if (result != null) {
4303
- const items = Array.isArray(result) ? result : [result];
4304
- for (const item of items) {
4305
- if (isValidMetadataContribution(item)) {
4306
- metadata.push(item);
4307
- }
4308
- }
4309
- }
4310
- } catch (error) {
4311
- console.error(`EmDash: Sandboxed plugin ${id} page:metadata error:`, error);
4312
- }
4313
- }
4314
-
4315
5885
  return { metadata, fragments };
4316
5886
  }
4317
5887