emdash 0.32.0 → 0.34.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 (946) hide show
  1. package/dist/{adapters-C0ONzkUP.d.mts → adapters-34EN2eXO.d.mts} +34 -2
  2. package/dist/adapters-34EN2eXO.d.mts.map +1 -0
  3. package/dist/after-CBU2vkoO.mjs +96 -0
  4. package/dist/after-CBU2vkoO.mjs.map +1 -0
  5. package/dist/{allowed-origins-CCEi9bPI.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
  6. package/dist/{allowed-origins-CCEi9bPI.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -4
  8. package/dist/api/route-utils.d.mts.map +1 -1
  9. package/dist/api/route-utils.mjs +26 -25
  10. package/dist/api/route-utils.mjs.map +1 -1
  11. package/dist/api/schemas/index.d.mts +2 -2
  12. package/dist/api/schemas/index.mjs +6 -5
  13. package/dist/{api-DgPbMlng.mjs → api-DdAsirBe.mjs} +321 -428
  14. package/dist/api-DdAsirBe.mjs.map +1 -0
  15. package/dist/{api-tokens-p-iMzvXR.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
  16. package/dist/{api-tokens-p-iMzvXR.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
  17. package/dist/{apply-1_6ra7NP.mjs → apply-D5zfeD09.mjs} +139 -102
  18. package/dist/apply-D5zfeD09.mjs.map +1 -0
  19. package/dist/astro/index.d.mts +12 -12
  20. package/dist/astro/index.d.mts.map +1 -1
  21. package/dist/astro/index.mjs +103 -35
  22. package/dist/astro/index.mjs.map +1 -1
  23. package/dist/astro/middleware/auth.d.mts +10 -10
  24. package/dist/astro/middleware/auth.mjs +9 -9
  25. package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
  26. package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
  27. package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
  28. package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
  29. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  30. package/dist/astro/middleware/redirect.mjs +13 -10
  31. package/dist/astro/middleware/redirect.mjs.map +1 -1
  32. package/dist/astro/middleware/request-context.mjs +5 -5
  33. package/dist/astro/middleware/setup.mjs +1 -1
  34. package/dist/astro/middleware.d.mts +12 -11
  35. package/dist/astro/middleware.d.mts.map +1 -1
  36. package/dist/astro/middleware.mjs +1915 -223
  37. package/dist/astro/middleware.mjs.map +1 -1
  38. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
  39. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  40. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
  41. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  42. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  43. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  44. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  45. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
  46. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  47. package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
  48. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
  50. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  51. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +20 -20
  52. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +20 -20
  53. package/dist/astro/routes/api/admin/bylines/index.mjs +20 -20
  54. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -17
  55. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  56. package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
  57. package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
  58. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
  60. package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
  61. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  62. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
  63. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
  64. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
  65. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
  66. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
  67. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
  68. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
  69. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
  70. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
  71. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
  72. package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
  73. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  74. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
  75. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
  76. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
  77. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
  78. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
  79. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
  80. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
  81. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
  82. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  83. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  84. package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
  85. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -47
  86. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  87. package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -47
  89. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  90. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
  91. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
  93. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
  94. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -46
  97. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  99. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -46
  100. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  101. package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
  102. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  104. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
  105. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  107. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +55 -46
  108. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  109. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
  110. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
  112. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -46
  113. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  114. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  115. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +56 -47
  116. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  117. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +52 -46
  118. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  119. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  120. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -47
  121. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  122. package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
  123. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  124. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
  125. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  126. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  127. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
  128. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  129. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
  130. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  131. package/dist/astro/routes/api/admin/users/_id_/index.mjs +10 -9
  132. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  133. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
  134. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
  135. package/dist/astro/routes/api/admin/users/index.mjs +9 -8
  136. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  137. package/dist/astro/routes/api/auth/dev-bypass.mjs +8 -8
  138. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  139. package/dist/astro/routes/api/auth/invite/complete.mjs +14 -13
  140. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  141. package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
  142. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  143. package/dist/astro/routes/api/auth/invite/register-options.mjs +13 -12
  144. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  145. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  146. package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
  147. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  148. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  149. package/dist/astro/routes/api/auth/me.mjs +10 -9
  150. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  151. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  152. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  153. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  154. package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
  155. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  156. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  157. package/dist/astro/routes/api/auth/passkey/options.mjs +15 -14
  158. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  159. package/dist/astro/routes/api/auth/passkey/register/options.mjs +13 -12
  160. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  161. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +14 -13
  162. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/passkey/verify.mjs +14 -13
  164. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  165. package/dist/astro/routes/api/auth/signup/complete.mjs +14 -13
  166. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  167. package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
  168. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  169. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  170. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +20 -19
  171. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  172. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +17 -16
  173. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  174. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  175. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
  176. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  177. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  178. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +15 -15
  179. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
  180. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  181. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +21 -20
  182. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  183. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +21 -20
  184. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  185. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  186. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  187. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
  188. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  189. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  190. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +47 -34
  191. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  192. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  193. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
  194. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
  195. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  196. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  197. package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
  198. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  199. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  200. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  201. package/dist/astro/routes/api/dashboard.mjs +12 -10
  202. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  203. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  204. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  205. package/dist/astro/routes/api/import/probe.mjs +22 -20
  206. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  207. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  208. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  209. package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -18
  210. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  211. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  212. package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
  213. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  214. package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
  215. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  216. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  217. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +15 -15
  218. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  219. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +22 -20
  220. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  221. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  222. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  223. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +32 -31
  224. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  225. package/dist/astro/routes/api/manifest.mjs +6 -6
  226. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  227. package/dist/astro/routes/api/mcp.mjs +174 -72
  228. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  229. package/dist/astro/routes/api/media/_id_/confirm.mjs +14 -12
  230. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  231. package/dist/astro/routes/api/media/_id_/upload.mjs +7 -7
  232. package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
  233. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  234. package/dist/astro/routes/api/media/_id_.mjs +16 -15
  235. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  236. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  237. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  238. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  239. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  240. package/dist/astro/routes/api/media/upload-url.mjs +11 -10
  241. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  242. package/dist/astro/routes/api/media.mjs +19 -16
  243. package/dist/astro/routes/api/media.mjs.map +1 -1
  244. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +13 -13
  245. package/dist/astro/routes/api/menus/_name_/items.mjs +13 -13
  246. package/dist/astro/routes/api/menus/_name_/reorder.mjs +13 -13
  247. package/dist/astro/routes/api/menus/_name_/translations.mjs +13 -13
  248. package/dist/astro/routes/api/menus/_name_.mjs +13 -13
  249. package/dist/astro/routes/api/menus/index.mjs +13 -13
  250. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  251. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  252. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  253. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  254. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  255. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  256. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  257. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  258. package/dist/astro/routes/api/openapi.json.mjs +183 -6
  259. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  260. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  261. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
  262. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  263. package/dist/astro/routes/api/redirects/404s/index.mjs +14 -13
  264. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  265. package/dist/astro/routes/api/redirects/404s/summary.mjs +14 -13
  266. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  267. package/dist/astro/routes/api/redirects/_id_.mjs +16 -14
  268. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  269. package/dist/astro/routes/api/redirects/index.mjs +16 -14
  270. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  271. package/dist/astro/routes/api/relations/_id_/index.mjs +21 -20
  272. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  273. package/dist/astro/routes/api/relations/_id_/translations.mjs +16 -15
  274. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  275. package/dist/astro/routes/api/relations/index.mjs +21 -20
  276. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  277. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  278. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  279. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
  280. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  281. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
  282. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  283. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
  284. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  285. package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
  286. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
  287. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  288. package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
  289. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  290. package/dist/astro/routes/api/schema/collections/reorder.d.mts +8 -0
  291. package/dist/astro/routes/api/schema/collections/reorder.d.mts.map +1 -0
  292. package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -0
  293. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -0
  294. package/dist/astro/routes/api/schema/index.mjs +14 -11
  295. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  296. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
  297. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  298. package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
  299. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  300. package/dist/astro/routes/api/search/enable.d.mts +1 -0
  301. package/dist/astro/routes/api/search/enable.d.mts.map +1 -1
  302. package/dist/astro/routes/api/search/enable.mjs +19 -15
  303. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  304. package/dist/astro/routes/api/search/index.mjs +13 -13
  305. package/dist/astro/routes/api/search/rebuild.mjs +15 -15
  306. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  307. package/dist/astro/routes/api/search/stats.mjs +9 -9
  308. package/dist/astro/routes/api/search/suggest.mjs +13 -13
  309. package/dist/astro/routes/api/sections/_slug_.mjs +13 -12
  310. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  311. package/dist/astro/routes/api/sections/index.mjs +13 -12
  312. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  313. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -8
  314. package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
  315. package/dist/astro/routes/api/settings/backups/export.mjs +8 -8
  316. package/dist/astro/routes/api/settings/backups/index.mjs +9 -9
  317. package/dist/astro/routes/api/settings/email.mjs +6 -6
  318. package/dist/astro/routes/api/settings.mjs +18 -17
  319. package/dist/astro/routes/api/settings.mjs.map +1 -1
  320. package/dist/astro/routes/api/setup/admin-verify.mjs +15 -14
  321. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  322. package/dist/astro/routes/api/setup/admin.mjs +14 -13
  323. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  324. package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -32
  325. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  326. package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
  327. package/dist/astro/routes/api/setup/index.mjs +36 -34
  328. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  329. package/dist/astro/routes/api/setup/status.mjs +5 -5
  330. package/dist/astro/routes/api/snapshot.mjs +10 -10
  331. package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts +8 -0
  332. package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -0
  333. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +44 -0
  334. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -0
  335. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +19 -18
  336. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  337. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +19 -18
  338. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  339. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  340. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +21 -19
  341. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  342. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
  343. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
  344. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
  345. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
  346. package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
  347. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
  348. package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
  349. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
  350. package/dist/astro/routes/api/taxonomies/index.mjs +19 -18
  351. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  352. package/dist/astro/routes/api/themes/preview.mjs +8 -8
  353. package/dist/astro/routes/api/typegen.mjs +6 -6
  354. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  355. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  356. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  357. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
  358. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  359. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +13 -12
  360. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  361. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +13 -12
  362. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  363. package/dist/astro/routes/api/widget-areas/_name_.mjs +8 -7
  364. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  365. package/dist/astro/routes/api/widget-areas/index.mjs +13 -12
  366. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  367. package/dist/astro/routes/api/widget-components.mjs +4 -4
  368. package/dist/astro/routes/robots.txt.mjs +11 -10
  369. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  370. package/dist/astro/routes/sitemap-_collection_.xml.mjs +14 -14
  371. package/dist/astro/routes/sitemap.xml.mjs +12 -11
  372. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  373. package/dist/astro/types.d.mts +41 -17
  374. package/dist/astro/types.d.mts.map +1 -1
  375. package/dist/auth/providers/github.d.mts +1 -1
  376. package/dist/auth/providers/google.d.mts +1 -1
  377. package/dist/{authorize-R5iYYrpa.mjs → authorize-1iFJzPaZ.mjs} +2 -2
  378. package/dist/{authorize-R5iYYrpa.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
  379. package/dist/{backup-LpQ1V_sR.mjs → backup-LHjIAVNP.mjs} +5 -5
  380. package/dist/{backup-LpQ1V_sR.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
  381. package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
  382. package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
  383. package/dist/bundle-CBEXrBnb.mjs +663 -0
  384. package/dist/bundle-CBEXrBnb.mjs.map +1 -0
  385. package/dist/{byline-fields-T9ZLZMxJ.mjs → byline-fields-C5S8wygO.mjs} +2 -2
  386. package/dist/{byline-fields-T9ZLZMxJ.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
  387. package/dist/{byline-C5TAqs8N.mjs → byline-pEcoDps8.mjs} +9 -9
  388. package/dist/{byline-C5TAqs8N.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
  389. package/dist/{byline-registry-BCuOp4UF.mjs → byline-registry-DDun8sxh.mjs} +4 -4
  390. package/dist/{byline-registry-BCuOp4UF.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
  391. package/dist/{bylines-B8-WGdja.mjs → bylines-BWemdX2k.mjs} +8 -8
  392. package/dist/{bylines-B8-WGdja.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
  393. package/dist/{bylines-DGuqBLjV.mjs → bylines-DJfSfXCL.mjs} +5 -5
  394. package/dist/{bylines-DGuqBLjV.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
  395. package/dist/cache-CBJaDy_E.mjs +108 -0
  396. package/dist/cache-CBJaDy_E.mjs.map +1 -0
  397. package/dist/{challenge-store-BFzgFRog.mjs → challenge-store-CZyFVaad.mjs} +1 -1
  398. package/dist/{challenge-store-BFzgFRog.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
  399. package/dist/{chunks-D5dlPeRb.mjs → chunks-DfPax1I0.mjs} +2 -2
  400. package/dist/{chunks-D5dlPeRb.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
  401. package/dist/cli/index.mjs +193 -707
  402. package/dist/cli/index.mjs.map +1 -1
  403. package/dist/client/cf-access.d.mts +1 -1
  404. package/dist/client/index.d.mts +74 -4
  405. package/dist/client/index.d.mts.map +1 -1
  406. package/dist/client/index.mjs +25 -2
  407. package/dist/client/index.mjs.map +1 -1
  408. package/dist/collection-deletion-C8BugSGz.mjs +779 -0
  409. package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
  410. package/dist/{comment-DnTxxVHv.mjs → comment-Dxlep6cE.mjs} +3 -3
  411. package/dist/{comment-DnTxxVHv.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
  412. package/dist/{comment-reaction-BUlUz2bh.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
  413. package/dist/{comment-reaction-BUlUz2bh.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
  414. package/dist/{comments-QPKgK3nk.mjs → comments-CTOuhTSX.mjs} +3 -3
  415. package/dist/{comments-QPKgK3nk.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
  416. package/dist/{components-DZkL4Fx0.mjs → components-CQy3lh9x.mjs} +1 -1
  417. package/dist/{components-DZkL4Fx0.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
  418. package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
  419. package/dist/config-C3EBstHi.mjs.map +1 -0
  420. package/dist/{content-CpfKV9QE.mjs → content-BNfwh7xX.mjs} +667 -183
  421. package/dist/content-BNfwh7xX.mjs.map +1 -0
  422. package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
  423. package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
  424. package/dist/content-refresh-DNERP4-f.mjs +2560 -0
  425. package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
  426. package/dist/{context-kO4YZB03.mjs → context-DRgOvSnW.mjs} +28 -20
  427. package/dist/context-DRgOvSnW.mjs.map +1 -0
  428. package/dist/{cron-BlKIMD_e.mjs → cron-Y9aLtgrA.mjs} +1 -1
  429. package/dist/{cron-BlKIMD_e.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
  430. package/dist/{dashboard-DoBwPnON.mjs → dashboard-CB8J89rJ.mjs} +45 -11
  431. package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
  432. package/dist/db/index.d.mts +4 -4
  433. package/dist/db/index.mjs +3 -3
  434. package/dist/db/index.mjs.map +1 -1
  435. package/dist/db/libsql.d.mts +1 -1
  436. package/dist/db/postgres.d.mts +1 -1
  437. package/dist/db/sqlite.d.mts +1 -1
  438. package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
  439. package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
  440. package/dist/{default-C4v9KP56.mjs → default-DdDl3fxv.mjs} +1 -1
  441. package/dist/{default-C4v9KP56.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
  442. package/dist/deferred-tasks-BMsa9F64.d.mts +9 -0
  443. package/dist/deferred-tasks-BMsa9F64.d.mts.map +1 -0
  444. package/dist/{device-flow-DhUsyJU6.mjs → device-flow-CaioYGK3.mjs} +5 -5
  445. package/dist/{device-flow-DhUsyJU6.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
  446. package/dist/{dialect-helpers-DFwyWjTM.mjs → dialect-helpers-BSbfaO3F.mjs} +24 -16
  447. package/dist/dialect-helpers-BSbfaO3F.mjs.map +1 -0
  448. package/dist/dist-D_6CKR8z.mjs +810 -0
  449. package/dist/dist-D_6CKR8z.mjs.map +1 -0
  450. package/dist/{email-console-C-9Ng8DM.mjs → email-console-BHNjIwiB.mjs} +1 -1
  451. package/dist/{email-console-C-9Ng8DM.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
  452. package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
  453. package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
  454. package/dist/{error-DmmN74gW.mjs → error-DupyJZE2.mjs} +3 -3
  455. package/dist/{error-DmmN74gW.mjs.map → error-DupyJZE2.mjs.map} +1 -1
  456. package/dist/{errors-DtEXIQQV.mjs → errors-CLUPm7aD.mjs} +15 -2
  457. package/dist/errors-CLUPm7aD.mjs.map +1 -0
  458. package/dist/{escape-CdW31WsG.mjs → escape-C_aLKP3H.mjs} +1 -1
  459. package/dist/{escape-CdW31WsG.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
  460. package/dist/{field-defs-cache-QMVnzTH6.mjs → field-defs-cache-hg6QkjV9.mjs} +5 -5
  461. package/dist/{field-defs-cache-QMVnzTH6.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
  462. package/dist/{fts-manager-vncsZIJm.mjs → fts-manager-CZ-5dVwJ.mjs} +111 -61
  463. package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
  464. package/dist/{hash-Cb7U1b5M.mjs → hash-Cg0TXJPV.mjs} +1 -1
  465. package/dist/{hash-Cb7U1b5M.mjs.map → hash-Cg0TXJPV.mjs.map} +1 -1
  466. package/dist/{hreflang-CZbSx6Bg.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
  467. package/dist/{hreflang-CZbSx6Bg.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
  468. package/dist/{hreflang-Bg-zMpkr.mjs → hreflang-DNswoYsi.mjs} +7 -7
  469. package/dist/{hreflang-Bg-zMpkr.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
  470. package/dist/{import-PKhLeXvn.mjs → import-B41u8Rot.mjs} +7 -7
  471. package/dist/{import-PKhLeXvn.mjs.map → import-B41u8Rot.mjs.map} +1 -1
  472. package/dist/{index-j9rTh3cp.d.mts → index-C3Y-rsyN.d.mts} +286 -93
  473. package/dist/index-C3Y-rsyN.d.mts.map +1 -0
  474. package/dist/{index-BeNto_Vs.d.mts → index-Cof4-Fab.d.mts} +5 -10
  475. package/dist/index-Cof4-Fab.d.mts.map +1 -0
  476. package/dist/index.d.mts +20 -19
  477. package/dist/index.mjs +79 -71
  478. package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
  479. package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
  480. package/dist/{load-BwTdWE8B.mjs → load-DmvJrtxS.mjs} +2 -2
  481. package/dist/{load-BwTdWE8B.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
  482. package/dist/{loader-C1XOLV5b.mjs → loader-DLBqRiGP.mjs} +120 -64
  483. package/dist/loader-DLBqRiGP.mjs.map +1 -0
  484. package/dist/{manifest-schema-bCq54i7F.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
  485. package/dist/{manifest-schema-bCq54i7F.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
  486. package/dist/media/image-endpoint.mjs +1 -1
  487. package/dist/media/index.d.mts +1 -1
  488. package/dist/media/index.mjs +3 -2
  489. package/dist/media/index.mjs.map +1 -1
  490. package/dist/media/local-runtime.d.mts +12 -12
  491. package/dist/media/local-runtime.mjs +10 -9
  492. package/dist/media/local-runtime.mjs.map +1 -1
  493. package/dist/{media-DWIzoVFL.mjs → media-BWWWb8kg.mjs} +92 -3
  494. package/dist/media-BWWWb8kg.mjs.map +1 -0
  495. package/dist/{media-tQDZEdu7.mjs → media-CzhAn2--.mjs} +2 -2
  496. package/dist/{media-tQDZEdu7.mjs.map → media-CzhAn2--.mjs.map} +1 -1
  497. package/dist/{media-allowlist-B7TPD6aq.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
  498. package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
  499. package/dist/{media-upload-Bc9Nc5Zf.mjs → media-upload-9yas9zGH.mjs} +11 -9
  500. package/dist/{media-upload-Bc9Nc5Zf.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
  501. package/dist/{media-url-BCm5vBn6.mjs → media-url-BvoX88FA.mjs} +1 -1
  502. package/dist/{media-url-BCm5vBn6.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
  503. package/dist/{media-usage-DPJcNaAq.mjs → media-usage-De3qvwjc.mjs} +138 -61
  504. package/dist/media-usage-De3qvwjc.mjs.map +1 -0
  505. package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
  506. package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
  507. package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
  508. package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
  509. package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
  510. package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
  511. package/dist/{menus-C6qhqP4X.mjs → menus-CC-yn1F9.mjs} +554 -121
  512. package/dist/menus-CC-yn1F9.mjs.map +1 -0
  513. package/dist/{menus-BxPLFfIc.mjs → menus-fxvEDYE6.mjs} +5 -5
  514. package/dist/{menus-BxPLFfIc.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
  515. package/dist/{mode-fiXRMfeA.mjs → mode-Bpe-tNNy.mjs} +1 -1
  516. package/dist/{mode-fiXRMfeA.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
  517. package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
  518. package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
  519. package/dist/{oauth-authorization-C9tFinly.mjs → oauth-authorization-BXO1MKej.mjs} +5 -5
  520. package/dist/{oauth-authorization-C9tFinly.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
  521. package/dist/{oauth-clients-BwTxE1aE.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
  522. package/dist/{oauth-clients-BwTxE1aE.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
  523. package/dist/{oauth-state-store-DaRUwPWO.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
  524. package/dist/{oauth-state-store-DaRUwPWO.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
  525. package/dist/{oauth-user-lookup-CzuzOSlF.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
  526. package/dist/{oauth-user-lookup-CzuzOSlF.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
  527. package/dist/object-cache/memory.d.mts +1 -1
  528. package/dist/{object-cache-Bok5j2ae.mjs → object-cache-71tPfV4A.mjs} +106 -5
  529. package/dist/object-cache-71tPfV4A.mjs.map +1 -0
  530. package/dist/{options-BlmBHTvX.mjs → options-CBXIHUoT.mjs} +1 -1
  531. package/dist/{options-BlmBHTvX.mjs.map → options-CBXIHUoT.mjs.map} +1 -1
  532. package/dist/{options-aeIs8WFu.d.mts → options-DRe1pIBL.d.mts} +4 -3
  533. package/dist/options-DRe1pIBL.d.mts.map +1 -0
  534. package/dist/page/index.d.mts +2 -2
  535. package/dist/{parse-BL49yb9D.mjs → parse-DFbuc-Fh.mjs} +2 -2
  536. package/dist/{parse-BL49yb9D.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
  537. package/dist/{passkey-config-KMhgFjdQ.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
  538. package/dist/{passkey-config-KMhgFjdQ.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
  539. package/dist/{placeholder-Dxo5Zt58.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
  540. package/dist/{placeholder-Dxo5Zt58.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
  541. package/dist/plugin-types.d.mts +8 -1
  542. package/dist/plugin-types.d.mts.map +1 -1
  543. package/dist/plugin-utils.d.mts +10 -10
  544. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  545. package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
  546. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  547. package/dist/{portable-text-CNahFfj6.mjs → portable-text-DhKbDAGr.mjs} +1 -1
  548. package/dist/{portable-text-CNahFfj6.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
  549. package/dist/{preview-D4Jnbfx7.mjs → preview-PW-nOnq4.mjs} +2 -2
  550. package/dist/{preview-D4Jnbfx7.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
  551. package/dist/{public-url-DSGTnJFw.mjs → public-url-CWoFhQT8.mjs} +14 -3
  552. package/dist/public-url-CWoFhQT8.mjs.map +1 -0
  553. package/dist/{query-DCiXI7OZ.mjs → query-BgjfiLUQ.mjs} +38 -77
  554. package/dist/query-BgjfiLUQ.mjs.map +1 -0
  555. package/dist/{query-1xOcEy18.mjs → query-vgZNwIhh.mjs} +4 -4
  556. package/dist/{query-1xOcEy18.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
  557. package/dist/{rate-limit-Vkp9gQ1Y.mjs → rate-limit-L5anykgo.mjs} +2 -2
  558. package/dist/{rate-limit-Vkp9gQ1Y.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
  559. package/dist/{redirect-BJ6d1yHh.mjs → redirect-COsS6SWJ.mjs} +3 -3
  560. package/dist/{redirect-BJ6d1yHh.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
  561. package/dist/{redirect-CDgfv9vh.mjs → redirect-KBNE3UEs.mjs} +1 -1
  562. package/dist/{redirect-CDgfv9vh.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
  563. package/dist/{redirects-Cwkq2H-G.mjs → redirects-CufRQt0Q.mjs} +5 -5
  564. package/dist/{redirects-Cwkq2H-G.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
  565. package/dist/{redirects-BOn2DIUs.mjs → redirects-cV9g7V-0.mjs} +149 -16
  566. package/dist/redirects-cV9g7V-0.mjs.map +1 -0
  567. package/dist/registry-C581V4TP.mjs +1275 -0
  568. package/dist/registry-C581V4TP.mjs.map +1 -0
  569. package/dist/{relation-DJ5DZh7B.mjs → relation-DtmAQH7s.mjs} +3 -3
  570. package/dist/{relation-DJ5DZh7B.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
  571. package/dist/{relations-C4duJnwI.mjs → relations-BBApI4e2.mjs} +3 -3
  572. package/dist/{relations-C4duJnwI.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
  573. package/dist/{relations-DpTjftCl.mjs → relations-DICSyfsh.mjs} +6 -6
  574. package/dist/{relations-DpTjftCl.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
  575. package/dist/{relations-DHFQGX0h.d.mts → relations-DItI3MN9.d.mts} +401 -40
  576. package/dist/relations-DItI3MN9.d.mts.map +1 -0
  577. package/dist/request-context.d.mts +3 -0
  578. package/dist/request-context.d.mts.map +1 -1
  579. package/dist/request-context.mjs.map +1 -1
  580. package/dist/{request-meta-DzXYYI-n.mjs → request-meta-DgZJh3zG.mjs} +2 -2
  581. package/dist/{request-meta-DzXYYI-n.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
  582. package/dist/{resolve-BUvFE0Lr.mjs → resolve-DOhVEvty.mjs} +2 -2
  583. package/dist/{resolve-BUvFE0Lr.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
  584. package/dist/{runner-BsI18UgP.mjs → runner-BiDvq5dv.mjs} +1404 -235
  585. package/dist/runner-BiDvq5dv.mjs.map +1 -0
  586. package/dist/{runner-CHdkERbt.d.mts → runner-CgQbELYC.d.mts} +2 -2
  587. package/dist/{runner-CHdkERbt.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
  588. package/dist/runtime.d.mts +11 -11
  589. package/dist/runtime.mjs +5 -4
  590. package/dist/runtime.mjs.map +1 -1
  591. package/dist/{schema-eqZhkR0S.mjs → schema-BFyTg38T.mjs} +3 -3
  592. package/dist/{schema-eqZhkR0S.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
  593. package/dist/schema-CeQSNwB3.mjs +390 -0
  594. package/dist/schema-CeQSNwB3.mjs.map +1 -0
  595. package/dist/{scopes-Be2d8bSb.mjs → scopes-BVeuhGBE.mjs} +3 -3
  596. package/dist/{scopes-Be2d8bSb.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
  597. package/dist/{search-CeO678cp.mjs → search-BpuPKZb0.mjs} +26 -16
  598. package/dist/search-BpuPKZb0.mjs.map +1 -0
  599. package/dist/{secrets-870d-7yA.mjs → secrets-dtYs2hDo.mjs} +5 -5
  600. package/dist/{secrets-870d-7yA.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
  601. package/dist/{sections-DSuEfirn.mjs → sections-COctk6hf.mjs} +3 -3
  602. package/dist/{sections-DSuEfirn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
  603. package/dist/seed/index.d.mts +2 -2
  604. package/dist/seed/index.mjs +28 -26
  605. package/dist/seo/index.d.mts +3 -3
  606. package/dist/seo/index.mjs +20 -19
  607. package/dist/seo/index.mjs.map +1 -1
  608. package/dist/{seo-C3GDfT0V.mjs → seo-AFpVvTqr.mjs} +6 -4
  609. package/dist/seo-AFpVvTqr.mjs.map +1 -0
  610. package/dist/{seo-BAPKiED8.mjs → seo-CvDxfvFb.mjs} +3 -3
  611. package/dist/{seo-BAPKiED8.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
  612. package/dist/{service-DyFT-k-R.mjs → service-xMiMSTsR.mjs} +4 -4
  613. package/dist/{service-DyFT-k-R.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
  614. package/dist/{session-user-BrK2zz7S.mjs → session-user-BzVemLkw.mjs} +2 -2
  615. package/dist/{session-user-BrK2zz7S.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
  616. package/dist/{settings-DgKouY2S.mjs → settings-B372MV0p.mjs} +7 -7
  617. package/dist/{settings-DgKouY2S.mjs.map → settings-B372MV0p.mjs.map} +1 -1
  618. package/dist/{settings-CREKyj8I.mjs → settings-BE9aqn_9.mjs} +3 -3
  619. package/dist/{settings-CREKyj8I.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
  620. package/dist/{setup-complete-L7bMK2VI.mjs → setup-complete-DF7TAfD5.mjs} +2 -2
  621. package/dist/{setup-complete-L7bMK2VI.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
  622. package/dist/{setup-nonce-Ct5tjWcq.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
  623. package/dist/{setup-nonce-Ct5tjWcq.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
  624. package/dist/{single-flight-cache-C2exrGAi.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
  625. package/dist/{single-flight-cache-C2exrGAi.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
  626. package/dist/site-url-Bowsi9T-.mjs +16 -0
  627. package/dist/site-url-Bowsi9T-.mjs.map +1 -0
  628. package/dist/slugify-B994zkXC.mjs +17 -0
  629. package/dist/slugify-B994zkXC.mjs.map +1 -0
  630. package/dist/{snapshot-BdpUJKD-.mjs → snapshot-B5h6aPMh.mjs} +2 -2
  631. package/dist/{snapshot-BdpUJKD-.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
  632. package/dist/{ssrf-CviKqWmq.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
  633. package/dist/{ssrf-CviKqWmq.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
  634. package/dist/{state-xxv6ZTMv.mjs → state-DYwPue40.mjs} +1 -1
  635. package/dist/{state-xxv6ZTMv.mjs.map → state-DYwPue40.mjs.map} +1 -1
  636. package/dist/{status-COfycGIU.mjs → status-CWVYpbD8.mjs} +1 -1
  637. package/dist/{status-COfycGIU.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
  638. package/dist/storage/local.d.mts +1 -1
  639. package/dist/storage/local.mjs +1 -1
  640. package/dist/storage/s3.d.mts +1 -1
  641. package/dist/storage/s3.mjs +1 -1
  642. package/dist/{taxonomies-B61CRSha.mjs → taxonomies-B0o9ExNV.mjs} +85 -75
  643. package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
  644. package/dist/{taxonomies-Dbdki3Un.mjs → taxonomies-Dk3MAklL.mjs} +360 -56
  645. package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
  646. package/dist/taxonomy-CPeM8d2o.mjs +484 -0
  647. package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
  648. package/dist/{tokens-DVltpO2D.mjs → tokens-C39klX8R.mjs} +2 -2
  649. package/dist/{tokens-DVltpO2D.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
  650. package/dist/{transaction-D0FOsb3X.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
  651. package/dist/{transaction-D0FOsb3X.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
  652. package/dist/{transport-BWQo0Foy.mjs → transport--CfOflrp.mjs} +1 -1
  653. package/dist/{transport-BWQo0Foy.mjs.map → transport--CfOflrp.mjs.map} +1 -1
  654. package/dist/{transport-DokJOScS.d.mts → transport-B_rfeU-a.d.mts} +1 -1
  655. package/dist/{transport-DokJOScS.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
  656. package/dist/{trusted-proxy-CwjQj0YG.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
  657. package/dist/{trusted-proxy-CwjQj0YG.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
  658. package/dist/{types-o7xo7VgH.mjs → types-75KIqzL9.mjs} +20 -3
  659. package/dist/types-75KIqzL9.mjs.map +1 -0
  660. package/dist/{types-C9P3ysxq.d.mts → types-BPuloofb.d.mts} +1 -1
  661. package/dist/{types-C9P3ysxq.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
  662. package/dist/{types-BD40g7zz.mjs → types-BW1syrlw.mjs} +1 -1
  663. package/dist/{types-BD40g7zz.mjs.map → types-BW1syrlw.mjs.map} +1 -1
  664. package/dist/{types-jISEalTX.d.mts → types-C3_AbkDP.d.mts} +53 -10
  665. package/dist/types-C3_AbkDP.d.mts.map +1 -0
  666. package/dist/{types-84Bque3-.d.mts → types-CAGdePf3.d.mts} +133 -1
  667. package/dist/types-CAGdePf3.d.mts.map +1 -0
  668. package/dist/{types-CcSZCcF1.d.mts → types-CFUfyt1s.d.mts} +1 -1
  669. package/dist/{types-CcSZCcF1.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
  670. package/dist/{types-DQLZ58GV.d.mts → types-CPx2lIGo.d.mts} +1 -1
  671. package/dist/{types-DQLZ58GV.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
  672. package/dist/{types-XrQQ-Aex.mjs → types-CQPWhPxE.mjs} +15 -11
  673. package/dist/types-CQPWhPxE.mjs.map +1 -0
  674. package/dist/{types-BQY7VYw6.d.mts → types-Cs-TFLNf.d.mts} +28 -8
  675. package/dist/types-Cs-TFLNf.d.mts.map +1 -0
  676. package/dist/types-CtzytVXn.mjs +18 -0
  677. package/dist/types-CtzytVXn.mjs.map +1 -0
  678. package/dist/{types-DqZVPILm.d.mts → types-D4_1Ga4L.d.mts} +44 -5
  679. package/dist/types-D4_1Ga4L.d.mts.map +1 -0
  680. package/dist/{types-Dr2GLlwu.d.mts → types-DefV3OiO.d.mts} +1 -1
  681. package/dist/{types-Dr2GLlwu.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
  682. package/dist/{types-DsAMyD_o.d.mts → types-X0MThr7x.d.mts} +1 -1
  683. package/dist/{types-DsAMyD_o.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
  684. package/dist/{upload-attempts-C5yd6Gae.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
  685. package/dist/{upload-attempts-C5yd6Gae.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
  686. package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
  687. package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
  688. package/dist/{user-BAumEmpA.mjs → user-sHgJ62SP.mjs} +3 -3
  689. package/dist/{user-BAumEmpA.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
  690. package/dist/{utils-BbUgmeZG.mjs → utils-D4vD_Y98.mjs} +3 -3
  691. package/dist/{utils-BbUgmeZG.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
  692. package/dist/{validate-Bs_wT2ul.mjs → validate-BkqIG_Uj.mjs} +24 -5
  693. package/dist/validate-BkqIG_Uj.mjs.map +1 -0
  694. package/dist/{validate-VPnKoIzW.mjs → validate-BpwoDOgt.mjs} +25 -2
  695. package/dist/validate-BpwoDOgt.mjs.map +1 -0
  696. package/dist/{validate-CdYcY-Ln.d.mts → validate-DeGtBN84.d.mts} +25 -7
  697. package/dist/validate-DeGtBN84.d.mts.map +1 -0
  698. package/dist/{validation-BngR0nit.mjs → validation-CiJb_X5o.mjs} +14 -13
  699. package/dist/{validation-BngR0nit.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
  700. package/dist/version-x0NjTScG.mjs +7 -0
  701. package/dist/{version-Bdfb34kk.mjs.map → version-x0NjTScG.mjs.map} +1 -1
  702. package/dist/{widgets-L-VndTKD.mjs → widgets-Cwzvww0W.mjs} +3 -3
  703. package/dist/{widgets-L-VndTKD.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
  704. package/dist/{wxr-taxonomies-DueaLFd9.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
  705. package/dist/{wxr-taxonomies-DueaLFd9.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
  706. package/dist/{wxr-taxonomies-BexjtNid.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
  707. package/dist/{wxr-taxonomies-BexjtNid.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
  708. package/dist/{zod-generator-DMzfga3g.mjs → zod-generator-D11MhxG4.mjs} +53 -6
  709. package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
  710. package/package.json +18 -7
  711. package/src/after.ts +6 -1
  712. package/src/api/errors.ts +13 -0
  713. package/src/api/handlers/content.ts +172 -33
  714. package/src/api/handlers/dashboard.ts +12 -2
  715. package/src/api/handlers/index.ts +16 -0
  716. package/src/api/handlers/manifest.ts +2 -0
  717. package/src/api/handlers/marketplace.ts +14 -0
  718. package/src/api/handlers/media-allowlist.ts +13 -2
  719. package/src/api/handlers/media-usage-work.ts +188 -0
  720. package/src/api/handlers/media-usage.ts +5 -3
  721. package/src/api/handlers/registry.ts +14 -0
  722. package/src/api/handlers/revision.ts +19 -5
  723. package/src/api/handlers/schema.ts +46 -3
  724. package/src/api/handlers/seo.ts +5 -2
  725. package/src/api/handlers/snapshot.ts +1 -1
  726. package/src/api/handlers/taxonomies.ts +446 -83
  727. package/src/api/media-usage-write-fence.ts +64 -0
  728. package/src/api/openapi/document.ts +247 -0
  729. package/src/api/public-url.ts +14 -2
  730. package/src/api/schemas/common.ts +3 -1
  731. package/src/api/schemas/content.ts +122 -1
  732. package/src/api/schemas/media-usage.ts +133 -0
  733. package/src/api/schemas/schema.ts +91 -3
  734. package/src/api/schemas/search.ts +2 -0
  735. package/src/api/schemas/taxonomies.ts +58 -8
  736. package/src/api/site-url.ts +16 -6
  737. package/src/api/types.ts +1 -0
  738. package/src/astro/integration/index.ts +55 -42
  739. package/src/astro/integration/routes.ts +48 -4
  740. package/src/astro/integration/runtime.ts +28 -0
  741. package/src/astro/integration/virtual-modules.ts +27 -1
  742. package/src/astro/integration/vite-config.ts +16 -1
  743. package/src/astro/middleware/media-usage-write-fence.ts +30 -0
  744. package/src/astro/middleware/redirect.ts +14 -12
  745. package/src/astro/middleware/scoped-db.ts +35 -4
  746. package/src/astro/middleware.ts +131 -30
  747. package/src/astro/routes/api/admin/comments/[id]/status.ts +1 -1
  748. package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
  749. package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
  750. package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
  751. package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
  752. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
  753. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
  754. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
  755. package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
  756. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
  757. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
  758. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  759. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  760. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +2 -2
  761. package/src/astro/routes/api/auth/invite/index.ts +2 -2
  762. package/src/astro/routes/api/auth/magic-link/send.ts +2 -2
  763. package/src/astro/routes/api/auth/signup/request.ts +2 -2
  764. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +1 -1
  765. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
  766. package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
  767. package/src/astro/routes/api/mcp.ts +1 -0
  768. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
  769. package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
  770. package/src/astro/routes/api/schema/collections/reorder.ts +31 -0
  771. package/src/astro/routes/api/schema/index.ts +2 -0
  772. package/src/astro/routes/api/search/enable.ts +5 -1
  773. package/src/astro/routes/api/search/rebuild.ts +6 -1
  774. package/src/astro/routes/api/taxonomies/[name]/reorder.ts +41 -0
  775. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
  776. package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
  777. package/src/astro/routes/api/taxonomies/[name].ts +102 -0
  778. package/src/astro/types.ts +31 -4
  779. package/src/cleanup.ts +25 -26
  780. package/src/cli/commands/dev.ts +13 -3
  781. package/src/cli/commands/doctor.ts +193 -6
  782. package/src/cli/commands/export-seed.ts +6 -1
  783. package/src/cli/index.ts +1 -1
  784. package/src/client/index.ts +119 -0
  785. package/src/components/Block.astro +12 -0
  786. package/src/components/CommentForm.astro +1 -1
  787. package/src/components/Comments.astro +1 -1
  788. package/src/components/InlinePortableTextEditor.tsx +260 -32
  789. package/src/components/OrderedList.astro +14 -0
  790. package/src/components/PortableText.astro +26 -4
  791. package/src/components/index.ts +12 -1
  792. package/src/components/ordered-list.ts +549 -0
  793. package/src/content/converters/mark-safety.ts +120 -0
  794. package/src/content/converters/numbered-list.ts +148 -0
  795. package/src/content/converters/portable-text-to-prosemirror.ts +84 -25
  796. package/src/content/converters/prosemirror-to-portable-text.ts +35 -14
  797. package/src/content/converters/types.ts +2 -0
  798. package/src/content/portable-text-lists.ts +205 -0
  799. package/src/content-list-query.ts +29 -0
  800. package/src/database/dialect-helpers.ts +3 -22
  801. package/src/database/migrations/055_content_translation_group_locale_index.ts +67 -0
  802. package/src/database/migrations/056_taxonomy_term_sort_order.ts +189 -0
  803. package/src/database/migrations/057_collection_hidden.ts +24 -0
  804. package/src/database/migrations/058_collection_sort_order.ts +20 -0
  805. package/src/database/migrations/059_revision_prune_queue.ts +36 -0
  806. package/src/database/migrations/060_collection_admin_config.ts +16 -0
  807. package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
  808. package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
  809. package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
  810. package/src/database/migrations/064_fts_plain_text.ts +253 -0
  811. package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
  812. package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
  813. package/src/database/migrations/067_indexed_content_fields.ts +39 -0
  814. package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
  815. package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
  816. package/src/database/migrations/070_collection_routable.ts +18 -0
  817. package/src/database/migrations/runner.ts +32 -0
  818. package/src/database/repositories/content.ts +1051 -273
  819. package/src/database/repositories/media-usage-work.ts +848 -0
  820. package/src/database/repositories/media-usage.ts +1502 -138
  821. package/src/database/repositories/revision.ts +98 -22
  822. package/src/database/repositories/taxonomy.ts +429 -103
  823. package/src/database/repositories/types.ts +58 -2
  824. package/src/database/types.ts +147 -17
  825. package/src/database/validate.ts +39 -0
  826. package/src/db/adapters.ts +36 -0
  827. package/src/db/index.ts +3 -0
  828. package/src/deferred-tasks.ts +71 -0
  829. package/src/emdash-runtime.ts +493 -110
  830. package/src/fields/file.ts +7 -4
  831. package/src/fields/image.ts +8 -1
  832. package/src/fields/types.ts +18 -5
  833. package/src/i18n/config.ts +32 -0
  834. package/src/i18n/repair-locale-casing.ts +0 -14
  835. package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
  836. package/src/import/wxr-taxonomies.ts +7 -47
  837. package/src/index.ts +27 -2
  838. package/src/loader.ts +165 -89
  839. package/src/mcp/server.ts +242 -34
  840. package/src/media/usage/activation.ts +626 -0
  841. package/src/media/usage/capture-triggers.ts +543 -0
  842. package/src/media/usage/cleanup.ts +305 -0
  843. package/src/media/usage/collection-deletion-processor.ts +356 -0
  844. package/src/media/usage/collection-deletion.ts +836 -0
  845. package/src/media/usage/content-fields.ts +40 -3
  846. package/src/media/usage/content-refresh.ts +356 -47
  847. package/src/media/usage/content-repair.ts +180 -61
  848. package/src/media/usage/content-snapshots.ts +85 -105
  849. package/src/media/usage/gc.ts +50 -0
  850. package/src/media/usage/projection-fingerprint.ts +129 -0
  851. package/src/media/usage/reconciliation-processor.ts +372 -0
  852. package/src/media/usage/reconciliation.ts +1117 -0
  853. package/src/media/usage/source-key.ts +4 -0
  854. package/src/media/usage/types.ts +2 -0
  855. package/src/media/usage/work-processor.ts +248 -0
  856. package/src/menus/index.ts +246 -141
  857. package/src/object-cache/index.ts +160 -3
  858. package/src/plugin-types.ts +8 -0
  859. package/src/plugins/adapt-sandbox-entry.ts +24 -5
  860. package/src/plugins/context.ts +24 -7
  861. package/src/plugins/index.ts +9 -0
  862. package/src/plugins/routes.ts +77 -1
  863. package/src/plugins/sandbox/index.ts +10 -1
  864. package/src/plugins/sandbox/types.ts +78 -1
  865. package/src/plugins/scheduler/node.ts +13 -1
  866. package/src/plugins/scheduler/types.ts +2 -0
  867. package/src/plugins/storage-indexes.ts +50 -11
  868. package/src/plugins/types.ts +39 -2
  869. package/src/query.ts +50 -79
  870. package/src/redirects/cache.ts +79 -19
  871. package/src/request-context.ts +3 -0
  872. package/src/scheduled-publish.ts +6 -1
  873. package/src/scheduler-health.ts +46 -0
  874. package/src/schema/registry.ts +863 -249
  875. package/src/schema/types.ts +74 -8
  876. package/src/schema/url-pattern.ts +33 -0
  877. package/src/schema/zod-generator.ts +77 -5
  878. package/src/search/fts-manager.ts +167 -86
  879. package/src/search/index.ts +2 -0
  880. package/src/search/query.ts +32 -11
  881. package/src/search/types.ts +8 -0
  882. package/src/seed/apply.ts +89 -16
  883. package/src/seed/types.ts +21 -3
  884. package/src/seed/validate.ts +52 -2
  885. package/src/taxonomies/index.ts +88 -82
  886. package/src/taxonomies/term-counts.ts +23 -12
  887. package/src/ui-comments.ts +19 -0
  888. package/src/ui.ts +11 -3
  889. package/src/utils/slugify.ts +1 -29
  890. package/src/virtual-modules.d.ts +24 -0
  891. package/dist/adapters-C0ONzkUP.d.mts.map +0 -1
  892. package/dist/after-B1IIdH3Y.mjs +0 -29
  893. package/dist/after-B1IIdH3Y.mjs.map +0 -1
  894. package/dist/api-DgPbMlng.mjs.map +0 -1
  895. package/dist/apply-1_6ra7NP.mjs.map +0 -1
  896. package/dist/cache-B26cufFd.mjs +0 -65
  897. package/dist/cache-B26cufFd.mjs.map +0 -1
  898. package/dist/config-DyF3OKg_.mjs.map +0 -1
  899. package/dist/content-CpfKV9QE.mjs.map +0 -1
  900. package/dist/content-refresh-DgqUBeTv.mjs +0 -1629
  901. package/dist/content-refresh-DgqUBeTv.mjs.map +0 -1
  902. package/dist/context-kO4YZB03.mjs.map +0 -1
  903. package/dist/dashboard-DoBwPnON.mjs.map +0 -1
  904. package/dist/dialect-helpers-DFwyWjTM.mjs.map +0 -1
  905. package/dist/errors-DtEXIQQV.mjs.map +0 -1
  906. package/dist/fts-manager-vncsZIJm.mjs.map +0 -1
  907. package/dist/index-BeNto_Vs.d.mts.map +0 -1
  908. package/dist/index-j9rTh3cp.d.mts.map +0 -1
  909. package/dist/loader-C1XOLV5b.mjs.map +0 -1
  910. package/dist/media-DWIzoVFL.mjs.map +0 -1
  911. package/dist/media-allowlist-B7TPD6aq.mjs.map +0 -1
  912. package/dist/media-usage-DPJcNaAq.mjs.map +0 -1
  913. package/dist/menus-C6qhqP4X.mjs.map +0 -1
  914. package/dist/object-cache-Bok5j2ae.mjs.map +0 -1
  915. package/dist/options-aeIs8WFu.d.mts.map +0 -1
  916. package/dist/public-url-DSGTnJFw.mjs.map +0 -1
  917. package/dist/query-DCiXI7OZ.mjs.map +0 -1
  918. package/dist/redirects-BOn2DIUs.mjs.map +0 -1
  919. package/dist/registry-BP1JK2xh.mjs +0 -795
  920. package/dist/registry-BP1JK2xh.mjs.map +0 -1
  921. package/dist/relations-DHFQGX0h.d.mts.map +0 -1
  922. package/dist/runner-BsI18UgP.mjs.map +0 -1
  923. package/dist/search-CeO678cp.mjs.map +0 -1
  924. package/dist/seo-C3GDfT0V.mjs.map +0 -1
  925. package/dist/site-url-8N3kSyVZ.mjs +0 -13
  926. package/dist/site-url-8N3kSyVZ.mjs.map +0 -1
  927. package/dist/slugify-C_tqlU4G.mjs +0 -30
  928. package/dist/slugify-C_tqlU4G.mjs.map +0 -1
  929. package/dist/taxonomies-B61CRSha.mjs.map +0 -1
  930. package/dist/taxonomies-Dbdki3Un.mjs.map +0 -1
  931. package/dist/taxonomy-DvwWAPvA.mjs +0 -304
  932. package/dist/taxonomy-DvwWAPvA.mjs.map +0 -1
  933. package/dist/types-84Bque3-.d.mts.map +0 -1
  934. package/dist/types-BQY7VYw6.d.mts.map +0 -1
  935. package/dist/types-DqZVPILm.d.mts.map +0 -1
  936. package/dist/types-XrQQ-Aex.mjs.map +0 -1
  937. package/dist/types-jISEalTX.d.mts.map +0 -1
  938. package/dist/types-o7xo7VgH.mjs.map +0 -1
  939. package/dist/validate-Bs_wT2ul.mjs.map +0 -1
  940. package/dist/validate-CdYcY-Ln.d.mts.map +0 -1
  941. package/dist/validate-VPnKoIzW.mjs.map +0 -1
  942. package/dist/version-Bdfb34kk.mjs +0 -7
  943. package/dist/zod-generator-DMzfga3g.mjs.map +0 -1
  944. /package/dist/{api-tokens-CEsW_jCg.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
  945. /package/dist/{ssrf-BB__xfNM.mjs → ssrf-DpXgKsna.mjs} +0 -0
  946. /package/dist/{types-CK0LxPUQ.mjs → types-CP1CflWf.mjs} +0 -0
@@ -1,93 +1,106 @@
1
- import { a as runMigrations, n as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-BsI18UgP.mjs";
2
- import { t as validateIdentifier } from "../validate-VPnKoIzW.mjs";
3
- import { c as isSqlite, l as listTablesLike } from "../dialect-helpers-DFwyWjTM.mjs";
4
- import { a as setI18nConfig, n as getI18nConfig } from "../config-DyF3OKg_.mjs";
5
- import { E as AuditRepository, _ as createHookPipeline, h as EmailPipeline, m as buildRouteMeta, p as PluginRouteRegistry, t as getMenu, v as resolveExclusiveHooks, y as definePlugin } from "../menus-C6qhqP4X.mjs";
6
- import { t as after } from "../after-B1IIdH3Y.mjs";
1
+ import { a as runMigrations, n as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-BiDvq5dv.mjs";
2
+ import { t as validateIdentifier } from "../validate-BpwoDOgt.mjs";
3
+ import { c as isSqlite, p as tableExists, s as isPostgres, u as listTablesLike } from "../dialect-helpers-BSbfaO3F.mjs";
4
+ import { r as getI18nConfig, s as setI18nConfig } from "../config-C3EBstHi.mjs";
5
+ import { C as createHookPipeline, M as AuditRepository, T as definePlugin, _ as PluginRouteRegistry, b as toRouteCallerInfo, c as getSandboxRouteErrorDetails, t as getMenu, v as buildRouteMeta, w as resolveExclusiveHooks, x as EmailPipeline, y as parseRouteInput } from "../menus-CC-yn1F9.mjs";
6
+ import { n as createDeferredTaskTracker, t as after } from "../after-CBU2vkoO.mjs";
7
7
  import { createRequestMetrics, getRequestContext, runWithContext } from "../request-context.mjs";
8
- import "../object-cache-Bok5j2ae.mjs";
9
- import { r as RevisionRepository, t as ContentRepository } from "../content-CpfKV9QE.mjs";
10
- import { n as isMissingTableError } from "../db-errors-CcWLaRiR.mjs";
11
- import "../base64-B-PsqheR.mjs";
12
- import "../types-XrQQ-Aex.mjs";
13
- import { t as MediaRepository } from "../media-tQDZEdu7.mjs";
14
- import "../user-BAumEmpA.mjs";
15
- import "../taxonomy-DvwWAPvA.mjs";
16
- import "../comment-DnTxxVHv.mjs";
17
- import { t as OptionsRepository } from "../options-BlmBHTvX.mjs";
18
- import { r as createSiteInfo } from "../context-kO4YZB03.mjs";
19
- import "../menus-BxPLFfIc.mjs";
20
- import "../redirect-BJ6d1yHh.mjs";
8
+ import { o as getLastContentWriteAt } from "../object-cache-71tPfV4A.mjs";
9
+ import { i as MAX_COLLECTION_LIST_COLUMNS } from "../types-75KIqzL9.mjs";
10
+ import { i as RevisionRepository, t as ContentRepository } from "../content-BNfwh7xX.mjs";
11
+ import { n as isMissingTableError } from "../db-errors-Cl-sKYTm.mjs";
12
+ import "../slugify-B994zkXC.mjs";
13
+ import "../base64-CvLhUKQv.mjs";
14
+ import { n as ContentMutationConflictError } from "../types-CQPWhPxE.mjs";
15
+ import { t as MediaRepository } from "../media-CzhAn2--.mjs";
16
+ import "../user-sHgJ62SP.mjs";
17
+ import { t as withTransaction } from "../transaction-NQj4VJ7Z.mjs";
18
+ import "../taxonomy-CPeM8d2o.mjs";
19
+ import "../comment-Dxlep6cE.mjs";
20
+ import { t as OptionsRepository } from "../options-CBXIHUoT.mjs";
21
+ import { r as createSiteInfo } from "../context-DRgOvSnW.mjs";
22
+ import "../menus-fxvEDYE6.mjs";
23
+ import "../redirect-COsS6SWJ.mjs";
21
24
  import { i as setRequestCacheEntry, r as requestCached } from "../request-cache-BSUptuJR.mjs";
22
- import "../byline-registry-BCuOp4UF.mjs";
23
- import { n as initWithLock, t as createInitLock } from "../init-lock-DJkX6Hto.mjs";
24
- import "../field-defs-cache-QMVnzTH6.mjs";
25
- import "../byline-C5TAqs8N.mjs";
26
- import "../relation-DJ5DZh7B.mjs";
27
- import { n as normalizeMediaValue } from "../normalize-C-SHXmra.mjs";
28
- import "../enrich-CFJJgxs_.mjs";
29
- import "../seo-BAPKiED8.mjs";
30
- import { t as FTSManager } from "../fts-manager-vncsZIJm.mjs";
31
- import "../resolve-BUvFE0Lr.mjs";
25
+ import "../byline-registry-DDun8sxh.mjs";
26
+ import { n as initWithLock, t as createInitLock } from "../init-lock-6b309ZrF.mjs";
27
+ import "../field-defs-cache-hg6QkjV9.mjs";
28
+ import "../byline-pEcoDps8.mjs";
29
+ import "../relation-DtmAQH7s.mjs";
30
+ import { n as normalizeMediaValue } from "../normalize-Bc-s_r-a.mjs";
31
+ import "../dist-D_6CKR8z.mjs";
32
+ import "../enrich-_ySMEvEE.mjs";
33
+ import "../seo-CvDxfvFb.mjs";
34
+ import { r as singleFlightCached, t as createSingleFlightCache } from "../single-flight-cache-B0eLKS6K.mjs";
35
+ import "../cache-CBJaDy_E.mjs";
36
+ import { t as FTSManager } from "../fts-manager-CZ-5dVwJ.mjs";
37
+ import "../resolve-DOhVEvty.mjs";
32
38
  import { createRecorder, flushRecorder, isInstrumentationEnabled, kyselyLogOption } from "../database/instrumentation.mjs";
33
- import { a as getDb } from "../loader-C1XOLV5b.mjs";
34
- import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-B61CRSha.mjs";
35
- import { $ as handleContentDiscardDraft, B as handleMediaGet, G as handleRevisionRestore, H as handleMediaUpdate, J as handleContentCompare, Q as handleContentDelete, R as handleMediaCreate, U as handleRevisionGet, V as handleMediaList, W as handleRevisionList, X as handleContentCountTrashed, Y as handleContentCountScheduled, Z as handleContentCreate, _ as loadBundleFromR2, at as handleContentPermanentDelete, ct as handleContentSchedule, dt as handleContentUnschedule, et as handleContentDuplicate, ft as handleContentUpdate, it as handleContentListTrashed, lt as handleContentTranslations, nt as handleContentGetIncludingTrashed, ot as handleContentPublish, pt as validateRev, q as handleContentAuthors, rt as handleContentList, s as normalizeRegistryConfig, st as handleContentRestore, tt as handleContentGet, ut as handleContentUnpublish, z as handleMediaDelete } from "../api-DgPbMlng.mjs";
36
- import "../dashboard-DoBwPnON.mjs";
37
- import { r as hashString } from "../hash-Cb7U1b5M.mjs";
38
- import { a as findNonTranslatableSiblingContentIds, c as refreshContentMediaUsageAfterWrite, o as markContentMediaUsageCollectionStale, r as deleteContentMediaUsage } from "../content-refresh-DgqUBeTv.mjs";
39
- import "../media-usage-DPJcNaAq.mjs";
40
- import { n as SchemaRegistry } from "../registry-BP1JK2xh.mjs";
41
- import "../query-1xOcEy18.mjs";
42
- import "../zod-generator-DMzfga3g.mjs";
43
- import "../schema-eqZhkR0S.mjs";
44
- import "../seo-C3GDfT0V.mjs";
45
- import { t as PluginStateRepository } from "../state-xxv6ZTMv.mjs";
46
- import "../sections-DSuEfirn.mjs";
47
- import { r as singleFlightCached, t as createSingleFlightCache } from "../single-flight-cache-C2exrGAi.mjs";
48
- import { i as getSiteSettings, o as invalidateSiteSettingsCache } from "../settings-DgKouY2S.mjs";
49
- import "../settings-CREKyj8I.mjs";
50
- import "../taxonomies-Dbdki3Un.mjs";
51
- import { r as normalizeManifestRoute } from "../manifest-schema-bCq54i7F.mjs";
52
- import "../types-CK0LxPUQ.mjs";
53
- import "../ssrf-CviKqWmq.mjs";
54
- import "../error-DmmN74gW.mjs";
55
- import "../parse-BL49yb9D.mjs";
56
- import "../media-DWIzoVFL.mjs";
57
- import "../redirects-BOn2DIUs.mjs";
58
- import "../relations-C4duJnwI.mjs";
59
- import "../status-COfycGIU.mjs";
60
- import { a as invalidateUrlPatternCache } from "../query-DCiXI7OZ.mjs";
61
- import "../import-PKhLeXvn.mjs";
62
- import { t as getTrustedProxyHeaders } from "../trusted-proxy-CwjQj0YG.mjs";
63
- import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "../request-meta-DzXYYI-n.mjs";
64
- import "../ssrf-BB__xfNM.mjs";
65
- import { n as CronExecutor } from "../cron-BlKIMD_e.mjs";
66
- import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "../email-console-C-9Ng8DM.mjs";
67
- import "../utils-BbUgmeZG.mjs";
68
- import "../tokens-DVltpO2D.mjs";
69
- import "../preview-D4Jnbfx7.mjs";
70
- import "../hreflang-Bg-zMpkr.mjs";
71
- import "../comment-reaction-BUlUz2bh.mjs";
72
- import "../bylines-B8-WGdja.mjs";
73
- import { n as getWidgetAreas } from "../widgets-L-VndTKD.mjs";
74
- import "../validate-Bs_wT2ul.mjs";
75
- import "../apply-1_6ra7NP.mjs";
76
- import "../load-BwTdWE8B.mjs";
77
- import "../search-CeO678cp.mjs";
39
+ import { s as getDb } from "../loader-DLBqRiGP.mjs";
40
+ import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-B0o9ExNV.mjs";
41
+ import { $ as handleContentUnschedule, A as handleMediaUpdate, B as handleContentDelete, D as handleMediaDelete, E as handleMediaCreate, F as handleContentAuthors, G as handleContentList, H as handleContentDuplicate, I as handleContentCompare, J as handleContentPublish, K as handleContentListTrashed, L as handleContentCountScheduled, M as handleRevisionList, N as handleRevisionRestore, O as handleMediaGet, Q as handleContentUnpublish, R as handleContentCountTrashed, U as handleContentGet, V as handleContentDiscardDraft, W as handleContentGetIncludingTrashed, X as handleContentSchedule, Y as handleContentRestore, Z as handleContentTranslations, _ as loadBundleFromR2, et as handleContentUpdate, j as handleRevisionGet, k as handleMediaList, q as handleContentPermanentDelete, s as normalizeRegistryConfig, tt as validateRev, v as syncDeclaredStorageIndexes, z as handleContentCreate } from "../api-DdAsirBe.mjs";
42
+ import { n as recordSchedulerHeartbeatSafely } from "../dashboard-CB8J89rJ.mjs";
43
+ import { r as hashString } from "../hash-Cg0TXJPV.mjs";
44
+ import { _ as loadContentMediaUsageFields, a as findNonTranslatableSiblingContentIds, g as buildContentMediaUsageFieldFingerprint, l as refreshContentMediaUsageAfterWrite, r as deleteContentMediaUsage, s as markContentMediaUsageCollectionStale, u as refreshContentMediaUsageForWork, v as CONTENT_SOURCE_SCHEMA_VERSION, y as MediaUsageRepository } from "../content-refresh-DNERP4-f.mjs";
45
+ import { t as MediaUsageWorkRepository } from "../media-usage-work-DrCbqBcl.mjs";
46
+ import "../media-usage-De3qvwjc.mjs";
47
+ import { n as collectionDeletionCurrentTimestamp, r as deleteActivatedMediaUsageCollection, t as MediaUsageCollectionDeletionRepository } from "../collection-deletion-C8BugSGz.mjs";
48
+ import "../media-usage-work-B9Bksh_b.mjs";
49
+ import { n as SchemaRegistry } from "../registry-C581V4TP.mjs";
50
+ import "../query-vgZNwIhh.mjs";
51
+ import "../zod-generator-D11MhxG4.mjs";
52
+ import "../schema-BFyTg38T.mjs";
53
+ import "../schema-CeQSNwB3.mjs";
54
+ import "../seo-AFpVvTqr.mjs";
55
+ import { t as PluginStateRepository } from "../state-DYwPue40.mjs";
56
+ import "../sections-COctk6hf.mjs";
57
+ import { i as getSiteSettings, o as invalidateSiteSettingsCache } from "../settings-B372MV0p.mjs";
58
+ import "../settings-BE9aqn_9.mjs";
59
+ import "../taxonomies-Dk3MAklL.mjs";
60
+ import { r as normalizeManifestRoute } from "../manifest-schema-BXbUAHod.mjs";
61
+ import "../types-CP1CflWf.mjs";
62
+ import "../ssrf-Cpvg_n3A.mjs";
63
+ import "../error-DupyJZE2.mjs";
64
+ import "../parse-DFbuc-Fh.mjs";
65
+ import "../media-BWWWb8kg.mjs";
66
+ import "../redirects-cV9g7V-0.mjs";
67
+ import "../relations-BBApI4e2.mjs";
68
+ import "../status-CWVYpbD8.mjs";
69
+ import { a as invalidateUrlPatternCache } from "../query-BgjfiLUQ.mjs";
70
+ import "../import-B41u8Rot.mjs";
71
+ import { t as getTrustedProxyHeaders } from "../trusted-proxy-CEVekpby.mjs";
72
+ import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "../request-meta-DgZJh3zG.mjs";
73
+ import "../ssrf-DpXgKsna.mjs";
74
+ import { n as CronExecutor } from "../cron-Y9aLtgrA.mjs";
75
+ import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "../email-console-BHNjIwiB.mjs";
76
+ import { n as assertMediaUsageActivationWriteAllowed } from "../media-usage-write-fence-aHEgN1zB.mjs";
77
+ import "../utils-D4vD_Y98.mjs";
78
+ import "../tokens-C39klX8R.mjs";
79
+ import "../preview-PW-nOnq4.mjs";
80
+ import "../hreflang-DNswoYsi.mjs";
81
+ import "../comment-reaction-D6qfHKBc.mjs";
82
+ import "../bylines-BWemdX2k.mjs";
83
+ import { n as getWidgetAreas } from "../widgets-Cwzvww0W.mjs";
84
+ import "../validate-BkqIG_Uj.mjs";
85
+ import "../apply-D5zfeD09.mjs";
86
+ import "../load-DmvJrtxS.mjs";
87
+ import "../search-BpuPKZb0.mjs";
78
88
  import "../index.mjs";
79
- import { n as VERSION, t as COMMIT } from "../version-Bdfb34kk.mjs";
80
- import { t as getAuthMode } from "../mode-fiXRMfeA.mjs";
81
- import "../snapshot-BdpUJKD-.mjs";
82
- import { u as maybeRunScheduledBackup } from "../backup-LpQ1V_sR.mjs";
83
- import { t as cleanupExpiredChallenges } from "../challenge-store-BFzgFRog.mjs";
84
- import { t as removeUploadAttempt } from "../upload-attempts-C5yd6Gae.mjs";
85
- import { a as validateEncryptionKeyAtStartup } from "../secrets-870d-7yA.mjs";
86
- import { t as resolveSessionUser } from "../session-user-BrK2zz7S.mjs";
89
+ import { n as VERSION, t as COMMIT } from "../version-x0NjTScG.mjs";
90
+ import { t as getAuthMode } from "../mode-Bpe-tNNy.mjs";
91
+ import "../snapshot-B5h6aPMh.mjs";
92
+ import { u as maybeRunScheduledBackup } from "../backup-LHjIAVNP.mjs";
93
+ import { t as cleanupExpiredChallenges } from "../challenge-store-CZyFVaad.mjs";
94
+ import { t as removeUploadAttempt } from "../upload-attempts-hlne3iA1.mjs";
95
+ import { a as validateEncryptionKeyAtStartup } from "../secrets-dtYs2hDo.mjs";
96
+ import { isRecord } from "../plugin-utils.mjs";
97
+ import { t as resolveSessionUser } from "../session-user-BzVemLkw.mjs";
87
98
  import { Kysely, sql } from "kysely";
99
+ import { ulid } from "ulidx";
88
100
  import { z } from "zod";
89
101
  import { Permissions } from "@emdash-cms/auth";
90
102
  import { defineMiddleware } from "astro:middleware";
103
+ import { buildTime } from "virtual:emdash/build";
91
104
  import virtualConfig from "virtual:emdash/config";
92
105
  import { createCoalescingDialect, createDialect, createRequestScopedDb } from "virtual:emdash/dialect";
93
106
  import { mediaProviders } from "virtual:emdash/media-providers";
@@ -104,9 +117,7 @@ async function repairLocaleCasing(db, configuredLocales) {
104
117
  const tableNames = await listTablesLike(db, "ec_%");
105
118
  for (const tableName of tableNames) {
106
119
  const table = sql.ref(tableName);
107
- const slug = tableName.slice(3);
108
- for (const locale of configuredLocales) {
109
- await sql`
120
+ for (const locale of configuredLocales) await sql`
110
121
  UPDATE ${table} AS target
111
122
  SET locale = ${locale}
112
123
  WHERE lower(target.locale) = lower(${locale})
@@ -124,20 +135,1165 @@ async function repairLocaleCasing(db, configuredLocales) {
124
135
  AND candidate.locale != ${locale}
125
136
  )
126
137
  `.execute(db);
127
- await sql`
128
- UPDATE content_taxonomies AS pivot
129
- SET locale = ${locale}
130
- WHERE pivot.collection = ${slug}
131
- AND lower(pivot.locale) = lower(${locale})
132
- AND pivot.locale != ${locale}
133
- AND EXISTS (
134
- SELECT 1
135
- FROM ${table} AS content
136
- WHERE content.id = pivot.entry_id AND content.locale = ${locale}
137
- )
138
- `.execute(db);
138
+ }
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/i18n/taxonomy-locale-diagnostic.ts
143
+ const REPAIR_GUIDE = "https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches";
144
+ async function warnAboutUnconfiguredTaxonomyLocales(db, configuredLocales, definitionLocales) {
145
+ const supportedLocales = configuredLocales.length > 0 ? configuredLocales : ["en"];
146
+ const definitionRows = definitionLocales === void 0 ? await db.selectFrom("_emdash_taxonomy_defs").select("locale").distinct().where("locale", "not in", supportedLocales).execute() : [...new Set(definitionLocales)].filter((locale) => !supportedLocales.includes(locale)).map((locale) => ({ locale }));
147
+ const termRows = await db.selectFrom("taxonomies").select("locale").distinct().where("locale", "not in", supportedLocales).execute();
148
+ const mismatches = [...definitionRows.map(({ locale }) => ({
149
+ source: "definitions",
150
+ locale
151
+ })), ...termRows.map(({ locale }) => ({
152
+ source: "terms",
153
+ locale
154
+ }))].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));
155
+ if (mismatches.length === 0) return;
156
+ const details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join("; ");
157
+ console.warn(`EmDash: Taxonomy rows use locales outside the configured locales (${supportedLocales.join(", ")}): ${details}. Locale-scoped reads may not return these rows. Review and repair them explicitly: ${REPAIR_GUIDE}`);
158
+ }
159
+
160
+ //#endregion
161
+ //#region src/media/usage/collection-deletion-processor.ts
162
+ const MEDIA_USAGE_COLLECTION_DELETION_LIMITS = Object.freeze({
163
+ candidatesPerTick: 4,
164
+ deletionsPerTick: 1,
165
+ rowsPerBatch: 50,
166
+ leaseDurationSeconds: 300,
167
+ maxAttempts: 5,
168
+ retryBaseSeconds: 30,
169
+ retryMaxSeconds: 900,
170
+ maxQueriesPerTick: 30
171
+ });
172
+ async function processDueMediaUsageCollectionDeletions(db) {
173
+ const repository = new MediaUsageCollectionDeletionRepository(db);
174
+ const candidates = await repository.findDue(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.candidatesPerTick);
175
+ if (candidates.length === 0) return {
176
+ candidateCount: 0,
177
+ claimedCount: 0,
178
+ outcome: "idle"
179
+ };
180
+ let claim = null;
181
+ for (const candidate of candidates) {
182
+ claim = await repository.claim({
183
+ collectionId: candidate.collectionId,
184
+ phase: candidate.phase,
185
+ leaseDurationSeconds: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.leaseDurationSeconds
186
+ });
187
+ if (claim?.leaseToken) break;
188
+ }
189
+ if (!claim?.leaseToken) return {
190
+ candidateCount: candidates.length,
191
+ claimedCount: 0,
192
+ outcome: "claim_lost"
193
+ };
194
+ try {
195
+ const processed = await processClaimedDeletion(db, claim);
196
+ if (!processed.finalized && !processed.released && !await repository.release(claim)) return {
197
+ candidateCount: candidates.length,
198
+ claimedCount: 1,
199
+ outcome: "claim_lost"
200
+ };
201
+ return {
202
+ candidateCount: candidates.length,
203
+ claimedCount: 1,
204
+ outcome: processed.finalized ? "finalized" : "progress"
205
+ };
206
+ } catch (error) {
207
+ const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxAttempts;
208
+ if (!await repository.recordFailure({
209
+ collectionId: claim.collectionId,
210
+ leaseToken: claim.leaseToken,
211
+ errorCode: "MEDIA_USAGE_COLLECTION_DELETION_FAILED",
212
+ terminal,
213
+ retryDelaySeconds: retryDelaySeconds$2(claim.attemptCount)
214
+ })) return {
215
+ candidateCount: candidates.length,
216
+ claimedCount: 1,
217
+ outcome: "claim_lost"
218
+ };
219
+ console.error("[media-usage:collection-deletion] Processing failed:", error);
220
+ return {
221
+ candidateCount: candidates.length,
222
+ claimedCount: 1,
223
+ outcome: terminal ? "failed" : "retry"
224
+ };
225
+ }
226
+ }
227
+ async function processClaimedDeletion(db, claim) {
228
+ if (claim.phase === "fence" || claim.phase === "registry" || claim.phase === "table") {
229
+ await deleteActivatedMediaUsageCollection(db, {
230
+ collectionId: claim.collectionId,
231
+ collectionSlug: claim.collectionSlug,
232
+ forceDelete: claim.forceDelete
233
+ }, {
234
+ frontPhaseLimit: 1,
235
+ claimed: claim
236
+ });
237
+ return {
238
+ finalized: false,
239
+ released: true
240
+ };
241
+ }
242
+ if (claim.phase === "work") await processWorkBatch(db, claim);
243
+ if (claim.phase === "sources") await processSourceBatch(db, claim);
244
+ if (claim.phase === "status") await processStatus(db, claim);
245
+ if (claim.phase === "finalize") {
246
+ await finalizeDeletion(db, claim);
247
+ return {
248
+ finalized: true,
249
+ released: true
250
+ };
251
+ }
252
+ return {
253
+ finalized: false,
254
+ released: false
255
+ };
256
+ }
257
+ async function processWorkBatch(db, claim) {
258
+ await withTransaction(db, async (trx) => {
259
+ const rows = await trx.selectFrom("_emdash_media_usage_work").select("content_id").where("collection_id", "=", claim.collectionId).$if(claim.workCursor !== null, (query) => query.where("content_id", ">", claim.workCursor)).orderBy("content_id", "asc").limit(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch + 1).execute();
260
+ const batch = rows.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
261
+ if (batch.length > 0) await trx.deleteFrom("_emdash_media_usage_work").where("collection_id", "=", claim.collectionId).where("content_id", "in", batch.map((row) => row.content_id)).where(liveLeaseGuard(trx, claim)).execute();
262
+ await updateDeletion(trx, claim, {
263
+ phase: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? "work" : "sources",
264
+ work_cursor: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? batch.at(-1).content_id : null
265
+ });
266
+ });
267
+ return false;
268
+ }
269
+ async function processSourceBatch(db, claim) {
270
+ await withTransaction(db, async (trx) => {
271
+ let sourceKey = claim.sourceKey;
272
+ if (!sourceKey) {
273
+ const source = await trx.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_id", "=", claim.collectionId).orderBy("source_key", "asc").limit(1).executeTakeFirst();
274
+ if (!source) {
275
+ await updateDeletion(trx, claim, {
276
+ phase: "status",
277
+ source_key: null,
278
+ occurrence_cursor: null
279
+ });
280
+ return;
281
+ }
282
+ sourceKey = source.source_key;
283
+ await updateDeletion(trx, claim, {
284
+ source_key: sourceKey,
285
+ occurrence_cursor: null
286
+ });
287
+ }
288
+ const occurrences = await trx.selectFrom("_emdash_media_usage").select("id").where("source_key", "=", sourceKey).$if(claim.occurrenceCursor !== null, (query) => query.where("id", ">", claim.occurrenceCursor)).orderBy("id", "asc").limit(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch + 1).execute();
289
+ const batch = occurrences.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
290
+ if (batch.length > 0) await trx.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).where("id", "in", batch.map((row) => row.id)).where(liveLeaseGuard(trx, claim)).execute();
291
+ if (occurrences.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch) {
292
+ await updateDeletion(trx, claim, {
293
+ source_key: sourceKey,
294
+ occurrence_cursor: batch.at(-1).id
295
+ });
296
+ return;
297
+ }
298
+ await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where("source_type", "=", "content").where("collection_id", "=", claim.collectionId).where(liveLeaseGuard(trx, claim)).execute();
299
+ await updateDeletion(trx, claim, {
300
+ source_key: null,
301
+ occurrence_cursor: null
302
+ });
303
+ });
304
+ return false;
305
+ }
306
+ async function processStatus(db, claim) {
307
+ await withTransaction(db, async (trx) => {
308
+ if (await exactCleanupRowsRemain(trx, claim, false)) throw new Error("Collection deletion cleanup is incomplete");
309
+ await trx.deleteFrom("_emdash_media_usage_reconciliations").where("collection_id", "=", claim.collectionId).where("collection_slug", "=", claim.collectionSlug).where(liveLeaseGuard(trx, claim)).execute();
310
+ await trx.deleteFrom("_emdash_media_usage_index_status").where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", claim.collectionSlug).where("collection_id", "=", claim.collectionId).where(liveLeaseGuard(trx, claim)).execute();
311
+ await updateDeletion(trx, claim, { phase: "finalize" });
312
+ });
313
+ return false;
314
+ }
315
+ async function finalizeDeletion(db, claim) {
316
+ if (await tableExists(db, `ec_${claim.collectionSlug}`)) throw new Error("Collection table still exists during deletion finalization");
317
+ if (await exactCleanupRowsRemain(db, claim)) throw new Error("Collection deletion is incomplete");
318
+ if (await db.selectFrom("_emdash_collections").select("id").where("id", "=", claim.collectionId).where("slug", "=", claim.collectionSlug).executeTakeFirst()) throw new Error("Collection registry identity still exists");
319
+ const result = await db.deleteFrom("_emdash_media_usage_collection_deletions").where("collection_id", "=", claim.collectionId).where("collection_slug", "=", claim.collectionSlug).where("state", "=", "leased").where("phase", "=", "finalize").where("lease_token", "=", claim.leaseToken).where(liveLeaseGuard(db, claim)).executeTakeFirst();
320
+ if (Number(result.numDeletedRows ?? 0) !== 1) throw new Error("Collection deletion lost finalization");
321
+ return true;
322
+ }
323
+ async function exactCleanupRowsRemain(db, claim, includeStatus = true) {
324
+ const row = (await sql`
325
+ SELECT
326
+ EXISTS (
327
+ SELECT 1 FROM _emdash_media_usage_work
328
+ WHERE collection_id = ${claim.collectionId}
329
+ ) AS work_present,
330
+ EXISTS (
331
+ SELECT 1 FROM _emdash_media_usage_sources
332
+ WHERE source_type = 'content' AND collection_id = ${claim.collectionId}
333
+ ) AS source_present,
334
+ EXISTS (
335
+ SELECT 1 FROM _emdash_media_usage_index_status
336
+ WHERE adapter_id = 'content-media'
337
+ AND scope_type = 'collection'
338
+ AND scope_key = ${claim.collectionSlug}
339
+ AND collection_id = ${claim.collectionId}
340
+ ) AS status_present
341
+ `.execute(db)).rows[0];
342
+ return Boolean(row?.work_present) || Boolean(row?.source_present) || includeStatus && Boolean(row?.status_present);
343
+ }
344
+ async function updateDeletion(db, claim, values) {
345
+ const result = await db.updateTable("_emdash_media_usage_collection_deletions").set({
346
+ ...values,
347
+ attempt_count: 0,
348
+ last_error_code: null,
349
+ updated_at: collectionDeletionCurrentTimestamp(db)
350
+ }).where("collection_id", "=", claim.collectionId).where("state", "=", "leased").where("lease_token", "=", claim.leaseToken).where(liveLeaseGuard(db, claim)).executeTakeFirst();
351
+ if (Number(result.numUpdatedRows ?? 0) !== 1) throw new Error("Collection deletion lease was lost");
352
+ }
353
+ function liveLeaseGuard(db, claim) {
354
+ return isPostgres(db) ? sql`EXISTS (
355
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
356
+ WHERE deletion.collection_id = ${claim.collectionId}
357
+ AND deletion.state = 'leased'
358
+ AND deletion.lease_token = ${claim.leaseToken}
359
+ AND deletion.lease_expires_at::timestamptz > clock_timestamp()
360
+ )` : sql`EXISTS (
361
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
362
+ WHERE deletion.collection_id = ${claim.collectionId}
363
+ AND deletion.state = 'leased'
364
+ AND deletion.lease_token = ${claim.leaseToken}
365
+ AND deletion.lease_expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
366
+ )`;
367
+ }
368
+ function retryDelaySeconds$2(attemptCount) {
369
+ return Math.min(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryMaxSeconds, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
370
+ }
371
+
372
+ //#endregion
373
+ //#region src/media/usage/reconciliation.ts
374
+ const ACTIVATION_KEY = "incremental_capture";
375
+ const CONTENT_ADAPTER_ID = "content-media";
376
+ const COLLECTION_SCOPE = "collection";
377
+ const MAX_CANDIDATES = 100;
378
+ const MAX_PORTABLE_DURATION_SECONDS = 365 * 24 * 60 * 60;
379
+ const STABLE_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
380
+ var MediaUsageReconciliationRepository = class {
381
+ constructor(db) {
382
+ this.db = db;
383
+ }
384
+ async findByIdentity(collectionId, runToken) {
385
+ assertIdentity({
386
+ collectionId,
387
+ runToken
388
+ });
389
+ const row = await this.db.selectFrom("_emdash_media_usage_reconciliations").selectAll().where("collection_id", "=", collectionId).where("run_token", "=", runToken).executeTakeFirst();
390
+ return row ? rowToRecord(row) : null;
391
+ }
392
+ async beginRun(claim) {
393
+ const now = timestampOffset(this.db, 0);
394
+ const sameRun = sql`status = 'running' AND cursor = ${claim.runToken}`;
395
+ return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
396
+ status: "running",
397
+ started_at: sql`CASE WHEN ${sameRun} THEN started_at ELSE ${now} END`,
398
+ completed_at: null,
399
+ cursor: claim.runToken,
400
+ indexed_source_count: 0,
401
+ failed_source_count: 0,
402
+ last_error_code: null,
403
+ change_epoch: sql`CASE WHEN ${sameRun} THEN change_epoch ELSE change_epoch + 1 END`,
404
+ reconciliation_required: 1,
405
+ updated_at: now
406
+ }).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where((eb) => eb.or([eb("status.status", "!=", "running"), eb("status.cursor", "=", claim.runToken)])).where(this.liveClaimExists(claim)).returning("change_epoch").executeTakeFirst())?.change_epoch ?? null;
407
+ }
408
+ async findScanUpperId(claim) {
409
+ const tableName = contentTableName(claim.collectionSlug);
410
+ return (await sql`
411
+ SELECT content.id
412
+ FROM ${sql.ref(tableName)} AS content
413
+ WHERE ${this.liveClaimExistsSql(claim)}
414
+ ORDER BY content.id DESC
415
+ LIMIT 1
416
+ `.execute(this.db)).rows[0]?.id ?? null;
417
+ }
418
+ async initializeScan(input) {
419
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
420
+ target_epoch: input.targetEpoch,
421
+ field_fingerprint: input.fieldFingerprint,
422
+ phase: "scan",
423
+ scan_cursor: null,
424
+ scan_upper_id: input.scanUpperId,
425
+ source_cursor: null,
426
+ source_upper_key: null,
427
+ attempt_count: 0,
428
+ last_error_code: null,
429
+ updated_at: timestampOffset(this.db, 0)
430
+ }).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "is", null).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).executeTakeFirst();
431
+ return Number(result.numUpdatedRows ?? 0) === 1;
432
+ }
433
+ async findScanPage(reconciliation, limit) {
434
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation scan page limit must be from 1 to 50");
435
+ if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
436
+ const tableName = contentTableName(reconciliation.collectionSlug);
437
+ const lowerBound = reconciliation.scanCursor ? sql`AND content.id > ${reconciliation.scanCursor}` : sql``;
438
+ const upperBound = reconciliation.scanUpperId ? sql`AND content.id <= ${reconciliation.scanUpperId}` : sql`AND 1 = 0`;
439
+ return (await sql`
440
+ SELECT content.id
441
+ FROM ${sql.ref(tableName)} AS content
442
+ WHERE 1 = 1
443
+ ${lowerBound}
444
+ ${upperBound}
445
+ AND ${this.liveClaimExistsSql(reconciliation)}
446
+ ORDER BY content.id ASC
447
+ LIMIT ${limit}
448
+ `.execute(this.db)).rows.map((row) => row.id);
449
+ }
450
+ async checkpointScan(input) {
451
+ let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
452
+ scan_cursor: input.nextCursor,
453
+ attempt_count: 0,
454
+ last_error_code: null,
455
+ updated_at: timestampOffset(this.db, 0)
456
+ }).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "scan").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch));
457
+ query = input.previousCursor ? query.where("reconciliation.scan_cursor", "=", input.previousCursor) : query.where("reconciliation.scan_cursor", "is", null);
458
+ const result = await query.executeTakeFirst();
459
+ return Number(result.numUpdatedRows ?? 0) === 1;
460
+ }
461
+ async ownsRun(claim, targetEpoch) {
462
+ const result = await sql`
463
+ SELECT ${this.statusOwnsRun(claim, targetEpoch)} AS owned
464
+ `.execute(this.db);
465
+ return Boolean(result.rows[0]?.owned);
466
+ }
467
+ async restartRun(claim, previousEpoch) {
468
+ const now = timestampOffset(this.db, 0);
469
+ const interruptedRestart = sql`status = 'running'
470
+ AND cursor = ${claim.runToken}
471
+ AND change_epoch > ${previousEpoch}`;
472
+ return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
473
+ status: "running",
474
+ started_at: sql`CASE WHEN ${interruptedRestart} THEN started_at ELSE ${now} END`,
475
+ completed_at: null,
476
+ cursor: claim.runToken,
477
+ last_error_code: null,
478
+ change_epoch: sql`CASE WHEN ${interruptedRestart} THEN change_epoch ELSE change_epoch + 1 END`,
479
+ reconciliation_required: 1,
480
+ updated_at: now
481
+ }).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where((eb) => eb.or([eb("status.status", "!=", "running"), eb("status.cursor", "=", claim.runToken)])).where(this.liveClaimExists(claim)).returning("change_epoch").executeTakeFirst())?.change_epoch ?? null;
482
+ }
483
+ async restartScan(input) {
484
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
485
+ target_epoch: input.targetEpoch,
486
+ field_fingerprint: input.fieldFingerprint,
487
+ phase: "scan",
488
+ scan_cursor: null,
489
+ scan_upper_id: input.scanUpperId,
490
+ source_cursor: null,
491
+ source_upper_key: null,
492
+ attempt_count: 0,
493
+ next_attempt_at: timestampOffset(this.db, 0),
494
+ last_error_code: null,
495
+ updated_at: timestampOffset(this.db, 0)
496
+ }).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.previousEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).executeTakeFirst();
497
+ return Number(result.numUpdatedRows ?? 0) === 1;
498
+ }
499
+ async findWorkBarrier(collectionId) {
500
+ if (!collectionId) throw new Error("Reconciliation work barrier requires a collection ID");
501
+ const failed = await this.db.selectFrom("_emdash_media_usage_work").select("last_error_code").where("collection_id", "=", collectionId).where("state", "=", "failed").orderBy("content_id").limit(1).executeTakeFirst();
502
+ if (failed) return {
503
+ state: "failed",
504
+ errorCode: failed.last_error_code ?? "MEDIA_USAGE_PROCESSING_FAILED"
505
+ };
506
+ return await this.db.selectFrom("_emdash_media_usage_work").select("content_id").where("collection_id", "=", collectionId).limit(1).executeTakeFirst() ? { state: "pending" } : { state: "empty" };
507
+ }
508
+ async findSourceUpperKey(claim, targetEpoch) {
509
+ return (await this.db.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").where("source.source_type", "=", "content").where("source.collection_id", "=", claim.collectionId).where("source.identity_version", "=", 1).where(this.liveClaimExists(claim)).where(this.statusOwnsRun(claim, targetEpoch)).orderBy("source.source_key", "desc").limit(1).executeTakeFirst())?.source_key ?? null;
510
+ }
511
+ async transitionToSources(input) {
512
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
513
+ phase: "sources",
514
+ source_cursor: null,
515
+ source_upper_key: input.sourceUpperKey,
516
+ attempt_count: 0,
517
+ last_error_code: null,
518
+ updated_at: timestampOffset(this.db, 0)
519
+ }).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.field_fingerprint", "=", input.fieldFingerprint).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "scan").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", input.claim.collectionId)))).executeTakeFirst();
520
+ return Number(result.numUpdatedRows ?? 0) === 1;
521
+ }
522
+ async findSourcePage(reconciliation, limit) {
523
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation source page limit must be from 1 to 50");
524
+ if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
525
+ let query = this.db.selectFrom("_emdash_media_usage_sources as source").select([
526
+ "source.source_key",
527
+ "source.content_id",
528
+ "source.source_variant"
529
+ ]).where("source.source_type", "=", "content").where("source.collection_id", "=", reconciliation.collectionId).where("source.identity_version", "=", 1).where(this.liveClaimExists(reconciliation)).where(this.statusOwnsRun(reconciliation, reconciliation.targetEpoch));
530
+ if (reconciliation.sourceCursor) query = query.where("source.source_key", ">", reconciliation.sourceCursor);
531
+ if (reconciliation.sourceUpperKey) query = query.where("source.source_key", "<=", reconciliation.sourceUpperKey);
532
+ else query = query.where(sql`1 = 0`);
533
+ return (await query.orderBy("source.source_key").limit(limit).execute()).map((row) => ({
534
+ sourceKey: row.source_key,
535
+ contentId: row.content_id,
536
+ sourceVariant: row.source_variant
537
+ }));
538
+ }
539
+ async findMissingContentIds(collectionSlug, contentIds) {
540
+ const unique = [...new Set(contentIds)];
541
+ if (unique.length === 0) return [];
542
+ if (unique.length > 50 || unique.some((contentId) => !contentId)) throw new Error("Reconciliation source page has invalid content identity");
543
+ const tableName = contentTableName(collectionSlug);
544
+ const existing = await sql`
545
+ SELECT id FROM ${sql.ref(tableName)} WHERE id IN (${sql.join(unique)})
546
+ `.execute(this.db);
547
+ const present = new Set(existing.rows.map((row) => row.id));
548
+ return unique.filter((contentId) => !present.has(contentId));
549
+ }
550
+ async checkpointSources(input) {
551
+ let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
552
+ source_cursor: input.nextCursor,
553
+ attempt_count: 0,
554
+ last_error_code: null,
555
+ updated_at: timestampOffset(this.db, 0)
556
+ }).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "sources").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch));
557
+ query = input.previousCursor ? query.where("reconciliation.source_cursor", "=", input.previousCursor) : query.where("reconciliation.source_cursor", "is", null);
558
+ const result = await query.executeTakeFirst();
559
+ return Number(result.numUpdatedRows ?? 0) === 1;
560
+ }
561
+ async finishFailedCoverage(collectionId, runToken) {
562
+ assertIdentity({
563
+ collectionId,
564
+ runToken
565
+ });
566
+ const now = timestampOffset(this.db, 0);
567
+ const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
568
+ status: sql`CASE WHEN EXISTS (
569
+ SELECT 1 FROM _emdash_media_usage_sources AS source
570
+ WHERE source.source_type = 'content'
571
+ AND source.collection_id = ${collectionId}
572
+ AND source.identity_version = 1
573
+ LIMIT 1
574
+ ) THEN 'partial' ELSE 'failed' END`,
575
+ completed_at: null,
576
+ cursor: null,
577
+ last_error_code: sql`CASE WHEN (
578
+ SELECT reconciliation.last_error_code
579
+ FROM _emdash_media_usage_reconciliations AS reconciliation
580
+ WHERE reconciliation.collection_id = ${collectionId}
581
+ AND reconciliation.run_token = ${runToken}
582
+ ) = 'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
583
+ THEN COALESCE(
584
+ (SELECT work.last_error_code
585
+ FROM _emdash_media_usage_work AS work
586
+ WHERE work.collection_id = ${collectionId} AND work.state = 'failed'
587
+ ORDER BY work.content_id LIMIT 1),
588
+ 'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
589
+ )
590
+ ELSE (
591
+ SELECT reconciliation.last_error_code
592
+ FROM _emdash_media_usage_reconciliations AS reconciliation
593
+ WHERE reconciliation.collection_id = ${collectionId}
594
+ AND reconciliation.run_token = ${runToken}
595
+ ) END`,
596
+ reconciliation_required: 1,
597
+ updated_at: now
598
+ }).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", collectionId).where("status.status", "=", "running").where("status.cursor", "=", runToken).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_reconciliations as reconciliation").select("reconciliation.collection_id").where("reconciliation.collection_id", "=", collectionId).where("reconciliation.run_token", "=", runToken).where("reconciliation.state", "=", "failed"))).executeTakeFirst();
599
+ return Number(result.numUpdatedRows ?? 0) === 1;
600
+ }
601
+ async finalizeCoverage(input) {
602
+ const now = timestampOffset(this.db, 0);
603
+ const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
604
+ status: "complete",
605
+ schema_version: input.schemaVersion,
606
+ completed_at: now,
607
+ cursor: null,
608
+ last_error_code: null,
609
+ reconciliation_required: 0,
610
+ updated_at: now
611
+ }).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", input.claim.collectionId).where("status.scope_key", "=", input.claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where("status.status", "=", "running").where("status.cursor", "=", input.claim.runToken).where("status.change_epoch", "=", input.targetEpoch).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", input.claim.collectionId)))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_reconciliations as reconciliation").innerJoin("_emdash_collections as collection", (join) => join.onRef("collection.id", "=", "reconciliation.collection_id").onRef("collection.slug", "=", "reconciliation.collection_slug")).select("reconciliation.collection_id").where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.field_fingerprint", "=", input.fieldFingerprint).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "sources").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db, "reconciliation.lease_expires_at")).where((inner) => inner.not(inner.exists(inner.selectFrom("_emdash_media_usage_collection_deletions as deletion").select("deletion.collection_id").where("deletion.collection_id", "=", input.claim.collectionId)))))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_activation as activation").select("activation.task_key").where("activation.task_key", "=", ACTIVATION_KEY).where("activation.state", "=", "active"))).executeTakeFirst();
612
+ return Number(result.numUpdatedRows ?? 0) === 1;
613
+ }
614
+ async deleteFinalized(claim) {
615
+ const result = await this.db.deleteFrom("_emdash_media_usage_reconciliations as reconciliation").where("reconciliation.collection_id", "=", claim.collectionId).where("reconciliation.run_token", "=", claim.runToken).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", claim.leaseToken).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").select("status.collection_id").where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.status", "=", "complete").where("status.reconciliation_required", "=", 0))).executeTakeFirst();
616
+ return Number(result.numDeletedRows ?? 0) === 1;
617
+ }
618
+ async deleteOneObsolete() {
619
+ return (await sql`
620
+ DELETE FROM _emdash_media_usage_reconciliations
621
+ WHERE (collection_id, run_token) IN (
622
+ SELECT reconciliation.collection_id, reconciliation.run_token
623
+ FROM _emdash_media_usage_reconciliations AS reconciliation
624
+ INNER JOIN _emdash_media_usage_index_status AS status
625
+ ON status.collection_id = reconciliation.collection_id
626
+ AND status.scope_key = reconciliation.collection_slug
627
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
628
+ AND status.scope_type = ${COLLECTION_SCOPE}
629
+ AND status.reconciliation_required = 0
630
+ ORDER BY reconciliation.updated_at, reconciliation.collection_id
631
+ LIMIT 1
632
+ )
633
+ RETURNING collection_id
634
+ `.execute(this.db)).rows.length === 1;
635
+ }
636
+ liveClaimExists(claim) {
637
+ return this.liveClaimExistsSql(claim);
638
+ }
639
+ liveClaimExistsSql(claim) {
640
+ return sql`EXISTS (
641
+ SELECT 1
642
+ FROM _emdash_media_usage_reconciliations AS reconciliation
643
+ INNER JOIN _emdash_collections AS collection
644
+ ON collection.id = reconciliation.collection_id
645
+ AND collection.slug = reconciliation.collection_slug
646
+ WHERE reconciliation.collection_id = ${claim.collectionId}
647
+ AND reconciliation.collection_slug = ${claim.collectionSlug}
648
+ AND reconciliation.run_token = ${claim.runToken}
649
+ AND reconciliation.state = 'leased'
650
+ AND reconciliation.lease_token = ${claim.leaseToken}
651
+ AND ${liveLease(this.db, "reconciliation.lease_expires_at")}
652
+ AND EXISTS (
653
+ SELECT 1 FROM _emdash_media_usage_activation AS activation
654
+ WHERE activation.task_key = ${ACTIVATION_KEY}
655
+ AND activation.state = 'active'
656
+ )
657
+ AND NOT EXISTS (
658
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
659
+ WHERE deletion.collection_id = reconciliation.collection_id
660
+ )
661
+ )`;
662
+ }
663
+ statusOwnsRun(claim, targetEpoch) {
664
+ return sql`EXISTS (
665
+ SELECT 1 FROM _emdash_media_usage_index_status AS status
666
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
667
+ AND status.scope_type = ${COLLECTION_SCOPE}
668
+ AND status.collection_id = ${claim.collectionId}
669
+ AND status.scope_key = ${claim.collectionSlug}
670
+ AND status.capture_state = 'active'
671
+ AND status.reconciliation_required = 1
672
+ AND status.status = 'running'
673
+ AND status.cursor = ${claim.runToken}
674
+ AND status.change_epoch = ${targetEpoch}
675
+ )`;
676
+ }
677
+ async seedNextCandidate() {
678
+ const runToken = ulid();
679
+ const now = timestampOffset(this.db, 0);
680
+ return (await sql`
681
+ INSERT INTO _emdash_media_usage_reconciliations (
682
+ collection_id,
683
+ collection_slug,
684
+ run_token,
685
+ next_attempt_at,
686
+ updated_at
687
+ )
688
+ SELECT status.collection_id, status.scope_key, ${runToken}, ${now}, ${now}
689
+ FROM _emdash_media_usage_index_status AS status
690
+ INNER JOIN _emdash_collections AS collection
691
+ ON collection.id = status.collection_id
692
+ AND collection.slug = status.scope_key
693
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
694
+ AND status.scope_type = ${COLLECTION_SCOPE}
695
+ AND status.capture_state = 'active'
696
+ AND status.reconciliation_required = 1
697
+ AND status.collection_id IS NOT NULL
698
+ AND EXISTS (
699
+ SELECT 1 FROM _emdash_media_usage_activation AS activation
700
+ WHERE activation.task_key = ${ACTIVATION_KEY}
701
+ AND activation.state = 'active'
702
+ )
703
+ AND NOT EXISTS (
704
+ SELECT 1 FROM _emdash_media_usage_reconciliations AS existing
705
+ WHERE existing.collection_id = status.collection_id
706
+ )
707
+ AND NOT EXISTS (
708
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
709
+ WHERE deletion.collection_id = status.collection_id
710
+ )
711
+ ORDER BY status.collection_id
712
+ LIMIT 1
713
+ ON CONFLICT (collection_id) DO NOTHING
714
+ RETURNING collection_id
715
+ `.execute(this.db)).rows.length === 1;
716
+ }
717
+ async findDue(limit) {
718
+ assertLimit(limit);
719
+ const nextAttemptIsDue = timestampIsDue(this.db, "next_attempt_at");
720
+ return (await sql`
721
+ WITH pending_candidates AS (
722
+ SELECT * FROM _emdash_media_usage_reconciliations
723
+ WHERE state = 'pending' AND ${nextAttemptIsDue}
724
+ ORDER BY next_attempt_at, updated_at, collection_id
725
+ LIMIT ${limit}
726
+ ), retry_candidates AS (
727
+ SELECT * FROM _emdash_media_usage_reconciliations
728
+ WHERE state = 'retry' AND ${nextAttemptIsDue}
729
+ ORDER BY next_attempt_at, updated_at, collection_id
730
+ LIMIT ${limit}
731
+ ), leased_candidates AS (
732
+ SELECT * FROM _emdash_media_usage_reconciliations
733
+ WHERE state = 'leased' AND ${timestampIsDue(this.db, "lease_expires_at")}
734
+ ORDER BY lease_expires_at, updated_at, collection_id
735
+ LIMIT ${limit}
736
+ ), candidates AS (
737
+ SELECT * FROM pending_candidates
738
+ UNION ALL SELECT * FROM retry_candidates
739
+ UNION ALL SELECT * FROM leased_candidates
740
+ )
741
+ SELECT * FROM candidates
742
+ ORDER BY CASE WHEN state = 'leased' THEN lease_expires_at ELSE next_attempt_at END,
743
+ updated_at,
744
+ collection_id
745
+ LIMIT ${limit}
746
+ `.execute(this.db)).rows.map(rowToRecord);
747
+ }
748
+ async findFailed(limit) {
749
+ assertLimit(limit);
750
+ return (await this.db.selectFrom("_emdash_media_usage_reconciliations as reconciliation").innerJoin("_emdash_media_usage_index_status as status", (join) => join.onRef("status.collection_id", "=", "reconciliation.collection_id").onRef("status.scope_key", "=", "reconciliation.collection_slug")).selectAll("reconciliation").where("reconciliation.state", "=", "failed").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where((eb) => eb.or([
751
+ eb("status.reconciliation_required", "=", 0),
752
+ eb.and([eb("status.status", "=", "running"), eb("status.cursor", "=", eb.ref("reconciliation.run_token"))]),
753
+ eb.and([eb("status.cursor", "is", null), eb("status.change_epoch", ">", eb.ref("reconciliation.target_epoch"))])
754
+ ])).orderBy("reconciliation.updated_at").orderBy("reconciliation.collection_id").limit(limit).execute()).map(rowToRecord);
755
+ }
756
+ async claim(input) {
757
+ assertIdentity(input);
758
+ assertDuration(input.leaseDurationSeconds, "lease duration");
759
+ const leaseToken = ulid();
760
+ const row = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
761
+ state: "leased",
762
+ lease_token: leaseToken,
763
+ lease_expires_at: timestampOffset(this.db, input.leaseDurationSeconds),
764
+ updated_at: timestampOffset(this.db, 0)
765
+ }).where("reconciliation.collection_id", "=", input.collectionId).where("reconciliation.run_token", "=", input.runToken).where((eb) => eb.or([eb.and([eb("reconciliation.state", "in", ["pending", "retry"]), timestampIsDue(this.db, "reconciliation.next_attempt_at")]), eb.and([eb("reconciliation.state", "=", "leased"), timestampIsDue(this.db, "reconciliation.lease_expires_at")])])).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_activation as activation").select("activation.task_key").where("activation.task_key", "=", ACTIVATION_KEY).where("activation.state", "=", "active"))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").innerJoin("_emdash_collections as collection", (join) => join.onRef("collection.id", "=", "status.collection_id").onRef("collection.slug", "=", "status.scope_key")).select("status.collection_id").whereRef("status.collection_id", "=", "reconciliation.collection_id").whereRef("status.scope_key", "=", "reconciliation.collection_slug").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1))).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_collection_deletions as deletion").select("deletion.collection_id").whereRef("deletion.collection_id", "=", "reconciliation.collection_id")))).returningAll().executeTakeFirst();
766
+ return row ? {
767
+ ...rowToRecord(row),
768
+ leaseToken
769
+ } : null;
770
+ }
771
+ async release(input) {
772
+ assertLeaseIdentity(input);
773
+ assertDuration(input.delaySeconds, "release delay", true);
774
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
775
+ state: "pending",
776
+ next_attempt_at: timestampOffset(this.db, input.delaySeconds),
777
+ lease_token: null,
778
+ lease_expires_at: null,
779
+ updated_at: timestampOffset(this.db, 0)
780
+ }).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
781
+ return Number(result.numUpdatedRows ?? 0) === 1;
782
+ }
783
+ async recordFailure(input) {
784
+ assertLeaseIdentity(input);
785
+ if (!STABLE_ERROR_CODE_PATTERN.test(input.errorCode)) throw new Error("Reconciliation failure requires a stable error code");
786
+ assertDuration(input.retryDelaySeconds, "retry delay", true);
787
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
788
+ state: input.terminal ? "failed" : sql`CASE WHEN attempt_count >= 4 THEN 'failed' ELSE 'retry' END`,
789
+ ...input.terminal ? { target_epoch: sql`COALESCE(
790
+ target_epoch,
791
+ (SELECT status.change_epoch
792
+ FROM _emdash_media_usage_index_status AS status
793
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
794
+ AND status.scope_type = ${COLLECTION_SCOPE}
795
+ AND status.collection_id = ${input.collectionId}
796
+ AND status.status = 'running'
797
+ AND status.cursor = ${input.runToken})
798
+ )` } : {},
799
+ attempt_count: sql`attempt_count + 1`,
800
+ next_attempt_at: timestampOffset(this.db, input.retryDelaySeconds),
801
+ lease_token: null,
802
+ lease_expires_at: null,
803
+ last_error_code: input.errorCode,
804
+ updated_at: timestampOffset(this.db, 0)
805
+ }).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
806
+ return Number(result.numUpdatedRows ?? 0) === 1;
807
+ }
808
+ async recordEntryFailure(claim) {
809
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
810
+ state: "failed",
811
+ attempt_count: sql`attempt_count + 1`,
812
+ next_attempt_at: timestampOffset(this.db, 0),
813
+ lease_token: null,
814
+ lease_expires_at: null,
815
+ last_error_code: "MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED",
816
+ updated_at: timestampOffset(this.db, 0)
817
+ }).where("reconciliation.collection_id", "=", claim.collectionId).where("reconciliation.run_token", "=", claim.runToken).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", claim.leaseToken).where(liveLease(this.db)).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", claim.collectionId).where("work.state", "=", "failed"))).executeTakeFirst();
818
+ return Number(result.numUpdatedRows ?? 0) === 1;
819
+ }
820
+ async resetFailedForNewEpoch(observed) {
821
+ const collectionId = "collection_id" in observed ? observed.collection_id : observed.collectionId;
822
+ const runToken = "run_token" in observed ? observed.run_token : observed.runToken;
823
+ const targetEpoch = "target_epoch" in observed ? observed.target_epoch : observed.targetEpoch;
824
+ if (targetEpoch === null) return false;
825
+ const now = timestampOffset(this.db, 0);
826
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
827
+ state: "pending",
828
+ phase: "scan",
829
+ target_epoch: null,
830
+ field_fingerprint: null,
831
+ scan_cursor: null,
832
+ scan_upper_id: null,
833
+ source_cursor: null,
834
+ source_upper_key: null,
835
+ attempt_count: 0,
836
+ next_attempt_at: now,
837
+ lease_token: null,
838
+ lease_expires_at: null,
839
+ last_error_code: null,
840
+ updated_at: now
841
+ }).where("reconciliation.collection_id", "=", collectionId).where("reconciliation.run_token", "=", runToken).where("reconciliation.state", "=", "failed").where("reconciliation.target_epoch", "=", targetEpoch).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").select("status.collection_id").whereRef("status.collection_id", "=", "reconciliation.collection_id").whereRef("status.scope_key", "=", "reconciliation.collection_slug").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where("status.cursor", "is", null).where("status.change_epoch", ">", targetEpoch))).executeTakeFirst();
842
+ return Number(result.numUpdatedRows ?? 0) === 1;
843
+ }
844
+ };
845
+ function rowToRecord(row) {
846
+ if (!isState(row.state) || !isPhase(row.phase) || !Number.isSafeInteger(row.attempt_count)) throw new Error("Invalid media usage reconciliation lifecycle");
847
+ return {
848
+ collectionId: row.collection_id,
849
+ collectionSlug: row.collection_slug,
850
+ runToken: row.run_token,
851
+ targetEpoch: row.target_epoch,
852
+ fieldFingerprint: row.field_fingerprint,
853
+ state: row.state,
854
+ phase: row.phase,
855
+ scanCursor: row.scan_cursor,
856
+ scanUpperId: row.scan_upper_id,
857
+ sourceCursor: row.source_cursor,
858
+ sourceUpperKey: row.source_upper_key,
859
+ attemptCount: row.attempt_count,
860
+ nextAttemptAt: row.next_attempt_at,
861
+ leaseToken: row.lease_token,
862
+ leaseExpiresAt: row.lease_expires_at,
863
+ lastErrorCode: row.last_error_code,
864
+ createdAt: row.created_at,
865
+ updatedAt: row.updated_at
866
+ };
867
+ }
868
+ function isState(state) {
869
+ return state === "pending" || state === "retry" || state === "leased" || state === "failed";
870
+ }
871
+ function isPhase(phase) {
872
+ return phase === "scan" || phase === "sources";
873
+ }
874
+ function assertLimit(limit) {
875
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CANDIDATES) throw new Error("Reconciliation candidate limit must be from 1 to 100");
876
+ }
877
+ function assertIdentity(input) {
878
+ if (!input.collectionId || !input.runToken) throw new Error("Reconciliation requires an exact collection and run token");
879
+ }
880
+ function assertLeaseIdentity(input) {
881
+ assertIdentity(input);
882
+ if (!input.leaseToken) throw new Error("Reconciliation requires a lease token");
883
+ }
884
+ function assertDuration(value, label, allowZero = false) {
885
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > MAX_PORTABLE_DURATION_SECONDS) throw new Error(`Reconciliation ${label} is outside the portable range`);
886
+ }
887
+ function liveLease(db, column = "lease_expires_at") {
888
+ const expiry = sql.ref(column);
889
+ return isPostgres(db) ? sql`${expiry} > to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`${expiry} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
890
+ }
891
+ function timestampIsDue(db, column) {
892
+ const value = sql.ref(column);
893
+ return isPostgres(db) ? sql`${value} <= to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`${value} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
894
+ }
895
+ function timestampOffset(db, offsetSeconds) {
896
+ if (isPostgres(db)) return sql`to_char(
897
+ (clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
898
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
899
+ )`;
900
+ return sql`strftime(
901
+ '%Y-%m-%dT%H:%M:%fZ',
902
+ 'now',
903
+ ${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
904
+ )`;
905
+ }
906
+ function contentTableName(collectionSlug) {
907
+ validateIdentifier(collectionSlug, "collection slug");
908
+ const tableName = `ec_${collectionSlug}`;
909
+ validateIdentifier(tableName, "content table");
910
+ return tableName;
911
+ }
912
+
913
+ //#endregion
914
+ //#region src/media/usage/reconciliation-processor.ts
915
+ const MEDIA_USAGE_RECONCILIATION_LIMITS = Object.freeze({
916
+ candidatesPerTick: 4,
917
+ pageSize: 50,
918
+ leaseDurationSeconds: 60,
919
+ maxAttempts: 5,
920
+ retryBaseSeconds: 30,
921
+ retryMaxSeconds: 900,
922
+ retryJitterRatio: .25,
923
+ maxQueriesPerTick: 20
924
+ });
925
+ async function processDueMediaUsageReconciliation(db) {
926
+ if ((await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state !== "active") return "inactive";
927
+ const reconciliation = new MediaUsageReconciliationRepository(db);
928
+ if (await reconciliation.deleteOneObsolete()) return "completed";
929
+ const [failed] = await reconciliation.findFailed(1);
930
+ if (failed) {
931
+ if (await reconciliation.finishFailedCoverage(failed.collectionId, failed.runToken)) return "failed";
932
+ if (await reconciliation.resetFailedForNewEpoch(failed)) return "advanced";
933
+ }
934
+ await reconciliation.seedNextCandidate();
935
+ const candidates = await reconciliation.findDue(MEDIA_USAGE_RECONCILIATION_LIMITS.candidatesPerTick);
936
+ let claim = null;
937
+ for (const candidate of candidates) {
938
+ claim = await reconciliation.claim({
939
+ collectionId: candidate.collectionId,
940
+ runToken: candidate.runToken,
941
+ leaseDurationSeconds: MEDIA_USAGE_RECONCILIATION_LIMITS.leaseDurationSeconds
942
+ });
943
+ if (claim) break;
944
+ }
945
+ if (!claim) return candidates.length === 0 ? "not_due" : "claim_lost";
946
+ try {
947
+ return await processClaimedReconciliation(db, claim);
948
+ } catch (error) {
949
+ const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_RECONCILIATION_LIMITS.maxAttempts;
950
+ if (!await reconciliation.recordFailure({
951
+ collectionId: claim.collectionId,
952
+ runToken: claim.runToken,
953
+ leaseToken: claim.leaseToken,
954
+ errorCode: "MEDIA_USAGE_RECONCILIATION_FAILED",
955
+ retryDelaySeconds: retryDelaySeconds$1(claim.attemptCount),
956
+ terminal
957
+ })) return "claim_lost";
958
+ if (terminal) await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
959
+ console.error("[media-usage:reconciliation] Processing failed:", error);
960
+ return terminal ? "failed" : "retry";
961
+ }
962
+ }
963
+ async function processClaimedMediaUsageReconciliationScan(db, claim, options = {}) {
964
+ const reconciliation = new MediaUsageReconciliationRepository(db);
965
+ let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
966
+ if (!current || current.leaseToken !== claim.leaseToken || current.phase !== "scan") return "deferred";
967
+ let fields;
968
+ let fieldFingerprint;
969
+ if (current.targetEpoch === null) {
970
+ const targetEpoch = await reconciliation.beginRun(claim);
971
+ if (targetEpoch === null) {
972
+ await reconciliation.release({
973
+ ...claim,
974
+ delaySeconds: 30
975
+ });
976
+ return "deferred";
977
+ }
978
+ fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
979
+ fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
980
+ const scanUpperId = fields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
981
+ if (!await reconciliation.initializeScan({
982
+ claim,
983
+ targetEpoch,
984
+ fieldFingerprint,
985
+ scanUpperId
986
+ })) return "deferred";
987
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
988
+ if (!current) return "deferred";
989
+ } else {
990
+ fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
991
+ fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
992
+ }
993
+ if (current.fieldFingerprint !== fieldFingerprint || current.targetEpoch === null) return "restart_required";
994
+ const contentIds = await reconciliation.findScanPage(current, 50);
995
+ if (contentIds.length === 0) {
996
+ if (options.releaseOnExhausted ?? true) await reconciliation.release({
997
+ ...claim,
998
+ delaySeconds: 30
999
+ });
1000
+ return "exhausted";
1001
+ }
1002
+ await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
1003
+ collectionId: claim.collectionId,
1004
+ collectionSlug: claim.collectionSlug,
1005
+ runToken: claim.runToken,
1006
+ leaseToken: claim.leaseToken,
1007
+ changeEpoch: current.targetEpoch,
1008
+ phase: "scan",
1009
+ contentIds
1010
+ });
1011
+ const nextCursor = contentIds.at(-1);
1012
+ if (!await reconciliation.checkpointScan({
1013
+ claim,
1014
+ targetEpoch: current.targetEpoch,
1015
+ previousCursor: current.scanCursor,
1016
+ nextCursor
1017
+ })) return "deferred";
1018
+ if (!await reconciliation.release({
1019
+ ...claim,
1020
+ delaySeconds: 0
1021
+ })) return "deferred";
1022
+ return "advanced";
1023
+ }
1024
+ async function processClaimedReconciliation(db, claim) {
1025
+ const reconciliation = new MediaUsageReconciliationRepository(db);
1026
+ let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
1027
+ if (!current || current.leaseToken !== claim.leaseToken) return "claim_lost";
1028
+ if (current.targetEpoch !== null && !await reconciliation.ownsRun(claim, current.targetEpoch)) return restartReconciliation(db, reconciliation, claim, current);
1029
+ if (current.phase === "scan") {
1030
+ const outcome = await processClaimedMediaUsageReconciliationScan(db, claim, { releaseOnExhausted: false });
1031
+ if (outcome === "restart_required") {
1032
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
1033
+ return restartReconciliation(db, reconciliation, claim, current);
1034
+ }
1035
+ if (outcome !== "exhausted") return outcome;
1036
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
1037
+ const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
1038
+ if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
1039
+ if (barrier.state === "pending") {
1040
+ await reconciliation.release({
1041
+ ...claim,
1042
+ delaySeconds: 30
1043
+ });
1044
+ return "deferred";
139
1045
  }
1046
+ if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
1047
+ const sourceUpperKey = await reconciliation.findSourceUpperKey(claim, current.targetEpoch);
1048
+ if (!await reconciliation.transitionToSources({
1049
+ claim,
1050
+ targetEpoch: current.targetEpoch,
1051
+ fieldFingerprint: current.fieldFingerprint,
1052
+ sourceUpperKey
1053
+ })) return "claim_lost";
1054
+ if (!await reconciliation.release({
1055
+ ...claim,
1056
+ delaySeconds: 0
1057
+ })) return "claim_lost";
1058
+ return "advanced";
140
1059
  }
1060
+ return processSourcePhase(db, reconciliation, claim, current);
1061
+ }
1062
+ async function processSourcePhase(db, reconciliation, claim, current) {
1063
+ if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
1064
+ const fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
1065
+ const fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
1066
+ if (fieldFingerprint !== current.fieldFingerprint) return restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint);
1067
+ const page = await reconciliation.findSourcePage(current, MEDIA_USAGE_RECONCILIATION_LIMITS.pageSize);
1068
+ if (page.length > 0) {
1069
+ if (page.some((source) => !source.contentId || source.sourceVariant !== "columns" && source.sourceVariant !== "draft_overlay")) return failReconciliation(reconciliation, claim, "MEDIA_USAGE_RECONCILIATION_INVALID_SOURCE", false);
1070
+ const contentIds = [...new Set(page.map((source) => source.contentId))];
1071
+ const enqueueIds = fields.extractionFields.length === 0 ? contentIds : await reconciliation.findMissingContentIds(claim.collectionSlug, contentIds);
1072
+ if (enqueueIds.length > 0) await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
1073
+ collectionId: claim.collectionId,
1074
+ collectionSlug: claim.collectionSlug,
1075
+ runToken: claim.runToken,
1076
+ leaseToken: claim.leaseToken,
1077
+ changeEpoch: current.targetEpoch,
1078
+ phase: "sources",
1079
+ contentIds: enqueueIds
1080
+ });
1081
+ if (!await reconciliation.checkpointSources({
1082
+ claim,
1083
+ targetEpoch: current.targetEpoch,
1084
+ previousCursor: current.sourceCursor,
1085
+ nextCursor: page.at(-1).sourceKey
1086
+ })) return "claim_lost";
1087
+ if (!await reconciliation.release({
1088
+ ...claim,
1089
+ delaySeconds: 0
1090
+ })) return "claim_lost";
1091
+ return "advanced";
1092
+ }
1093
+ const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
1094
+ if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
1095
+ if (barrier.state === "pending") {
1096
+ await reconciliation.release({
1097
+ ...claim,
1098
+ delaySeconds: 30
1099
+ });
1100
+ return "deferred";
1101
+ }
1102
+ if (!await reconciliation.finalizeCoverage({
1103
+ claim,
1104
+ targetEpoch: current.targetEpoch,
1105
+ fieldFingerprint,
1106
+ schemaVersion: CONTENT_SOURCE_SCHEMA_VERSION
1107
+ })) return "claim_lost";
1108
+ if (!await reconciliation.deleteFinalized(claim)) return "claim_lost";
1109
+ return "completed";
1110
+ }
1111
+ async function restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint) {
1112
+ if (current.targetEpoch === null) return "claim_lost";
1113
+ const discoveredFields = fields ?? await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
1114
+ const fingerprint = fieldFingerprint ?? await buildContentMediaUsageFieldFingerprint(discoveredFields);
1115
+ const targetEpoch = await reconciliation.restartRun(claim, current.targetEpoch);
1116
+ if (targetEpoch === null) {
1117
+ await reconciliation.release({
1118
+ ...claim,
1119
+ delaySeconds: 30
1120
+ });
1121
+ return "deferred";
1122
+ }
1123
+ const scanUpperId = discoveredFields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
1124
+ if (!await reconciliation.restartScan({
1125
+ claim,
1126
+ previousEpoch: current.targetEpoch,
1127
+ targetEpoch,
1128
+ fieldFingerprint: fingerprint,
1129
+ scanUpperId
1130
+ })) return "claim_lost";
1131
+ if (!await reconciliation.release({
1132
+ ...claim,
1133
+ delaySeconds: 0
1134
+ })) return "claim_lost";
1135
+ return "advanced";
1136
+ }
1137
+ async function failReconciliation(reconciliation, claim, errorCode, entryFailure) {
1138
+ if (!(entryFailure ? await reconciliation.recordEntryFailure(claim) : await reconciliation.recordFailure({
1139
+ collectionId: claim.collectionId,
1140
+ runToken: claim.runToken,
1141
+ leaseToken: claim.leaseToken,
1142
+ errorCode,
1143
+ retryDelaySeconds: 0,
1144
+ terminal: true
1145
+ }))) return "claim_lost";
1146
+ await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
1147
+ return "failed";
1148
+ }
1149
+ function retryDelaySeconds$1(attemptCount) {
1150
+ const exponential = Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, MEDIA_USAGE_RECONCILIATION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
1151
+ const jitter = Math.floor(exponential * MEDIA_USAGE_RECONCILIATION_LIMITS.retryJitterRatio * Math.random());
1152
+ return Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, exponential + jitter);
1153
+ }
1154
+
1155
+ //#endregion
1156
+ //#region src/media/usage/work-processor.ts
1157
+ const MEDIA_USAGE_WORK_PROCESSING_LIMITS = Object.freeze({
1158
+ candidatesPerTick: 4,
1159
+ jobsPerTick: 1,
1160
+ maxTickDurationMs: 5e3,
1161
+ leaseDurationSeconds: 60,
1162
+ maxAttempts: 5,
1163
+ retryBaseSeconds: 30,
1164
+ retryMaxSeconds: 900,
1165
+ retryJitterRatio: .25,
1166
+ ordinaryStatementsPerJob: 20
1167
+ });
1168
+ async function processMediaUsageWorkAfterWrite(db, collectionSlug, contentId) {
1169
+ if (!await isIncrementalCaptureActive(db)) return {
1170
+ outcome: "inactive",
1171
+ claimed: false
1172
+ };
1173
+ const repo = new MediaUsageWorkRepository(db);
1174
+ const work = await repo.findWorkForContent(collectionSlug, contentId);
1175
+ if (!work) return {
1176
+ outcome: "not_due",
1177
+ claimed: false
1178
+ };
1179
+ return processCandidate(db, repo, work);
1180
+ }
1181
+ async function processDueMediaUsageWork(db) {
1182
+ const startedAt = Date.now();
1183
+ const result = {
1184
+ candidateCount: 0,
1185
+ claimedCount: 0,
1186
+ completedCount: 0,
1187
+ retryCount: 0,
1188
+ failedCount: 0,
1189
+ supersededCount: 0,
1190
+ obsoleteCount: 0,
1191
+ durationMs: 0,
1192
+ admissionClosed: false
1193
+ };
1194
+ if (!await isIncrementalCaptureActive(db)) {
1195
+ result.durationMs = Date.now() - startedAt;
1196
+ return result;
1197
+ }
1198
+ const repo = new MediaUsageWorkRepository(db);
1199
+ const candidates = await repo.findDueWork(MEDIA_USAGE_WORK_PROCESSING_LIMITS.candidatesPerTick);
1200
+ result.candidateCount = candidates.length;
1201
+ for (const candidate of candidates) {
1202
+ if (result.claimedCount >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.jobsPerTick || Date.now() - startedAt >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxTickDurationMs) {
1203
+ result.admissionClosed = true;
1204
+ break;
1205
+ }
1206
+ const processed = await processCandidate(db, repo, candidate);
1207
+ if (processed.claimed) result.claimedCount++;
1208
+ if (processed.outcome === "completed") result.completedCount++;
1209
+ if (processed.outcome === "retry") result.retryCount++;
1210
+ if (processed.outcome === "failed") result.failedCount++;
1211
+ if (processed.outcome === "superseded") result.supersededCount++;
1212
+ if (processed.outcome === "obsolete") result.obsoleteCount++;
1213
+ }
1214
+ result.durationMs = Date.now() - startedAt;
1215
+ return result;
1216
+ }
1217
+ async function processCandidate(db, repo, candidate) {
1218
+ const claimed = await repo.claimWork({
1219
+ collectionId: candidate.collectionId,
1220
+ contentId: candidate.contentId,
1221
+ workVersion: candidate.workVersion,
1222
+ leaseDurationSeconds: MEDIA_USAGE_WORK_PROCESSING_LIMITS.leaseDurationSeconds
1223
+ });
1224
+ if (!claimed?.leaseToken) return {
1225
+ outcome: "claim_lost",
1226
+ claimed: false
1227
+ };
1228
+ const lease = {
1229
+ collectionId: claimed.collectionId,
1230
+ contentId: claimed.contentId,
1231
+ workVersion: claimed.workVersion,
1232
+ leaseToken: claimed.leaseToken
1233
+ };
1234
+ if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
1235
+ outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
1236
+ claimed: true
1237
+ };
1238
+ const refresh = await refreshContentMediaUsageForWork(db, claimed.collectionId, claimed.collectionSlug, claimed.contentId);
1239
+ if (refresh.success) {
1240
+ const completed = await repo.completeWork(lease);
1241
+ if (completed) await new MediaUsageRepository(db).recordIncrementalSuccess({
1242
+ collectionId: claimed.collectionId,
1243
+ collectionSlug: claimed.collectionSlug
1244
+ });
1245
+ return {
1246
+ outcome: completed ? "completed" : "superseded",
1247
+ claimed: true
1248
+ };
1249
+ }
1250
+ if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
1251
+ outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
1252
+ claimed: true
1253
+ };
1254
+ const errorCode = processingErrorCode(refresh.errorCode);
1255
+ if (errorCode === "MEDIA_USAGE_RESOURCE_LIMIT" || claimed.attemptCount + 1 >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxAttempts) {
1256
+ const failed = await repo.failWork({
1257
+ ...lease,
1258
+ errorCode
1259
+ });
1260
+ if (failed) await new MediaUsageRepository(db).recordIncrementalFailure({
1261
+ collectionId: claimed.collectionId,
1262
+ collectionSlug: claimed.collectionSlug,
1263
+ contentId: claimed.contentId,
1264
+ workVersion: claimed.workVersion,
1265
+ errorCode
1266
+ });
1267
+ return {
1268
+ outcome: failed ? "failed" : "superseded",
1269
+ claimed: true
1270
+ };
1271
+ }
1272
+ return {
1273
+ outcome: await repo.retryWork({
1274
+ ...lease,
1275
+ errorCode,
1276
+ retryDelaySeconds: retryDelaySeconds(claimed.attemptCount)
1277
+ }) ? "retry" : "superseded",
1278
+ claimed: true
1279
+ };
1280
+ }
1281
+ async function isIncrementalCaptureActive(db) {
1282
+ return (await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state === "active";
1283
+ }
1284
+ async function collectionIdentityIsCurrent(db, collectionId, collectionSlug) {
1285
+ return await db.selectFrom("_emdash_collections").select("id").where("id", "=", collectionId).where("slug", "=", collectionSlug).executeTakeFirst() !== void 0;
1286
+ }
1287
+ function retryDelaySeconds(attemptCount) {
1288
+ const exponential = Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryBaseSeconds * 2 ** attemptCount);
1289
+ const jitter = Math.floor(exponential * MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryJitterRatio * Math.random());
1290
+ return Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, exponential + jitter);
1291
+ }
1292
+ function processingErrorCode(errorCode) {
1293
+ if (errorCode === "DRAFT_REVISION_NOT_FOUND" || errorCode === "DRAFT_REVISION_MISMATCH" || errorCode === "DRAFT_REVISION_INVALID") return "MEDIA_USAGE_SNAPSHOT_FAILED";
1294
+ if (errorCode === "CONTENT_USAGE_GENERATION_CONFLICT") return "MEDIA_USAGE_GENERATION_CONFLICT";
1295
+ if (errorCode === "CONTENT_USAGE_RESOURCE_LIMIT") return "MEDIA_USAGE_RESOURCE_LIMIT";
1296
+ return "MEDIA_USAGE_PROCESSING_FAILED";
141
1297
  }
142
1298
 
143
1299
  //#endregion
@@ -153,6 +1309,227 @@ function createSandboxRunnerOptions(options, siteInfo) {
153
1309
  };
154
1310
  }
155
1311
 
1312
+ //#endregion
1313
+ //#region src/media/usage/cleanup.ts
1314
+ const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;
1315
+ const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;
1316
+ const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;
1317
+ const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1e3;
1318
+ const MEDIA_USAGE_CLEANUP_LEASE_MS = 300 * 1e3;
1319
+ const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 3600 * 1e3;
1320
+ const MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1e3;
1321
+ /**
1322
+ * Reclaims a bounded window of obsolete media-usage occurrences.
1323
+ *
1324
+ * The persisted claim makes a cron tick single-flight across Worker isolates
1325
+ * and Node processes.
1326
+ */
1327
+ async function cleanupMediaUsage(db) {
1328
+ const startedMs = Date.now();
1329
+ const canIssueStatement = () => withinBudget(startedMs);
1330
+ const repo = new MediaUsageRepository(db);
1331
+ const leaseToken = ulid();
1332
+ const claim = await repo.claimMediaUsageCleanup({
1333
+ leaseToken,
1334
+ leaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1e3,
1335
+ nextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1e3,
1336
+ sweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1e3
1337
+ });
1338
+ if (!claim) return emptyResult("skipped", elapsedSince(startedMs));
1339
+ let candidateRows = 0;
1340
+ let deletedOrphans = 0;
1341
+ let deletedStale = 0;
1342
+ let deletedAbandoned = 0;
1343
+ let deletedWriteLeases = 0;
1344
+ let backlogLowerBound = 0;
1345
+ let scanHasMore = false;
1346
+ let nextCursor = claim.cursor;
1347
+ let sweepComplete = false;
1348
+ try {
1349
+ if (canIssueStatement()) deletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT, cleanupLease(leaseToken), canIssueStatement);
1350
+ if (canIssueStatement()) {
1351
+ const cutoff = claim.scanBeforeAt;
1352
+ const candidates = await repo.findMediaUsageCleanupCandidates({
1353
+ cutoff,
1354
+ cursor: claim.cursor,
1355
+ limit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,
1356
+ cleanupLease: cleanupLease(leaseToken)
1357
+ });
1358
+ candidateRows = candidates.length;
1359
+ scanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;
1360
+ const selected = selectCleanupCandidates(candidates, claim.claimedAt);
1361
+ backlogLowerBound = selected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;
1362
+ const completedTargets = /* @__PURE__ */ new Set();
1363
+ let canContinue = true;
1364
+ if (canIssueStatement() && selected.orphanIds.length > 0) {
1365
+ deletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(cutoff, selected.orphanIds.length, {
1366
+ candidateIds: selected.orphanIds,
1367
+ cleanupLease: cleanupLease(leaseToken),
1368
+ canIssueStatement
1369
+ });
1370
+ canContinue = deletedOrphans === selected.orphanIds.length;
1371
+ if (canContinue) completedTargets.add("orphan");
1372
+ }
1373
+ if (canContinue && canIssueStatement() && selected.staleIds.length > 0) {
1374
+ deletedStale = await repo.deleteStaleGenerationsOlderThan(cutoff, selected.staleIds.length, {
1375
+ candidateIds: selected.staleIds,
1376
+ cleanupLease: cleanupLease(leaseToken),
1377
+ canIssueStatement
1378
+ });
1379
+ canContinue = deletedStale === selected.staleIds.length;
1380
+ if (canContinue) completedTargets.add("stale");
1381
+ }
1382
+ if (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {
1383
+ deletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(cutoff, selected.abandonedIds.length, {
1384
+ candidateIds: selected.abandonedIds,
1385
+ cleanupLease: cleanupLease(leaseToken),
1386
+ canIssueStatement
1387
+ });
1388
+ if (deletedAbandoned === selected.abandonedIds.length) completedTargets.add("abandoned");
1389
+ }
1390
+ const hasIncompleteTargets = selected.entries.some((entry) => entry.target !== null && !completedTargets.has(entry.target));
1391
+ sweepComplete = !scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;
1392
+ nextCursor = sweepComplete ? null : cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);
1393
+ }
1394
+ const durationMs = elapsedSince(startedMs);
1395
+ return {
1396
+ status: await repo.completeMediaUsageCleanup({
1397
+ leaseToken,
1398
+ nextCursor,
1399
+ sweepComplete,
1400
+ candidateCount: candidateRows,
1401
+ deletedOrphans,
1402
+ deletedStale,
1403
+ deletedAbandoned,
1404
+ deletedWriteLeases,
1405
+ backlogLowerBound,
1406
+ scanHasMore,
1407
+ durationMs
1408
+ }) ? "completed" : "skipped",
1409
+ candidateRows,
1410
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
1411
+ deletedOrphans,
1412
+ deletedStale,
1413
+ deletedAbandoned,
1414
+ deletedWriteLeases,
1415
+ backlogLowerBound,
1416
+ scanHasMore,
1417
+ durationMs
1418
+ };
1419
+ } catch (error) {
1420
+ const durationMs = elapsedSince(startedMs);
1421
+ const failures = Math.min(claim.consecutiveFailures + 1, 5);
1422
+ try {
1423
+ await repo.failMediaUsageCleanup({
1424
+ leaseToken,
1425
+ retryDelaySeconds: failureDelayMs(failures) / 1e3,
1426
+ consecutiveFailures: failures,
1427
+ durationMs,
1428
+ errorCode: "MEDIA_USAGE_CLEANUP_FAILED"
1429
+ });
1430
+ } catch (failureError) {
1431
+ console.error("[media-usage-cleanup] Failed to record cleanup failure:", failureError);
1432
+ }
1433
+ console.error("[media-usage-cleanup] Cleanup failed:", error);
1434
+ return {
1435
+ ...emptyResult("failed", durationMs),
1436
+ candidateRows,
1437
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
1438
+ deletedOrphans,
1439
+ deletedStale,
1440
+ deletedAbandoned,
1441
+ deletedWriteLeases,
1442
+ backlogLowerBound,
1443
+ scanHasMore
1444
+ };
1445
+ }
1446
+ }
1447
+ function selectCleanupCandidates(candidates, activeLeaseAt) {
1448
+ const orphanIds = [];
1449
+ const staleIds = [];
1450
+ const abandonedIds = [];
1451
+ const entries = [];
1452
+ for (const candidate of candidates) {
1453
+ if (hasActiveWriteLease(candidate, activeLeaseAt)) {
1454
+ entries.push({
1455
+ candidate,
1456
+ target: null
1457
+ });
1458
+ continue;
1459
+ }
1460
+ const target = cleanupTarget(candidate);
1461
+ if (target === null) {
1462
+ entries.push({
1463
+ candidate,
1464
+ target: null
1465
+ });
1466
+ continue;
1467
+ }
1468
+ if (orphanIds.length + staleIds.length + abandonedIds.length >= MEDIA_USAGE_CLEANUP_DELETE_LIMIT) break;
1469
+ if (target === "orphan") orphanIds.push(candidate.id);
1470
+ if (target === "stale") staleIds.push(candidate.id);
1471
+ if (target === "abandoned") abandonedIds.push(candidate.id);
1472
+ entries.push({
1473
+ candidate,
1474
+ target
1475
+ });
1476
+ }
1477
+ return {
1478
+ orphanIds,
1479
+ staleIds,
1480
+ abandonedIds,
1481
+ entries
1482
+ };
1483
+ }
1484
+ function cleanupTarget(candidate) {
1485
+ if (candidate.currentGeneration === null) return "orphan";
1486
+ if (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null) return null;
1487
+ return candidate.createdAt < candidate.indexedAt ? "stale" : "abandoned";
1488
+ }
1489
+ function hasActiveWriteLease(candidate, activeLeaseAt) {
1490
+ return candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;
1491
+ }
1492
+ function cursorFor(candidate) {
1493
+ return {
1494
+ createdAt: candidate.createdAt,
1495
+ id: candidate.id
1496
+ };
1497
+ }
1498
+ function cursorAfterCompletedCandidates(selected, completedTargets, priorCursor) {
1499
+ let cursor = priorCursor;
1500
+ for (const entry of selected.entries) {
1501
+ if (entry.target !== null && !completedTargets.has(entry.target)) break;
1502
+ cursor = cursorFor(entry.candidate);
1503
+ }
1504
+ return cursor;
1505
+ }
1506
+ function emptyResult(status, durationMs) {
1507
+ return {
1508
+ status,
1509
+ candidateRows: 0,
1510
+ deletedRows: 0,
1511
+ deletedOrphans: 0,
1512
+ deletedStale: 0,
1513
+ deletedAbandoned: 0,
1514
+ deletedWriteLeases: 0,
1515
+ backlogLowerBound: 0,
1516
+ scanHasMore: false,
1517
+ durationMs
1518
+ };
1519
+ }
1520
+ function withinBudget(startedMs) {
1521
+ return elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;
1522
+ }
1523
+ function elapsedSince(startedMs) {
1524
+ return Math.max(0, Date.now() - startedMs);
1525
+ }
1526
+ function failureDelayMs(consecutiveFailures) {
1527
+ return Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;
1528
+ }
1529
+ function cleanupLease(leaseToken) {
1530
+ return { leaseToken };
1531
+ }
1532
+
156
1533
  //#endregion
157
1534
  //#region src/cleanup.ts
158
1535
  /**
@@ -165,15 +1542,13 @@ function createSandboxRunnerOptions(options, siteInfo) {
165
1542
  * Each subsystem cleanup is independent and non-fatal -- if one fails, the
166
1543
  * rest still run. Failures are logged but never surface to callers.
167
1544
  */
168
- /** Max revisions to keep per entry during periodic pruning */
169
1545
  const REVISION_KEEP_COUNT = 50;
170
- /** Only prune entries that exceed this threshold */
171
- const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;
1546
+ const REVISION_PRUNE_BATCH_SIZE = 10;
172
1547
  /**
173
1548
  * Run all system cleanup tasks.
174
1549
  *
175
- * Safe to call frequently -- each task is a single DELETE with a WHERE clause,
176
- * so repeated calls with nothing to clean are cheap (no-op queries).
1550
+ * Safe to call frequently -- each subsystem tolerates repeated calls, and
1551
+ * repeated calls with nothing to clean are cheap.
177
1552
  *
178
1553
  * @param db - The database instance
179
1554
  * @param storage - Optional storage backend for deleting orphaned files.
@@ -187,7 +1562,8 @@ async function runSystemCleanup(db, storage) {
187
1562
  pendingUploads: -1,
188
1563
  pendingUploadFiles: -1,
189
1564
  uploadAttempts: -1,
190
- revisionsPruned: -1
1565
+ revisionsPruned: -1,
1566
+ mediaUsage: -1
191
1567
  };
192
1568
  try {
193
1569
  result.challenges = await cleanupExpiredChallenges(db);
@@ -230,29 +1606,24 @@ async function runSystemCleanup(db, storage) {
230
1606
  console.error("[cleanup] Failed to clean media upload attempts:", error);
231
1607
  }
232
1608
  try {
233
- result.revisionsPruned = await pruneExcessiveRevisions(db);
1609
+ result.revisionsPruned = await pruneQueuedRevisions(db);
234
1610
  } catch (error) {
235
1611
  console.error("[cleanup] Failed to prune revisions:", error);
236
1612
  }
1613
+ try {
1614
+ const mediaUsage = await cleanupMediaUsage(db);
1615
+ result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
1616
+ } catch (error) {
1617
+ console.error("[cleanup] Failed to clean media usage:", error);
1618
+ }
237
1619
  return result;
238
1620
  }
239
- /**
240
- * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune
241
- * them down to REVISION_KEEP_COUNT.
242
- */
243
- async function pruneExcessiveRevisions(db) {
244
- const entries = await sql`
245
- SELECT collection, entry_id
246
- FROM revisions
247
- GROUP BY collection, entry_id
248
- HAVING COUNT(*) > ${REVISION_PRUNE_THRESHOLD}
249
- `.execute(db);
250
- if (entries.rows.length === 0) return 0;
1621
+ async function pruneQueuedRevisions(db) {
1622
+ const queued = await db.selectFrom("_emdash_revision_prune_queue").selectAll().orderBy("revision_id").limit(REVISION_PRUNE_BATCH_SIZE).execute();
251
1623
  const revisionRepo = new RevisionRepository(db);
252
1624
  let totalPruned = 0;
253
- for (const row of entries.rows) try {
254
- const pruned = await revisionRepo.pruneOldRevisions(row.collection, row.entry_id, REVISION_KEEP_COUNT);
255
- totalPruned += pruned;
1625
+ for (const row of queued) try {
1626
+ totalPruned += await revisionRepo.pruneQueuedEntry(row.collection, row.entry_id, row.revision_id, REVISION_KEEP_COUNT);
256
1627
  } catch (error) {
257
1628
  console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
258
1629
  }
@@ -330,7 +1701,8 @@ async function publishDueContent(db, options = {}) {
330
1701
  const publishedAt = item.publishedAt == null ? item.scheduledAt ?? void 0 : void 0;
331
1702
  const result = await doPublish(collection.slug, item.id, {
332
1703
  publishedAt,
333
- requireScheduledDue: true
1704
+ requireScheduledDue: true,
1705
+ expectedScheduledAt: item.scheduledAt ?? void 0
334
1706
  });
335
1707
  if (result.success) batch.push({
336
1708
  collection: collection.slug,
@@ -440,6 +1812,16 @@ const DRAFT_ONLY_UPDATE_KEYS = new Set([
440
1812
  "locale",
441
1813
  "skipRevision"
442
1814
  ]);
1815
+ const MAX_DRAFT_STAGE_ATTEMPTS = 32;
1816
+ const LIST_COLUMN_FIELD_TYPES = new Set([
1817
+ "string",
1818
+ "number",
1819
+ "integer",
1820
+ "boolean",
1821
+ "datetime",
1822
+ "select",
1823
+ "multiSelect"
1824
+ ]);
443
1825
  /**
444
1826
  * Convert a ContentItem to Record<string, unknown> for hook consumption.
445
1827
  * Hooks receive the full item as a flat record.
@@ -523,6 +1905,49 @@ const marketplaceManifestCache = /* @__PURE__ */ new Map();
523
1905
  /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
524
1906
  const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
525
1907
  let sandboxRunner = null;
1908
+ const MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS = Object.freeze({
1909
+ entryWork: MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob,
1910
+ collectionDeletion: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick,
1911
+ reconciliation: MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick,
1912
+ maxClassQueries: Math.max(MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick, MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick),
1913
+ eventCeiling: 40
1914
+ });
1915
+ async function runScheduledMediaUsageLane(db) {
1916
+ if ((getRequestContext()?.metrics?.dbCount ?? 0) + 1 + MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.maxClassQueries > MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.eventCeiling) return {
1917
+ outcome: "admission_closed",
1918
+ taskClass: null,
1919
+ turn: null
1920
+ };
1921
+ const activation = await db.updateTable("_emdash_media_usage_activation").set({ media_usage_maintenance_turn: sql`(media_usage_maintenance_turn + 1) % 3` }).where("task_key", "=", "incremental_capture").where("state", "=", "active").returning("media_usage_maintenance_turn").executeTakeFirst();
1922
+ if (!activation) return {
1923
+ outcome: "inactive",
1924
+ taskClass: null,
1925
+ turn: null
1926
+ };
1927
+ const turn = activation.media_usage_maintenance_turn;
1928
+ if (turn === 0) {
1929
+ await processDueMediaUsageWork(db);
1930
+ return {
1931
+ outcome: "processed",
1932
+ taskClass: "entry_work",
1933
+ turn
1934
+ };
1935
+ }
1936
+ if (turn === 1) {
1937
+ await processDueMediaUsageCollectionDeletions(db);
1938
+ return {
1939
+ outcome: "processed",
1940
+ taskClass: "collection_deletion",
1941
+ turn
1942
+ };
1943
+ }
1944
+ await processDueMediaUsageReconciliation(db);
1945
+ return {
1946
+ outcome: "processed",
1947
+ taskClass: "reconciliation",
1948
+ turn
1949
+ };
1950
+ }
526
1951
  /**
527
1952
  * EmDashRuntime - singleton per worker
528
1953
  */
@@ -574,6 +1999,8 @@ var EmDashRuntime = class EmDashRuntime {
574
1999
  /** All plugins eligible for the hook pipeline (includes built-in plugins).
575
2000
  * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
576
2001
  allPipelinePlugins;
2002
+ /** Guards the once-per-process plugin storage-index sync. */
2003
+ storageIndexesSynced = false;
577
2004
  /** Factory options for the hook pipeline context factory */
578
2005
  pipelineFactoryOptions;
579
2006
  /** Dependencies needed for exclusive hook resolution */
@@ -632,7 +2059,14 @@ var EmDashRuntime = class EmDashRuntime {
632
2059
  * Returns the items promoted so callers can invalidate their cache tags.
633
2060
  */
634
2061
  async publishScheduled() {
635
- return publishDueContent(this.db, { publish: (collection, id, options) => this.handleContentPublish(collection, id, options) });
2062
+ return this.publishScheduledWithFence();
2063
+ }
2064
+ async publishScheduledWithFence(onPublished) {
2065
+ await assertMediaUsageActivationWriteAllowed(this.db);
2066
+ return publishDueContent(this.db, {
2067
+ publish: (collection, id, options) => this.handleContentPublish(collection, id, options),
2068
+ onPublished
2069
+ });
636
2070
  }
637
2071
  /**
638
2072
  * Run the full scheduled-maintenance batch: cron tasks, scheduled
@@ -663,10 +2097,7 @@ var EmDashRuntime = class EmDashRuntime {
663
2097
  }
664
2098
  let published = [];
665
2099
  try {
666
- published = await publishDueContent(this.db, {
667
- publish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),
668
- onPublished: options.onPublished
669
- });
2100
+ published = await this.publishScheduledWithFence(options.onPublished);
670
2101
  } catch (error) {
671
2102
  console.error("[scheduled-publish] Sweep failed:", error);
672
2103
  }
@@ -675,9 +2106,29 @@ var EmDashRuntime = class EmDashRuntime {
675
2106
  } catch (error) {
676
2107
  console.error("[cleanup] System cleanup failed:", error);
677
2108
  }
2109
+ try {
2110
+ await this.syncPluginStorageIndexesOnce();
2111
+ } catch (error) {
2112
+ console.error("[plugins] Storage index sync failed:", error);
2113
+ }
678
2114
  await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
2115
+ await recordSchedulerHeartbeatSafely(this.db);
679
2116
  return { published };
680
2117
  }
2118
+ async runScheduledMediaUsageTasks() {
2119
+ return runScheduledMediaUsageLane(this.db);
2120
+ }
2121
+ /**
2122
+ * Materialize plugin-declared storage indexes, once per process.
2123
+ *
2124
+ * Called from the scheduler path, not from request handlers — configured
2125
+ * plugins have no install handler, so the tick is their only sync moment.
2126
+ */
2127
+ async syncPluginStorageIndexesOnce() {
2128
+ if (this.storageIndexesSynced) return;
2129
+ this.storageIndexesSynced = true;
2130
+ await syncDeclaredStorageIndexes(this.db, [...this.allPipelinePlugins, ...marketplaceManifestCache.values()]);
2131
+ }
681
2132
  /**
682
2133
  * Stop the cron scheduler gracefully.
683
2134
  * Call during worker shutdown or hot-reload.
@@ -819,7 +2270,8 @@ var EmDashRuntime = class EmDashRuntime {
819
2270
  id: bundle.manifest.id,
820
2271
  version: bundle.manifest.version,
821
2272
  admin: bundle.manifest.admin,
822
- mcp: bundle.manifest.mcp
2273
+ mcp: bundle.manifest.mcp,
2274
+ storage: bundle.manifest.storage
823
2275
  });
824
2276
  if (bundle.manifest.routes.length > 0) {
825
2277
  const routeMetaMap = /* @__PURE__ */ new Map();
@@ -904,7 +2356,8 @@ var EmDashRuntime = class EmDashRuntime {
904
2356
  id: bundle.manifest.id,
905
2357
  version: bundle.manifest.version,
906
2358
  admin: bundle.manifest.admin,
907
- mcp: bundle.manifest.mcp
2359
+ mcp: bundle.manifest.mcp,
2360
+ storage: bundle.manifest.storage
908
2361
  });
909
2362
  if (bundle.manifest.routes.length > 0) {
910
2363
  const routeMetaMap = /* @__PURE__ */ new Map();
@@ -1047,9 +2500,9 @@ var EmDashRuntime = class EmDashRuntime {
1047
2500
  const seedHolder = getSeedHolder();
1048
2501
  try {
1049
2502
  await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
1050
- const { applySeed } = await import("../apply-1_6ra7NP.mjs").then((n) => n.n);
1051
- const { loadSeed } = await import("../load-BwTdWE8B.mjs").then((n) => n.r);
1052
- const { validateSeed } = await import("../validate-Bs_wT2ul.mjs").then((n) => n.n);
2503
+ const { applySeed } = await import("../apply-D5zfeD09.mjs").then((n) => n.n);
2504
+ const { loadSeed } = await import("../load-DmvJrtxS.mjs").then((n) => n.r);
2505
+ const { validateSeed } = await import("../validate-BkqIG_Uj.mjs").then((n) => n.n);
1053
2506
  const seed = await loadSeed();
1054
2507
  if (validateSeed(seed).valid) {
1055
2508
  await applySeed(db, seed, { onConflict: "skip" });
@@ -1130,6 +2583,7 @@ var EmDashRuntime = class EmDashRuntime {
1130
2583
  const pipelineFactoryOptions = {
1131
2584
  db,
1132
2585
  getDb: resolveDb,
2586
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
1133
2587
  storage: storage ?? void 0,
1134
2588
  siteInfo
1135
2589
  };
@@ -1180,10 +2634,19 @@ var EmDashRuntime = class EmDashRuntime {
1180
2634
  if (deps.createScheduler) {
1181
2635
  const scheduler = deps.createScheduler(cronExecutor);
1182
2636
  cronScheduler = scheduler;
2637
+ const runMediaUsageMaintenance = async () => {
2638
+ const runtime = runtimeRef.current;
2639
+ if (runtime) await runtime.runScheduledMediaUsageTasks();
2640
+ else await runScheduledMediaUsageLane(db);
2641
+ };
1183
2642
  scheduler.setSystemCleanup(async () => {
1184
2643
  try {
1185
2644
  const runtime = runtimeRef.current;
1186
- await publishDueContent(db, { publish: runtime ? (collection, id, options) => runtime.handleContentPublish(collection, id, options) : void 0 });
2645
+ if (runtime) await runtime.publishScheduled();
2646
+ else {
2647
+ await assertMediaUsageActivationWriteAllowed(db);
2648
+ await publishDueContent(db);
2649
+ }
1187
2650
  } catch (error) {
1188
2651
  console.error("[scheduled-publish] Sweep failed:", error);
1189
2652
  }
@@ -1192,8 +2655,20 @@ var EmDashRuntime = class EmDashRuntime {
1192
2655
  } catch (error) {
1193
2656
  console.error("[cleanup] System cleanup failed:", error);
1194
2657
  }
2658
+ try {
2659
+ await runtimeRef.current?.syncPluginStorageIndexesOnce();
2660
+ } catch (error) {
2661
+ console.error("[plugins] Storage index sync failed:", error);
2662
+ }
1195
2663
  await maybeRunScheduledBackup(db, storage ?? void 0);
2664
+ await recordSchedulerHeartbeatSafely(db);
2665
+ if (!scheduler.setMediaUsageMaintenance) try {
2666
+ await runMediaUsageMaintenance();
2667
+ } catch (error) {
2668
+ console.error("[media-usage] Scheduled maintenance failed:", error);
2669
+ }
1196
2670
  });
2671
+ scheduler.setMediaUsageMaintenance?.(runMediaUsageMaintenance);
1197
2672
  scheduler.start();
1198
2673
  }
1199
2674
  } catch (error) {
@@ -1353,6 +2828,7 @@ var EmDashRuntime = class EmDashRuntime {
1353
2828
  if (!deps.sandboxEnabled) return sandboxedPluginCache;
1354
2829
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1355
2830
  db,
2831
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1356
2832
  mediaStorage: mediaStorage ? {
1357
2833
  upload: (opts) => mediaStorage.upload({
1358
2834
  key: opts.key,
@@ -1379,14 +2855,22 @@ var EmDashRuntime = class EmDashRuntime {
1379
2855
  capabilities: entry.capabilities ?? [],
1380
2856
  allowedHosts: entry.allowedHosts ?? [],
1381
2857
  storage: entry.storage ?? {},
1382
- hooks: [],
1383
- routes: [],
2858
+ hooks: entry.hooks ?? [],
2859
+ routes: entry.routes ?? [],
1384
2860
  admin: {},
1385
2861
  mcp: entry.mcp
1386
2862
  };
1387
2863
  const plugin = await sandboxRunner.load(manifest, entry.code);
1388
2864
  sandboxedPluginCache.set(pluginKey, plugin);
1389
2865
  console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
2866
+ if (manifest.routes.length > 0) {
2867
+ const routeMetaMap = /* @__PURE__ */ new Map();
2868
+ for (const routeEntry of manifest.routes) {
2869
+ const normalized = normalizeManifestRoute(routeEntry);
2870
+ routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
2871
+ }
2872
+ sandboxedRouteMetaCache.set(entry.id, routeMetaMap);
2873
+ } else sandboxedRouteMetaCache.delete(entry.id);
1390
2874
  } catch (error) {
1391
2875
  console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
1392
2876
  }
@@ -1410,6 +2894,7 @@ var EmDashRuntime = class EmDashRuntime {
1410
2894
  static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
1411
2895
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1412
2896
  db,
2897
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1413
2898
  mediaStorage: {
1414
2899
  upload: (opts) => storage.upload({
1415
2900
  key: opts.key,
@@ -1443,7 +2928,8 @@ var EmDashRuntime = class EmDashRuntime {
1443
2928
  id: bundle.manifest.id,
1444
2929
  version: bundle.manifest.version,
1445
2930
  admin: bundle.manifest.admin,
1446
- mcp: bundle.manifest.mcp
2931
+ mcp: bundle.manifest.mcp,
2932
+ storage: bundle.manifest.storage
1447
2933
  });
1448
2934
  if (bundle.manifest.routes.length > 0) {
1449
2935
  const routeMeta = /* @__PURE__ */ new Map();
@@ -1493,7 +2979,8 @@ var EmDashRuntime = class EmDashRuntime {
1493
2979
  id: bundle.manifest.id,
1494
2980
  version: bundle.manifest.version,
1495
2981
  admin: bundle.manifest.admin,
1496
- mcp: bundle.manifest.mcp
2982
+ mcp: bundle.manifest.mcp,
2983
+ storage: bundle.manifest.storage
1497
2984
  });
1498
2985
  if (bundle.manifest.routes.length > 0) {
1499
2986
  const routeMeta = /* @__PURE__ */ new Map();
@@ -1609,12 +3096,33 @@ var EmDashRuntime = class EmDashRuntime {
1609
3096
  if ((field.type === "repeater" || field.type === "file" || field.type === "image") && field.validation) entry.validation = { ...field.validation };
1610
3097
  fields[field.slug] = entry;
1611
3098
  }
3099
+ const configuredListColumns = collection.admin?.listColumns ?? [];
3100
+ const fieldTypes = new Map(collection.fields.map((field) => [field.slug, field.type]));
3101
+ const listColumns = [];
3102
+ for (const slug of configuredListColumns) {
3103
+ if (listColumns.includes(slug)) continue;
3104
+ const fieldType = fieldTypes.get(slug);
3105
+ if (!fieldType || !LIST_COLUMN_FIELD_TYPES.has(fieldType)) {
3106
+ console.warn(`EmDash: Ignoring unsupported or unknown list column "${slug}" in collection "${collection.slug}".`);
3107
+ continue;
3108
+ }
3109
+ if (listColumns.length >= MAX_COLLECTION_LIST_COLUMNS) {
3110
+ console.warn(`EmDash: Collection "${collection.slug}" declares more than ${MAX_COLLECTION_LIST_COLUMNS} list columns; extra columns are ignored.`);
3111
+ break;
3112
+ }
3113
+ listColumns.push(slug);
3114
+ }
1612
3115
  manifestCollections[collection.slug] = {
1613
3116
  label: collection.label,
1614
3117
  labelSingular: collection.labelSingular || collection.label,
1615
3118
  supports: collection.supports || [],
1616
3119
  hasSeo: collection.hasSeo,
1617
3120
  urlPattern: collection.urlPattern,
3121
+ routable: collection.routable !== false,
3122
+ titleField: collection.titleField,
3123
+ dateField: collection.dateField,
3124
+ ...collection.hidden ? { hidden: true } : {},
3125
+ listColumns: listColumns.length > 0 ? listColumns : void 0,
1618
3126
  fields
1619
3127
  };
1620
3128
  }
@@ -1674,21 +3182,33 @@ var EmDashRuntime = class EmDashRuntime {
1674
3182
  };
1675
3183
  }
1676
3184
  let manifestTaxonomies = [];
3185
+ let taxonomyDefinitionLocales = [];
1677
3186
  try {
1678
- manifestTaxonomies = (await this.db.selectFrom("_emdash_taxonomy_defs").selectAll().orderBy("name").execute()).map((row) => ({
3187
+ const rows = await this.db.selectFrom("_emdash_taxonomy_defs").selectAll().orderBy("name").execute();
3188
+ taxonomyDefinitionLocales = rows.map((row) => row.locale);
3189
+ manifestTaxonomies = rows.map((row) => ({
3190
+ id: row.id,
1679
3191
  name: row.name,
1680
3192
  label: row.label,
1681
3193
  labelSingular: row.label_singular ?? void 0,
1682
3194
  hierarchical: row.hierarchical === 1,
1683
- collections: parseStringArray(row.collections).toSorted()
3195
+ collections: parseStringArray(row.collections).toSorted(),
3196
+ locale: row.locale,
3197
+ translationGroup: row.translation_group ?? row.id
1684
3198
  }));
1685
3199
  } catch (error) {
1686
3200
  console.debug("EmDash: Could not load taxonomy definitions:", error);
1687
3201
  }
3202
+ try {
3203
+ const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
3204
+ await warnAboutUnconfiguredTaxonomyLocales(this.db, configuredLocales, taxonomyDefinitionLocales);
3205
+ } catch (error) {
3206
+ console.warn("[i18n] taxonomy locale diagnostic failed:", error);
3207
+ }
1688
3208
  const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
1689
3209
  const authMode = getAuthMode(this.config);
1690
3210
  const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
1691
- const i18nConfig = virtualConfig?.i18n;
3211
+ const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
1692
3212
  const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
1693
3213
  defaultLocale: i18nConfig.defaultLocale,
1694
3214
  locales: i18nConfig.locales
@@ -1704,6 +3224,10 @@ var EmDashRuntime = class EmDashRuntime {
1704
3224
  taxonomies: manifestTaxonomies,
1705
3225
  authMode: authModeValue,
1706
3226
  i18n,
3227
+ contentLocale: {
3228
+ defaultLocale: i18nConfig?.defaultLocale ?? "en",
3229
+ implicit: i18nConfig === null
3230
+ },
1707
3231
  marketplace: !!this.config.marketplace,
1708
3232
  registry
1709
3233
  };
@@ -1809,7 +3333,7 @@ var EmDashRuntime = class EmDashRuntime {
1809
3333
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
1810
3334
  processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
1811
3335
  processedData = await this.normalizeMediaFields(collection, processedData);
1812
- const { validateContentData } = await import("../validation-BngR0nit.mjs");
3336
+ const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
1813
3337
  const validation = await validateContentData(this.db, collection, processedData, { partial: false });
1814
3338
  if (!validation.ok) return {
1815
3339
  success: false,
@@ -1826,7 +3350,6 @@ var EmDashRuntime = class EmDashRuntime {
1826
3350
  return result;
1827
3351
  }
1828
3352
  async handleContentUpdate(collection, id, body) {
1829
- const { ContentRepository } = await import("../content-CpfKV9QE.mjs").then((n) => n.n);
1830
3353
  const repo = new ContentRepository(this.db);
1831
3354
  const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
1832
3355
  const resolvedId = resolvedItem?.id ?? id;
@@ -1853,7 +3376,7 @@ var EmDashRuntime = class EmDashRuntime {
1853
3376
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
1854
3377
  processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
1855
3378
  processedData = await this.normalizeMediaFields(collection, processedData);
1856
- const { validateContentData } = await import("../validation-BngR0nit.mjs");
3379
+ const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
1857
3380
  const validation = await validateContentData(this.db, collection, processedData, { partial: true });
1858
3381
  if (!validation.ok) return {
1859
3382
  success: false,
@@ -1862,12 +3385,12 @@ var EmDashRuntime = class EmDashRuntime {
1862
3385
  }
1863
3386
  let usesDraftRevisions = false;
1864
3387
  let draftStorageChanged = false;
1865
- if (processedData) try {
3388
+ if (processedData) {
1866
3389
  if ((await this.schemaRegistry.getCollectionWithFields(collection))?.supports?.includes("revisions")) {
1867
3390
  usesDraftRevisions = true;
1868
3391
  const revisionRepo = new RevisionRepository(this.db);
1869
- const existing = await repo.findById(collection, resolvedId);
1870
- if (existing) {
3392
+ let existing = await repo.findById(collection, resolvedId);
3393
+ for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
1871
3394
  let baseData;
1872
3395
  if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
1873
3396
  else baseData = existing.data;
@@ -1876,37 +3399,64 @@ var EmDashRuntime = class EmDashRuntime {
1876
3399
  ...processedData
1877
3400
  };
1878
3401
  if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
1879
- if (bodyWithoutRev.skipRevision && existing.draftRevisionId) {
1880
- await revisionRepo.updateData(existing.draftRevisionId, mergedData);
1881
- draftStorageChanged = true;
1882
- } else {
1883
- const revision = await revisionRepo.create({
1884
- collection,
1885
- entryId: resolvedId,
1886
- data: mergedData,
1887
- authorId: bodyWithoutRev.authorId ?? void 0
1888
- });
1889
- validateIdentifier(collection, "collection");
1890
- const tableName = `ec_${collection}`;
1891
- await sql`
1892
- UPDATE ${sql.ref(tableName)}
1893
- SET draft_revision_id = ${revision.id}
1894
- WHERE id = ${resolvedId}
1895
- `.execute(this.db);
1896
- draftStorageChanged = true;
1897
- revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});
3402
+ const revision = await revisionRepo.create({
3403
+ collection,
3404
+ entryId: resolvedId,
3405
+ data: mergedData,
3406
+ authorId: bodyWithoutRev.authorId ?? void 0
3407
+ });
3408
+ let staged;
3409
+ try {
3410
+ staged = await repo.replaceDraftRevision(collection, resolvedId, revision.id, existing);
3411
+ } catch (error) {
3412
+ try {
3413
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
3414
+ } catch (cleanupError) {
3415
+ console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
3416
+ }
3417
+ throw error;
1898
3418
  }
3419
+ if (!staged) {
3420
+ try {
3421
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
3422
+ } catch (cleanupError) {
3423
+ console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
3424
+ }
3425
+ if (body._rev || attempt === MAX_DRAFT_STAGE_ATTEMPTS - 1) return {
3426
+ success: false,
3427
+ error: {
3428
+ code: "CONFLICT",
3429
+ message: new ContentMutationConflictError().message
3430
+ }
3431
+ };
3432
+ existing = await repo.findById(collection, resolvedId);
3433
+ continue;
3434
+ }
3435
+ draftStorageChanged = true;
3436
+ if (bodyWithoutRev.skipRevision && existing.draftRevisionId) try {
3437
+ await revisionRepo.deleteIfUnreferenced(collection, resolvedId, existing.draftRevisionId);
3438
+ } catch (error) {
3439
+ console.error(`[emdash] Failed to clean up superseded revision ${existing.draftRevisionId}:`, error);
3440
+ }
3441
+ else after(async () => {
3442
+ try {
3443
+ await revisionRepo.pruneQueuedEntry(collection, resolvedId, revision.id, 50);
3444
+ } catch (error) {
3445
+ console.error(`[revisions] Failed to prune revisions for ${collection}/${resolvedId}:`, error);
3446
+ }
3447
+ });
3448
+ break;
1899
3449
  }
1900
3450
  }
1901
- } catch {}
1902
- const result = await handleContentUpdate(this.db, collection, resolvedId, {
3451
+ }
3452
+ const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
3453
+ const result = usesDraftRevisions && !liveMetaTouched ? await handleContentGet(this.db, collection, resolvedId) : await handleContentUpdate(this.db, collection, resolvedId, {
1903
3454
  ...bodyWithoutRev,
1904
3455
  data: usesDraftRevisions ? void 0 : processedData,
1905
3456
  slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
1906
3457
  authorId: bodyWithoutRev.authorId,
1907
3458
  bylines: bodyWithoutRev.bylines
1908
3459
  });
1909
- const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
1910
3460
  const liveContentChanged = usesDraftRevisions ? liveMetaTouched : Boolean(processedData || bodyWithoutRev.slug !== void 0 || liveMetaTouched);
1911
3461
  const hydrated = await this.hydrateDraftData(result);
1912
3462
  if (hydrated.success && hydrated.data) {
@@ -2086,25 +3636,50 @@ var EmDashRuntime = class EmDashRuntime {
2086
3636
  return this.hydrateDraftData(result);
2087
3637
  }
2088
3638
  try {
3639
+ const contentRepo = new ContentRepository(this.db);
3640
+ const existing = await contentRepo.findById(revision.collection, revision.entryId);
3641
+ if (!existing) return {
3642
+ success: false,
3643
+ error: {
3644
+ code: "NOT_FOUND",
3645
+ message: `Content item not found: ${revision.entryId}`
3646
+ }
3647
+ };
2089
3648
  const newDraft = await revisionRepo.create({
2090
3649
  collection: revision.collection,
2091
3650
  entryId: revision.entryId,
2092
3651
  data: revision.data,
2093
3652
  authorId: callerUserId
2094
3653
  });
2095
- validateIdentifier(revision.collection, "collection");
2096
- const tableName = `ec_${revision.collection}`;
2097
- await sql`
2098
- UPDATE ${sql.ref(tableName)}
2099
- SET draft_revision_id = ${newDraft.id}
2100
- WHERE id = ${revision.entryId}
2101
- `.execute(this.db);
2102
- revisionRepo.pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});
3654
+ try {
3655
+ if (!await contentRepo.replaceDraftRevision(revision.collection, revision.entryId, newDraft.id, existing)) throw new ContentMutationConflictError();
3656
+ } catch (error) {
3657
+ try {
3658
+ await revisionRepo.deleteIfUnreferenced(revision.collection, revision.entryId, newDraft.id);
3659
+ } catch (cleanupError) {
3660
+ console.error(`[emdash] Failed to clean up unrestored revision ${newDraft.id}:`, cleanupError);
3661
+ }
3662
+ throw error;
3663
+ }
3664
+ after(async () => {
3665
+ try {
3666
+ await revisionRepo.pruneQueuedEntry(revision.collection, revision.entryId, newDraft.id, 50);
3667
+ } catch (error) {
3668
+ console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
3669
+ }
3670
+ });
2103
3671
  const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
2104
3672
  const hydrated = await this.hydrateDraftData(refetched);
2105
3673
  if (hydrated.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
2106
3674
  return hydrated;
2107
3675
  } catch (error) {
3676
+ if (error instanceof ContentMutationConflictError) return {
3677
+ success: false,
3678
+ error: {
3679
+ code: "CONFLICT",
3680
+ message: error.message
3681
+ }
3682
+ };
2108
3683
  console.error("[emdash] revision restore failed:", error);
2109
3684
  return {
2110
3685
  success: false,
@@ -2117,13 +3692,16 @@ var EmDashRuntime = class EmDashRuntime {
2117
3692
  }
2118
3693
  async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
2119
3694
  for (const contentId of new Set(contentIds)) try {
3695
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2120
3696
  await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
2121
3697
  } catch (error) {
2122
3698
  console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
3699
+ return;
2123
3700
  }
2124
3701
  }
2125
3702
  async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
2126
3703
  try {
3704
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2127
3705
  const result = await deleteContentMediaUsage(this.db, collection, contentId);
2128
3706
  if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
2129
3707
  } catch (error) {
@@ -2169,7 +3747,7 @@ var EmDashRuntime = class EmDashRuntime {
2169
3747
  if (!meta) return null;
2170
3748
  return meta.admin?.settingsSchema ?? {};
2171
3749
  }
2172
- async handlePluginApiRoute(pluginId, _method, path, request) {
3750
+ async handlePluginApiRoute(pluginId, _method, path, request, user) {
2173
3751
  if (!this.isPluginEnabled(pluginId)) return {
2174
3752
  success: false,
2175
3753
  error: {
@@ -2177,6 +3755,7 @@ var EmDashRuntime = class EmDashRuntime {
2177
3755
  message: `Plugin not enabled: ${pluginId}`
2178
3756
  }
2179
3757
  };
3758
+ const caller = user ? toRouteCallerInfo(user) : void 0;
2180
3759
  const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
2181
3760
  if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
2182
3761
  const routeRegistry = new PluginRouteRegistry({
@@ -2187,17 +3766,15 @@ var EmDashRuntime = class EmDashRuntime {
2187
3766
  });
2188
3767
  routeRegistry.register(trustedPlugin);
2189
3768
  const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
2190
- let body = void 0;
2191
- try {
2192
- body = await request.json();
2193
- } catch {}
3769
+ const body = await parseRouteInput(request);
2194
3770
  return routeRegistry.invoke(pluginId, routeKey, {
2195
3771
  request,
2196
- body
3772
+ body,
3773
+ user: caller
2197
3774
  });
2198
3775
  }
2199
3776
  const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
2200
- if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
3777
+ if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request, caller);
2201
3778
  return {
2202
3779
  success: false,
2203
3780
  error: {
@@ -2272,7 +3849,7 @@ var EmDashRuntime = class EmDashRuntime {
2272
3849
  ...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
2273
3850
  })).toSorted((a, b) => a.name.localeCompare(b.name)));
2274
3851
  }
2275
- async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request) {
3852
+ async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller) {
2276
3853
  const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
2277
3854
  const audit = new AuditRepository(this.db);
2278
3855
  const headers = new Headers(request.headers);
@@ -2283,7 +3860,7 @@ var EmDashRuntime = class EmDashRuntime {
2283
3860
  headers,
2284
3861
  body: JSON.stringify(input)
2285
3862
  });
2286
- const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest);
3863
+ const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller);
2287
3864
  await audit.log({
2288
3865
  actorId,
2289
3866
  actorIp: requestMeta.ip ?? void 0,
@@ -2332,7 +3909,8 @@ var EmDashRuntime = class EmDashRuntime {
2332
3909
  }
2333
3910
  if (!collectionInfo?.fields) return data;
2334
3911
  const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
2335
- if (imageFields.length === 0) return data;
3912
+ const repeaterFields = collectionInfo.fields.filter((f) => f.type === "repeater" && Array.isArray(f.validation?.subFields));
3913
+ if (imageFields.length === 0 && repeaterFields.length === 0) return data;
2336
3914
  const getProvider = (id) => this.getMediaProvider(id);
2337
3915
  const result = { ...data };
2338
3916
  for (const field of imageFields) {
@@ -2343,6 +3921,26 @@ var EmDashRuntime = class EmDashRuntime {
2343
3921
  if (normalized) result[field.slug] = normalized;
2344
3922
  } catch {}
2345
3923
  }
3924
+ for (const field of repeaterFields) {
3925
+ const value = result[field.slug];
3926
+ if (!Array.isArray(value)) continue;
3927
+ const mediaSubFieldSlugs = (field.validation?.subFields ?? []).filter((sub) => sub.type === "image").map((sub) => sub.slug);
3928
+ if (mediaSubFieldSlugs.length === 0) continue;
3929
+ const items = value;
3930
+ result[field.slug] = await Promise.all(items.map(async (item) => {
3931
+ if (!isRecord(item)) return item;
3932
+ const normalizedItem = { ...item };
3933
+ for (const slug of mediaSubFieldSlugs) {
3934
+ const subValue = normalizedItem[slug];
3935
+ if (subValue == null) continue;
3936
+ try {
3937
+ const normalized = await normalizeMediaValue(subValue, getProvider);
3938
+ if (normalized) normalizedItem[slug] = normalized;
3939
+ } catch {}
3940
+ }
3941
+ return normalizedItem;
3942
+ }));
3943
+ }
2346
3944
  return result;
2347
3945
  }
2348
3946
  async runSandboxedBeforeSave(content, collection, isNew) {
@@ -2491,12 +4089,9 @@ var EmDashRuntime = class EmDashRuntime {
2491
4089
  runAfterUnscheduleHooks(content, collection) {
2492
4090
  this.runDeferredContentHook("content:afterUnschedule", content, collection);
2493
4091
  }
2494
- async handleSandboxedRoute(plugin, path, request) {
4092
+ async handleSandboxedRoute(plugin, path, request, user) {
2495
4093
  const routeName = path.replace(LEADING_SLASH_PATTERN, "");
2496
- let body = void 0;
2497
- try {
2498
- body = await request.json();
2499
- } catch {}
4094
+ const body = await parseRouteInput(request);
2500
4095
  try {
2501
4096
  const headers = sanitizeHeadersForSandbox(request.headers);
2502
4097
  const meta = extractRequestMeta(request, this.config);
@@ -2506,11 +4101,21 @@ var EmDashRuntime = class EmDashRuntime {
2506
4101
  url: request.url,
2507
4102
  method: request.method,
2508
4103
  headers,
2509
- meta
4104
+ meta,
4105
+ user
2510
4106
  })
2511
4107
  };
2512
4108
  } catch (error) {
2513
4109
  console.error(`EmDash: Sandboxed plugin route error:`, error);
4110
+ const sandboxRouteError = getSandboxRouteErrorDetails(error);
4111
+ if (sandboxRouteError) return {
4112
+ success: false,
4113
+ status: sandboxRouteError.status,
4114
+ error: {
4115
+ code: sandboxRouteError.code,
4116
+ message: sandboxRouteError.message
4117
+ }
4118
+ };
2514
4119
  return {
2515
4120
  success: false,
2516
4121
  error: {
@@ -2629,6 +4234,26 @@ function createPublicMediaUrlResolver(storage) {
2629
4234
  * middleware.ts.
2630
4235
  */
2631
4236
  const ASTRO_COOKIES_SYMBOL = Symbol.for("astro.cookies");
4237
+ /** Hold the real adapter close behind both response and deferred-task completion. */
4238
+ function coordinateScopedDbLifecycle(scoped) {
4239
+ if (!scoped.close) return { lifecycle: scoped };
4240
+ const close = scoped.close;
4241
+ const deferredTasks = createDeferredTaskTracker(() => {
4242
+ try {
4243
+ close();
4244
+ } catch (error) {
4245
+ console.error("[emdash] request-scoped db close failed:", error);
4246
+ }
4247
+ });
4248
+ return {
4249
+ closed: deferredTasks.settled,
4250
+ deferredTasks,
4251
+ lifecycle: {
4252
+ commit: scoped.commit,
4253
+ close: deferredTasks.settle
4254
+ }
4255
+ };
4256
+ }
2632
4257
  /**
2633
4258
  * Run a request-scoped db's `close()` once the response body has finished
2634
4259
  * streaming. Astro streams HTML and components issue DB queries during that
@@ -2668,9 +4293,9 @@ function wrapResponseForScopedClose(response, close) {
2668
4293
  * Run the request body under a request-scoped db, then settle its lifecycle:
2669
4294
  * `commit()` runs before the response is returned (so per-request state like a
2670
4295
  * D1 bookmark cookie is persisted in the headers, even if render throws), while
2671
- * `close()` (if any) is deferred to stream-end so a connection-backed adapter
2672
- * isn't torn down mid-render. On error the connection is closed immediately
2673
- * before rethrowing so it never leaks.
4296
+ * `close()` (if any) is deferred to lifecycle settlement so a
4297
+ * connection-backed adapter isn't torn down mid-render or mid-task. On error
4298
+ * the lifecycle is settled before rethrowing so it never leaks.
2674
4299
  *
2675
4300
  * On the error path both `commit()` and `close()` are defended: a throw from
2676
4301
  * either is logged and swallowed so it can't replace the propagating render
@@ -2870,12 +4495,6 @@ function createPublicPluginApiRouteHandler(runtime) {
2870
4495
  //#endregion
2871
4496
  //#region src/astro/middleware.ts
2872
4497
  /**
2873
- * EmDash middleware
2874
- *
2875
- * Thin wrapper that initializes EmDashRuntime and attaches it to locals.
2876
- * All heavy lifting happens in EmDashRuntime.
2877
- */
2878
- /**
2879
4498
  * Runtime init lock reclaim deadline. Must be strictly larger than the db
2880
4499
  * init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →
2881
4500
  * the db init lock, and equal deadlines would let this outer lock reclaim
@@ -3019,6 +4638,15 @@ async function runScheduledTasks(options = {}) {
3019
4638
  if (!config) return { published: [] };
3020
4639
  return runOutsideRequest(config, (runtime) => runtime.runScheduledTasks(options));
3021
4640
  }
4641
+ async function runScheduledMediaUsageTasks() {
4642
+ const config = getConfig();
4643
+ if (!config) return {
4644
+ outcome: "inactive",
4645
+ taskClass: null,
4646
+ turn: null
4647
+ };
4648
+ return runOutsideRequest(config, (runtime) => runtime.runScheduledMediaUsageTasks());
4649
+ }
3022
4650
  /**
3023
4651
  * Run a callback against the EmDash runtime outside any HTTP request — from a
3024
4652
  * Cloudflare Queue consumer, a `scheduled()` handler, or any other
@@ -3062,49 +4690,71 @@ async function withEmDashRuntime(run) {
3062
4690
  /**
3063
4691
  * Shared plumbing for request-free entry points (`runScheduledTasks`,
3064
4692
  * `withEmDashRuntime`): resolve the runtime singleton, then run the callback
3065
- * under an event-scoped db connection when the adapter needs one.
4693
+ * under an event-scoped db when the adapter needs one.
3066
4694
  *
3067
4695
  * Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse
3068
4696
  * the per-isolate singleton from a platform event: its socket belongs to the
3069
4697
  * request that opened it, and workerd rejects cross-event I/O. Open an
3070
4698
  * event-scoped connection and run the callback under it in ALS — the
3071
4699
  * runtime's db getter, the cron executor, and plugin contexts all resolve
3072
- * the connection from ALS — then close it. Gated on the adapter being
3073
- * connection-backed (it exposes `close()`); stateless adapters (D1, Node
3074
- * SQLite) return null or a close-less scope and keep using the singleton.
4700
+ * the connection from ALS — then close it when required. Stateless adapters
4701
+ * that need primary routing can return a close-less scope; adapters with no
4702
+ * event scoping return null and keep using the singleton.
3075
4703
  */
3076
4704
  async function runOutsideRequest(config, fn) {
3077
- const runtime = await getRuntime(config);
4705
+ if (getRequestContext()) return runOutsideRequestWithRuntime(config, await getRuntime(config), fn);
4706
+ const deferredTasks = createDeferredTaskTracker(() => {});
4707
+ return runWithContext({
4708
+ editMode: false,
4709
+ metrics: createRequestMetrics(performance.now()),
4710
+ deferredTasks
4711
+ }, async () => {
4712
+ return runOutsideRequestWithRuntime(config, await (async () => {
4713
+ try {
4714
+ return await getRuntime(config);
4715
+ } finally {
4716
+ deferredTasks.settle();
4717
+ await deferredTasks.settled;
4718
+ }
4719
+ })(), fn);
4720
+ });
4721
+ }
4722
+ async function runOutsideRequestWithRuntime(config, runtime, fn) {
3078
4723
  const scoped = createRequestScopedDb$1({
3079
4724
  config: config.database?.config,
3080
4725
  isAuthenticated: false,
3081
4726
  isWrite: true,
4727
+ canUseCachedBinding: false,
3082
4728
  cookies: NOOP_COOKIE_JAR,
3083
4729
  url: CRON_EVENT_URL
3084
4730
  });
3085
- if (!scoped?.close) return fn(runtime);
4731
+ if (!scoped) return fn(runtime);
4732
+ const { closed, deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
3086
4733
  const parent = getRequestContext();
3087
4734
  const ctx = parent ? {
3088
4735
  ...parent,
3089
- db: scoped.db
4736
+ db: scoped.db,
4737
+ deferredTasks
3090
4738
  } : {
3091
4739
  editMode: false,
3092
4740
  db: scoped.db,
3093
- metrics: createRequestMetrics(performance.now())
4741
+ metrics: createRequestMetrics(performance.now()),
4742
+ deferredTasks
3094
4743
  };
3095
4744
  try {
3096
4745
  return await runWithContext(ctx, () => fn(runtime));
3097
4746
  } finally {
3098
4747
  try {
3099
- scoped.commit();
4748
+ lifecycle.commit();
3100
4749
  } catch (error) {
3101
4750
  console.error("[emdash] event-scoped db commit failed:", error);
3102
4751
  }
3103
4752
  try {
3104
- scoped.close();
4753
+ lifecycle.close?.();
3105
4754
  } catch (error) {
3106
4755
  console.error("[emdash] event-scoped db close failed:", error);
3107
4756
  }
4757
+ await closed;
3108
4758
  }
3109
4759
  }
3110
4760
  /**
@@ -3203,12 +4853,39 @@ function createRequestScopedDb$1(opts) {
3203
4853
  if (typeof createRequestScopedDb !== "function") return null;
3204
4854
  return createRequestScopedDb(opts);
3205
4855
  }
4856
+ const buildDate = buildTime ? new Date(buildTime) : null;
4857
+ /**
4858
+ * Fold the build timestamp into the route cache validator.
4859
+ *
4860
+ * `CacheHint.lastModified` describes the content, but the response also depends
4861
+ * on the build: `/_astro/*` names are content-hashed, and a deployment only
4862
+ * serves its own. Without the build dimension a code-only deploy answers a
4863
+ * returning visitor's conditional request with 304, leaving them on HTML whose
4864
+ * assets 404.
4865
+ *
4866
+ * Prerendered pages are served by the host's static layer, which manages its
4867
+ * own validators — only on-demand responses need the build dimension.
4868
+ *
4869
+ * Only forward moves are covered. `Last-Modified` expresses newer, not
4870
+ * different, so after a rollback the earlier build still answers a conditional
4871
+ * request with 304 and the browser stays on the newer build's HTML.
4872
+ *
4873
+ * Must run before next(): Astro keeps the later of two dates, so a route's own
4874
+ * hint still wins when content is newer, and a route that opts out with
4875
+ * `Astro.cache.set(false)` stays opted out — calling set() afterwards would
4876
+ * clear that opt-out.
4877
+ */
4878
+ function applyBuildValidator(context) {
4879
+ if (context.isPrerendered || !buildDate || !context.cache?.enabled) return;
4880
+ context.cache.set({ lastModified: buildDate });
4881
+ }
3206
4882
  const onRequest = defineMiddleware(async (context, next) => {
3207
4883
  const { request, locals, cookies } = context;
3208
4884
  const url = context.url;
3209
4885
  if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
3210
4886
  if (virtualConfig.authProviders.some((p) => p.routes?.some((r) => r.pattern && url.pathname === r.pattern))) return finalizeResponse(await next());
3211
4887
  }
4888
+ applyBuildValidator(context);
3212
4889
  const queryRecorder = isInstrumentationEnabled() ? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default") : void 0;
3213
4890
  const metrics = createRequestMetrics(performance.now());
3214
4891
  const run = async () => {
@@ -3220,6 +4897,9 @@ const onRequest = defineMiddleware(async (context, next) => {
3220
4897
  const hasSessionCookie = cookies.get("astro-session") !== void 0;
3221
4898
  const sessionUser = context.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);
3222
4899
  const hasBearerAuth = (request.headers.get("authorization") ?? "").toLowerCase().startsWith("bearer ");
4900
+ const isWrite = request.method !== "GET" && request.method !== "HEAD";
4901
+ const isAuthenticated = !!sessionUser || hasBearerAuth;
4902
+ const canUseCachedBinding = !isAuthenticated && !isWrite && !playgroundDb && !isEmDashRoute && !hasEditCookie && !hasPreviewToken;
3223
4903
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
3224
4904
  if (!sessionUser && !playgroundDb) {
3225
4905
  const timings = [];
@@ -3227,7 +4907,7 @@ const onRequest = defineMiddleware(async (context, next) => {
3227
4907
  if (!isSetupVerified() && !context.isPrerendered) {
3228
4908
  const t0 = performance.now();
3229
4909
  try {
3230
- const { getDb } = await import("../loader-C1XOLV5b.mjs").then((n) => n.o);
4910
+ const { getDb } = await import("../loader-DLBqRiGP.mjs").then((n) => n.c);
3231
4911
  await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
3232
4912
  markSetupVerified();
3233
4913
  } catch (error) {
@@ -3267,12 +4947,15 @@ const onRequest = defineMiddleware(async (context, next) => {
3267
4947
  });
3268
4948
  for (const sub of initSubTimings) timings.push(sub);
3269
4949
  }
4950
+ const lastContentWriteAt = canUseCachedBinding && config?.database?.needsLastContentWriteAt ? await getLastContentWriteAt() : void 0;
3270
4951
  const anonScoped = createRequestScopedDb$1({
3271
4952
  config: config?.database?.config,
3272
- isAuthenticated: false,
3273
- isWrite: request.method !== "GET" && request.method !== "HEAD",
4953
+ isAuthenticated,
4954
+ isWrite,
4955
+ canUseCachedBinding,
3274
4956
  cookies,
3275
- url
4957
+ url,
4958
+ lastContentWriteAt
3276
4959
  });
3277
4960
  const runAnon = async () => {
3278
4961
  const t0 = performance.now();
@@ -3291,19 +4974,22 @@ const onRequest = defineMiddleware(async (context, next) => {
3291
4974
  return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
3292
4975
  };
3293
4976
  if (anonScoped) {
4977
+ const { deferredTasks, lifecycle } = coordinateScopedDbLifecycle(anonScoped);
3294
4978
  const parent = getRequestContext();
3295
4979
  const ctx = parent ? {
3296
4980
  ...parent,
3297
- db: anonScoped.db
4981
+ db: anonScoped.db,
4982
+ deferredTasks
3298
4983
  } : {
3299
4984
  editMode: false,
3300
4985
  db: anonScoped.db,
3301
- metrics
4986
+ metrics,
4987
+ deferredTasks
3302
4988
  };
3303
4989
  const acceptsHtml = (request.headers.get("accept") ?? "").split(",", 1)[0].trim().startsWith("text/html");
3304
4990
  return runWithContext(ctx, async () => {
3305
4991
  if (acceptsHtml) after(() => prefetchLayoutData());
3306
- return finishScoped(anonScoped, runAnon);
4992
+ return finishScoped(lifecycle, runAnon);
3307
4993
  });
3308
4994
  }
3309
4995
  return runAnon();
@@ -3391,12 +5077,15 @@ const onRequest = defineMiddleware(async (context, next) => {
3391
5077
  } catch (error) {
3392
5078
  console.error("EmDash middleware error:", error);
3393
5079
  }
5080
+ const lastContentWriteAt = canUseCachedBinding && config?.database?.needsLastContentWriteAt ? await getLastContentWriteAt() : void 0;
3394
5081
  const scoped = createRequestScopedDb$1({
3395
5082
  config: config?.database?.config,
3396
- isAuthenticated: !!sessionUser || hasBearerAuth,
3397
- isWrite: request.method !== "GET" && request.method !== "HEAD",
5083
+ isAuthenticated,
5084
+ isWrite,
5085
+ canUseCachedBinding,
3398
5086
  cookies: context.cookies,
3399
- url
5087
+ url,
5088
+ lastContentWriteAt
3400
5089
  });
3401
5090
  const renderAndFinalize = async () => {
3402
5091
  const t0 = performance.now();
@@ -3415,15 +5104,18 @@ const onRequest = defineMiddleware(async (context, next) => {
3415
5104
  return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
3416
5105
  };
3417
5106
  if (scoped) {
5107
+ const { deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
3418
5108
  const parent = getRequestContext();
3419
5109
  return runWithContext(parent ? {
3420
5110
  ...parent,
3421
- db: scoped.db
5111
+ db: scoped.db,
5112
+ deferredTasks
3422
5113
  } : {
3423
5114
  editMode: false,
3424
5115
  db: scoped.db,
3425
- metrics
3426
- }, () => finishScoped(scoped, renderAndFinalize));
5116
+ metrics,
5117
+ deferredTasks
5118
+ }, () => finishScoped(lifecycle, renderAndFinalize));
3427
5119
  }
3428
5120
  return renderAndFinalize();
3429
5121
  };
@@ -3456,5 +5148,5 @@ const onRequest = defineMiddleware(async (context, next) => {
3456
5148
  });
3457
5149
 
3458
5150
  //#endregion
3459
- export { onRequest as default, onRequest, runScheduledTasks, withEmDashRuntime };
5151
+ export { onRequest as default, onRequest, runScheduledMediaUsageTasks, runScheduledTasks, withEmDashRuntime };
3460
5152
  //# sourceMappingURL=middleware.mjs.map