emdash 0.33.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 (860) hide show
  1. package/dist/{adapters-BMyNSSZi.d.mts → adapters-34EN2eXO.d.mts} +26 -2
  2. package/dist/adapters-34EN2eXO.d.mts.map +1 -0
  3. package/dist/{allowed-origins-CG1pACtS.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
  4. package/dist/{allowed-origins-CG1pACtS.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +22 -21
  7. package/dist/api/route-utils.mjs.map +1 -1
  8. package/dist/api/schemas/index.d.mts +2 -2
  9. package/dist/api/schemas/index.mjs +6 -5
  10. package/dist/{api-D2hunekQ.mjs → api-DdAsirBe.mjs} +98 -407
  11. package/dist/api-DdAsirBe.mjs.map +1 -0
  12. package/dist/{api-tokens-CSND0i1z.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
  13. package/dist/{api-tokens-CSND0i1z.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
  14. package/dist/{apply-kwNEJW2l.mjs → apply-D5zfeD09.mjs} +131 -107
  15. package/dist/apply-D5zfeD09.mjs.map +1 -0
  16. package/dist/astro/index.d.mts +12 -12
  17. package/dist/astro/index.d.mts.map +1 -1
  18. package/dist/astro/index.mjs +93 -35
  19. package/dist/astro/index.mjs.map +1 -1
  20. package/dist/astro/middleware/auth.d.mts +10 -10
  21. package/dist/astro/middleware/auth.mjs +8 -8
  22. package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
  23. package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
  24. package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
  25. package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
  26. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  27. package/dist/astro/middleware/redirect.mjs +10 -7
  28. package/dist/astro/middleware/redirect.mjs.map +1 -1
  29. package/dist/astro/middleware/request-context.mjs +3 -3
  30. package/dist/astro/middleware/setup.mjs +1 -1
  31. package/dist/astro/middleware.d.mts +12 -11
  32. package/dist/astro/middleware.d.mts.map +1 -1
  33. package/dist/astro/middleware.mjs +1677 -156
  34. package/dist/astro/middleware.mjs.map +1 -1
  35. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
  36. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  37. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
  38. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  39. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  40. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  41. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  42. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
  43. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  44. package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
  45. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  46. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
  47. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  48. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +17 -17
  49. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +17 -17
  50. package/dist/astro/routes/api/admin/bylines/index.mjs +17 -17
  51. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +14 -13
  52. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  54. package/dist/astro/routes/api/admin/comments/bulk.mjs +12 -11
  55. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  56. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  57. package/dist/astro/routes/api/admin/comments/index.mjs +12 -11
  58. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
  60. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
  61. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
  62. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
  63. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
  64. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
  65. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
  66. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
  67. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
  68. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
  69. package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
  70. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  71. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
  72. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
  73. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
  74. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
  75. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
  76. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
  77. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
  78. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
  79. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  80. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  81. package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +51 -43
  83. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
  85. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +51 -43
  86. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  87. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +47 -42
  88. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
  90. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +47 -42
  91. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +50 -42
  94. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +50 -42
  97. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/index.mjs +47 -42
  99. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  101. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +47 -42
  102. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  104. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +50 -42
  105. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +47 -42
  107. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  108. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
  109. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +50 -42
  110. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  112. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +51 -43
  113. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  114. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +47 -42
  115. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  116. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  117. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +51 -43
  118. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  119. package/dist/astro/routes/api/admin/plugins/updates.mjs +47 -42
  120. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  121. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +47 -42
  122. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  123. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  124. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +47 -42
  125. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  126. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
  127. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  128. package/dist/astro/routes/api/admin/users/_id_/index.mjs +9 -8
  129. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  130. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
  131. package/dist/astro/routes/api/admin/users/index.mjs +9 -8
  132. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  133. package/dist/astro/routes/api/auth/dev-bypass.mjs +7 -7
  134. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  135. package/dist/astro/routes/api/auth/invite/complete.mjs +13 -12
  136. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  137. package/dist/astro/routes/api/auth/invite/index.mjs +10 -9
  138. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  139. package/dist/astro/routes/api/auth/invite/register-options.mjs +12 -11
  140. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  141. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  142. package/dist/astro/routes/api/auth/magic-link/send.mjs +12 -11
  143. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  144. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  145. package/dist/astro/routes/api/auth/me.mjs +10 -9
  146. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  147. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  148. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  149. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  150. package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
  151. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  152. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  153. package/dist/astro/routes/api/auth/passkey/options.mjs +14 -13
  154. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  155. package/dist/astro/routes/api/auth/passkey/register/options.mjs +12 -11
  156. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  157. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +13 -12
  158. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  159. package/dist/astro/routes/api/auth/passkey/verify.mjs +13 -12
  160. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  161. package/dist/astro/routes/api/auth/signup/complete.mjs +13 -12
  162. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
  164. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  165. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  166. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +15 -14
  167. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  168. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +14 -13
  169. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  170. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  171. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
  172. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  173. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  174. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +13 -13
  175. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
  176. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  177. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +18 -17
  178. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  179. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +18 -17
  180. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  181. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  182. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  183. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
  184. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  185. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  186. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +44 -31
  187. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  188. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  189. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
  190. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
  191. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  192. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  193. package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
  194. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  195. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  196. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  197. package/dist/astro/routes/api/dashboard.mjs +10 -8
  198. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  199. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  200. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  201. package/dist/astro/routes/api/import/probe.mjs +18 -16
  202. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  203. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  204. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  205. package/dist/astro/routes/api/import/wordpress/execute.mjs +15 -15
  206. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  207. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  208. package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
  209. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  210. package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
  211. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  212. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  213. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +14 -14
  214. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  215. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +18 -16
  216. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  217. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  218. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  219. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +28 -27
  220. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  221. package/dist/astro/routes/api/manifest.mjs +5 -5
  222. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  223. package/dist/astro/routes/api/mcp.mjs +174 -72
  224. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  225. package/dist/astro/routes/api/media/_id_/confirm.mjs +13 -11
  226. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  227. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  228. package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
  229. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  230. package/dist/astro/routes/api/media/_id_.mjs +16 -15
  231. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  232. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  233. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  234. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  235. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  236. package/dist/astro/routes/api/media/upload-url.mjs +11 -10
  237. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  238. package/dist/astro/routes/api/media.mjs +18 -15
  239. package/dist/astro/routes/api/media.mjs.map +1 -1
  240. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +11 -11
  241. package/dist/astro/routes/api/menus/_name_/items.mjs +11 -11
  242. package/dist/astro/routes/api/menus/_name_/reorder.mjs +11 -11
  243. package/dist/astro/routes/api/menus/_name_/translations.mjs +11 -11
  244. package/dist/astro/routes/api/menus/_name_.mjs +11 -11
  245. package/dist/astro/routes/api/menus/index.mjs +11 -11
  246. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  247. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  248. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  249. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  250. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  251. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  252. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  253. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  254. package/dist/astro/routes/api/openapi.json.mjs +152 -6
  255. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  256. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  257. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
  258. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  259. package/dist/astro/routes/api/redirects/404s/index.mjs +12 -11
  260. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  261. package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
  262. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  263. package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
  264. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  265. package/dist/astro/routes/api/redirects/index.mjs +13 -12
  266. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  267. package/dist/astro/routes/api/relations/_id_/index.mjs +18 -17
  268. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/relations/_id_/translations.mjs +13 -12
  270. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  271. package/dist/astro/routes/api/relations/index.mjs +18 -17
  272. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  273. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  274. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  275. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +47 -42
  276. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  277. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +47 -42
  278. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  279. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +47 -42
  280. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  281. package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
  282. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +47 -42
  283. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  284. package/dist/astro/routes/api/schema/collections/index.mjs +47 -42
  285. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  286. package/dist/astro/routes/api/schema/collections/reorder.mjs +47 -42
  287. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  288. package/dist/astro/routes/api/schema/index.mjs +10 -9
  289. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  290. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +47 -42
  291. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  292. package/dist/astro/routes/api/schema/orphans/index.mjs +47 -42
  293. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  294. package/dist/astro/routes/api/search/enable.mjs +13 -13
  295. package/dist/astro/routes/api/search/index.mjs +12 -12
  296. package/dist/astro/routes/api/search/rebuild.mjs +13 -13
  297. package/dist/astro/routes/api/search/stats.mjs +8 -8
  298. package/dist/astro/routes/api/search/suggest.mjs +12 -12
  299. package/dist/astro/routes/api/sections/_slug_.mjs +12 -11
  300. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  301. package/dist/astro/routes/api/sections/index.mjs +12 -11
  302. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  303. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
  304. package/dist/astro/routes/api/settings/backups/archives/index.mjs +6 -6
  305. package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
  306. package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
  307. package/dist/astro/routes/api/settings/email.mjs +5 -5
  308. package/dist/astro/routes/api/settings.mjs +14 -13
  309. package/dist/astro/routes/api/settings.mjs.map +1 -1
  310. package/dist/astro/routes/api/setup/admin-verify.mjs +14 -13
  311. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  312. package/dist/astro/routes/api/setup/admin.mjs +13 -12
  313. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  314. package/dist/astro/routes/api/setup/dev-bypass.mjs +30 -28
  315. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  316. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  317. package/dist/astro/routes/api/setup/index.mjs +32 -30
  318. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  319. package/dist/astro/routes/api/setup/status.mjs +5 -5
  320. package/dist/astro/routes/api/snapshot.mjs +8 -8
  321. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +16 -15
  322. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
  323. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -15
  324. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  325. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -15
  326. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  327. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  328. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +18 -16
  329. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  330. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
  331. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
  332. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
  333. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
  334. package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
  335. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
  336. package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
  337. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
  338. package/dist/astro/routes/api/taxonomies/index.mjs +16 -15
  339. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  340. package/dist/astro/routes/api/themes/preview.mjs +6 -6
  341. package/dist/astro/routes/api/typegen.mjs +6 -6
  342. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  343. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  344. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  345. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
  346. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  347. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +12 -11
  348. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  349. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -11
  350. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  351. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -6
  352. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  353. package/dist/astro/routes/api/widget-areas/index.mjs +12 -11
  354. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  355. package/dist/astro/routes/api/widget-components.mjs +4 -4
  356. package/dist/astro/routes/robots.txt.mjs +7 -6
  357. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  358. package/dist/astro/routes/sitemap-_collection_.xml.mjs +10 -10
  359. package/dist/astro/routes/sitemap.xml.mjs +8 -7
  360. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  361. package/dist/astro/types.d.mts +35 -17
  362. package/dist/astro/types.d.mts.map +1 -1
  363. package/dist/auth/providers/github.d.mts +1 -1
  364. package/dist/auth/providers/google.d.mts +1 -1
  365. package/dist/{authorize-qRhWQnel.mjs → authorize-1iFJzPaZ.mjs} +2 -2
  366. package/dist/{authorize-qRhWQnel.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
  367. package/dist/{backup-CtA6LDNw.mjs → backup-LHjIAVNP.mjs} +4 -4
  368. package/dist/{backup-CtA6LDNw.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
  369. package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
  370. package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
  371. package/dist/bundle-CBEXrBnb.mjs +663 -0
  372. package/dist/bundle-CBEXrBnb.mjs.map +1 -0
  373. package/dist/{byline-fields-CNc0Yq5i.mjs → byline-fields-C5S8wygO.mjs} +2 -2
  374. package/dist/{byline-fields-CNc0Yq5i.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
  375. package/dist/{byline-Cp6csL84.mjs → byline-pEcoDps8.mjs} +5 -5
  376. package/dist/{byline-Cp6csL84.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
  377. package/dist/{byline-registry-ZkPKqEwC.mjs → byline-registry-DDun8sxh.mjs} +2 -2
  378. package/dist/{byline-registry-ZkPKqEwC.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
  379. package/dist/{bylines-CEj5CdT4.mjs → bylines-BWemdX2k.mjs} +7 -7
  380. package/dist/{bylines-CEj5CdT4.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
  381. package/dist/{bylines-cf-C9lmF.mjs → bylines-DJfSfXCL.mjs} +5 -5
  382. package/dist/{bylines-cf-C9lmF.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
  383. package/dist/{cache-C9FptTFR.mjs → cache-CBJaDy_E.mjs} +3 -3
  384. package/dist/{cache-C9FptTFR.mjs.map → cache-CBJaDy_E.mjs.map} +1 -1
  385. package/dist/{challenge-store-D9VNKELo.mjs → challenge-store-CZyFVaad.mjs} +1 -1
  386. package/dist/{challenge-store-D9VNKELo.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
  387. package/dist/{chunks-DbYzib_k.mjs → chunks-DfPax1I0.mjs} +2 -2
  388. package/dist/{chunks-DbYzib_k.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
  389. package/dist/cli/index.mjs +176 -698
  390. package/dist/cli/index.mjs.map +1 -1
  391. package/dist/client/cf-access.d.mts +1 -1
  392. package/dist/client/index.d.mts +74 -4
  393. package/dist/client/index.d.mts.map +1 -1
  394. package/dist/client/index.mjs +25 -2
  395. package/dist/client/index.mjs.map +1 -1
  396. package/dist/collection-deletion-C8BugSGz.mjs +779 -0
  397. package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
  398. package/dist/{comment-BIkJ6Fil.mjs → comment-Dxlep6cE.mjs} +2 -2
  399. package/dist/{comment-BIkJ6Fil.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
  400. package/dist/{comment-reaction-D-laEceb.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
  401. package/dist/{comment-reaction-D-laEceb.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
  402. package/dist/{comments-0_w6KChA.mjs → comments-CTOuhTSX.mjs} +3 -3
  403. package/dist/{comments-0_w6KChA.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
  404. package/dist/{components-BPxrRjpE.mjs → components-CQy3lh9x.mjs} +1 -1
  405. package/dist/{components-BPxrRjpE.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
  406. package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
  407. package/dist/config-C3EBstHi.mjs.map +1 -0
  408. package/dist/{content-CJ94NEx5.mjs → content-BNfwh7xX.mjs} +509 -78
  409. package/dist/content-BNfwh7xX.mjs.map +1 -0
  410. package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
  411. package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
  412. package/dist/content-refresh-DNERP4-f.mjs +2560 -0
  413. package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
  414. package/dist/{context-C_1nrqnW.mjs → context-DRgOvSnW.mjs} +25 -17
  415. package/dist/context-DRgOvSnW.mjs.map +1 -0
  416. package/dist/{cron-DRV-UC4K.mjs → cron-Y9aLtgrA.mjs} +1 -1
  417. package/dist/{cron-DRV-UC4K.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
  418. package/dist/{dashboard-DINnNVVU.mjs → dashboard-CB8J89rJ.mjs} +44 -10
  419. package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
  420. package/dist/db/index.d.mts +4 -4
  421. package/dist/db/index.mjs +2 -2
  422. package/dist/db/index.mjs.map +1 -1
  423. package/dist/db/libsql.d.mts +1 -1
  424. package/dist/db/postgres.d.mts +1 -1
  425. package/dist/db/sqlite.d.mts +1 -1
  426. package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
  427. package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
  428. package/dist/{default-BD2FG0xV.mjs → default-DdDl3fxv.mjs} +1 -1
  429. package/dist/{default-BD2FG0xV.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
  430. package/dist/{deferred-tasks-C-WRCcKo.d.mts → deferred-tasks-BMsa9F64.d.mts} +1 -1
  431. package/dist/{deferred-tasks-C-WRCcKo.d.mts.map → deferred-tasks-BMsa9F64.d.mts.map} +1 -1
  432. package/dist/{device-flow-CTqd8mud.mjs → device-flow-CaioYGK3.mjs} +4 -4
  433. package/dist/{device-flow-CTqd8mud.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
  434. package/dist/dist-D_6CKR8z.mjs +810 -0
  435. package/dist/dist-D_6CKR8z.mjs.map +1 -0
  436. package/dist/{email-console-C6WdcWbL.mjs → email-console-BHNjIwiB.mjs} +1 -1
  437. package/dist/{email-console-C6WdcWbL.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
  438. package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
  439. package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
  440. package/dist/{error-oh_9i6bR.mjs → error-DupyJZE2.mjs} +3 -3
  441. package/dist/{error-oh_9i6bR.mjs.map → error-DupyJZE2.mjs.map} +1 -1
  442. package/dist/{errors-BucQ8VYz.mjs → errors-CLUPm7aD.mjs} +14 -2
  443. package/dist/errors-CLUPm7aD.mjs.map +1 -0
  444. package/dist/{escape-CXps5F_v.mjs → escape-C_aLKP3H.mjs} +1 -1
  445. package/dist/{escape-CXps5F_v.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
  446. package/dist/{field-defs-cache-WD82dMQq.mjs → field-defs-cache-hg6QkjV9.mjs} +4 -4
  447. package/dist/{field-defs-cache-WD82dMQq.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
  448. package/dist/{fts-manager-BITakW_R.mjs → fts-manager-CZ-5dVwJ.mjs} +76 -50
  449. package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
  450. package/dist/{hreflang-BaLCNcDx.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
  451. package/dist/{hreflang-BaLCNcDx.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
  452. package/dist/{hreflang-BdZuVBhL.mjs → hreflang-DNswoYsi.mjs} +7 -7
  453. package/dist/{hreflang-BdZuVBhL.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
  454. package/dist/{import-C-9uqyT3.mjs → import-B41u8Rot.mjs} +5 -5
  455. package/dist/{import-C-9uqyT3.mjs.map → import-B41u8Rot.mjs.map} +1 -1
  456. package/dist/{index-M37iz29I.d.mts → index-C3Y-rsyN.d.mts} +226 -73
  457. package/dist/index-C3Y-rsyN.d.mts.map +1 -0
  458. package/dist/{index-MGw47uWq.d.mts → index-Cof4-Fab.d.mts} +5 -10
  459. package/dist/index-Cof4-Fab.d.mts.map +1 -0
  460. package/dist/index.d.mts +20 -19
  461. package/dist/index.mjs +73 -67
  462. package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
  463. package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
  464. package/dist/{load-DV1DZuUJ.mjs → load-DmvJrtxS.mjs} +2 -2
  465. package/dist/{load-DV1DZuUJ.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
  466. package/dist/{loader-CGugY7Zl.mjs → loader-DLBqRiGP.mjs} +95 -53
  467. package/dist/loader-DLBqRiGP.mjs.map +1 -0
  468. package/dist/{manifest-schema-Da7kj23y.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
  469. package/dist/{manifest-schema-Da7kj23y.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
  470. package/dist/media/image-endpoint.mjs +1 -1
  471. package/dist/media/index.d.mts +1 -1
  472. package/dist/media/index.mjs +3 -2
  473. package/dist/media/index.mjs.map +1 -1
  474. package/dist/media/local-runtime.d.mts +12 -12
  475. package/dist/media/local-runtime.mjs +6 -5
  476. package/dist/media/local-runtime.mjs.map +1 -1
  477. package/dist/{media-OhYL-7Io.mjs → media-BWWWb8kg.mjs} +92 -3
  478. package/dist/media-BWWWb8kg.mjs.map +1 -0
  479. package/dist/{media-BP7S-nHT.mjs → media-CzhAn2--.mjs} +2 -2
  480. package/dist/{media-BP7S-nHT.mjs.map → media-CzhAn2--.mjs.map} +1 -1
  481. package/dist/{media-allowlist-D-peH1H5.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
  482. package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
  483. package/dist/{media-upload-DtrwU2B0.mjs → media-upload-9yas9zGH.mjs} +10 -8
  484. package/dist/{media-upload-DtrwU2B0.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
  485. package/dist/{media-url-DYs_YLOx.mjs → media-url-BvoX88FA.mjs} +1 -1
  486. package/dist/{media-url-DYs_YLOx.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
  487. package/dist/{media-usage-BlxfTkN0.mjs → media-usage-De3qvwjc.mjs} +137 -60
  488. package/dist/media-usage-De3qvwjc.mjs.map +1 -0
  489. package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
  490. package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
  491. package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
  492. package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
  493. package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
  494. package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
  495. package/dist/{menus-Y54RDPIN.mjs → menus-CC-yn1F9.mjs} +351 -95
  496. package/dist/menus-CC-yn1F9.mjs.map +1 -0
  497. package/dist/{menus-BcQWxeXu.mjs → menus-fxvEDYE6.mjs} +3 -3
  498. package/dist/{menus-BcQWxeXu.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
  499. package/dist/{mode-BWyTBe3I.mjs → mode-Bpe-tNNy.mjs} +1 -1
  500. package/dist/{mode-BWyTBe3I.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
  501. package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
  502. package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
  503. package/dist/{oauth-authorization-ClvktmrL.mjs → oauth-authorization-BXO1MKej.mjs} +4 -4
  504. package/dist/{oauth-authorization-ClvktmrL.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
  505. package/dist/{oauth-clients-8ao-U66l.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
  506. package/dist/{oauth-clients-8ao-U66l.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
  507. package/dist/{oauth-state-store-B1ofdTiQ.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
  508. package/dist/{oauth-state-store-B1ofdTiQ.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
  509. package/dist/{oauth-user-lookup-CscEuIiR.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
  510. package/dist/{oauth-user-lookup-CscEuIiR.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
  511. package/dist/object-cache/memory.d.mts +1 -1
  512. package/dist/{options-CA4eVX6P.d.mts → options-DRe1pIBL.d.mts} +4 -3
  513. package/dist/options-DRe1pIBL.d.mts.map +1 -0
  514. package/dist/page/index.d.mts +2 -2
  515. package/dist/{parse-CJKNuewM.mjs → parse-DFbuc-Fh.mjs} +2 -2
  516. package/dist/{parse-CJKNuewM.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
  517. package/dist/{passkey-config-D-zrzMJI.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
  518. package/dist/{passkey-config-D-zrzMJI.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
  519. package/dist/{placeholder-DaeNS-VG.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
  520. package/dist/{placeholder-DaeNS-VG.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
  521. package/dist/plugin-types.d.mts +8 -1
  522. package/dist/plugin-types.d.mts.map +1 -1
  523. package/dist/plugin-utils.d.mts +10 -10
  524. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  525. package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
  526. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  527. package/dist/{portable-text-B_kqBmXB.mjs → portable-text-DhKbDAGr.mjs} +1 -1
  528. package/dist/{portable-text-B_kqBmXB.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
  529. package/dist/{preview-BU6PaLpM.mjs → preview-PW-nOnq4.mjs} +2 -2
  530. package/dist/{preview-BU6PaLpM.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
  531. package/dist/{public-url-Ox9oOlgp.mjs → public-url-CWoFhQT8.mjs} +1 -1
  532. package/dist/{public-url-Ox9oOlgp.mjs.map → public-url-CWoFhQT8.mjs.map} +1 -1
  533. package/dist/{query-C5w7PsRQ.mjs → query-BgjfiLUQ.mjs} +31 -49
  534. package/dist/query-BgjfiLUQ.mjs.map +1 -0
  535. package/dist/{query-rC9HKv1N.mjs → query-vgZNwIhh.mjs} +3 -3
  536. package/dist/{query-rC9HKv1N.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
  537. package/dist/{rate-limit-kug6q99V.mjs → rate-limit-L5anykgo.mjs} +2 -2
  538. package/dist/{rate-limit-kug6q99V.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
  539. package/dist/{redirect-DIJxXKOG.mjs → redirect-COsS6SWJ.mjs} +2 -2
  540. package/dist/{redirect-DIJxXKOG.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
  541. package/dist/{redirect-Egf2R9yP.mjs → redirect-KBNE3UEs.mjs} +1 -1
  542. package/dist/{redirect-Egf2R9yP.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
  543. package/dist/{redirects-Bzm812bY.mjs → redirects-CufRQt0Q.mjs} +4 -4
  544. package/dist/{redirects-Bzm812bY.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
  545. package/dist/{redirects-G8wAhWAf.mjs → redirects-cV9g7V-0.mjs} +133 -14
  546. package/dist/redirects-cV9g7V-0.mjs.map +1 -0
  547. package/dist/registry-C581V4TP.mjs +1275 -0
  548. package/dist/registry-C581V4TP.mjs.map +1 -0
  549. package/dist/{relation-Cbe3Sd3m.mjs → relation-DtmAQH7s.mjs} +3 -3
  550. package/dist/{relation-Cbe3Sd3m.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
  551. package/dist/{relations-Bw_E1TXo.mjs → relations-BBApI4e2.mjs} +3 -3
  552. package/dist/{relations-Bw_E1TXo.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
  553. package/dist/{relations-qHwpUe66.mjs → relations-DICSyfsh.mjs} +6 -6
  554. package/dist/{relations-qHwpUe66.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
  555. package/dist/{relations-G2ta7GIl.d.mts → relations-DItI3MN9.d.mts} +369 -35
  556. package/dist/relations-DItI3MN9.d.mts.map +1 -0
  557. package/dist/request-context.d.mts +1 -1
  558. package/dist/{request-meta-Df-jFbAd.mjs → request-meta-DgZJh3zG.mjs} +2 -2
  559. package/dist/{request-meta-Df-jFbAd.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
  560. package/dist/{resolve-DZzG4HqM.mjs → resolve-DOhVEvty.mjs} +2 -2
  561. package/dist/{resolve-DZzG4HqM.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
  562. package/dist/{runner-BFTrkYhu.mjs → runner-BiDvq5dv.mjs} +1157 -254
  563. package/dist/runner-BiDvq5dv.mjs.map +1 -0
  564. package/dist/{runner-BaADli8l.d.mts → runner-CgQbELYC.d.mts} +2 -2
  565. package/dist/{runner-BaADli8l.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
  566. package/dist/runtime.d.mts +11 -11
  567. package/dist/runtime.mjs +4 -3
  568. package/dist/runtime.mjs.map +1 -1
  569. package/dist/{schema-CiQXr6_F.mjs → schema-BFyTg38T.mjs} +3 -3
  570. package/dist/{schema-CiQXr6_F.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
  571. package/dist/schema-CeQSNwB3.mjs +390 -0
  572. package/dist/schema-CeQSNwB3.mjs.map +1 -0
  573. package/dist/{scopes-CNJVT5i5.mjs → scopes-BVeuhGBE.mjs} +3 -3
  574. package/dist/{scopes-CNJVT5i5.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
  575. package/dist/{search-FZhx5dDU.mjs → search-BpuPKZb0.mjs} +25 -15
  576. package/dist/search-BpuPKZb0.mjs.map +1 -0
  577. package/dist/{secrets-DLd8RXee.mjs → secrets-dtYs2hDo.mjs} +3 -3
  578. package/dist/{secrets-DLd8RXee.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
  579. package/dist/{sections-BrIrMUCn.mjs → sections-COctk6hf.mjs} +3 -3
  580. package/dist/{sections-BrIrMUCn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
  581. package/dist/seed/index.d.mts +2 -2
  582. package/dist/seed/index.mjs +24 -22
  583. package/dist/seo/index.d.mts +3 -3
  584. package/dist/seo/index.mjs +16 -15
  585. package/dist/seo/index.mjs.map +1 -1
  586. package/dist/{seo-CobNfOvb.mjs → seo-AFpVvTqr.mjs} +5 -3
  587. package/dist/seo-AFpVvTqr.mjs.map +1 -0
  588. package/dist/{seo-DTHRntNC.mjs → seo-CvDxfvFb.mjs} +2 -2
  589. package/dist/{seo-DTHRntNC.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
  590. package/dist/{service-B-pYGPgR.mjs → service-xMiMSTsR.mjs} +3 -3
  591. package/dist/{service-B-pYGPgR.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
  592. package/dist/{session-user-BvM9-zNl.mjs → session-user-BzVemLkw.mjs} +1 -1
  593. package/dist/{session-user-BvM9-zNl.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
  594. package/dist/{settings-kJhGiQ4z.mjs → settings-B372MV0p.mjs} +4 -4
  595. package/dist/{settings-kJhGiQ4z.mjs.map → settings-B372MV0p.mjs.map} +1 -1
  596. package/dist/{settings-DPHau-WH.mjs → settings-BE9aqn_9.mjs} +3 -3
  597. package/dist/{settings-DPHau-WH.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
  598. package/dist/{setup-complete-u50Qm8rY.mjs → setup-complete-DF7TAfD5.mjs} +1 -1
  599. package/dist/{setup-complete-u50Qm8rY.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
  600. package/dist/{setup-nonce-DCjiiOD2.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
  601. package/dist/{setup-nonce-DCjiiOD2.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
  602. package/dist/{single-flight-cache-DW3w11Pu.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
  603. package/dist/{single-flight-cache-DW3w11Pu.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
  604. package/dist/{site-url-I-lJ5kma.mjs → site-url-Bowsi9T-.mjs} +2 -2
  605. package/dist/{site-url-I-lJ5kma.mjs.map → site-url-Bowsi9T-.mjs.map} +1 -1
  606. package/dist/slugify-B994zkXC.mjs +17 -0
  607. package/dist/slugify-B994zkXC.mjs.map +1 -0
  608. package/dist/{snapshot-I_fjLDaD.mjs → snapshot-B5h6aPMh.mjs} +1 -1
  609. package/dist/{snapshot-I_fjLDaD.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
  610. package/dist/{ssrf-CTa6v21i.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
  611. package/dist/{ssrf-CTa6v21i.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
  612. package/dist/{state-CQvoefYP.mjs → state-DYwPue40.mjs} +1 -1
  613. package/dist/{state-CQvoefYP.mjs.map → state-DYwPue40.mjs.map} +1 -1
  614. package/dist/{status-CIsmlC1E.mjs → status-CWVYpbD8.mjs} +1 -1
  615. package/dist/{status-CIsmlC1E.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
  616. package/dist/storage/local.d.mts +1 -1
  617. package/dist/storage/local.mjs +1 -1
  618. package/dist/storage/s3.d.mts +1 -1
  619. package/dist/storage/s3.mjs +1 -1
  620. package/dist/{taxonomies-CLN1N92E.mjs → taxonomies-B0o9ExNV.mjs} +75 -64
  621. package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
  622. package/dist/{taxonomies-CKuaZER5.mjs → taxonomies-Dk3MAklL.mjs} +280 -56
  623. package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
  624. package/dist/{taxonomy-pOrB2kZe.mjs → taxonomy-CPeM8d2o.mjs} +131 -78
  625. package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
  626. package/dist/{tokens-DagaJGK8.mjs → tokens-C39klX8R.mjs} +2 -2
  627. package/dist/{tokens-DagaJGK8.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
  628. package/dist/{transport-DVsa5YYo.mjs → transport--CfOflrp.mjs} +1 -1
  629. package/dist/{transport-DVsa5YYo.mjs.map → transport--CfOflrp.mjs.map} +1 -1
  630. package/dist/{transport-CAwTaTp_.d.mts → transport-B_rfeU-a.d.mts} +1 -1
  631. package/dist/{transport-CAwTaTp_.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
  632. package/dist/{trusted-proxy-D20_7fyX.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
  633. package/dist/{trusted-proxy-D20_7fyX.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
  634. package/dist/{types-NBI3rF-_.mjs → types-75KIqzL9.mjs} +18 -2
  635. package/dist/types-75KIqzL9.mjs.map +1 -0
  636. package/dist/{types-CbUcuxxc.d.mts → types-BPuloofb.d.mts} +1 -1
  637. package/dist/{types-CbUcuxxc.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
  638. package/dist/{types-BjxTKxrZ.mjs → types-BW1syrlw.mjs} +1 -1
  639. package/dist/{types-BjxTKxrZ.mjs.map → types-BW1syrlw.mjs.map} +1 -1
  640. package/dist/{types-BnFP1M3J.d.mts → types-C3_AbkDP.d.mts} +32 -10
  641. package/dist/types-C3_AbkDP.d.mts.map +1 -0
  642. package/dist/{types-Dc4ntQjQ.d.mts → types-CAGdePf3.d.mts} +124 -1
  643. package/dist/types-CAGdePf3.d.mts.map +1 -0
  644. package/dist/{types-DLG4IFEQ.d.mts → types-CFUfyt1s.d.mts} +1 -1
  645. package/dist/{types-DLG4IFEQ.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
  646. package/dist/{types-LoagAGzH.d.mts → types-CPx2lIGo.d.mts} +1 -1
  647. package/dist/{types-LoagAGzH.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
  648. package/dist/{types-Cd8iuS1J.mjs → types-CQPWhPxE.mjs} +9 -12
  649. package/dist/types-CQPWhPxE.mjs.map +1 -0
  650. package/dist/{types-Cz8hNch2.d.mts → types-Cs-TFLNf.d.mts} +25 -5
  651. package/dist/types-Cs-TFLNf.d.mts.map +1 -0
  652. package/dist/types-CtzytVXn.mjs.map +1 -1
  653. package/dist/{types-BGWPMiBO.d.mts → types-D4_1Ga4L.d.mts} +44 -5
  654. package/dist/types-D4_1Ga4L.d.mts.map +1 -0
  655. package/dist/{types-C8oz_ayJ.d.mts → types-DefV3OiO.d.mts} +1 -1
  656. package/dist/{types-C8oz_ayJ.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
  657. package/dist/{types-BK6U01Ro.d.mts → types-X0MThr7x.d.mts} +1 -1
  658. package/dist/{types-BK6U01Ro.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
  659. package/dist/{upload-attempts-D8QAFLUJ.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
  660. package/dist/{upload-attempts-D8QAFLUJ.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
  661. package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
  662. package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
  663. package/dist/{user-DBrq4UVq.mjs → user-sHgJ62SP.mjs} +3 -3
  664. package/dist/{user-DBrq4UVq.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
  665. package/dist/{utils-hLzi_2Fn.mjs → utils-D4vD_Y98.mjs} +3 -3
  666. package/dist/{utils-hLzi_2Fn.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
  667. package/dist/{validate-mKo3RxBM.mjs → validate-BkqIG_Uj.mjs} +24 -5
  668. package/dist/validate-BkqIG_Uj.mjs.map +1 -0
  669. package/dist/{validate-C3oSmA7F.d.mts → validate-DeGtBN84.d.mts} +15 -7
  670. package/dist/validate-DeGtBN84.d.mts.map +1 -0
  671. package/dist/{validation-BgpucsvF.mjs → validation-CiJb_X5o.mjs} +11 -10
  672. package/dist/{validation-BgpucsvF.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
  673. package/dist/version-x0NjTScG.mjs +7 -0
  674. package/dist/{version-C4PnpmoV.mjs.map → version-x0NjTScG.mjs.map} +1 -1
  675. package/dist/{widgets-CJJtkiKh.mjs → widgets-Cwzvww0W.mjs} +3 -3
  676. package/dist/{widgets-CJJtkiKh.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
  677. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
  678. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
  679. package/dist/{wxr-taxonomies-9PPSy8X6.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
  680. package/dist/{wxr-taxonomies-9PPSy8X6.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
  681. package/dist/{zod-generator-CG64Hnbh.mjs → zod-generator-D11MhxG4.mjs} +52 -5
  682. package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
  683. package/package.json +16 -8
  684. package/src/api/errors.ts +12 -0
  685. package/src/api/handlers/content.ts +127 -29
  686. package/src/api/handlers/dashboard.ts +12 -2
  687. package/src/api/handlers/index.ts +16 -0
  688. package/src/api/handlers/manifest.ts +2 -0
  689. package/src/api/handlers/media-allowlist.ts +13 -2
  690. package/src/api/handlers/media-usage-work.ts +188 -0
  691. package/src/api/handlers/media-usage.ts +5 -3
  692. package/src/api/handlers/schema.ts +9 -3
  693. package/src/api/handlers/seo.ts +5 -2
  694. package/src/api/handlers/taxonomies.ts +345 -82
  695. package/src/api/media-usage-write-fence.ts +64 -0
  696. package/src/api/openapi/document.ts +199 -0
  697. package/src/api/schemas/common.ts +3 -1
  698. package/src/api/schemas/content.ts +122 -1
  699. package/src/api/schemas/media-usage.ts +133 -0
  700. package/src/api/schemas/schema.ts +78 -3
  701. package/src/api/schemas/taxonomies.ts +40 -8
  702. package/src/api/types.ts +1 -0
  703. package/src/astro/integration/index.ts +55 -42
  704. package/src/astro/integration/routes.ts +34 -4
  705. package/src/astro/integration/runtime.ts +15 -0
  706. package/src/astro/integration/virtual-modules.ts +25 -1
  707. package/src/astro/integration/vite-config.ts +16 -1
  708. package/src/astro/middleware/media-usage-write-fence.ts +30 -0
  709. package/src/astro/middleware/redirect.ts +12 -2
  710. package/src/astro/middleware.ts +75 -8
  711. package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
  712. package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
  713. package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
  714. package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
  715. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
  716. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
  717. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
  718. package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
  719. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
  720. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
  721. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  722. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  723. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
  724. package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
  725. package/src/astro/routes/api/mcp.ts +1 -0
  726. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
  727. package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
  728. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
  729. package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
  730. package/src/astro/routes/api/taxonomies/[name].ts +102 -0
  731. package/src/astro/types.ts +25 -4
  732. package/src/cleanup.ts +6 -12
  733. package/src/cli/commands/doctor.ts +193 -6
  734. package/src/cli/commands/export-seed.ts +4 -1
  735. package/src/client/index.ts +119 -0
  736. package/src/components/Block.astro +12 -0
  737. package/src/components/InlinePortableTextEditor.tsx +89 -1
  738. package/src/content/converters/mark-safety.ts +120 -0
  739. package/src/content/converters/portable-text-to-prosemirror.ts +25 -16
  740. package/src/content/converters/prosemirror-to-portable-text.ts +12 -2
  741. package/src/content-list-query.ts +29 -0
  742. package/src/database/migrations/060_collection_admin_config.ts +16 -0
  743. package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
  744. package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
  745. package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
  746. package/src/database/migrations/064_fts_plain_text.ts +253 -0
  747. package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
  748. package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
  749. package/src/database/migrations/067_indexed_content_fields.ts +39 -0
  750. package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
  751. package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
  752. package/src/database/migrations/070_collection_routable.ts +18 -0
  753. package/src/database/migrations/runner.ts +22 -0
  754. package/src/database/repositories/content.ts +861 -96
  755. package/src/database/repositories/media-usage-work.ts +848 -0
  756. package/src/database/repositories/media-usage.ts +1502 -138
  757. package/src/database/repositories/revision.ts +17 -6
  758. package/src/database/repositories/taxonomy.ts +234 -103
  759. package/src/database/repositories/types.ts +51 -2
  760. package/src/database/types.ts +134 -16
  761. package/src/db/adapters.ts +28 -0
  762. package/src/db/index.ts +3 -0
  763. package/src/emdash-runtime.ts +227 -20
  764. package/src/fields/file.ts +7 -4
  765. package/src/fields/image.ts +8 -1
  766. package/src/fields/types.ts +18 -5
  767. package/src/i18n/config.ts +32 -0
  768. package/src/i18n/repair-locale-casing.ts +0 -14
  769. package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
  770. package/src/import/wxr-taxonomies.ts +7 -47
  771. package/src/index.ts +25 -2
  772. package/src/loader.ts +119 -65
  773. package/src/mcp/server.ts +242 -34
  774. package/src/media/usage/activation.ts +626 -0
  775. package/src/media/usage/capture-triggers.ts +543 -0
  776. package/src/media/usage/cleanup.ts +305 -0
  777. package/src/media/usage/collection-deletion-processor.ts +356 -0
  778. package/src/media/usage/collection-deletion.ts +836 -0
  779. package/src/media/usage/content-fields.ts +40 -3
  780. package/src/media/usage/content-refresh.ts +356 -47
  781. package/src/media/usage/content-repair.ts +180 -61
  782. package/src/media/usage/content-snapshots.ts +85 -105
  783. package/src/media/usage/projection-fingerprint.ts +129 -0
  784. package/src/media/usage/reconciliation-processor.ts +372 -0
  785. package/src/media/usage/reconciliation.ts +1117 -0
  786. package/src/media/usage/source-key.ts +4 -0
  787. package/src/media/usage/types.ts +2 -0
  788. package/src/media/usage/work-processor.ts +248 -0
  789. package/src/menus/index.ts +246 -141
  790. package/src/plugin-types.ts +8 -0
  791. package/src/plugins/adapt-sandbox-entry.ts +24 -5
  792. package/src/plugins/context.ts +24 -7
  793. package/src/plugins/index.ts +9 -0
  794. package/src/plugins/routes.ts +43 -1
  795. package/src/plugins/sandbox/index.ts +10 -1
  796. package/src/plugins/sandbox/types.ts +78 -1
  797. package/src/plugins/scheduler/node.ts +13 -1
  798. package/src/plugins/scheduler/types.ts +2 -0
  799. package/src/plugins/types.ts +39 -2
  800. package/src/query.ts +29 -37
  801. package/src/scheduler-health.ts +46 -0
  802. package/src/schema/registry.ts +777 -255
  803. package/src/schema/types.ts +50 -8
  804. package/src/schema/url-pattern.ts +33 -0
  805. package/src/schema/zod-generator.ts +77 -5
  806. package/src/search/fts-manager.ts +117 -76
  807. package/src/search/query.ts +32 -11
  808. package/src/search/types.ts +2 -0
  809. package/src/seed/apply.ts +68 -15
  810. package/src/seed/types.ts +11 -3
  811. package/src/seed/validate.ts +52 -2
  812. package/src/taxonomies/index.ts +77 -75
  813. package/src/taxonomies/term-counts.ts +21 -8
  814. package/src/utils/slugify.ts +1 -29
  815. package/src/virtual-modules.d.ts +16 -0
  816. package/dist/adapters-BMyNSSZi.d.mts.map +0 -1
  817. package/dist/api-D2hunekQ.mjs.map +0 -1
  818. package/dist/apply-kwNEJW2l.mjs.map +0 -1
  819. package/dist/config-DyF3OKg_.mjs.map +0 -1
  820. package/dist/content-CJ94NEx5.mjs.map +0 -1
  821. package/dist/content-refresh-Xw7aKmou.mjs +0 -1629
  822. package/dist/content-refresh-Xw7aKmou.mjs.map +0 -1
  823. package/dist/context-C_1nrqnW.mjs.map +0 -1
  824. package/dist/dashboard-DINnNVVU.mjs.map +0 -1
  825. package/dist/errors-BucQ8VYz.mjs.map +0 -1
  826. package/dist/fts-manager-BITakW_R.mjs.map +0 -1
  827. package/dist/index-M37iz29I.d.mts.map +0 -1
  828. package/dist/index-MGw47uWq.d.mts.map +0 -1
  829. package/dist/loader-CGugY7Zl.mjs.map +0 -1
  830. package/dist/media-OhYL-7Io.mjs.map +0 -1
  831. package/dist/media-allowlist-D-peH1H5.mjs.map +0 -1
  832. package/dist/media-usage-BlxfTkN0.mjs.map +0 -1
  833. package/dist/menus-Y54RDPIN.mjs.map +0 -1
  834. package/dist/options-CA4eVX6P.d.mts.map +0 -1
  835. package/dist/query-C5w7PsRQ.mjs.map +0 -1
  836. package/dist/redirects-G8wAhWAf.mjs.map +0 -1
  837. package/dist/registry-Dbodb4Uo.mjs +0 -840
  838. package/dist/registry-Dbodb4Uo.mjs.map +0 -1
  839. package/dist/relations-G2ta7GIl.d.mts.map +0 -1
  840. package/dist/runner-BFTrkYhu.mjs.map +0 -1
  841. package/dist/search-FZhx5dDU.mjs.map +0 -1
  842. package/dist/seo-CobNfOvb.mjs.map +0 -1
  843. package/dist/slugify-C_tqlU4G.mjs +0 -30
  844. package/dist/slugify-C_tqlU4G.mjs.map +0 -1
  845. package/dist/taxonomies-CKuaZER5.mjs.map +0 -1
  846. package/dist/taxonomies-CLN1N92E.mjs.map +0 -1
  847. package/dist/taxonomy-pOrB2kZe.mjs.map +0 -1
  848. package/dist/types-BGWPMiBO.d.mts.map +0 -1
  849. package/dist/types-BnFP1M3J.d.mts.map +0 -1
  850. package/dist/types-Cd8iuS1J.mjs.map +0 -1
  851. package/dist/types-Cz8hNch2.d.mts.map +0 -1
  852. package/dist/types-Dc4ntQjQ.d.mts.map +0 -1
  853. package/dist/types-NBI3rF-_.mjs.map +0 -1
  854. package/dist/validate-C3oSmA7F.d.mts.map +0 -1
  855. package/dist/validate-mKo3RxBM.mjs.map +0 -1
  856. package/dist/version-C4PnpmoV.mjs +0 -7
  857. package/dist/zod-generator-CG64Hnbh.mjs.map +0 -1
  858. /package/dist/{api-tokens-Dai9dND0.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
  859. /package/dist/{ssrf-Cj568Dgs.mjs → ssrf-DpXgKsna.mjs} +0 -0
  860. /package/dist/{types-DKuWPYV5.mjs → types-CP1CflWf.mjs} +0 -0
@@ -1,94 +1,106 @@
1
- import { a as runMigrations, n as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-BFTrkYhu.mjs";
2
- import { c as isSqlite, u as listTablesLike } from "../dialect-helpers-BSbfaO3F.mjs";
3
- import { a as setI18nConfig, n as getI18nConfig } from "../config-DyF3OKg_.mjs";
4
- import { D as AuditRepository, b as definePlugin, g as EmailPipeline, h as parseRouteInput, m as buildRouteMeta, p as PluginRouteRegistry, t as getMenu, v as createHookPipeline, y as resolveExclusiveHooks } from "../menus-Y54RDPIN.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";
5
6
  import { n as createDeferredTaskTracker, t as after } from "../after-CBU2vkoO.mjs";
6
7
  import { createRequestMetrics, getRequestContext, runWithContext } from "../request-context.mjs";
7
8
  import { o as getLastContentWriteAt } from "../object-cache-71tPfV4A.mjs";
8
- import { r as RevisionRepository, t as ContentRepository } from "../content-CJ94NEx5.mjs";
9
- import { n as isMissingTableError } from "../db-errors-CcWLaRiR.mjs";
10
- import "../base64-B-PsqheR.mjs";
11
- import { t as ContentMutationConflictError } from "../types-Cd8iuS1J.mjs";
12
- import { t as MediaRepository } from "../media-BP7S-nHT.mjs";
13
- import "../user-DBrq4UVq.mjs";
14
- import "../taxonomy-pOrB2kZe.mjs";
15
- import "../comment-BIkJ6Fil.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";
16
20
  import { t as OptionsRepository } from "../options-CBXIHUoT.mjs";
17
- import { r as createSiteInfo } from "../context-C_1nrqnW.mjs";
18
- import "../menus-BcQWxeXu.mjs";
19
- import "../redirect-DIJxXKOG.mjs";
21
+ import { r as createSiteInfo } from "../context-DRgOvSnW.mjs";
22
+ import "../menus-fxvEDYE6.mjs";
23
+ import "../redirect-COsS6SWJ.mjs";
20
24
  import { i as setRequestCacheEntry, r as requestCached } from "../request-cache-BSUptuJR.mjs";
21
- import "../byline-registry-ZkPKqEwC.mjs";
22
- import { n as initWithLock, t as createInitLock } from "../init-lock-DJkX6Hto.mjs";
23
- import "../field-defs-cache-WD82dMQq.mjs";
24
- import "../byline-Cp6csL84.mjs";
25
- import "../relation-Cbe3Sd3m.mjs";
26
- import { n as normalizeMediaValue } from "../normalize-C-SHXmra.mjs";
27
- import "../enrich-CFJJgxs_.mjs";
28
- import "../seo-DTHRntNC.mjs";
29
- import { r as singleFlightCached, t as createSingleFlightCache } from "../single-flight-cache-DW3w11Pu.mjs";
30
- import "../cache-C9FptTFR.mjs";
31
- import { t as FTSManager } from "../fts-manager-BITakW_R.mjs";
32
- import "../resolve-DZzG4HqM.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";
33
38
  import { createRecorder, flushRecorder, isInstrumentationEnabled, kyselyLogOption } from "../database/instrumentation.mjs";
34
- import { o as getDb } from "../loader-CGugY7Zl.mjs";
35
- import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-CLN1N92E.mjs";
36
- import { $ as handleContentDelete, B as handleMediaDelete, G as handleRevisionList, H as handleMediaList, J as handleContentAuthors, K as handleRevisionRestore, Q as handleContentCreate, U as handleMediaUpdate, V as handleMediaGet, W as handleRevisionGet, X as handleContentCountScheduled, Y as handleContentCompare, Z as handleContentCountTrashed, _ as loadBundleFromR2, at as handleContentListTrashed, ct as handleContentRestore, dt as handleContentUnpublish, et as handleContentDiscardDraft, ft as handleContentUnschedule, it as handleContentList, lt as handleContentSchedule, mt as validateRev, nt as handleContentGet, ot as handleContentPermanentDelete, pt as handleContentUpdate, rt as handleContentGetIncludingTrashed, s as normalizeRegistryConfig, st as handleContentPublish, tt as handleContentDuplicate, ut as handleContentTranslations, v as syncDeclaredStorageIndexes, z as handleMediaCreate } from "../api-D2hunekQ.mjs";
37
- import "../dashboard-DINnNVVU.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";
38
43
  import { r as hashString } from "../hash-Cg0TXJPV.mjs";
39
- import { a as findNonTranslatableSiblingContentIds, c as refreshContentMediaUsageAfterWrite, g as MediaUsageRepository, o as markContentMediaUsageCollectionStale, r as deleteContentMediaUsage } from "../content-refresh-Xw7aKmou.mjs";
40
- import "../media-usage-BlxfTkN0.mjs";
41
- import { n as SchemaRegistry } from "../registry-Dbodb4Uo.mjs";
42
- import "../query-rC9HKv1N.mjs";
43
- import "../zod-generator-CG64Hnbh.mjs";
44
- import "../schema-CiQXr6_F.mjs";
45
- import "../seo-CobNfOvb.mjs";
46
- import { t as PluginStateRepository } from "../state-CQvoefYP.mjs";
47
- import "../sections-BrIrMUCn.mjs";
48
- import { i as getSiteSettings, o as invalidateSiteSettingsCache } from "../settings-kJhGiQ4z.mjs";
49
- import "../settings-DPHau-WH.mjs";
50
- import "../taxonomies-CKuaZER5.mjs";
51
- import { r as normalizeManifestRoute } from "../manifest-schema-Da7kj23y.mjs";
52
- import "../types-DKuWPYV5.mjs";
53
- import "../ssrf-CTa6v21i.mjs";
54
- import "../error-oh_9i6bR.mjs";
55
- import "../parse-CJKNuewM.mjs";
56
- import "../media-OhYL-7Io.mjs";
57
- import "../redirects-G8wAhWAf.mjs";
58
- import "../relations-Bw_E1TXo.mjs";
59
- import "../status-CIsmlC1E.mjs";
60
- import { a as invalidateUrlPatternCache } from "../query-C5w7PsRQ.mjs";
61
- import "../import-C-9uqyT3.mjs";
62
- import { t as getTrustedProxyHeaders } from "../trusted-proxy-D20_7fyX.mjs";
63
- import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "../request-meta-Df-jFbAd.mjs";
64
- import "../ssrf-Cj568Dgs.mjs";
65
- import { n as CronExecutor } from "../cron-DRV-UC4K.mjs";
66
- import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "../email-console-C6WdcWbL.mjs";
67
- import "../utils-hLzi_2Fn.mjs";
68
- import "../tokens-DagaJGK8.mjs";
69
- import "../preview-BU6PaLpM.mjs";
70
- import "../hreflang-BdZuVBhL.mjs";
71
- import "../comment-reaction-D-laEceb.mjs";
72
- import "../bylines-CEj5CdT4.mjs";
73
- import { n as getWidgetAreas } from "../widgets-CJJtkiKh.mjs";
74
- import "../validate-mKo3RxBM.mjs";
75
- import "../apply-kwNEJW2l.mjs";
76
- import "../load-DV1DZuUJ.mjs";
77
- import "../search-FZhx5dDU.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-C4PnpmoV.mjs";
80
- import { t as getAuthMode } from "../mode-BWyTBe3I.mjs";
81
- import "../snapshot-I_fjLDaD.mjs";
82
- import { u as maybeRunScheduledBackup } from "../backup-CtA6LDNw.mjs";
83
- import { t as cleanupExpiredChallenges } from "../challenge-store-D9VNKELo.mjs";
84
- import { t as removeUploadAttempt } from "../upload-attempts-D8QAFLUJ.mjs";
85
- import { a as validateEncryptionKeyAtStartup } from "../secrets-DLd8RXee.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";
86
96
  import { isRecord } from "../plugin-utils.mjs";
87
- import { t as resolveSessionUser } from "../session-user-BvM9-zNl.mjs";
97
+ import { t as resolveSessionUser } from "../session-user-BzVemLkw.mjs";
88
98
  import { Kysely, sql } from "kysely";
99
+ import { ulid } from "ulidx";
89
100
  import { z } from "zod";
90
101
  import { Permissions } from "@emdash-cms/auth";
91
102
  import { defineMiddleware } from "astro:middleware";
103
+ import { buildTime } from "virtual:emdash/build";
92
104
  import virtualConfig from "virtual:emdash/config";
93
105
  import { createCoalescingDialect, createDialect, createRequestScopedDb } from "virtual:emdash/dialect";
94
106
  import { mediaProviders } from "virtual:emdash/media-providers";
@@ -105,9 +117,7 @@ async function repairLocaleCasing(db, configuredLocales) {
105
117
  const tableNames = await listTablesLike(db, "ec_%");
106
118
  for (const tableName of tableNames) {
107
119
  const table = sql.ref(tableName);
108
- const slug = tableName.slice(3);
109
- for (const locale of configuredLocales) {
110
- await sql`
120
+ for (const locale of configuredLocales) await sql`
111
121
  UPDATE ${table} AS target
112
122
  SET locale = ${locale}
113
123
  WHERE lower(target.locale) = lower(${locale})
@@ -125,20 +135,1165 @@ async function repairLocaleCasing(db, configuredLocales) {
125
135
  AND candidate.locale != ${locale}
126
136
  )
127
137
  `.execute(db);
128
- await sql`
129
- UPDATE content_taxonomies AS pivot
130
- SET locale = ${locale}
131
- WHERE pivot.collection = ${slug}
132
- AND lower(pivot.locale) = lower(${locale})
133
- AND pivot.locale != ${locale}
134
- AND EXISTS (
135
- SELECT 1
136
- FROM ${table} AS content
137
- WHERE content.id = pivot.entry_id AND content.locale = ${locale}
138
- )
139
- `.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
+ });
140
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;
141
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";
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";
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";
142
1297
  }
143
1298
 
144
1299
  //#endregion
@@ -155,32 +1310,225 @@ function createSandboxRunnerOptions(options, siteInfo) {
155
1310
  }
156
1311
 
157
1312
  //#endregion
158
- //#region src/media/usage/gc.ts
159
- /**
160
- * Only rows older than this are collected. Guarded writers insert occurrence
161
- * rows before winning the source CAS, so the window must exceed any plausible
162
- * in-flight write; mirrors the pending-upload abandonment window.
163
- */
164
- const GC_MAX_AGE_MS = 3600 * 1e3;
165
- /**
166
- * Per-tick cap on rows considered by each GC method, so a large backlog is
167
- * amortized across ticks instead of one oversized batch (D1 bind limits).
168
- */
169
- const GC_BATCH_LIMIT = 500;
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;
170
1321
  /**
171
- * Delete media-usage occurrence rows that can no longer be read: superseded
172
- * generations, generations abandoned by losing CAS writers, and occurrences
173
- * whose source row is gone.
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.
174
1326
  */
175
- async function cleanupMediaUsageGenerations(db) {
176
- const cutoff = new Date(Date.now() - GC_MAX_AGE_MS).toISOString();
1327
+ async function cleanupMediaUsage(db) {
1328
+ const startedMs = Date.now();
1329
+ const canIssueStatement = () => withinBudget(startedMs);
177
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) {
178
1493
  return {
179
- staleGenerations: await repo.deleteStaleGenerationsOlderThan(cutoff, GC_BATCH_LIMIT),
180
- abandonedGenerations: await repo.deleteAbandonedGenerationsOlderThan(cutoff, GC_BATCH_LIMIT),
181
- orphanOccurrences: await repo.deleteOrphanOccurrencesOlderThan(cutoff, GC_BATCH_LIMIT)
1494
+ createdAt: candidate.createdAt,
1495
+ id: candidate.id
182
1496
  };
183
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
+ }
184
1532
 
185
1533
  //#endregion
186
1534
  //#region src/cleanup.ts
@@ -215,9 +1563,7 @@ async function runSystemCleanup(db, storage) {
215
1563
  pendingUploadFiles: -1,
216
1564
  uploadAttempts: -1,
217
1565
  revisionsPruned: -1,
218
- mediaUsageStaleGenerations: -1,
219
- mediaUsageAbandonedGenerations: -1,
220
- mediaUsageOrphanOccurrences: -1
1566
+ mediaUsage: -1
221
1567
  };
222
1568
  try {
223
1569
  result.challenges = await cleanupExpiredChallenges(db);
@@ -265,12 +1611,10 @@ async function runSystemCleanup(db, storage) {
265
1611
  console.error("[cleanup] Failed to prune revisions:", error);
266
1612
  }
267
1613
  try {
268
- const usage = await cleanupMediaUsageGenerations(db);
269
- result.mediaUsageStaleGenerations = usage.staleGenerations;
270
- result.mediaUsageAbandonedGenerations = usage.abandonedGenerations;
271
- result.mediaUsageOrphanOccurrences = usage.orphanOccurrences;
1614
+ const mediaUsage = await cleanupMediaUsage(db);
1615
+ result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
272
1616
  } catch (error) {
273
- console.error("[cleanup] Failed to clean media-usage generations:", error);
1617
+ console.error("[cleanup] Failed to clean media usage:", error);
274
1618
  }
275
1619
  return result;
276
1620
  }
@@ -469,6 +1813,15 @@ const DRAFT_ONLY_UPDATE_KEYS = new Set([
469
1813
  "skipRevision"
470
1814
  ]);
471
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
+ ]);
472
1825
  /**
473
1826
  * Convert a ContentItem to Record<string, unknown> for hook consumption.
474
1827
  * Hooks receive the full item as a flat record.
@@ -552,6 +1905,49 @@ const marketplaceManifestCache = /* @__PURE__ */ new Map();
552
1905
  /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
553
1906
  const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
554
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
+ }
555
1951
  /**
556
1952
  * EmDashRuntime - singleton per worker
557
1953
  */
@@ -663,7 +2059,14 @@ var EmDashRuntime = class EmDashRuntime {
663
2059
  * Returns the items promoted so callers can invalidate their cache tags.
664
2060
  */
665
2061
  async publishScheduled() {
666
- 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
+ });
667
2070
  }
668
2071
  /**
669
2072
  * Run the full scheduled-maintenance batch: cron tasks, scheduled
@@ -694,10 +2097,7 @@ var EmDashRuntime = class EmDashRuntime {
694
2097
  }
695
2098
  let published = [];
696
2099
  try {
697
- published = await publishDueContent(this.db, {
698
- publish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),
699
- onPublished: options.onPublished
700
- });
2100
+ published = await this.publishScheduledWithFence(options.onPublished);
701
2101
  } catch (error) {
702
2102
  console.error("[scheduled-publish] Sweep failed:", error);
703
2103
  }
@@ -712,8 +2112,12 @@ var EmDashRuntime = class EmDashRuntime {
712
2112
  console.error("[plugins] Storage index sync failed:", error);
713
2113
  }
714
2114
  await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
2115
+ await recordSchedulerHeartbeatSafely(this.db);
715
2116
  return { published };
716
2117
  }
2118
+ async runScheduledMediaUsageTasks() {
2119
+ return runScheduledMediaUsageLane(this.db);
2120
+ }
717
2121
  /**
718
2122
  * Materialize plugin-declared storage indexes, once per process.
719
2123
  *
@@ -1096,9 +2500,9 @@ var EmDashRuntime = class EmDashRuntime {
1096
2500
  const seedHolder = getSeedHolder();
1097
2501
  try {
1098
2502
  await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
1099
- const { applySeed } = await import("../apply-kwNEJW2l.mjs").then((n) => n.n);
1100
- const { loadSeed } = await import("../load-DV1DZuUJ.mjs").then((n) => n.r);
1101
- const { validateSeed } = await import("../validate-mKo3RxBM.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);
1102
2506
  const seed = await loadSeed();
1103
2507
  if (validateSeed(seed).valid) {
1104
2508
  await applySeed(db, seed, { onConflict: "skip" });
@@ -1179,6 +2583,7 @@ var EmDashRuntime = class EmDashRuntime {
1179
2583
  const pipelineFactoryOptions = {
1180
2584
  db,
1181
2585
  getDb: resolveDb,
2586
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
1182
2587
  storage: storage ?? void 0,
1183
2588
  siteInfo
1184
2589
  };
@@ -1229,10 +2634,19 @@ var EmDashRuntime = class EmDashRuntime {
1229
2634
  if (deps.createScheduler) {
1230
2635
  const scheduler = deps.createScheduler(cronExecutor);
1231
2636
  cronScheduler = scheduler;
2637
+ const runMediaUsageMaintenance = async () => {
2638
+ const runtime = runtimeRef.current;
2639
+ if (runtime) await runtime.runScheduledMediaUsageTasks();
2640
+ else await runScheduledMediaUsageLane(db);
2641
+ };
1232
2642
  scheduler.setSystemCleanup(async () => {
1233
2643
  try {
1234
2644
  const runtime = runtimeRef.current;
1235
- 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
+ }
1236
2650
  } catch (error) {
1237
2651
  console.error("[scheduled-publish] Sweep failed:", error);
1238
2652
  }
@@ -1247,7 +2661,14 @@ var EmDashRuntime = class EmDashRuntime {
1247
2661
  console.error("[plugins] Storage index sync failed:", error);
1248
2662
  }
1249
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
+ }
1250
2670
  });
2671
+ scheduler.setMediaUsageMaintenance?.(runMediaUsageMaintenance);
1251
2672
  scheduler.start();
1252
2673
  }
1253
2674
  } catch (error) {
@@ -1407,6 +2828,7 @@ var EmDashRuntime = class EmDashRuntime {
1407
2828
  if (!deps.sandboxEnabled) return sandboxedPluginCache;
1408
2829
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1409
2830
  db,
2831
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1410
2832
  mediaStorage: mediaStorage ? {
1411
2833
  upload: (opts) => mediaStorage.upload({
1412
2834
  key: opts.key,
@@ -1472,6 +2894,7 @@ var EmDashRuntime = class EmDashRuntime {
1472
2894
  static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
1473
2895
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1474
2896
  db,
2897
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1475
2898
  mediaStorage: {
1476
2899
  upload: (opts) => storage.upload({
1477
2900
  key: opts.key,
@@ -1673,13 +3096,33 @@ var EmDashRuntime = class EmDashRuntime {
1673
3096
  if ((field.type === "repeater" || field.type === "file" || field.type === "image") && field.validation) entry.validation = { ...field.validation };
1674
3097
  fields[field.slug] = entry;
1675
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
+ }
1676
3115
  manifestCollections[collection.slug] = {
1677
3116
  label: collection.label,
1678
3117
  labelSingular: collection.labelSingular || collection.label,
1679
3118
  supports: collection.supports || [],
1680
3119
  hasSeo: collection.hasSeo,
1681
3120
  urlPattern: collection.urlPattern,
3121
+ routable: collection.routable !== false,
3122
+ titleField: collection.titleField,
3123
+ dateField: collection.dateField,
1682
3124
  ...collection.hidden ? { hidden: true } : {},
3125
+ listColumns: listColumns.length > 0 ? listColumns : void 0,
1683
3126
  fields
1684
3127
  };
1685
3128
  }
@@ -1739,21 +3182,33 @@ var EmDashRuntime = class EmDashRuntime {
1739
3182
  };
1740
3183
  }
1741
3184
  let manifestTaxonomies = [];
3185
+ let taxonomyDefinitionLocales = [];
1742
3186
  try {
1743
- 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,
1744
3191
  name: row.name,
1745
3192
  label: row.label,
1746
3193
  labelSingular: row.label_singular ?? void 0,
1747
3194
  hierarchical: row.hierarchical === 1,
1748
- collections: parseStringArray(row.collections).toSorted()
3195
+ collections: parseStringArray(row.collections).toSorted(),
3196
+ locale: row.locale,
3197
+ translationGroup: row.translation_group ?? row.id
1749
3198
  }));
1750
3199
  } catch (error) {
1751
3200
  console.debug("EmDash: Could not load taxonomy definitions:", error);
1752
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
+ }
1753
3208
  const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
1754
3209
  const authMode = getAuthMode(this.config);
1755
3210
  const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
1756
- const i18nConfig = virtualConfig?.i18n;
3211
+ const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
1757
3212
  const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
1758
3213
  defaultLocale: i18nConfig.defaultLocale,
1759
3214
  locales: i18nConfig.locales
@@ -1769,6 +3224,10 @@ var EmDashRuntime = class EmDashRuntime {
1769
3224
  taxonomies: manifestTaxonomies,
1770
3225
  authMode: authModeValue,
1771
3226
  i18n,
3227
+ contentLocale: {
3228
+ defaultLocale: i18nConfig?.defaultLocale ?? "en",
3229
+ implicit: i18nConfig === null
3230
+ },
1772
3231
  marketplace: !!this.config.marketplace,
1773
3232
  registry
1774
3233
  };
@@ -1874,7 +3333,7 @@ var EmDashRuntime = class EmDashRuntime {
1874
3333
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
1875
3334
  processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
1876
3335
  processedData = await this.normalizeMediaFields(collection, processedData);
1877
- const { validateContentData } = await import("../validation-BgpucsvF.mjs");
3336
+ const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
1878
3337
  const validation = await validateContentData(this.db, collection, processedData, { partial: false });
1879
3338
  if (!validation.ok) return {
1880
3339
  success: false,
@@ -1917,7 +3376,7 @@ var EmDashRuntime = class EmDashRuntime {
1917
3376
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
1918
3377
  processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
1919
3378
  processedData = await this.normalizeMediaFields(collection, processedData);
1920
- const { validateContentData } = await import("../validation-BgpucsvF.mjs");
3379
+ const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
1921
3380
  const validation = await validateContentData(this.db, collection, processedData, { partial: true });
1922
3381
  if (!validation.ok) return {
1923
3382
  success: false,
@@ -2233,13 +3692,16 @@ var EmDashRuntime = class EmDashRuntime {
2233
3692
  }
2234
3693
  async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
2235
3694
  for (const contentId of new Set(contentIds)) try {
3695
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2236
3696
  await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
2237
3697
  } catch (error) {
2238
3698
  console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
3699
+ return;
2239
3700
  }
2240
3701
  }
2241
3702
  async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
2242
3703
  try {
3704
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2243
3705
  const result = await deleteContentMediaUsage(this.db, collection, contentId);
2244
3706
  if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
2245
3707
  } catch (error) {
@@ -2285,7 +3747,7 @@ var EmDashRuntime = class EmDashRuntime {
2285
3747
  if (!meta) return null;
2286
3748
  return meta.admin?.settingsSchema ?? {};
2287
3749
  }
2288
- async handlePluginApiRoute(pluginId, _method, path, request) {
3750
+ async handlePluginApiRoute(pluginId, _method, path, request, user) {
2289
3751
  if (!this.isPluginEnabled(pluginId)) return {
2290
3752
  success: false,
2291
3753
  error: {
@@ -2293,6 +3755,7 @@ var EmDashRuntime = class EmDashRuntime {
2293
3755
  message: `Plugin not enabled: ${pluginId}`
2294
3756
  }
2295
3757
  };
3758
+ const caller = user ? toRouteCallerInfo(user) : void 0;
2296
3759
  const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
2297
3760
  if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
2298
3761
  const routeRegistry = new PluginRouteRegistry({
@@ -2306,11 +3769,12 @@ var EmDashRuntime = class EmDashRuntime {
2306
3769
  const body = await parseRouteInput(request);
2307
3770
  return routeRegistry.invoke(pluginId, routeKey, {
2308
3771
  request,
2309
- body
3772
+ body,
3773
+ user: caller
2310
3774
  });
2311
3775
  }
2312
3776
  const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
2313
- if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
3777
+ if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request, caller);
2314
3778
  return {
2315
3779
  success: false,
2316
3780
  error: {
@@ -2385,7 +3849,7 @@ var EmDashRuntime = class EmDashRuntime {
2385
3849
  ...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
2386
3850
  })).toSorted((a, b) => a.name.localeCompare(b.name)));
2387
3851
  }
2388
- async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request) {
3852
+ async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller) {
2389
3853
  const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
2390
3854
  const audit = new AuditRepository(this.db);
2391
3855
  const headers = new Headers(request.headers);
@@ -2396,7 +3860,7 @@ var EmDashRuntime = class EmDashRuntime {
2396
3860
  headers,
2397
3861
  body: JSON.stringify(input)
2398
3862
  });
2399
- const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest);
3863
+ const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller);
2400
3864
  await audit.log({
2401
3865
  actorId,
2402
3866
  actorIp: requestMeta.ip ?? void 0,
@@ -2625,7 +4089,7 @@ var EmDashRuntime = class EmDashRuntime {
2625
4089
  runAfterUnscheduleHooks(content, collection) {
2626
4090
  this.runDeferredContentHook("content:afterUnschedule", content, collection);
2627
4091
  }
2628
- async handleSandboxedRoute(plugin, path, request) {
4092
+ async handleSandboxedRoute(plugin, path, request, user) {
2629
4093
  const routeName = path.replace(LEADING_SLASH_PATTERN, "");
2630
4094
  const body = await parseRouteInput(request);
2631
4095
  try {
@@ -2637,11 +4101,21 @@ var EmDashRuntime = class EmDashRuntime {
2637
4101
  url: request.url,
2638
4102
  method: request.method,
2639
4103
  headers,
2640
- meta
4104
+ meta,
4105
+ user
2641
4106
  })
2642
4107
  };
2643
4108
  } catch (error) {
2644
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
+ };
2645
4119
  return {
2646
4120
  success: false,
2647
4121
  error: {
@@ -3021,12 +4495,6 @@ function createPublicPluginApiRouteHandler(runtime) {
3021
4495
  //#endregion
3022
4496
  //#region src/astro/middleware.ts
3023
4497
  /**
3024
- * EmDash middleware
3025
- *
3026
- * Thin wrapper that initializes EmDashRuntime and attaches it to locals.
3027
- * All heavy lifting happens in EmDashRuntime.
3028
- */
3029
- /**
3030
4498
  * Runtime init lock reclaim deadline. Must be strictly larger than the db
3031
4499
  * init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →
3032
4500
  * the db init lock, and equal deadlines would let this outer lock reclaim
@@ -3170,6 +4638,15 @@ async function runScheduledTasks(options = {}) {
3170
4638
  if (!config) return { published: [] };
3171
4639
  return runOutsideRequest(config, (runtime) => runtime.runScheduledTasks(options));
3172
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
+ }
3173
4650
  /**
3174
4651
  * Run a callback against the EmDash runtime outside any HTTP request — from a
3175
4652
  * Cloudflare Queue consumer, a `scheduled()` handler, or any other
@@ -3213,19 +4690,36 @@ async function withEmDashRuntime(run) {
3213
4690
  /**
3214
4691
  * Shared plumbing for request-free entry points (`runScheduledTasks`,
3215
4692
  * `withEmDashRuntime`): resolve the runtime singleton, then run the callback
3216
- * under an event-scoped db connection when the adapter needs one.
4693
+ * under an event-scoped db when the adapter needs one.
3217
4694
  *
3218
4695
  * Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse
3219
4696
  * the per-isolate singleton from a platform event: its socket belongs to the
3220
4697
  * request that opened it, and workerd rejects cross-event I/O. Open an
3221
4698
  * event-scoped connection and run the callback under it in ALS — the
3222
4699
  * runtime's db getter, the cron executor, and plugin contexts all resolve
3223
- * the connection from ALS — then close it. Gated on the adapter being
3224
- * connection-backed (it exposes `close()`); stateless adapters (D1, Node
3225
- * 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.
3226
4703
  */
3227
4704
  async function runOutsideRequest(config, fn) {
3228
- 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) {
3229
4723
  const scoped = createRequestScopedDb$1({
3230
4724
  config: config.database?.config,
3231
4725
  isAuthenticated: false,
@@ -3234,7 +4728,7 @@ async function runOutsideRequest(config, fn) {
3234
4728
  cookies: NOOP_COOKIE_JAR,
3235
4729
  url: CRON_EVENT_URL
3236
4730
  });
3237
- if (!scoped?.close) return fn(runtime);
4731
+ if (!scoped) return fn(runtime);
3238
4732
  const { closed, deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
3239
4733
  const parent = getRequestContext();
3240
4734
  const ctx = parent ? {
@@ -3359,12 +4853,39 @@ function createRequestScopedDb$1(opts) {
3359
4853
  if (typeof createRequestScopedDb !== "function") return null;
3360
4854
  return createRequestScopedDb(opts);
3361
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
+ }
3362
4882
  const onRequest = defineMiddleware(async (context, next) => {
3363
4883
  const { request, locals, cookies } = context;
3364
4884
  const url = context.url;
3365
4885
  if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
3366
4886
  if (virtualConfig.authProviders.some((p) => p.routes?.some((r) => r.pattern && url.pathname === r.pattern))) return finalizeResponse(await next());
3367
4887
  }
4888
+ applyBuildValidator(context);
3368
4889
  const queryRecorder = isInstrumentationEnabled() ? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default") : void 0;
3369
4890
  const metrics = createRequestMetrics(performance.now());
3370
4891
  const run = async () => {
@@ -3386,7 +4907,7 @@ const onRequest = defineMiddleware(async (context, next) => {
3386
4907
  if (!isSetupVerified() && !context.isPrerendered) {
3387
4908
  const t0 = performance.now();
3388
4909
  try {
3389
- const { getDb } = await import("../loader-CGugY7Zl.mjs").then((n) => n.s);
4910
+ const { getDb } = await import("../loader-DLBqRiGP.mjs").then((n) => n.c);
3390
4911
  await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
3391
4912
  markSetupVerified();
3392
4913
  } catch (error) {
@@ -3627,5 +5148,5 @@ const onRequest = defineMiddleware(async (context, next) => {
3627
5148
  });
3628
5149
 
3629
5150
  //#endregion
3630
- export { onRequest as default, onRequest, runScheduledTasks, withEmDashRuntime };
5151
+ export { onRequest as default, onRequest, runScheduledMediaUsageTasks, runScheduledTasks, withEmDashRuntime };
3631
5152
  //# sourceMappingURL=middleware.mjs.map