emdash 0.33.0 → 0.35.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 (916) hide show
  1. package/dist/{adapters-BMyNSSZi.d.mts → adapters-Bb1vG41D.d.mts} +33 -2
  2. package/dist/adapters-Bb1vG41D.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-BeBglVxU.mjs} +98 -407
  11. package/dist/api-BeBglVxU.mjs.map +1 -0
  12. package/dist/{api-tokens-CSND0i1z.mjs → api-tokens-KO24G1Fd.mjs} +2 -2
  13. package/dist/{api-tokens-CSND0i1z.mjs.map → api-tokens-KO24G1Fd.mjs.map} +1 -1
  14. package/dist/{apply-kwNEJW2l.mjs → apply-kUDt80-G.mjs} +131 -107
  15. package/dist/apply-kUDt80-G.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 +163 -49
  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 +1763 -173
  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.d.mts.map +1 -1
  134. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -9
  135. package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -1
  136. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  137. package/dist/astro/routes/api/auth/invite/complete.mjs +13 -12
  138. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  139. package/dist/astro/routes/api/auth/invite/index.mjs +10 -9
  140. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  141. package/dist/astro/routes/api/auth/invite/register-options.mjs +12 -11
  142. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  143. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  144. package/dist/astro/routes/api/auth/magic-link/send.mjs +12 -11
  145. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  146. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  147. package/dist/astro/routes/api/auth/me.mjs +10 -9
  148. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  149. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  150. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  151. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  152. package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
  153. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  154. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  155. package/dist/astro/routes/api/auth/passkey/options.mjs +14 -13
  156. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  157. package/dist/astro/routes/api/auth/passkey/register/options.mjs +12 -11
  158. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  159. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +13 -12
  160. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  161. package/dist/astro/routes/api/auth/passkey/verify.mjs +13 -12
  162. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/signup/complete.mjs +13 -12
  164. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  165. package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
  166. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  167. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  168. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +15 -14
  169. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  170. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +14 -13
  171. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  172. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  173. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
  174. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  175. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  176. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +13 -13
  177. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
  178. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  179. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +18 -17
  180. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  181. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +18 -17
  182. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  183. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  184. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  185. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
  186. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  187. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  188. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +44 -31
  189. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  190. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  191. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
  192. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
  193. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  194. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  195. package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
  196. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  197. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  198. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  199. package/dist/astro/routes/api/dashboard.mjs +10 -8
  200. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  201. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  202. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  203. package/dist/astro/routes/api/import/probe.mjs +18 -16
  204. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  205. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  206. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  207. package/dist/astro/routes/api/import/wordpress/execute.mjs +15 -15
  208. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  209. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  210. package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
  211. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  212. package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
  213. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  214. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  215. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +14 -14
  216. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  217. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +18 -16
  218. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  219. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  220. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  221. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +28 -27
  222. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  223. package/dist/astro/routes/api/manifest.mjs +5 -5
  224. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  225. package/dist/astro/routes/api/mcp.mjs +174 -72
  226. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  227. package/dist/astro/routes/api/media/_id_/confirm.mjs +13 -11
  228. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  229. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  230. package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
  231. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  232. package/dist/astro/routes/api/media/_id_.mjs +16 -15
  233. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  234. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  235. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  236. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  237. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  238. package/dist/astro/routes/api/media/upload-url.mjs +11 -10
  239. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  240. package/dist/astro/routes/api/media.mjs +18 -15
  241. package/dist/astro/routes/api/media.mjs.map +1 -1
  242. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +11 -11
  243. package/dist/astro/routes/api/menus/_name_/items.mjs +11 -11
  244. package/dist/astro/routes/api/menus/_name_/reorder.mjs +11 -11
  245. package/dist/astro/routes/api/menus/_name_/translations.mjs +11 -11
  246. package/dist/astro/routes/api/menus/_name_.mjs +11 -11
  247. package/dist/astro/routes/api/menus/index.mjs +11 -11
  248. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  249. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  250. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  251. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  252. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  253. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  254. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  255. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  256. package/dist/astro/routes/api/openapi.json.mjs +152 -6
  257. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  258. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  259. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
  260. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  261. package/dist/astro/routes/api/redirects/404s/index.mjs +12 -11
  262. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  263. package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
  264. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  265. package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
  266. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  267. package/dist/astro/routes/api/redirects/index.mjs +13 -12
  268. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/relations/_id_/index.mjs +18 -17
  270. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  271. package/dist/astro/routes/api/relations/_id_/translations.mjs +13 -12
  272. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  273. package/dist/astro/routes/api/relations/index.mjs +18 -17
  274. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  275. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  276. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  277. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +47 -42
  278. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  279. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +47 -42
  280. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  281. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +47 -42
  282. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  283. package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
  284. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +47 -42
  285. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  286. package/dist/astro/routes/api/schema/collections/index.mjs +47 -42
  287. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  288. package/dist/astro/routes/api/schema/collections/reorder.mjs +47 -42
  289. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  290. package/dist/astro/routes/api/schema/index.mjs +10 -9
  291. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  292. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +47 -42
  293. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  294. package/dist/astro/routes/api/schema/orphans/index.mjs +47 -42
  295. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  296. package/dist/astro/routes/api/search/enable.mjs +13 -13
  297. package/dist/astro/routes/api/search/index.mjs +12 -12
  298. package/dist/astro/routes/api/search/rebuild.mjs +13 -13
  299. package/dist/astro/routes/api/search/stats.mjs +8 -8
  300. package/dist/astro/routes/api/search/suggest.mjs +12 -12
  301. package/dist/astro/routes/api/sections/_slug_.mjs +12 -11
  302. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  303. package/dist/astro/routes/api/sections/index.mjs +12 -11
  304. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  305. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
  306. package/dist/astro/routes/api/settings/backups/archives/index.mjs +6 -6
  307. package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
  308. package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
  309. package/dist/astro/routes/api/settings/email.mjs +5 -5
  310. package/dist/astro/routes/api/settings.mjs +14 -13
  311. package/dist/astro/routes/api/settings.mjs.map +1 -1
  312. package/dist/astro/routes/api/setup/admin-verify.mjs +14 -13
  313. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  314. package/dist/astro/routes/api/setup/admin.mjs +13 -12
  315. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  316. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  317. package/dist/astro/routes/api/setup/dev-bypass.mjs +30 -31
  318. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  319. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  320. package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
  321. package/dist/astro/routes/api/setup/index.mjs +31 -35
  322. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  323. package/dist/astro/routes/api/setup/status.mjs +5 -5
  324. package/dist/astro/routes/api/snapshot.mjs +8 -8
  325. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +16 -15
  326. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
  327. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -15
  328. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  329. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -15
  330. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  331. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  332. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +18 -16
  333. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  334. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
  335. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
  336. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
  337. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
  338. package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
  339. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
  340. package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
  341. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
  342. package/dist/astro/routes/api/taxonomies/index.mjs +16 -15
  343. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  344. package/dist/astro/routes/api/themes/preview.mjs +6 -6
  345. package/dist/astro/routes/api/typegen.mjs +6 -6
  346. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  347. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  348. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  349. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
  350. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  351. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +12 -11
  352. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  353. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -11
  354. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  355. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -6
  356. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  357. package/dist/astro/routes/api/widget-areas/index.mjs +12 -11
  358. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  359. package/dist/astro/routes/api/widget-components.mjs +4 -4
  360. package/dist/astro/routes/robots.txt.mjs +7 -6
  361. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  362. package/dist/astro/routes/sitemap-_collection_.xml.mjs +10 -10
  363. package/dist/astro/routes/sitemap.xml.mjs +8 -7
  364. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  365. package/dist/astro/types.d.mts +35 -17
  366. package/dist/astro/types.d.mts.map +1 -1
  367. package/dist/auth/providers/github.d.mts +1 -1
  368. package/dist/auth/providers/google.d.mts +1 -1
  369. package/dist/{authorize-qRhWQnel.mjs → authorize-DUElxgOQ.mjs} +2 -2
  370. package/dist/{authorize-qRhWQnel.mjs.map → authorize-DUElxgOQ.mjs.map} +1 -1
  371. package/dist/{backup-CtA6LDNw.mjs → backup-DWYpJlLj.mjs} +4 -4
  372. package/dist/{backup-CtA6LDNw.mjs.map → backup-DWYpJlLj.mjs.map} +1 -1
  373. package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
  374. package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
  375. package/dist/bundle-vn56sLos.mjs +663 -0
  376. package/dist/bundle-vn56sLos.mjs.map +1 -0
  377. package/dist/{byline-fields-CNc0Yq5i.mjs → byline-fields-txaLGoME.mjs} +2 -2
  378. package/dist/{byline-fields-CNc0Yq5i.mjs.map → byline-fields-txaLGoME.mjs.map} +1 -1
  379. package/dist/{byline-registry-ZkPKqEwC.mjs → byline-registry-DDun8sxh.mjs} +2 -2
  380. package/dist/{byline-registry-ZkPKqEwC.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
  381. package/dist/{byline-Cp6csL84.mjs → byline-yjvWq4ip.mjs} +5 -5
  382. package/dist/{byline-Cp6csL84.mjs.map → byline-yjvWq4ip.mjs.map} +1 -1
  383. package/dist/{bylines-cf-C9lmF.mjs → bylines-Bu7pchQ4.mjs} +5 -5
  384. package/dist/{bylines-cf-C9lmF.mjs.map → bylines-Bu7pchQ4.mjs.map} +1 -1
  385. package/dist/{bylines-CEj5CdT4.mjs → bylines-DjXS5p0P.mjs} +7 -7
  386. package/dist/{bylines-CEj5CdT4.mjs.map → bylines-DjXS5p0P.mjs.map} +1 -1
  387. package/dist/{cache-C9FptTFR.mjs → cache-C46-3Irl.mjs} +3 -3
  388. package/dist/{cache-C9FptTFR.mjs.map → cache-C46-3Irl.mjs.map} +1 -1
  389. package/dist/{challenge-store-D9VNKELo.mjs → challenge-store-SIL5DsF2.mjs} +1 -1
  390. package/dist/{challenge-store-D9VNKELo.mjs.map → challenge-store-SIL5DsF2.mjs.map} +1 -1
  391. package/dist/{chunks-DbYzib_k.mjs → chunks-fiZ8G9RJ.mjs} +2 -2
  392. package/dist/{chunks-DbYzib_k.mjs.map → chunks-fiZ8G9RJ.mjs.map} +1 -1
  393. package/dist/cli/index.mjs +655 -677
  394. package/dist/cli/index.mjs.map +1 -1
  395. package/dist/client/cf-access.d.mts +1 -1
  396. package/dist/client/index.d.mts +74 -4
  397. package/dist/client/index.d.mts.map +1 -1
  398. package/dist/client/index.mjs +25 -2
  399. package/dist/client/index.mjs.map +1 -1
  400. package/dist/collection-deletion-DQPvMD8R.mjs +779 -0
  401. package/dist/collection-deletion-DQPvMD8R.mjs.map +1 -0
  402. package/dist/{comment-BIkJ6Fil.mjs → comment-Dxlep6cE.mjs} +2 -2
  403. package/dist/{comment-BIkJ6Fil.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
  404. package/dist/{comment-reaction-D-laEceb.mjs → comment-reaction-D-2J84UE.mjs} +2 -2
  405. package/dist/{comment-reaction-D-laEceb.mjs.map → comment-reaction-D-2J84UE.mjs.map} +1 -1
  406. package/dist/{comments-0_w6KChA.mjs → comments-CTfZtLmM.mjs} +3 -3
  407. package/dist/{comments-0_w6KChA.mjs.map → comments-CTfZtLmM.mjs.map} +1 -1
  408. package/dist/{components-BPxrRjpE.mjs → components-CQy3lh9x.mjs} +1 -1
  409. package/dist/{components-BPxrRjpE.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
  410. package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
  411. package/dist/config-C3EBstHi.mjs.map +1 -0
  412. package/dist/config-COQzTe5Q.d.mts +34 -0
  413. package/dist/config-COQzTe5Q.d.mts.map +1 -0
  414. package/dist/{content-CJ94NEx5.mjs → content-ZHCc1ngN.mjs} +509 -78
  415. package/dist/content-ZHCc1ngN.mjs.map +1 -0
  416. package/dist/content-list-query-wJ2WN8Vp.d.mts +26 -0
  417. package/dist/content-list-query-wJ2WN8Vp.d.mts.map +1 -0
  418. package/dist/content-refresh-B_8SJzuU.mjs +2560 -0
  419. package/dist/content-refresh-B_8SJzuU.mjs.map +1 -0
  420. package/dist/{context-C_1nrqnW.mjs → context-ZldbZcle.mjs} +25 -17
  421. package/dist/context-ZldbZcle.mjs.map +1 -0
  422. package/dist/{cron-DRV-UC4K.mjs → cron-Y9aLtgrA.mjs} +1 -1
  423. package/dist/{cron-DRV-UC4K.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
  424. package/dist/{dashboard-DINnNVVU.mjs → dashboard-DvHbk_cv.mjs} +44 -10
  425. package/dist/dashboard-DvHbk_cv.mjs.map +1 -0
  426. package/dist/db/index.d.mts +4 -4
  427. package/dist/db/index.mjs +31 -8
  428. package/dist/db/index.mjs.map +1 -1
  429. package/dist/db/libsql-migrations.d.mts +11 -0
  430. package/dist/db/libsql-migrations.d.mts.map +1 -0
  431. package/dist/db/libsql-migrations.mjs +57 -0
  432. package/dist/db/libsql-migrations.mjs.map +1 -0
  433. package/dist/db/libsql.d.mts +1 -1
  434. package/dist/db/postgres-migrations.d.mts +10 -0
  435. package/dist/db/postgres-migrations.d.mts.map +1 -0
  436. package/dist/db/postgres-migrations.mjs +47 -0
  437. package/dist/db/postgres-migrations.mjs.map +1 -0
  438. package/dist/db/postgres.d.mts +1 -1
  439. package/dist/db/sqlite-migrations.d.mts +10 -0
  440. package/dist/db/sqlite-migrations.d.mts.map +1 -0
  441. package/dist/db/sqlite-migrations.mjs +28 -0
  442. package/dist/db/sqlite-migrations.mjs.map +1 -0
  443. package/dist/db/sqlite.d.mts +1 -1
  444. package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
  445. package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
  446. package/dist/{default-BD2FG0xV.mjs → default-DdDl3fxv.mjs} +1 -1
  447. package/dist/{default-BD2FG0xV.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
  448. package/dist/{deferred-tasks-C-WRCcKo.d.mts → deferred-tasks-SDWxrgzJ.d.mts} +1 -1
  449. package/dist/{deferred-tasks-C-WRCcKo.d.mts.map → deferred-tasks-SDWxrgzJ.d.mts.map} +1 -1
  450. package/dist/{device-flow-CTqd8mud.mjs → device-flow-C0GdD_z8.mjs} +4 -4
  451. package/dist/{device-flow-CTqd8mud.mjs.map → device-flow-C0GdD_z8.mjs.map} +1 -1
  452. package/dist/direct-executor-CDXeJCSs.mjs +85 -0
  453. package/dist/direct-executor-CDXeJCSs.mjs.map +1 -0
  454. package/dist/dist-D_6CKR8z.mjs +810 -0
  455. package/dist/dist-D_6CKR8z.mjs.map +1 -0
  456. package/dist/{email-console-C6WdcWbL.mjs → email-console-BHNjIwiB.mjs} +1 -1
  457. package/dist/{email-console-C6WdcWbL.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
  458. package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
  459. package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
  460. package/dist/{error-oh_9i6bR.mjs → error-DupyJZE2.mjs} +3 -3
  461. package/dist/{error-oh_9i6bR.mjs.map → error-DupyJZE2.mjs.map} +1 -1
  462. package/dist/{errors-BucQ8VYz.mjs → errors-CLUPm7aD.mjs} +14 -2
  463. package/dist/errors-CLUPm7aD.mjs.map +1 -0
  464. package/dist/{escape-CXps5F_v.mjs → escape-DUY2r2Zs.mjs} +1 -1
  465. package/dist/{escape-CXps5F_v.mjs.map → escape-DUY2r2Zs.mjs.map} +1 -1
  466. package/dist/{field-defs-cache-WD82dMQq.mjs → field-defs-cache-BzwBlJrR.mjs} +4 -4
  467. package/dist/{field-defs-cache-WD82dMQq.mjs.map → field-defs-cache-BzwBlJrR.mjs.map} +1 -1
  468. package/dist/{fts-manager-BITakW_R.mjs → fts-manager-DyZEohZC.mjs} +76 -50
  469. package/dist/fts-manager-DyZEohZC.mjs.map +1 -0
  470. package/dist/{hreflang-BdZuVBhL.mjs → hreflang-BBe83kKa.mjs} +7 -7
  471. package/dist/{hreflang-BdZuVBhL.mjs.map → hreflang-BBe83kKa.mjs.map} +1 -1
  472. package/dist/{hreflang-BaLCNcDx.d.mts → hreflang-DwT2E0Sa.d.mts} +2 -2
  473. package/dist/{hreflang-BaLCNcDx.d.mts.map → hreflang-DwT2E0Sa.d.mts.map} +1 -1
  474. package/dist/identity-Ys6Dlc9C.mjs +35 -0
  475. package/dist/identity-Ys6Dlc9C.mjs.map +1 -0
  476. package/dist/{import-C-9uqyT3.mjs → import-DPkLmeXO.mjs} +5 -5
  477. package/dist/{import-C-9uqyT3.mjs.map → import-DPkLmeXO.mjs.map} +1 -1
  478. package/dist/{index-M37iz29I.d.mts → index-BVBZj4-7.d.mts} +236 -104
  479. package/dist/index-BVBZj4-7.d.mts.map +1 -0
  480. package/dist/{index-MGw47uWq.d.mts → index-C9KuQyO-.d.mts} +5 -10
  481. package/dist/index-C9KuQyO-.d.mts.map +1 -0
  482. package/dist/index.d.mts +21 -19
  483. package/dist/index.mjs +73 -67
  484. package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
  485. package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
  486. package/dist/{load-DV1DZuUJ.mjs → load-YJ2xl3ra.mjs} +2 -2
  487. package/dist/{load-DV1DZuUJ.mjs.map → load-YJ2xl3ra.mjs.map} +1 -1
  488. package/dist/{loader-CGugY7Zl.mjs → loader-CdG_Ci3H.mjs} +95 -53
  489. package/dist/loader-CdG_Ci3H.mjs.map +1 -0
  490. package/dist/manifest-builder-B252yQUf.mjs +189 -0
  491. package/dist/manifest-builder-B252yQUf.mjs.map +1 -0
  492. package/dist/{manifest-schema-Da7kj23y.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
  493. package/dist/{manifest-schema-Da7kj23y.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
  494. package/dist/media/image-endpoint.mjs +1 -1
  495. package/dist/media/index.d.mts +1 -1
  496. package/dist/media/index.mjs +3 -2
  497. package/dist/media/index.mjs.map +1 -1
  498. package/dist/media/local-runtime.d.mts +12 -12
  499. package/dist/media/local-runtime.mjs +6 -5
  500. package/dist/media/local-runtime.mjs.map +1 -1
  501. package/dist/{media-OhYL-7Io.mjs → media-BWWWb8kg.mjs} +92 -3
  502. package/dist/media-BWWWb8kg.mjs.map +1 -0
  503. package/dist/{media-BP7S-nHT.mjs → media-CzhAn2--.mjs} +2 -2
  504. package/dist/{media-BP7S-nHT.mjs.map → media-CzhAn2--.mjs.map} +1 -1
  505. package/dist/{media-allowlist-D-peH1H5.mjs → media-allowlist-D6x-Us02.mjs} +14 -3
  506. package/dist/media-allowlist-D6x-Us02.mjs.map +1 -0
  507. package/dist/{media-upload-DtrwU2B0.mjs → media-upload-CHBVn6m-.mjs} +10 -8
  508. package/dist/{media-upload-DtrwU2B0.mjs.map → media-upload-CHBVn6m-.mjs.map} +1 -1
  509. package/dist/{media-url-DYs_YLOx.mjs → media-url-BvoX88FA.mjs} +1 -1
  510. package/dist/{media-url-DYs_YLOx.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
  511. package/dist/{media-usage-BlxfTkN0.mjs → media-usage-BZCydfvu.mjs} +137 -60
  512. package/dist/media-usage-BZCydfvu.mjs.map +1 -0
  513. package/dist/media-usage-work-CLo_6tct.mjs +152 -0
  514. package/dist/media-usage-work-CLo_6tct.mjs.map +1 -0
  515. package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
  516. package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
  517. package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
  518. package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
  519. package/dist/{menus-Y54RDPIN.mjs → menus-DSo5QeW0.mjs} +351 -95
  520. package/dist/menus-DSo5QeW0.mjs.map +1 -0
  521. package/dist/{menus-BcQWxeXu.mjs → menus-TCNSd2vp.mjs} +3 -3
  522. package/dist/{menus-BcQWxeXu.mjs.map → menus-TCNSd2vp.mjs.map} +1 -1
  523. package/dist/migrations/index.d.mts +22 -0
  524. package/dist/migrations/index.d.mts.map +1 -0
  525. package/dist/migrations/index.mjs +7 -0
  526. package/dist/{mode-BWyTBe3I.mjs → mode-BDoH_WcF.mjs} +1 -1
  527. package/dist/{mode-BWyTBe3I.mjs.map → mode-BDoH_WcF.mjs.map} +1 -1
  528. package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
  529. package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
  530. package/dist/{oauth-authorization-ClvktmrL.mjs → oauth-authorization-iOikmrJd.mjs} +4 -4
  531. package/dist/{oauth-authorization-ClvktmrL.mjs.map → oauth-authorization-iOikmrJd.mjs.map} +1 -1
  532. package/dist/{oauth-clients-8ao-U66l.mjs → oauth-clients-CvI0nHMJ.mjs} +1 -1
  533. package/dist/{oauth-clients-8ao-U66l.mjs.map → oauth-clients-CvI0nHMJ.mjs.map} +1 -1
  534. package/dist/{oauth-state-store-B1ofdTiQ.mjs → oauth-state-store-Ba8KZlwW.mjs} +1 -1
  535. package/dist/{oauth-state-store-B1ofdTiQ.mjs.map → oauth-state-store-Ba8KZlwW.mjs.map} +1 -1
  536. package/dist/{oauth-user-lookup-CscEuIiR.mjs → oauth-user-lookup-C5SQwGYC.mjs} +1 -1
  537. package/dist/{oauth-user-lookup-CscEuIiR.mjs.map → oauth-user-lookup-C5SQwGYC.mjs.map} +1 -1
  538. package/dist/object-cache/memory.d.mts +1 -1
  539. package/dist/{options-CA4eVX6P.d.mts → options--hhz-jZ1.d.mts} +4 -3
  540. package/dist/options--hhz-jZ1.d.mts.map +1 -0
  541. package/dist/page/index.d.mts +2 -2
  542. package/dist/{parse-CJKNuewM.mjs → parse-DFbuc-Fh.mjs} +2 -2
  543. package/dist/{parse-CJKNuewM.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
  544. package/dist/{passkey-config-D-zrzMJI.mjs → passkey-config-DFAmUyuE.mjs} +1 -1
  545. package/dist/{passkey-config-D-zrzMJI.mjs.map → passkey-config-DFAmUyuE.mjs.map} +1 -1
  546. package/dist/{placeholder-DaeNS-VG.d.mts → placeholder-Dp0K3RkD.d.mts} +1 -1
  547. package/dist/{placeholder-DaeNS-VG.d.mts.map → placeholder-Dp0K3RkD.d.mts.map} +1 -1
  548. package/dist/plugin-types.d.mts +8 -1
  549. package/dist/plugin-types.d.mts.map +1 -1
  550. package/dist/plugin-utils.d.mts +10 -10
  551. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  552. package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
  553. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  554. package/dist/policy-TluUIxuu.mjs +45 -0
  555. package/dist/policy-TluUIxuu.mjs.map +1 -0
  556. package/dist/{portable-text-B_kqBmXB.mjs → portable-text-imBW5cHK.mjs} +1 -1
  557. package/dist/{portable-text-B_kqBmXB.mjs.map → portable-text-imBW5cHK.mjs.map} +1 -1
  558. package/dist/{preview-BU6PaLpM.mjs → preview-PW-nOnq4.mjs} +2 -2
  559. package/dist/{preview-BU6PaLpM.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
  560. package/dist/protocol-DjsJHTlK.d.mts +52 -0
  561. package/dist/protocol-DjsJHTlK.d.mts.map +1 -0
  562. package/dist/{public-url-Ox9oOlgp.mjs → public-url-CWoFhQT8.mjs} +1 -1
  563. package/dist/{public-url-Ox9oOlgp.mjs.map → public-url-CWoFhQT8.mjs.map} +1 -1
  564. package/dist/{query-rC9HKv1N.mjs → query-BWs7O5Il.mjs} +3 -3
  565. package/dist/{query-rC9HKv1N.mjs.map → query-BWs7O5Il.mjs.map} +1 -1
  566. package/dist/{query-C5w7PsRQ.mjs → query-zTVAR1Sl.mjs} +31 -49
  567. package/dist/query-zTVAR1Sl.mjs.map +1 -0
  568. package/dist/{rate-limit-kug6q99V.mjs → rate-limit-Bx4g9m3p.mjs} +2 -2
  569. package/dist/{rate-limit-kug6q99V.mjs.map → rate-limit-Bx4g9m3p.mjs.map} +1 -1
  570. package/dist/{redirect-Egf2R9yP.mjs → redirect-BgAOGHDq.mjs} +1 -1
  571. package/dist/{redirect-Egf2R9yP.mjs.map → redirect-BgAOGHDq.mjs.map} +1 -1
  572. package/dist/{redirect-DIJxXKOG.mjs → redirect-COsS6SWJ.mjs} +2 -2
  573. package/dist/{redirect-DIJxXKOG.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
  574. package/dist/{redirects-G8wAhWAf.mjs → redirects-DTDHWH31.mjs} +133 -14
  575. package/dist/redirects-DTDHWH31.mjs.map +1 -0
  576. package/dist/{redirects-Bzm812bY.mjs → redirects-DyChwDkG.mjs} +4 -4
  577. package/dist/{redirects-Bzm812bY.mjs.map → redirects-DyChwDkG.mjs.map} +1 -1
  578. package/dist/registry-Cqa2I749.mjs +1275 -0
  579. package/dist/registry-Cqa2I749.mjs.map +1 -0
  580. package/dist/{relation-Cbe3Sd3m.mjs → relation-BnHB0T4J.mjs} +3 -3
  581. package/dist/{relation-Cbe3Sd3m.mjs.map → relation-BnHB0T4J.mjs.map} +1 -1
  582. package/dist/{relations-Bw_E1TXo.mjs → relations-BBApI4e2.mjs} +3 -3
  583. package/dist/{relations-Bw_E1TXo.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
  584. package/dist/{relations-G2ta7GIl.d.mts → relations-BHo5A8Zf.d.mts} +367 -33
  585. package/dist/relations-BHo5A8Zf.d.mts.map +1 -0
  586. package/dist/{relations-qHwpUe66.mjs → relations-D5sM5fW0.mjs} +6 -6
  587. package/dist/{relations-qHwpUe66.mjs.map → relations-D5sM5fW0.mjs.map} +1 -1
  588. package/dist/request-context.d.mts +1 -1
  589. package/dist/{request-meta-Df-jFbAd.mjs → request-meta-DgZJh3zG.mjs} +2 -2
  590. package/dist/{request-meta-Df-jFbAd.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
  591. package/dist/{resolve-DZzG4HqM.mjs → resolve-DOhVEvty.mjs} +2 -2
  592. package/dist/{resolve-DZzG4HqM.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
  593. package/dist/{runner-BaADli8l.d.mts → runner-BYHZn3Jy.d.mts} +16 -3
  594. package/dist/runner-BYHZn3Jy.d.mts.map +1 -0
  595. package/dist/{runner-BFTrkYhu.mjs → runner-C1T11CfC.mjs} +1190 -257
  596. package/dist/runner-C1T11CfC.mjs.map +1 -0
  597. package/dist/runtime.d.mts +11 -11
  598. package/dist/runtime.mjs +4 -3
  599. package/dist/runtime.mjs.map +1 -1
  600. package/dist/schema-2AOsSn2v.mjs +390 -0
  601. package/dist/schema-2AOsSn2v.mjs.map +1 -0
  602. package/dist/{schema-CiQXr6_F.mjs → schema-DZ5YVVZj.mjs} +3 -3
  603. package/dist/{schema-CiQXr6_F.mjs.map → schema-DZ5YVVZj.mjs.map} +1 -1
  604. package/dist/{scopes-CNJVT5i5.mjs → scopes-8ClPF2bC.mjs} +3 -3
  605. package/dist/{scopes-CNJVT5i5.mjs.map → scopes-8ClPF2bC.mjs.map} +1 -1
  606. package/dist/{search-FZhx5dDU.mjs → search-u1QYdOCG.mjs} +25 -15
  607. package/dist/search-u1QYdOCG.mjs.map +1 -0
  608. package/dist/{secrets-DLd8RXee.mjs → secrets-BEgsAQEU.mjs} +3 -3
  609. package/dist/{secrets-DLd8RXee.mjs.map → secrets-BEgsAQEU.mjs.map} +1 -1
  610. package/dist/{sections-BrIrMUCn.mjs → sections-DmHviGyM.mjs} +3 -3
  611. package/dist/{sections-BrIrMUCn.mjs.map → sections-DmHviGyM.mjs.map} +1 -1
  612. package/dist/seed/index.d.mts +2 -2
  613. package/dist/seed/index.mjs +24 -22
  614. package/dist/seo/index.d.mts +3 -3
  615. package/dist/seo/index.mjs +16 -15
  616. package/dist/seo/index.mjs.map +1 -1
  617. package/dist/{seo-CobNfOvb.mjs → seo-AFpVvTqr.mjs} +5 -3
  618. package/dist/seo-AFpVvTqr.mjs.map +1 -0
  619. package/dist/{seo-DTHRntNC.mjs → seo-Dq4G-RrO.mjs} +2 -2
  620. package/dist/{seo-DTHRntNC.mjs.map → seo-Dq4G-RrO.mjs.map} +1 -1
  621. package/dist/{service-B-pYGPgR.mjs → service-BpArayPr.mjs} +3 -3
  622. package/dist/{service-B-pYGPgR.mjs.map → service-BpArayPr.mjs.map} +1 -1
  623. package/dist/{session-user-BvM9-zNl.mjs → session-user-BEorKVT-.mjs} +1 -1
  624. package/dist/{session-user-BvM9-zNl.mjs.map → session-user-BEorKVT-.mjs.map} +1 -1
  625. package/dist/{settings-DPHau-WH.mjs → settings-DE9xtzqd.mjs} +3 -3
  626. package/dist/{settings-DPHau-WH.mjs.map → settings-DE9xtzqd.mjs.map} +1 -1
  627. package/dist/{settings-kJhGiQ4z.mjs → settings-xRdoUZr0.mjs} +4 -4
  628. package/dist/{settings-kJhGiQ4z.mjs.map → settings-xRdoUZr0.mjs.map} +1 -1
  629. package/dist/{setup-complete-u50Qm8rY.mjs → setup-complete-BnT8pq_C.mjs} +1 -1
  630. package/dist/{setup-complete-u50Qm8rY.mjs.map → setup-complete-BnT8pq_C.mjs.map} +1 -1
  631. package/dist/{setup-nonce-DCjiiOD2.mjs → setup-nonce-CBitf_oQ.mjs} +1 -1
  632. package/dist/{setup-nonce-DCjiiOD2.mjs.map → setup-nonce-CBitf_oQ.mjs.map} +1 -1
  633. package/dist/{single-flight-cache-DW3w11Pu.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
  634. package/dist/{single-flight-cache-DW3w11Pu.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
  635. package/dist/{site-url-I-lJ5kma.mjs → site-url-DjpIXMXJ.mjs} +2 -2
  636. package/dist/{site-url-I-lJ5kma.mjs.map → site-url-DjpIXMXJ.mjs.map} +1 -1
  637. package/dist/slugify-B994zkXC.mjs +17 -0
  638. package/dist/slugify-B994zkXC.mjs.map +1 -0
  639. package/dist/{snapshot-I_fjLDaD.mjs → snapshot-BBlGkb2q.mjs} +1 -1
  640. package/dist/{snapshot-I_fjLDaD.mjs.map → snapshot-BBlGkb2q.mjs.map} +1 -1
  641. package/dist/{ssrf-CTa6v21i.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
  642. package/dist/{ssrf-CTa6v21i.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
  643. package/dist/{state-CQvoefYP.mjs → state-DYwPue40.mjs} +1 -1
  644. package/dist/{state-CQvoefYP.mjs.map → state-DYwPue40.mjs.map} +1 -1
  645. package/dist/{status-CIsmlC1E.mjs → status-CWVYpbD8.mjs} +1 -1
  646. package/dist/{status-CIsmlC1E.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
  647. package/dist/storage/local.d.mts +1 -1
  648. package/dist/storage/local.mjs +3 -3
  649. package/dist/storage/s3.d.mts +1 -1
  650. package/dist/storage/s3.mjs +1 -1
  651. package/dist/target-DixqqV5i.mjs +27 -0
  652. package/dist/target-DixqqV5i.mjs.map +1 -0
  653. package/dist/{taxonomies-CLN1N92E.mjs → taxonomies-BlrncHi9.mjs} +75 -64
  654. package/dist/taxonomies-BlrncHi9.mjs.map +1 -0
  655. package/dist/{taxonomies-CKuaZER5.mjs → taxonomies-OHUIiIpI.mjs} +280 -56
  656. package/dist/taxonomies-OHUIiIpI.mjs.map +1 -0
  657. package/dist/{taxonomy-pOrB2kZe.mjs → taxonomy-BqCZ1TdU.mjs} +131 -78
  658. package/dist/taxonomy-BqCZ1TdU.mjs.map +1 -0
  659. package/dist/{tokens-DagaJGK8.mjs → tokens-C39klX8R.mjs} +2 -2
  660. package/dist/{tokens-DagaJGK8.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
  661. package/dist/{transport-DVsa5YYo.mjs → transport-DBJwa-Si.mjs} +1 -1
  662. package/dist/{transport-DVsa5YYo.mjs.map → transport-DBJwa-Si.mjs.map} +1 -1
  663. package/dist/{transport-CAwTaTp_.d.mts → transport-RQE36APG.d.mts} +1 -1
  664. package/dist/{transport-CAwTaTp_.d.mts.map → transport-RQE36APG.d.mts.map} +1 -1
  665. package/dist/{trusted-proxy-D20_7fyX.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
  666. package/dist/{trusted-proxy-D20_7fyX.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
  667. package/dist/{types-NBI3rF-_.mjs → types-75KIqzL9.mjs} +18 -2
  668. package/dist/types-75KIqzL9.mjs.map +1 -0
  669. package/dist/{types-BjxTKxrZ.mjs → types-BW1syrlw.mjs} +1 -1
  670. package/dist/{types-BjxTKxrZ.mjs.map → types-BW1syrlw.mjs.map} +1 -1
  671. package/dist/{types-DLG4IFEQ.d.mts → types-BcqdcT-r.d.mts} +1 -1
  672. package/dist/{types-DLG4IFEQ.d.mts.map → types-BcqdcT-r.d.mts.map} +1 -1
  673. package/dist/{types-LoagAGzH.d.mts → types-BntycPTF.d.mts} +1 -1
  674. package/dist/{types-LoagAGzH.d.mts.map → types-BntycPTF.d.mts.map} +1 -1
  675. package/dist/{types-BnFP1M3J.d.mts → types-C2pMvO62.d.mts} +32 -10
  676. package/dist/types-C2pMvO62.d.mts.map +1 -0
  677. package/dist/{types-Cd8iuS1J.mjs → types-CQPWhPxE.mjs} +9 -12
  678. package/dist/types-CQPWhPxE.mjs.map +1 -0
  679. package/dist/{types-C8oz_ayJ.d.mts → types-CXnOzDe1.d.mts} +1 -1
  680. package/dist/{types-C8oz_ayJ.d.mts.map → types-CXnOzDe1.d.mts.map} +1 -1
  681. package/dist/types-CtzytVXn.mjs.map +1 -1
  682. package/dist/{types-BGWPMiBO.d.mts → types-DN64_zzZ.d.mts} +44 -5
  683. package/dist/types-DN64_zzZ.d.mts.map +1 -0
  684. package/dist/{types-Dc4ntQjQ.d.mts → types-DQIDRKF2.d.mts} +124 -1
  685. package/dist/types-DQIDRKF2.d.mts.map +1 -0
  686. package/dist/{types-CbUcuxxc.d.mts → types-DWRhk9xZ.d.mts} +1 -1
  687. package/dist/{types-CbUcuxxc.d.mts.map → types-DWRhk9xZ.d.mts.map} +1 -1
  688. package/dist/{types-BK6U01Ro.d.mts → types-Dfk8KI5R.d.mts} +1 -1
  689. package/dist/{types-BK6U01Ro.d.mts.map → types-Dfk8KI5R.d.mts.map} +1 -1
  690. package/dist/{types-Cz8hNch2.d.mts → types-DrJrVCT1.d.mts} +25 -5
  691. package/dist/types-DrJrVCT1.d.mts.map +1 -0
  692. package/dist/{upload-attempts-D8QAFLUJ.mjs → upload-attempts-D4FS8LT6.mjs} +1 -1
  693. package/dist/{upload-attempts-D8QAFLUJ.mjs.map → upload-attempts-D4FS8LT6.mjs.map} +1 -1
  694. package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
  695. package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
  696. package/dist/{user-DBrq4UVq.mjs → user-DJZfUJVE.mjs} +3 -3
  697. package/dist/{user-DBrq4UVq.mjs.map → user-DJZfUJVE.mjs.map} +1 -1
  698. package/dist/{utils-hLzi_2Fn.mjs → utils-D4vD_Y98.mjs} +3 -3
  699. package/dist/{utils-hLzi_2Fn.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
  700. package/dist/{validate-mKo3RxBM.mjs → validate-BMLBvxOC.mjs} +24 -5
  701. package/dist/validate-BMLBvxOC.mjs.map +1 -0
  702. package/dist/{validate-C3oSmA7F.d.mts → validate-CTqt5nuE.d.mts} +15 -7
  703. package/dist/validate-CTqt5nuE.d.mts.map +1 -0
  704. package/dist/{validation-BgpucsvF.mjs → validation-npvENUCB.mjs} +11 -10
  705. package/dist/{validation-BgpucsvF.mjs.map → validation-npvENUCB.mjs.map} +1 -1
  706. package/dist/version-DB_fqCSI.mjs +7 -0
  707. package/dist/{version-C4PnpmoV.mjs.map → version-DB_fqCSI.mjs.map} +1 -1
  708. package/dist/{widgets-CJJtkiKh.mjs → widgets-BD3ho7Rq.mjs} +3 -3
  709. package/dist/{widgets-CJJtkiKh.mjs.map → widgets-BD3ho7Rq.mjs.map} +1 -1
  710. package/dist/{wxr-taxonomies-9PPSy8X6.mjs → wxr-taxonomies-B1R3vBoD.mjs} +13 -32
  711. package/dist/{wxr-taxonomies-9PPSy8X6.mjs.map → wxr-taxonomies-B1R3vBoD.mjs.map} +1 -1
  712. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts → wxr-taxonomies-Ca68766D.d.mts} +1 -1
  713. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts.map → wxr-taxonomies-Ca68766D.d.mts.map} +1 -1
  714. package/dist/{zod-generator-CG64Hnbh.mjs → zod-generator-LC-on_Vb.mjs} +52 -5
  715. package/dist/zod-generator-LC-on_Vb.mjs.map +1 -0
  716. package/package.json +32 -8
  717. package/src/api/errors.ts +12 -0
  718. package/src/api/handlers/content.ts +127 -29
  719. package/src/api/handlers/dashboard.ts +12 -2
  720. package/src/api/handlers/index.ts +16 -0
  721. package/src/api/handlers/manifest.ts +2 -0
  722. package/src/api/handlers/media-allowlist.ts +13 -2
  723. package/src/api/handlers/media-usage-work.ts +188 -0
  724. package/src/api/handlers/media-usage.ts +5 -3
  725. package/src/api/handlers/schema.ts +9 -3
  726. package/src/api/handlers/seo.ts +5 -2
  727. package/src/api/handlers/taxonomies.ts +345 -82
  728. package/src/api/media-usage-write-fence.ts +64 -0
  729. package/src/api/openapi/document.ts +199 -0
  730. package/src/api/schemas/common.ts +3 -1
  731. package/src/api/schemas/content.ts +122 -1
  732. package/src/api/schemas/media-usage.ts +133 -0
  733. package/src/api/schemas/schema.ts +78 -3
  734. package/src/api/schemas/taxonomies.ts +40 -8
  735. package/src/api/types.ts +1 -0
  736. package/src/astro/integration/index.ts +101 -56
  737. package/src/astro/integration/routes.ts +34 -4
  738. package/src/astro/integration/runtime.ts +18 -0
  739. package/src/astro/integration/virtual-modules.ts +25 -1
  740. package/src/astro/integration/vite-config.ts +16 -1
  741. package/src/astro/middleware/media-usage-write-fence.ts +30 -0
  742. package/src/astro/middleware/redirect.ts +12 -2
  743. package/src/astro/middleware/scoped-db.ts +21 -0
  744. package/src/astro/middleware.ts +140 -16
  745. package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
  746. package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
  747. package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
  748. package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
  749. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
  750. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
  751. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
  752. package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
  753. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
  754. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
  755. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  756. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  757. package/src/astro/routes/api/auth/dev-bypass.ts +0 -4
  758. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
  759. package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
  760. package/src/astro/routes/api/mcp.ts +1 -0
  761. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
  762. package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
  763. package/src/astro/routes/api/setup/dev-bypass.ts +2 -7
  764. package/src/astro/routes/api/setup/index.ts +5 -13
  765. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
  766. package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
  767. package/src/astro/routes/api/taxonomies/[name].ts +102 -0
  768. package/src/astro/types.ts +25 -4
  769. package/src/cleanup.ts +6 -12
  770. package/src/cli/commands/doctor.ts +193 -6
  771. package/src/cli/commands/export-seed.ts +4 -1
  772. package/src/cli/commands/migrate.ts +593 -0
  773. package/src/cli/index.ts +2 -23
  774. package/src/client/index.ts +119 -0
  775. package/src/components/Block.astro +12 -0
  776. package/src/components/InlinePortableTextEditor.tsx +89 -1
  777. package/src/content/converters/mark-safety.ts +120 -0
  778. package/src/content/converters/portable-text-to-prosemirror.ts +25 -16
  779. package/src/content/converters/prosemirror-to-portable-text.ts +12 -2
  780. package/src/content-list-query.ts +29 -0
  781. package/src/database/index.ts +8 -2
  782. package/src/database/migrations/060_collection_admin_config.ts +16 -0
  783. package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
  784. package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
  785. package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
  786. package/src/database/migrations/064_fts_plain_text.ts +253 -0
  787. package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
  788. package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
  789. package/src/database/migrations/067_indexed_content_fields.ts +39 -0
  790. package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
  791. package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
  792. package/src/database/migrations/070_collection_routable.ts +18 -0
  793. package/src/database/migrations/policy.ts +70 -0
  794. package/src/database/migrations/runner.ts +71 -3
  795. package/src/database/repositories/content.ts +861 -96
  796. package/src/database/repositories/media-usage-work.ts +848 -0
  797. package/src/database/repositories/media-usage.ts +1502 -138
  798. package/src/database/repositories/revision.ts +17 -6
  799. package/src/database/repositories/taxonomy.ts +234 -103
  800. package/src/database/repositories/types.ts +51 -2
  801. package/src/database/types.ts +134 -16
  802. package/src/db/adapters.ts +78 -2
  803. package/src/db/index.ts +6 -1
  804. package/src/db/libsql-migrations.ts +72 -0
  805. package/src/db/postgres-migrations.ts +59 -0
  806. package/src/db/sqlite-migrations.ts +37 -0
  807. package/src/emdash-runtime.ts +271 -35
  808. package/src/fields/file.ts +7 -4
  809. package/src/fields/image.ts +8 -1
  810. package/src/fields/types.ts +18 -5
  811. package/src/i18n/config.ts +32 -0
  812. package/src/i18n/normalize.ts +31 -0
  813. package/src/i18n/repair-locale-casing.ts +0 -14
  814. package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
  815. package/src/import/wxr-taxonomies.ts +7 -47
  816. package/src/index.ts +32 -3
  817. package/src/loader.ts +119 -65
  818. package/src/mcp/server.ts +242 -34
  819. package/src/media/usage/activation.ts +626 -0
  820. package/src/media/usage/capture-triggers.ts +543 -0
  821. package/src/media/usage/cleanup.ts +305 -0
  822. package/src/media/usage/collection-deletion-processor.ts +356 -0
  823. package/src/media/usage/collection-deletion.ts +836 -0
  824. package/src/media/usage/content-fields.ts +40 -3
  825. package/src/media/usage/content-refresh.ts +356 -47
  826. package/src/media/usage/content-repair.ts +180 -61
  827. package/src/media/usage/content-snapshots.ts +85 -105
  828. package/src/media/usage/projection-fingerprint.ts +129 -0
  829. package/src/media/usage/reconciliation-processor.ts +372 -0
  830. package/src/media/usage/reconciliation.ts +1117 -0
  831. package/src/media/usage/source-key.ts +4 -0
  832. package/src/media/usage/types.ts +2 -0
  833. package/src/media/usage/work-processor.ts +248 -0
  834. package/src/menus/index.ts +246 -141
  835. package/src/migrations/config-loader.ts +205 -0
  836. package/src/migrations/direct-executor.ts +141 -0
  837. package/src/migrations/identity.ts +46 -0
  838. package/src/migrations/index.ts +19 -0
  839. package/src/migrations/integration-metadata.ts +67 -0
  840. package/src/migrations/manifest-builder.ts +55 -0
  841. package/src/migrations/manifest-writer.ts +63 -0
  842. package/src/migrations/manifest.ts +197 -0
  843. package/src/migrations/protocol.ts +59 -0
  844. package/src/migrations/target.ts +35 -0
  845. package/src/plugin-types.ts +8 -0
  846. package/src/plugins/adapt-sandbox-entry.ts +24 -5
  847. package/src/plugins/context.ts +24 -7
  848. package/src/plugins/index.ts +9 -0
  849. package/src/plugins/routes.ts +43 -1
  850. package/src/plugins/sandbox/index.ts +10 -1
  851. package/src/plugins/sandbox/types.ts +78 -1
  852. package/src/plugins/scheduler/node.ts +13 -1
  853. package/src/plugins/scheduler/types.ts +2 -0
  854. package/src/plugins/types.ts +39 -2
  855. package/src/query.ts +29 -37
  856. package/src/scheduler-health.ts +46 -0
  857. package/src/schema/registry.ts +777 -255
  858. package/src/schema/types.ts +50 -8
  859. package/src/schema/url-pattern.ts +33 -0
  860. package/src/schema/zod-generator.ts +77 -5
  861. package/src/search/fts-manager.ts +117 -76
  862. package/src/search/query.ts +32 -11
  863. package/src/search/types.ts +2 -0
  864. package/src/seed/apply.ts +68 -15
  865. package/src/seed/types.ts +11 -3
  866. package/src/seed/validate.ts +52 -2
  867. package/src/taxonomies/index.ts +77 -75
  868. package/src/taxonomies/term-counts.ts +21 -8
  869. package/src/utils/slugify.ts +1 -29
  870. package/src/virtual-modules.d.ts +24 -0
  871. package/dist/adapters-BMyNSSZi.d.mts.map +0 -1
  872. package/dist/api-D2hunekQ.mjs.map +0 -1
  873. package/dist/apply-kwNEJW2l.mjs.map +0 -1
  874. package/dist/config-DyF3OKg_.mjs.map +0 -1
  875. package/dist/content-CJ94NEx5.mjs.map +0 -1
  876. package/dist/content-refresh-Xw7aKmou.mjs +0 -1629
  877. package/dist/content-refresh-Xw7aKmou.mjs.map +0 -1
  878. package/dist/context-C_1nrqnW.mjs.map +0 -1
  879. package/dist/dashboard-DINnNVVU.mjs.map +0 -1
  880. package/dist/errors-BucQ8VYz.mjs.map +0 -1
  881. package/dist/fts-manager-BITakW_R.mjs.map +0 -1
  882. package/dist/index-M37iz29I.d.mts.map +0 -1
  883. package/dist/index-MGw47uWq.d.mts.map +0 -1
  884. package/dist/loader-CGugY7Zl.mjs.map +0 -1
  885. package/dist/media-OhYL-7Io.mjs.map +0 -1
  886. package/dist/media-allowlist-D-peH1H5.mjs.map +0 -1
  887. package/dist/media-usage-BlxfTkN0.mjs.map +0 -1
  888. package/dist/menus-Y54RDPIN.mjs.map +0 -1
  889. package/dist/options-CA4eVX6P.d.mts.map +0 -1
  890. package/dist/query-C5w7PsRQ.mjs.map +0 -1
  891. package/dist/redirects-G8wAhWAf.mjs.map +0 -1
  892. package/dist/registry-Dbodb4Uo.mjs +0 -840
  893. package/dist/registry-Dbodb4Uo.mjs.map +0 -1
  894. package/dist/relations-G2ta7GIl.d.mts.map +0 -1
  895. package/dist/runner-BFTrkYhu.mjs.map +0 -1
  896. package/dist/runner-BaADli8l.d.mts.map +0 -1
  897. package/dist/search-FZhx5dDU.mjs.map +0 -1
  898. package/dist/seo-CobNfOvb.mjs.map +0 -1
  899. package/dist/slugify-C_tqlU4G.mjs +0 -30
  900. package/dist/slugify-C_tqlU4G.mjs.map +0 -1
  901. package/dist/taxonomies-CKuaZER5.mjs.map +0 -1
  902. package/dist/taxonomies-CLN1N92E.mjs.map +0 -1
  903. package/dist/taxonomy-pOrB2kZe.mjs.map +0 -1
  904. package/dist/types-BGWPMiBO.d.mts.map +0 -1
  905. package/dist/types-BnFP1M3J.d.mts.map +0 -1
  906. package/dist/types-Cd8iuS1J.mjs.map +0 -1
  907. package/dist/types-Cz8hNch2.d.mts.map +0 -1
  908. package/dist/types-Dc4ntQjQ.d.mts.map +0 -1
  909. package/dist/types-NBI3rF-_.mjs.map +0 -1
  910. package/dist/validate-C3oSmA7F.d.mts.map +0 -1
  911. package/dist/validate-mKo3RxBM.mjs.map +0 -1
  912. package/dist/version-C4PnpmoV.mjs +0 -7
  913. package/dist/zod-generator-CG64Hnbh.mjs.map +0 -1
  914. /package/dist/{api-tokens-Dai9dND0.mjs → api-tokens-rIXC6LdE.mjs} +0 -0
  915. /package/dist/{ssrf-Cj568Dgs.mjs → ssrf-DpXgKsna.mjs} +0 -0
  916. /package/dist/{types-DKuWPYV5.mjs → types-CP1CflWf.mjs} +0 -0
@@ -1,94 +1,107 @@
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 { r as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-C1T11CfC.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-DSo5QeW0.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-ZHCc1ngN.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-DJZfUJVE.mjs";
17
+ import { t as withTransaction } from "../transaction-NQj4VJ7Z.mjs";
18
+ import "../taxonomy-BqCZ1TdU.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-ZldbZcle.mjs";
22
+ import "../menus-TCNSd2vp.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-BzwBlJrR.mjs";
28
+ import "../byline-yjvWq4ip.mjs";
29
+ import "../relation-BnHB0T4J.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-Dq4G-RrO.mjs";
34
+ import { r as singleFlightCached, t as createSingleFlightCache } from "../single-flight-cache-B0eLKS6K.mjs";
35
+ import "../cache-C46-3Irl.mjs";
36
+ import { t as FTSManager } from "../fts-manager-DyZEohZC.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-CdG_Ci3H.mjs";
40
+ import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-BlrncHi9.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-BeBglVxU.mjs";
42
+ import { n as recordSchedulerHeartbeatSafely } from "../dashboard-DvHbk_cv.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-B_8SJzuU.mjs";
45
+ import { t as MediaUsageWorkRepository } from "../media-usage-work-DrCbqBcl.mjs";
46
+ import "../media-usage-BZCydfvu.mjs";
47
+ import { n as collectionDeletionCurrentTimestamp, r as deleteActivatedMediaUsageCollection, t as MediaUsageCollectionDeletionRepository } from "../collection-deletion-DQPvMD8R.mjs";
48
+ import "../media-usage-work-CLo_6tct.mjs";
49
+ import { n as SchemaRegistry } from "../registry-Cqa2I749.mjs";
50
+ import "../query-BWs7O5Il.mjs";
51
+ import "../zod-generator-LC-on_Vb.mjs";
52
+ import "../schema-DZ5YVVZj.mjs";
53
+ import "../schema-2AOsSn2v.mjs";
54
+ import "../seo-AFpVvTqr.mjs";
55
+ import { t as PluginStateRepository } from "../state-DYwPue40.mjs";
56
+ import "../sections-DmHviGyM.mjs";
57
+ import { i as getSiteSettings, o as invalidateSiteSettingsCache } from "../settings-xRdoUZr0.mjs";
58
+ import "../settings-DE9xtzqd.mjs";
59
+ import "../taxonomies-OHUIiIpI.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-DTDHWH31.mjs";
67
+ import "../relations-BBApI4e2.mjs";
68
+ import "../status-CWVYpbD8.mjs";
69
+ import { a as invalidateUrlPatternCache } from "../query-zTVAR1Sl.mjs";
70
+ import "../import-DPkLmeXO.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-BBe83kKa.mjs";
81
+ import "../comment-reaction-D-2J84UE.mjs";
82
+ import "../bylines-DjXS5p0P.mjs";
83
+ import { n as getWidgetAreas } from "../widgets-BD3ho7Rq.mjs";
84
+ import "../validate-BMLBvxOC.mjs";
85
+ import "../apply-kUDt80-G.mjs";
86
+ import "../load-YJ2xl3ra.mjs";
87
+ import "../search-u1QYdOCG.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 { i as resolveRuntimeMigrationMode, n as enforceRuntimeMigrationPolicy, t as PendingMigrationsError } from "../policy-TluUIxuu.mjs";
90
+ import { n as VERSION, t as COMMIT } from "../version-DB_fqCSI.mjs";
91
+ import { t as getAuthMode } from "../mode-BDoH_WcF.mjs";
92
+ import "../snapshot-BBlGkb2q.mjs";
93
+ import { u as maybeRunScheduledBackup } from "../backup-DWYpJlLj.mjs";
94
+ import { t as cleanupExpiredChallenges } from "../challenge-store-SIL5DsF2.mjs";
95
+ import { t as removeUploadAttempt } from "../upload-attempts-D4FS8LT6.mjs";
96
+ import { a as validateEncryptionKeyAtStartup } from "../secrets-BEgsAQEU.mjs";
86
97
  import { isRecord } from "../plugin-utils.mjs";
87
- import { t as resolveSessionUser } from "../session-user-BvM9-zNl.mjs";
98
+ import { t as resolveSessionUser } from "../session-user-BEorKVT-.mjs";
88
99
  import { Kysely, sql } from "kysely";
100
+ import { ulid } from "ulidx";
89
101
  import { z } from "zod";
90
102
  import { Permissions } from "@emdash-cms/auth";
91
103
  import { defineMiddleware } from "astro:middleware";
104
+ import { buildTime } from "virtual:emdash/build";
92
105
  import virtualConfig from "virtual:emdash/config";
93
106
  import { createCoalescingDialect, createDialect, createRequestScopedDb } from "virtual:emdash/dialect";
94
107
  import { mediaProviders } from "virtual:emdash/media-providers";
@@ -105,9 +118,7 @@ async function repairLocaleCasing(db, configuredLocales) {
105
118
  const tableNames = await listTablesLike(db, "ec_%");
106
119
  for (const tableName of tableNames) {
107
120
  const table = sql.ref(tableName);
108
- const slug = tableName.slice(3);
109
- for (const locale of configuredLocales) {
110
- await sql`
121
+ for (const locale of configuredLocales) await sql`
111
122
  UPDATE ${table} AS target
112
123
  SET locale = ${locale}
113
124
  WHERE lower(target.locale) = lower(${locale})
@@ -125,20 +136,1165 @@ async function repairLocaleCasing(db, configuredLocales) {
125
136
  AND candidate.locale != ${locale}
126
137
  )
127
138
  `.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);
139
+ }
140
+ }
141
+
142
+ //#endregion
143
+ //#region src/i18n/taxonomy-locale-diagnostic.ts
144
+ const REPAIR_GUIDE = "https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches";
145
+ async function warnAboutUnconfiguredTaxonomyLocales(db, configuredLocales, definitionLocales) {
146
+ const supportedLocales = configuredLocales.length > 0 ? configuredLocales : ["en"];
147
+ 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 }));
148
+ const termRows = await db.selectFrom("taxonomies").select("locale").distinct().where("locale", "not in", supportedLocales).execute();
149
+ const mismatches = [...definitionRows.map(({ locale }) => ({
150
+ source: "definitions",
151
+ locale
152
+ })), ...termRows.map(({ locale }) => ({
153
+ source: "terms",
154
+ locale
155
+ }))].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));
156
+ if (mismatches.length === 0) return;
157
+ const details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join("; ");
158
+ 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}`);
159
+ }
160
+
161
+ //#endregion
162
+ //#region src/media/usage/collection-deletion-processor.ts
163
+ const MEDIA_USAGE_COLLECTION_DELETION_LIMITS = Object.freeze({
164
+ candidatesPerTick: 4,
165
+ deletionsPerTick: 1,
166
+ rowsPerBatch: 50,
167
+ leaseDurationSeconds: 300,
168
+ maxAttempts: 5,
169
+ retryBaseSeconds: 30,
170
+ retryMaxSeconds: 900,
171
+ maxQueriesPerTick: 30
172
+ });
173
+ async function processDueMediaUsageCollectionDeletions(db) {
174
+ const repository = new MediaUsageCollectionDeletionRepository(db);
175
+ const candidates = await repository.findDue(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.candidatesPerTick);
176
+ if (candidates.length === 0) return {
177
+ candidateCount: 0,
178
+ claimedCount: 0,
179
+ outcome: "idle"
180
+ };
181
+ let claim = null;
182
+ for (const candidate of candidates) {
183
+ claim = await repository.claim({
184
+ collectionId: candidate.collectionId,
185
+ phase: candidate.phase,
186
+ leaseDurationSeconds: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.leaseDurationSeconds
187
+ });
188
+ if (claim?.leaseToken) break;
189
+ }
190
+ if (!claim?.leaseToken) return {
191
+ candidateCount: candidates.length,
192
+ claimedCount: 0,
193
+ outcome: "claim_lost"
194
+ };
195
+ try {
196
+ const processed = await processClaimedDeletion(db, claim);
197
+ if (!processed.finalized && !processed.released && !await repository.release(claim)) return {
198
+ candidateCount: candidates.length,
199
+ claimedCount: 1,
200
+ outcome: "claim_lost"
201
+ };
202
+ return {
203
+ candidateCount: candidates.length,
204
+ claimedCount: 1,
205
+ outcome: processed.finalized ? "finalized" : "progress"
206
+ };
207
+ } catch (error) {
208
+ const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxAttempts;
209
+ if (!await repository.recordFailure({
210
+ collectionId: claim.collectionId,
211
+ leaseToken: claim.leaseToken,
212
+ errorCode: "MEDIA_USAGE_COLLECTION_DELETION_FAILED",
213
+ terminal,
214
+ retryDelaySeconds: retryDelaySeconds$2(claim.attemptCount)
215
+ })) return {
216
+ candidateCount: candidates.length,
217
+ claimedCount: 1,
218
+ outcome: "claim_lost"
219
+ };
220
+ console.error("[media-usage:collection-deletion] Processing failed:", error);
221
+ return {
222
+ candidateCount: candidates.length,
223
+ claimedCount: 1,
224
+ outcome: terminal ? "failed" : "retry"
225
+ };
226
+ }
227
+ }
228
+ async function processClaimedDeletion(db, claim) {
229
+ if (claim.phase === "fence" || claim.phase === "registry" || claim.phase === "table") {
230
+ await deleteActivatedMediaUsageCollection(db, {
231
+ collectionId: claim.collectionId,
232
+ collectionSlug: claim.collectionSlug,
233
+ forceDelete: claim.forceDelete
234
+ }, {
235
+ frontPhaseLimit: 1,
236
+ claimed: claim
237
+ });
238
+ return {
239
+ finalized: false,
240
+ released: true
241
+ };
242
+ }
243
+ if (claim.phase === "work") await processWorkBatch(db, claim);
244
+ if (claim.phase === "sources") await processSourceBatch(db, claim);
245
+ if (claim.phase === "status") await processStatus(db, claim);
246
+ if (claim.phase === "finalize") {
247
+ await finalizeDeletion(db, claim);
248
+ return {
249
+ finalized: true,
250
+ released: true
251
+ };
252
+ }
253
+ return {
254
+ finalized: false,
255
+ released: false
256
+ };
257
+ }
258
+ async function processWorkBatch(db, claim) {
259
+ await withTransaction(db, async (trx) => {
260
+ 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();
261
+ const batch = rows.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
262
+ 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();
263
+ await updateDeletion(trx, claim, {
264
+ phase: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? "work" : "sources",
265
+ work_cursor: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? batch.at(-1).content_id : null
266
+ });
267
+ });
268
+ return false;
269
+ }
270
+ async function processSourceBatch(db, claim) {
271
+ await withTransaction(db, async (trx) => {
272
+ let sourceKey = claim.sourceKey;
273
+ if (!sourceKey) {
274
+ 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();
275
+ if (!source) {
276
+ await updateDeletion(trx, claim, {
277
+ phase: "status",
278
+ source_key: null,
279
+ occurrence_cursor: null
280
+ });
281
+ return;
282
+ }
283
+ sourceKey = source.source_key;
284
+ await updateDeletion(trx, claim, {
285
+ source_key: sourceKey,
286
+ occurrence_cursor: null
287
+ });
288
+ }
289
+ 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();
290
+ const batch = occurrences.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
291
+ 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();
292
+ if (occurrences.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch) {
293
+ await updateDeletion(trx, claim, {
294
+ source_key: sourceKey,
295
+ occurrence_cursor: batch.at(-1).id
296
+ });
297
+ return;
298
+ }
299
+ 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();
300
+ await updateDeletion(trx, claim, {
301
+ source_key: null,
302
+ occurrence_cursor: null
303
+ });
304
+ });
305
+ return false;
306
+ }
307
+ async function processStatus(db, claim) {
308
+ await withTransaction(db, async (trx) => {
309
+ if (await exactCleanupRowsRemain(trx, claim, false)) throw new Error("Collection deletion cleanup is incomplete");
310
+ await trx.deleteFrom("_emdash_media_usage_reconciliations").where("collection_id", "=", claim.collectionId).where("collection_slug", "=", claim.collectionSlug).where(liveLeaseGuard(trx, claim)).execute();
311
+ 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();
312
+ await updateDeletion(trx, claim, { phase: "finalize" });
313
+ });
314
+ return false;
315
+ }
316
+ async function finalizeDeletion(db, claim) {
317
+ if (await tableExists(db, `ec_${claim.collectionSlug}`)) throw new Error("Collection table still exists during deletion finalization");
318
+ if (await exactCleanupRowsRemain(db, claim)) throw new Error("Collection deletion is incomplete");
319
+ 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");
320
+ 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();
321
+ if (Number(result.numDeletedRows ?? 0) !== 1) throw new Error("Collection deletion lost finalization");
322
+ return true;
323
+ }
324
+ async function exactCleanupRowsRemain(db, claim, includeStatus = true) {
325
+ const row = (await sql`
326
+ SELECT
327
+ EXISTS (
328
+ SELECT 1 FROM _emdash_media_usage_work
329
+ WHERE collection_id = ${claim.collectionId}
330
+ ) AS work_present,
331
+ EXISTS (
332
+ SELECT 1 FROM _emdash_media_usage_sources
333
+ WHERE source_type = 'content' AND collection_id = ${claim.collectionId}
334
+ ) AS source_present,
335
+ EXISTS (
336
+ SELECT 1 FROM _emdash_media_usage_index_status
337
+ WHERE adapter_id = 'content-media'
338
+ AND scope_type = 'collection'
339
+ AND scope_key = ${claim.collectionSlug}
340
+ AND collection_id = ${claim.collectionId}
341
+ ) AS status_present
342
+ `.execute(db)).rows[0];
343
+ return Boolean(row?.work_present) || Boolean(row?.source_present) || includeStatus && Boolean(row?.status_present);
344
+ }
345
+ async function updateDeletion(db, claim, values) {
346
+ const result = await db.updateTable("_emdash_media_usage_collection_deletions").set({
347
+ ...values,
348
+ attempt_count: 0,
349
+ last_error_code: null,
350
+ updated_at: collectionDeletionCurrentTimestamp(db)
351
+ }).where("collection_id", "=", claim.collectionId).where("state", "=", "leased").where("lease_token", "=", claim.leaseToken).where(liveLeaseGuard(db, claim)).executeTakeFirst();
352
+ if (Number(result.numUpdatedRows ?? 0) !== 1) throw new Error("Collection deletion lease was lost");
353
+ }
354
+ function liveLeaseGuard(db, claim) {
355
+ return isPostgres(db) ? sql`EXISTS (
356
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
357
+ WHERE deletion.collection_id = ${claim.collectionId}
358
+ AND deletion.state = 'leased'
359
+ AND deletion.lease_token = ${claim.leaseToken}
360
+ AND deletion.lease_expires_at::timestamptz > clock_timestamp()
361
+ )` : sql`EXISTS (
362
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
363
+ WHERE deletion.collection_id = ${claim.collectionId}
364
+ AND deletion.state = 'leased'
365
+ AND deletion.lease_token = ${claim.leaseToken}
366
+ AND deletion.lease_expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
367
+ )`;
368
+ }
369
+ function retryDelaySeconds$2(attemptCount) {
370
+ return Math.min(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryMaxSeconds, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
371
+ }
372
+
373
+ //#endregion
374
+ //#region src/media/usage/reconciliation.ts
375
+ const ACTIVATION_KEY = "incremental_capture";
376
+ const CONTENT_ADAPTER_ID = "content-media";
377
+ const COLLECTION_SCOPE = "collection";
378
+ const MAX_CANDIDATES = 100;
379
+ const MAX_PORTABLE_DURATION_SECONDS = 365 * 24 * 60 * 60;
380
+ const STABLE_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
381
+ var MediaUsageReconciliationRepository = class {
382
+ constructor(db) {
383
+ this.db = db;
384
+ }
385
+ async findByIdentity(collectionId, runToken) {
386
+ assertIdentity({
387
+ collectionId,
388
+ runToken
389
+ });
390
+ const row = await this.db.selectFrom("_emdash_media_usage_reconciliations").selectAll().where("collection_id", "=", collectionId).where("run_token", "=", runToken).executeTakeFirst();
391
+ return row ? rowToRecord(row) : null;
392
+ }
393
+ async beginRun(claim) {
394
+ const now = timestampOffset(this.db, 0);
395
+ const sameRun = sql`status = 'running' AND cursor = ${claim.runToken}`;
396
+ return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
397
+ status: "running",
398
+ started_at: sql`CASE WHEN ${sameRun} THEN started_at ELSE ${now} END`,
399
+ completed_at: null,
400
+ cursor: claim.runToken,
401
+ indexed_source_count: 0,
402
+ failed_source_count: 0,
403
+ last_error_code: null,
404
+ change_epoch: sql`CASE WHEN ${sameRun} THEN change_epoch ELSE change_epoch + 1 END`,
405
+ reconciliation_required: 1,
406
+ updated_at: now
407
+ }).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;
408
+ }
409
+ async findScanUpperId(claim) {
410
+ const tableName = contentTableName(claim.collectionSlug);
411
+ return (await sql`
412
+ SELECT content.id
413
+ FROM ${sql.ref(tableName)} AS content
414
+ WHERE ${this.liveClaimExistsSql(claim)}
415
+ ORDER BY content.id DESC
416
+ LIMIT 1
417
+ `.execute(this.db)).rows[0]?.id ?? null;
418
+ }
419
+ async initializeScan(input) {
420
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
421
+ target_epoch: input.targetEpoch,
422
+ field_fingerprint: input.fieldFingerprint,
423
+ phase: "scan",
424
+ scan_cursor: null,
425
+ scan_upper_id: input.scanUpperId,
426
+ source_cursor: null,
427
+ source_upper_key: null,
428
+ attempt_count: 0,
429
+ last_error_code: null,
430
+ updated_at: timestampOffset(this.db, 0)
431
+ }).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();
432
+ return Number(result.numUpdatedRows ?? 0) === 1;
433
+ }
434
+ async findScanPage(reconciliation, limit) {
435
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation scan page limit must be from 1 to 50");
436
+ if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
437
+ const tableName = contentTableName(reconciliation.collectionSlug);
438
+ const lowerBound = reconciliation.scanCursor ? sql`AND content.id > ${reconciliation.scanCursor}` : sql``;
439
+ const upperBound = reconciliation.scanUpperId ? sql`AND content.id <= ${reconciliation.scanUpperId}` : sql`AND 1 = 0`;
440
+ return (await sql`
441
+ SELECT content.id
442
+ FROM ${sql.ref(tableName)} AS content
443
+ WHERE 1 = 1
444
+ ${lowerBound}
445
+ ${upperBound}
446
+ AND ${this.liveClaimExistsSql(reconciliation)}
447
+ ORDER BY content.id ASC
448
+ LIMIT ${limit}
449
+ `.execute(this.db)).rows.map((row) => row.id);
450
+ }
451
+ async checkpointScan(input) {
452
+ let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
453
+ scan_cursor: input.nextCursor,
454
+ attempt_count: 0,
455
+ last_error_code: null,
456
+ updated_at: timestampOffset(this.db, 0)
457
+ }).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));
458
+ query = input.previousCursor ? query.where("reconciliation.scan_cursor", "=", input.previousCursor) : query.where("reconciliation.scan_cursor", "is", null);
459
+ const result = await query.executeTakeFirst();
460
+ return Number(result.numUpdatedRows ?? 0) === 1;
461
+ }
462
+ async ownsRun(claim, targetEpoch) {
463
+ const result = await sql`
464
+ SELECT ${this.statusOwnsRun(claim, targetEpoch)} AS owned
465
+ `.execute(this.db);
466
+ return Boolean(result.rows[0]?.owned);
467
+ }
468
+ async restartRun(claim, previousEpoch) {
469
+ const now = timestampOffset(this.db, 0);
470
+ const interruptedRestart = sql`status = 'running'
471
+ AND cursor = ${claim.runToken}
472
+ AND change_epoch > ${previousEpoch}`;
473
+ return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
474
+ status: "running",
475
+ started_at: sql`CASE WHEN ${interruptedRestart} THEN started_at ELSE ${now} END`,
476
+ completed_at: null,
477
+ cursor: claim.runToken,
478
+ last_error_code: null,
479
+ change_epoch: sql`CASE WHEN ${interruptedRestart} THEN change_epoch ELSE change_epoch + 1 END`,
480
+ reconciliation_required: 1,
481
+ updated_at: now
482
+ }).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;
483
+ }
484
+ async restartScan(input) {
485
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
486
+ target_epoch: input.targetEpoch,
487
+ field_fingerprint: input.fieldFingerprint,
488
+ phase: "scan",
489
+ scan_cursor: null,
490
+ scan_upper_id: input.scanUpperId,
491
+ source_cursor: null,
492
+ source_upper_key: null,
493
+ attempt_count: 0,
494
+ next_attempt_at: timestampOffset(this.db, 0),
495
+ last_error_code: null,
496
+ updated_at: timestampOffset(this.db, 0)
497
+ }).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();
498
+ return Number(result.numUpdatedRows ?? 0) === 1;
499
+ }
500
+ async findWorkBarrier(collectionId) {
501
+ if (!collectionId) throw new Error("Reconciliation work barrier requires a collection ID");
502
+ 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();
503
+ if (failed) return {
504
+ state: "failed",
505
+ errorCode: failed.last_error_code ?? "MEDIA_USAGE_PROCESSING_FAILED"
506
+ };
507
+ return await this.db.selectFrom("_emdash_media_usage_work").select("content_id").where("collection_id", "=", collectionId).limit(1).executeTakeFirst() ? { state: "pending" } : { state: "empty" };
508
+ }
509
+ async findSourceUpperKey(claim, targetEpoch) {
510
+ 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;
511
+ }
512
+ async transitionToSources(input) {
513
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
514
+ phase: "sources",
515
+ source_cursor: null,
516
+ source_upper_key: input.sourceUpperKey,
517
+ attempt_count: 0,
518
+ last_error_code: null,
519
+ updated_at: timestampOffset(this.db, 0)
520
+ }).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();
521
+ return Number(result.numUpdatedRows ?? 0) === 1;
522
+ }
523
+ async findSourcePage(reconciliation, limit) {
524
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation source page limit must be from 1 to 50");
525
+ if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
526
+ let query = this.db.selectFrom("_emdash_media_usage_sources as source").select([
527
+ "source.source_key",
528
+ "source.content_id",
529
+ "source.source_variant"
530
+ ]).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));
531
+ if (reconciliation.sourceCursor) query = query.where("source.source_key", ">", reconciliation.sourceCursor);
532
+ if (reconciliation.sourceUpperKey) query = query.where("source.source_key", "<=", reconciliation.sourceUpperKey);
533
+ else query = query.where(sql`1 = 0`);
534
+ return (await query.orderBy("source.source_key").limit(limit).execute()).map((row) => ({
535
+ sourceKey: row.source_key,
536
+ contentId: row.content_id,
537
+ sourceVariant: row.source_variant
538
+ }));
539
+ }
540
+ async findMissingContentIds(collectionSlug, contentIds) {
541
+ const unique = [...new Set(contentIds)];
542
+ if (unique.length === 0) return [];
543
+ if (unique.length > 50 || unique.some((contentId) => !contentId)) throw new Error("Reconciliation source page has invalid content identity");
544
+ const tableName = contentTableName(collectionSlug);
545
+ const existing = await sql`
546
+ SELECT id FROM ${sql.ref(tableName)} WHERE id IN (${sql.join(unique)})
547
+ `.execute(this.db);
548
+ const present = new Set(existing.rows.map((row) => row.id));
549
+ return unique.filter((contentId) => !present.has(contentId));
550
+ }
551
+ async checkpointSources(input) {
552
+ let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
553
+ source_cursor: input.nextCursor,
554
+ attempt_count: 0,
555
+ last_error_code: null,
556
+ updated_at: timestampOffset(this.db, 0)
557
+ }).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));
558
+ query = input.previousCursor ? query.where("reconciliation.source_cursor", "=", input.previousCursor) : query.where("reconciliation.source_cursor", "is", null);
559
+ const result = await query.executeTakeFirst();
560
+ return Number(result.numUpdatedRows ?? 0) === 1;
561
+ }
562
+ async finishFailedCoverage(collectionId, runToken) {
563
+ assertIdentity({
564
+ collectionId,
565
+ runToken
566
+ });
567
+ const now = timestampOffset(this.db, 0);
568
+ const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
569
+ status: sql`CASE WHEN EXISTS (
570
+ SELECT 1 FROM _emdash_media_usage_sources AS source
571
+ WHERE source.source_type = 'content'
572
+ AND source.collection_id = ${collectionId}
573
+ AND source.identity_version = 1
574
+ LIMIT 1
575
+ ) THEN 'partial' ELSE 'failed' END`,
576
+ completed_at: null,
577
+ cursor: null,
578
+ last_error_code: sql`CASE WHEN (
579
+ SELECT reconciliation.last_error_code
580
+ FROM _emdash_media_usage_reconciliations AS reconciliation
581
+ WHERE reconciliation.collection_id = ${collectionId}
582
+ AND reconciliation.run_token = ${runToken}
583
+ ) = 'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
584
+ THEN COALESCE(
585
+ (SELECT work.last_error_code
586
+ FROM _emdash_media_usage_work AS work
587
+ WHERE work.collection_id = ${collectionId} AND work.state = 'failed'
588
+ ORDER BY work.content_id LIMIT 1),
589
+ 'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
590
+ )
591
+ ELSE (
592
+ SELECT reconciliation.last_error_code
593
+ FROM _emdash_media_usage_reconciliations AS reconciliation
594
+ WHERE reconciliation.collection_id = ${collectionId}
595
+ AND reconciliation.run_token = ${runToken}
596
+ ) END`,
597
+ reconciliation_required: 1,
598
+ updated_at: now
599
+ }).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();
600
+ return Number(result.numUpdatedRows ?? 0) === 1;
601
+ }
602
+ async finalizeCoverage(input) {
603
+ const now = timestampOffset(this.db, 0);
604
+ const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
605
+ status: "complete",
606
+ schema_version: input.schemaVersion,
607
+ completed_at: now,
608
+ cursor: null,
609
+ last_error_code: null,
610
+ reconciliation_required: 0,
611
+ updated_at: now
612
+ }).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();
613
+ return Number(result.numUpdatedRows ?? 0) === 1;
614
+ }
615
+ async deleteFinalized(claim) {
616
+ 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();
617
+ return Number(result.numDeletedRows ?? 0) === 1;
618
+ }
619
+ async deleteOneObsolete() {
620
+ return (await sql`
621
+ DELETE FROM _emdash_media_usage_reconciliations
622
+ WHERE (collection_id, run_token) IN (
623
+ SELECT reconciliation.collection_id, reconciliation.run_token
624
+ FROM _emdash_media_usage_reconciliations AS reconciliation
625
+ INNER JOIN _emdash_media_usage_index_status AS status
626
+ ON status.collection_id = reconciliation.collection_id
627
+ AND status.scope_key = reconciliation.collection_slug
628
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
629
+ AND status.scope_type = ${COLLECTION_SCOPE}
630
+ AND status.reconciliation_required = 0
631
+ ORDER BY reconciliation.updated_at, reconciliation.collection_id
632
+ LIMIT 1
633
+ )
634
+ RETURNING collection_id
635
+ `.execute(this.db)).rows.length === 1;
636
+ }
637
+ liveClaimExists(claim) {
638
+ return this.liveClaimExistsSql(claim);
639
+ }
640
+ liveClaimExistsSql(claim) {
641
+ return sql`EXISTS (
642
+ SELECT 1
643
+ FROM _emdash_media_usage_reconciliations AS reconciliation
644
+ INNER JOIN _emdash_collections AS collection
645
+ ON collection.id = reconciliation.collection_id
646
+ AND collection.slug = reconciliation.collection_slug
647
+ WHERE reconciliation.collection_id = ${claim.collectionId}
648
+ AND reconciliation.collection_slug = ${claim.collectionSlug}
649
+ AND reconciliation.run_token = ${claim.runToken}
650
+ AND reconciliation.state = 'leased'
651
+ AND reconciliation.lease_token = ${claim.leaseToken}
652
+ AND ${liveLease(this.db, "reconciliation.lease_expires_at")}
653
+ AND EXISTS (
654
+ SELECT 1 FROM _emdash_media_usage_activation AS activation
655
+ WHERE activation.task_key = ${ACTIVATION_KEY}
656
+ AND activation.state = 'active'
657
+ )
658
+ AND NOT EXISTS (
659
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
660
+ WHERE deletion.collection_id = reconciliation.collection_id
661
+ )
662
+ )`;
663
+ }
664
+ statusOwnsRun(claim, targetEpoch) {
665
+ return sql`EXISTS (
666
+ SELECT 1 FROM _emdash_media_usage_index_status AS status
667
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
668
+ AND status.scope_type = ${COLLECTION_SCOPE}
669
+ AND status.collection_id = ${claim.collectionId}
670
+ AND status.scope_key = ${claim.collectionSlug}
671
+ AND status.capture_state = 'active'
672
+ AND status.reconciliation_required = 1
673
+ AND status.status = 'running'
674
+ AND status.cursor = ${claim.runToken}
675
+ AND status.change_epoch = ${targetEpoch}
676
+ )`;
677
+ }
678
+ async seedNextCandidate() {
679
+ const runToken = ulid();
680
+ const now = timestampOffset(this.db, 0);
681
+ return (await sql`
682
+ INSERT INTO _emdash_media_usage_reconciliations (
683
+ collection_id,
684
+ collection_slug,
685
+ run_token,
686
+ next_attempt_at,
687
+ updated_at
688
+ )
689
+ SELECT status.collection_id, status.scope_key, ${runToken}, ${now}, ${now}
690
+ FROM _emdash_media_usage_index_status AS status
691
+ INNER JOIN _emdash_collections AS collection
692
+ ON collection.id = status.collection_id
693
+ AND collection.slug = status.scope_key
694
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
695
+ AND status.scope_type = ${COLLECTION_SCOPE}
696
+ AND status.capture_state = 'active'
697
+ AND status.reconciliation_required = 1
698
+ AND status.collection_id IS NOT NULL
699
+ AND EXISTS (
700
+ SELECT 1 FROM _emdash_media_usage_activation AS activation
701
+ WHERE activation.task_key = ${ACTIVATION_KEY}
702
+ AND activation.state = 'active'
703
+ )
704
+ AND NOT EXISTS (
705
+ SELECT 1 FROM _emdash_media_usage_reconciliations AS existing
706
+ WHERE existing.collection_id = status.collection_id
707
+ )
708
+ AND NOT EXISTS (
709
+ SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
710
+ WHERE deletion.collection_id = status.collection_id
711
+ )
712
+ ORDER BY status.collection_id
713
+ LIMIT 1
714
+ ON CONFLICT (collection_id) DO NOTHING
715
+ RETURNING collection_id
716
+ `.execute(this.db)).rows.length === 1;
717
+ }
718
+ async findDue(limit) {
719
+ assertLimit(limit);
720
+ const nextAttemptIsDue = timestampIsDue(this.db, "next_attempt_at");
721
+ return (await sql`
722
+ WITH pending_candidates AS (
723
+ SELECT * FROM _emdash_media_usage_reconciliations
724
+ WHERE state = 'pending' AND ${nextAttemptIsDue}
725
+ ORDER BY next_attempt_at, updated_at, collection_id
726
+ LIMIT ${limit}
727
+ ), retry_candidates AS (
728
+ SELECT * FROM _emdash_media_usage_reconciliations
729
+ WHERE state = 'retry' AND ${nextAttemptIsDue}
730
+ ORDER BY next_attempt_at, updated_at, collection_id
731
+ LIMIT ${limit}
732
+ ), leased_candidates AS (
733
+ SELECT * FROM _emdash_media_usage_reconciliations
734
+ WHERE state = 'leased' AND ${timestampIsDue(this.db, "lease_expires_at")}
735
+ ORDER BY lease_expires_at, updated_at, collection_id
736
+ LIMIT ${limit}
737
+ ), candidates AS (
738
+ SELECT * FROM pending_candidates
739
+ UNION ALL SELECT * FROM retry_candidates
740
+ UNION ALL SELECT * FROM leased_candidates
741
+ )
742
+ SELECT * FROM candidates
743
+ ORDER BY CASE WHEN state = 'leased' THEN lease_expires_at ELSE next_attempt_at END,
744
+ updated_at,
745
+ collection_id
746
+ LIMIT ${limit}
747
+ `.execute(this.db)).rows.map(rowToRecord);
748
+ }
749
+ async findFailed(limit) {
750
+ assertLimit(limit);
751
+ 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([
752
+ eb("status.reconciliation_required", "=", 0),
753
+ eb.and([eb("status.status", "=", "running"), eb("status.cursor", "=", eb.ref("reconciliation.run_token"))]),
754
+ eb.and([eb("status.cursor", "is", null), eb("status.change_epoch", ">", eb.ref("reconciliation.target_epoch"))])
755
+ ])).orderBy("reconciliation.updated_at").orderBy("reconciliation.collection_id").limit(limit).execute()).map(rowToRecord);
756
+ }
757
+ async claim(input) {
758
+ assertIdentity(input);
759
+ assertDuration(input.leaseDurationSeconds, "lease duration");
760
+ const leaseToken = ulid();
761
+ const row = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
762
+ state: "leased",
763
+ lease_token: leaseToken,
764
+ lease_expires_at: timestampOffset(this.db, input.leaseDurationSeconds),
765
+ updated_at: timestampOffset(this.db, 0)
766
+ }).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();
767
+ return row ? {
768
+ ...rowToRecord(row),
769
+ leaseToken
770
+ } : null;
771
+ }
772
+ async release(input) {
773
+ assertLeaseIdentity(input);
774
+ assertDuration(input.delaySeconds, "release delay", true);
775
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
776
+ state: "pending",
777
+ next_attempt_at: timestampOffset(this.db, input.delaySeconds),
778
+ lease_token: null,
779
+ lease_expires_at: null,
780
+ updated_at: timestampOffset(this.db, 0)
781
+ }).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
782
+ return Number(result.numUpdatedRows ?? 0) === 1;
783
+ }
784
+ async recordFailure(input) {
785
+ assertLeaseIdentity(input);
786
+ if (!STABLE_ERROR_CODE_PATTERN.test(input.errorCode)) throw new Error("Reconciliation failure requires a stable error code");
787
+ assertDuration(input.retryDelaySeconds, "retry delay", true);
788
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
789
+ state: input.terminal ? "failed" : sql`CASE WHEN attempt_count >= 4 THEN 'failed' ELSE 'retry' END`,
790
+ ...input.terminal ? { target_epoch: sql`COALESCE(
791
+ target_epoch,
792
+ (SELECT status.change_epoch
793
+ FROM _emdash_media_usage_index_status AS status
794
+ WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
795
+ AND status.scope_type = ${COLLECTION_SCOPE}
796
+ AND status.collection_id = ${input.collectionId}
797
+ AND status.status = 'running'
798
+ AND status.cursor = ${input.runToken})
799
+ )` } : {},
800
+ attempt_count: sql`attempt_count + 1`,
801
+ next_attempt_at: timestampOffset(this.db, input.retryDelaySeconds),
802
+ lease_token: null,
803
+ lease_expires_at: null,
804
+ last_error_code: input.errorCode,
805
+ updated_at: timestampOffset(this.db, 0)
806
+ }).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
807
+ return Number(result.numUpdatedRows ?? 0) === 1;
808
+ }
809
+ async recordEntryFailure(claim) {
810
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
811
+ state: "failed",
812
+ attempt_count: sql`attempt_count + 1`,
813
+ next_attempt_at: timestampOffset(this.db, 0),
814
+ lease_token: null,
815
+ lease_expires_at: null,
816
+ last_error_code: "MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED",
817
+ updated_at: timestampOffset(this.db, 0)
818
+ }).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();
819
+ return Number(result.numUpdatedRows ?? 0) === 1;
820
+ }
821
+ async resetFailedForNewEpoch(observed) {
822
+ const collectionId = "collection_id" in observed ? observed.collection_id : observed.collectionId;
823
+ const runToken = "run_token" in observed ? observed.run_token : observed.runToken;
824
+ const targetEpoch = "target_epoch" in observed ? observed.target_epoch : observed.targetEpoch;
825
+ if (targetEpoch === null) return false;
826
+ const now = timestampOffset(this.db, 0);
827
+ const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
828
+ state: "pending",
829
+ phase: "scan",
830
+ target_epoch: null,
831
+ field_fingerprint: null,
832
+ scan_cursor: null,
833
+ scan_upper_id: null,
834
+ source_cursor: null,
835
+ source_upper_key: null,
836
+ attempt_count: 0,
837
+ next_attempt_at: now,
838
+ lease_token: null,
839
+ lease_expires_at: null,
840
+ last_error_code: null,
841
+ updated_at: now
842
+ }).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();
843
+ return Number(result.numUpdatedRows ?? 0) === 1;
844
+ }
845
+ };
846
+ function rowToRecord(row) {
847
+ if (!isState(row.state) || !isPhase(row.phase) || !Number.isSafeInteger(row.attempt_count)) throw new Error("Invalid media usage reconciliation lifecycle");
848
+ return {
849
+ collectionId: row.collection_id,
850
+ collectionSlug: row.collection_slug,
851
+ runToken: row.run_token,
852
+ targetEpoch: row.target_epoch,
853
+ fieldFingerprint: row.field_fingerprint,
854
+ state: row.state,
855
+ phase: row.phase,
856
+ scanCursor: row.scan_cursor,
857
+ scanUpperId: row.scan_upper_id,
858
+ sourceCursor: row.source_cursor,
859
+ sourceUpperKey: row.source_upper_key,
860
+ attemptCount: row.attempt_count,
861
+ nextAttemptAt: row.next_attempt_at,
862
+ leaseToken: row.lease_token,
863
+ leaseExpiresAt: row.lease_expires_at,
864
+ lastErrorCode: row.last_error_code,
865
+ createdAt: row.created_at,
866
+ updatedAt: row.updated_at
867
+ };
868
+ }
869
+ function isState(state) {
870
+ return state === "pending" || state === "retry" || state === "leased" || state === "failed";
871
+ }
872
+ function isPhase(phase) {
873
+ return phase === "scan" || phase === "sources";
874
+ }
875
+ function assertLimit(limit) {
876
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CANDIDATES) throw new Error("Reconciliation candidate limit must be from 1 to 100");
877
+ }
878
+ function assertIdentity(input) {
879
+ if (!input.collectionId || !input.runToken) throw new Error("Reconciliation requires an exact collection and run token");
880
+ }
881
+ function assertLeaseIdentity(input) {
882
+ assertIdentity(input);
883
+ if (!input.leaseToken) throw new Error("Reconciliation requires a lease token");
884
+ }
885
+ function assertDuration(value, label, allowZero = false) {
886
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > MAX_PORTABLE_DURATION_SECONDS) throw new Error(`Reconciliation ${label} is outside the portable range`);
887
+ }
888
+ function liveLease(db, column = "lease_expires_at") {
889
+ const expiry = sql.ref(column);
890
+ 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')`;
891
+ }
892
+ function timestampIsDue(db, column) {
893
+ const value = sql.ref(column);
894
+ 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')`;
895
+ }
896
+ function timestampOffset(db, offsetSeconds) {
897
+ if (isPostgres(db)) return sql`to_char(
898
+ (clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
899
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
900
+ )`;
901
+ return sql`strftime(
902
+ '%Y-%m-%dT%H:%M:%fZ',
903
+ 'now',
904
+ ${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
905
+ )`;
906
+ }
907
+ function contentTableName(collectionSlug) {
908
+ validateIdentifier(collectionSlug, "collection slug");
909
+ const tableName = `ec_${collectionSlug}`;
910
+ validateIdentifier(tableName, "content table");
911
+ return tableName;
912
+ }
913
+
914
+ //#endregion
915
+ //#region src/media/usage/reconciliation-processor.ts
916
+ const MEDIA_USAGE_RECONCILIATION_LIMITS = Object.freeze({
917
+ candidatesPerTick: 4,
918
+ pageSize: 50,
919
+ leaseDurationSeconds: 60,
920
+ maxAttempts: 5,
921
+ retryBaseSeconds: 30,
922
+ retryMaxSeconds: 900,
923
+ retryJitterRatio: .25,
924
+ maxQueriesPerTick: 20
925
+ });
926
+ async function processDueMediaUsageReconciliation(db) {
927
+ if ((await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state !== "active") return "inactive";
928
+ const reconciliation = new MediaUsageReconciliationRepository(db);
929
+ if (await reconciliation.deleteOneObsolete()) return "completed";
930
+ const [failed] = await reconciliation.findFailed(1);
931
+ if (failed) {
932
+ if (await reconciliation.finishFailedCoverage(failed.collectionId, failed.runToken)) return "failed";
933
+ if (await reconciliation.resetFailedForNewEpoch(failed)) return "advanced";
934
+ }
935
+ await reconciliation.seedNextCandidate();
936
+ const candidates = await reconciliation.findDue(MEDIA_USAGE_RECONCILIATION_LIMITS.candidatesPerTick);
937
+ let claim = null;
938
+ for (const candidate of candidates) {
939
+ claim = await reconciliation.claim({
940
+ collectionId: candidate.collectionId,
941
+ runToken: candidate.runToken,
942
+ leaseDurationSeconds: MEDIA_USAGE_RECONCILIATION_LIMITS.leaseDurationSeconds
943
+ });
944
+ if (claim) break;
945
+ }
946
+ if (!claim) return candidates.length === 0 ? "not_due" : "claim_lost";
947
+ try {
948
+ return await processClaimedReconciliation(db, claim);
949
+ } catch (error) {
950
+ const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_RECONCILIATION_LIMITS.maxAttempts;
951
+ if (!await reconciliation.recordFailure({
952
+ collectionId: claim.collectionId,
953
+ runToken: claim.runToken,
954
+ leaseToken: claim.leaseToken,
955
+ errorCode: "MEDIA_USAGE_RECONCILIATION_FAILED",
956
+ retryDelaySeconds: retryDelaySeconds$1(claim.attemptCount),
957
+ terminal
958
+ })) return "claim_lost";
959
+ if (terminal) await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
960
+ console.error("[media-usage:reconciliation] Processing failed:", error);
961
+ return terminal ? "failed" : "retry";
962
+ }
963
+ }
964
+ async function processClaimedMediaUsageReconciliationScan(db, claim, options = {}) {
965
+ const reconciliation = new MediaUsageReconciliationRepository(db);
966
+ let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
967
+ if (!current || current.leaseToken !== claim.leaseToken || current.phase !== "scan") return "deferred";
968
+ let fields;
969
+ let fieldFingerprint;
970
+ if (current.targetEpoch === null) {
971
+ const targetEpoch = await reconciliation.beginRun(claim);
972
+ if (targetEpoch === null) {
973
+ await reconciliation.release({
974
+ ...claim,
975
+ delaySeconds: 30
976
+ });
977
+ return "deferred";
978
+ }
979
+ fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
980
+ fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
981
+ const scanUpperId = fields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
982
+ if (!await reconciliation.initializeScan({
983
+ claim,
984
+ targetEpoch,
985
+ fieldFingerprint,
986
+ scanUpperId
987
+ })) return "deferred";
988
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
989
+ if (!current) return "deferred";
990
+ } else {
991
+ fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
992
+ fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
993
+ }
994
+ if (current.fieldFingerprint !== fieldFingerprint || current.targetEpoch === null) return "restart_required";
995
+ const contentIds = await reconciliation.findScanPage(current, 50);
996
+ if (contentIds.length === 0) {
997
+ if (options.releaseOnExhausted ?? true) await reconciliation.release({
998
+ ...claim,
999
+ delaySeconds: 30
1000
+ });
1001
+ return "exhausted";
1002
+ }
1003
+ await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
1004
+ collectionId: claim.collectionId,
1005
+ collectionSlug: claim.collectionSlug,
1006
+ runToken: claim.runToken,
1007
+ leaseToken: claim.leaseToken,
1008
+ changeEpoch: current.targetEpoch,
1009
+ phase: "scan",
1010
+ contentIds
1011
+ });
1012
+ const nextCursor = contentIds.at(-1);
1013
+ if (!await reconciliation.checkpointScan({
1014
+ claim,
1015
+ targetEpoch: current.targetEpoch,
1016
+ previousCursor: current.scanCursor,
1017
+ nextCursor
1018
+ })) return "deferred";
1019
+ if (!await reconciliation.release({
1020
+ ...claim,
1021
+ delaySeconds: 0
1022
+ })) return "deferred";
1023
+ return "advanced";
1024
+ }
1025
+ async function processClaimedReconciliation(db, claim) {
1026
+ const reconciliation = new MediaUsageReconciliationRepository(db);
1027
+ let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
1028
+ if (!current || current.leaseToken !== claim.leaseToken) return "claim_lost";
1029
+ if (current.targetEpoch !== null && !await reconciliation.ownsRun(claim, current.targetEpoch)) return restartReconciliation(db, reconciliation, claim, current);
1030
+ if (current.phase === "scan") {
1031
+ const outcome = await processClaimedMediaUsageReconciliationScan(db, claim, { releaseOnExhausted: false });
1032
+ if (outcome === "restart_required") {
1033
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
1034
+ return restartReconciliation(db, reconciliation, claim, current);
1035
+ }
1036
+ if (outcome !== "exhausted") return outcome;
1037
+ current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
1038
+ const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
1039
+ if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
1040
+ if (barrier.state === "pending") {
1041
+ await reconciliation.release({
1042
+ ...claim,
1043
+ delaySeconds: 30
1044
+ });
1045
+ return "deferred";
1046
+ }
1047
+ if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
1048
+ const sourceUpperKey = await reconciliation.findSourceUpperKey(claim, current.targetEpoch);
1049
+ if (!await reconciliation.transitionToSources({
1050
+ claim,
1051
+ targetEpoch: current.targetEpoch,
1052
+ fieldFingerprint: current.fieldFingerprint,
1053
+ sourceUpperKey
1054
+ })) return "claim_lost";
1055
+ if (!await reconciliation.release({
1056
+ ...claim,
1057
+ delaySeconds: 0
1058
+ })) return "claim_lost";
1059
+ return "advanced";
1060
+ }
1061
+ return processSourcePhase(db, reconciliation, claim, current);
1062
+ }
1063
+ async function processSourcePhase(db, reconciliation, claim, current) {
1064
+ if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
1065
+ const fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
1066
+ const fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
1067
+ if (fieldFingerprint !== current.fieldFingerprint) return restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint);
1068
+ const page = await reconciliation.findSourcePage(current, MEDIA_USAGE_RECONCILIATION_LIMITS.pageSize);
1069
+ if (page.length > 0) {
1070
+ if (page.some((source) => !source.contentId || source.sourceVariant !== "columns" && source.sourceVariant !== "draft_overlay")) return failReconciliation(reconciliation, claim, "MEDIA_USAGE_RECONCILIATION_INVALID_SOURCE", false);
1071
+ const contentIds = [...new Set(page.map((source) => source.contentId))];
1072
+ const enqueueIds = fields.extractionFields.length === 0 ? contentIds : await reconciliation.findMissingContentIds(claim.collectionSlug, contentIds);
1073
+ if (enqueueIds.length > 0) await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
1074
+ collectionId: claim.collectionId,
1075
+ collectionSlug: claim.collectionSlug,
1076
+ runToken: claim.runToken,
1077
+ leaseToken: claim.leaseToken,
1078
+ changeEpoch: current.targetEpoch,
1079
+ phase: "sources",
1080
+ contentIds: enqueueIds
1081
+ });
1082
+ if (!await reconciliation.checkpointSources({
1083
+ claim,
1084
+ targetEpoch: current.targetEpoch,
1085
+ previousCursor: current.sourceCursor,
1086
+ nextCursor: page.at(-1).sourceKey
1087
+ })) return "claim_lost";
1088
+ if (!await reconciliation.release({
1089
+ ...claim,
1090
+ delaySeconds: 0
1091
+ })) return "claim_lost";
1092
+ return "advanced";
1093
+ }
1094
+ const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
1095
+ if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
1096
+ if (barrier.state === "pending") {
1097
+ await reconciliation.release({
1098
+ ...claim,
1099
+ delaySeconds: 30
1100
+ });
1101
+ return "deferred";
1102
+ }
1103
+ if (!await reconciliation.finalizeCoverage({
1104
+ claim,
1105
+ targetEpoch: current.targetEpoch,
1106
+ fieldFingerprint,
1107
+ schemaVersion: CONTENT_SOURCE_SCHEMA_VERSION
1108
+ })) return "claim_lost";
1109
+ if (!await reconciliation.deleteFinalized(claim)) return "claim_lost";
1110
+ return "completed";
1111
+ }
1112
+ async function restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint) {
1113
+ if (current.targetEpoch === null) return "claim_lost";
1114
+ const discoveredFields = fields ?? await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
1115
+ const fingerprint = fieldFingerprint ?? await buildContentMediaUsageFieldFingerprint(discoveredFields);
1116
+ const targetEpoch = await reconciliation.restartRun(claim, current.targetEpoch);
1117
+ if (targetEpoch === null) {
1118
+ await reconciliation.release({
1119
+ ...claim,
1120
+ delaySeconds: 30
1121
+ });
1122
+ return "deferred";
1123
+ }
1124
+ const scanUpperId = discoveredFields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
1125
+ if (!await reconciliation.restartScan({
1126
+ claim,
1127
+ previousEpoch: current.targetEpoch,
1128
+ targetEpoch,
1129
+ fieldFingerprint: fingerprint,
1130
+ scanUpperId
1131
+ })) return "claim_lost";
1132
+ if (!await reconciliation.release({
1133
+ ...claim,
1134
+ delaySeconds: 0
1135
+ })) return "claim_lost";
1136
+ return "advanced";
1137
+ }
1138
+ async function failReconciliation(reconciliation, claim, errorCode, entryFailure) {
1139
+ if (!(entryFailure ? await reconciliation.recordEntryFailure(claim) : await reconciliation.recordFailure({
1140
+ collectionId: claim.collectionId,
1141
+ runToken: claim.runToken,
1142
+ leaseToken: claim.leaseToken,
1143
+ errorCode,
1144
+ retryDelaySeconds: 0,
1145
+ terminal: true
1146
+ }))) return "claim_lost";
1147
+ await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
1148
+ return "failed";
1149
+ }
1150
+ function retryDelaySeconds$1(attemptCount) {
1151
+ const exponential = Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, MEDIA_USAGE_RECONCILIATION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
1152
+ const jitter = Math.floor(exponential * MEDIA_USAGE_RECONCILIATION_LIMITS.retryJitterRatio * Math.random());
1153
+ return Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, exponential + jitter);
1154
+ }
1155
+
1156
+ //#endregion
1157
+ //#region src/media/usage/work-processor.ts
1158
+ const MEDIA_USAGE_WORK_PROCESSING_LIMITS = Object.freeze({
1159
+ candidatesPerTick: 4,
1160
+ jobsPerTick: 1,
1161
+ maxTickDurationMs: 5e3,
1162
+ leaseDurationSeconds: 60,
1163
+ maxAttempts: 5,
1164
+ retryBaseSeconds: 30,
1165
+ retryMaxSeconds: 900,
1166
+ retryJitterRatio: .25,
1167
+ ordinaryStatementsPerJob: 20
1168
+ });
1169
+ async function processMediaUsageWorkAfterWrite(db, collectionSlug, contentId) {
1170
+ if (!await isIncrementalCaptureActive(db)) return {
1171
+ outcome: "inactive",
1172
+ claimed: false
1173
+ };
1174
+ const repo = new MediaUsageWorkRepository(db);
1175
+ const work = await repo.findWorkForContent(collectionSlug, contentId);
1176
+ if (!work) return {
1177
+ outcome: "not_due",
1178
+ claimed: false
1179
+ };
1180
+ return processCandidate(db, repo, work);
1181
+ }
1182
+ async function processDueMediaUsageWork(db) {
1183
+ const startedAt = Date.now();
1184
+ const result = {
1185
+ candidateCount: 0,
1186
+ claimedCount: 0,
1187
+ completedCount: 0,
1188
+ retryCount: 0,
1189
+ failedCount: 0,
1190
+ supersededCount: 0,
1191
+ obsoleteCount: 0,
1192
+ durationMs: 0,
1193
+ admissionClosed: false
1194
+ };
1195
+ if (!await isIncrementalCaptureActive(db)) {
1196
+ result.durationMs = Date.now() - startedAt;
1197
+ return result;
1198
+ }
1199
+ const repo = new MediaUsageWorkRepository(db);
1200
+ const candidates = await repo.findDueWork(MEDIA_USAGE_WORK_PROCESSING_LIMITS.candidatesPerTick);
1201
+ result.candidateCount = candidates.length;
1202
+ for (const candidate of candidates) {
1203
+ if (result.claimedCount >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.jobsPerTick || Date.now() - startedAt >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxTickDurationMs) {
1204
+ result.admissionClosed = true;
1205
+ break;
140
1206
  }
1207
+ const processed = await processCandidate(db, repo, candidate);
1208
+ if (processed.claimed) result.claimedCount++;
1209
+ if (processed.outcome === "completed") result.completedCount++;
1210
+ if (processed.outcome === "retry") result.retryCount++;
1211
+ if (processed.outcome === "failed") result.failedCount++;
1212
+ if (processed.outcome === "superseded") result.supersededCount++;
1213
+ if (processed.outcome === "obsolete") result.obsoleteCount++;
141
1214
  }
1215
+ result.durationMs = Date.now() - startedAt;
1216
+ return result;
1217
+ }
1218
+ async function processCandidate(db, repo, candidate) {
1219
+ const claimed = await repo.claimWork({
1220
+ collectionId: candidate.collectionId,
1221
+ contentId: candidate.contentId,
1222
+ workVersion: candidate.workVersion,
1223
+ leaseDurationSeconds: MEDIA_USAGE_WORK_PROCESSING_LIMITS.leaseDurationSeconds
1224
+ });
1225
+ if (!claimed?.leaseToken) return {
1226
+ outcome: "claim_lost",
1227
+ claimed: false
1228
+ };
1229
+ const lease = {
1230
+ collectionId: claimed.collectionId,
1231
+ contentId: claimed.contentId,
1232
+ workVersion: claimed.workVersion,
1233
+ leaseToken: claimed.leaseToken
1234
+ };
1235
+ if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
1236
+ outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
1237
+ claimed: true
1238
+ };
1239
+ const refresh = await refreshContentMediaUsageForWork(db, claimed.collectionId, claimed.collectionSlug, claimed.contentId);
1240
+ if (refresh.success) {
1241
+ const completed = await repo.completeWork(lease);
1242
+ if (completed) await new MediaUsageRepository(db).recordIncrementalSuccess({
1243
+ collectionId: claimed.collectionId,
1244
+ collectionSlug: claimed.collectionSlug
1245
+ });
1246
+ return {
1247
+ outcome: completed ? "completed" : "superseded",
1248
+ claimed: true
1249
+ };
1250
+ }
1251
+ if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
1252
+ outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
1253
+ claimed: true
1254
+ };
1255
+ const errorCode = processingErrorCode(refresh.errorCode);
1256
+ if (errorCode === "MEDIA_USAGE_RESOURCE_LIMIT" || claimed.attemptCount + 1 >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxAttempts) {
1257
+ const failed = await repo.failWork({
1258
+ ...lease,
1259
+ errorCode
1260
+ });
1261
+ if (failed) await new MediaUsageRepository(db).recordIncrementalFailure({
1262
+ collectionId: claimed.collectionId,
1263
+ collectionSlug: claimed.collectionSlug,
1264
+ contentId: claimed.contentId,
1265
+ workVersion: claimed.workVersion,
1266
+ errorCode
1267
+ });
1268
+ return {
1269
+ outcome: failed ? "failed" : "superseded",
1270
+ claimed: true
1271
+ };
1272
+ }
1273
+ return {
1274
+ outcome: await repo.retryWork({
1275
+ ...lease,
1276
+ errorCode,
1277
+ retryDelaySeconds: retryDelaySeconds(claimed.attemptCount)
1278
+ }) ? "retry" : "superseded",
1279
+ claimed: true
1280
+ };
1281
+ }
1282
+ async function isIncrementalCaptureActive(db) {
1283
+ return (await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state === "active";
1284
+ }
1285
+ async function collectionIdentityIsCurrent(db, collectionId, collectionSlug) {
1286
+ return await db.selectFrom("_emdash_collections").select("id").where("id", "=", collectionId).where("slug", "=", collectionSlug).executeTakeFirst() !== void 0;
1287
+ }
1288
+ function retryDelaySeconds(attemptCount) {
1289
+ const exponential = Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryBaseSeconds * 2 ** attemptCount);
1290
+ const jitter = Math.floor(exponential * MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryJitterRatio * Math.random());
1291
+ return Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, exponential + jitter);
1292
+ }
1293
+ function processingErrorCode(errorCode) {
1294
+ if (errorCode === "DRAFT_REVISION_NOT_FOUND" || errorCode === "DRAFT_REVISION_MISMATCH" || errorCode === "DRAFT_REVISION_INVALID") return "MEDIA_USAGE_SNAPSHOT_FAILED";
1295
+ if (errorCode === "CONTENT_USAGE_GENERATION_CONFLICT") return "MEDIA_USAGE_GENERATION_CONFLICT";
1296
+ if (errorCode === "CONTENT_USAGE_RESOURCE_LIMIT") return "MEDIA_USAGE_RESOURCE_LIMIT";
1297
+ return "MEDIA_USAGE_PROCESSING_FAILED";
142
1298
  }
143
1299
 
144
1300
  //#endregion
@@ -155,32 +1311,225 @@ function createSandboxRunnerOptions(options, siteInfo) {
155
1311
  }
156
1312
 
157
1313
  //#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;
1314
+ //#region src/media/usage/cleanup.ts
1315
+ const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;
1316
+ const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;
1317
+ const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;
1318
+ const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1e3;
1319
+ const MEDIA_USAGE_CLEANUP_LEASE_MS = 300 * 1e3;
1320
+ const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 3600 * 1e3;
1321
+ const MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1e3;
165
1322
  /**
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;
170
- /**
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.
1323
+ * Reclaims a bounded window of obsolete media-usage occurrences.
1324
+ *
1325
+ * The persisted claim makes a cron tick single-flight across Worker isolates
1326
+ * and Node processes.
174
1327
  */
175
- async function cleanupMediaUsageGenerations(db) {
176
- const cutoff = new Date(Date.now() - GC_MAX_AGE_MS).toISOString();
1328
+ async function cleanupMediaUsage(db) {
1329
+ const startedMs = Date.now();
1330
+ const canIssueStatement = () => withinBudget(startedMs);
177
1331
  const repo = new MediaUsageRepository(db);
1332
+ const leaseToken = ulid();
1333
+ const claim = await repo.claimMediaUsageCleanup({
1334
+ leaseToken,
1335
+ leaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1e3,
1336
+ nextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1e3,
1337
+ sweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1e3
1338
+ });
1339
+ if (!claim) return emptyResult("skipped", elapsedSince(startedMs));
1340
+ let candidateRows = 0;
1341
+ let deletedOrphans = 0;
1342
+ let deletedStale = 0;
1343
+ let deletedAbandoned = 0;
1344
+ let deletedWriteLeases = 0;
1345
+ let backlogLowerBound = 0;
1346
+ let scanHasMore = false;
1347
+ let nextCursor = claim.cursor;
1348
+ let sweepComplete = false;
1349
+ try {
1350
+ if (canIssueStatement()) deletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT, cleanupLease(leaseToken), canIssueStatement);
1351
+ if (canIssueStatement()) {
1352
+ const cutoff = claim.scanBeforeAt;
1353
+ const candidates = await repo.findMediaUsageCleanupCandidates({
1354
+ cutoff,
1355
+ cursor: claim.cursor,
1356
+ limit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,
1357
+ cleanupLease: cleanupLease(leaseToken)
1358
+ });
1359
+ candidateRows = candidates.length;
1360
+ scanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;
1361
+ const selected = selectCleanupCandidates(candidates, claim.claimedAt);
1362
+ backlogLowerBound = selected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;
1363
+ const completedTargets = /* @__PURE__ */ new Set();
1364
+ let canContinue = true;
1365
+ if (canIssueStatement() && selected.orphanIds.length > 0) {
1366
+ deletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(cutoff, selected.orphanIds.length, {
1367
+ candidateIds: selected.orphanIds,
1368
+ cleanupLease: cleanupLease(leaseToken),
1369
+ canIssueStatement
1370
+ });
1371
+ canContinue = deletedOrphans === selected.orphanIds.length;
1372
+ if (canContinue) completedTargets.add("orphan");
1373
+ }
1374
+ if (canContinue && canIssueStatement() && selected.staleIds.length > 0) {
1375
+ deletedStale = await repo.deleteStaleGenerationsOlderThan(cutoff, selected.staleIds.length, {
1376
+ candidateIds: selected.staleIds,
1377
+ cleanupLease: cleanupLease(leaseToken),
1378
+ canIssueStatement
1379
+ });
1380
+ canContinue = deletedStale === selected.staleIds.length;
1381
+ if (canContinue) completedTargets.add("stale");
1382
+ }
1383
+ if (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {
1384
+ deletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(cutoff, selected.abandonedIds.length, {
1385
+ candidateIds: selected.abandonedIds,
1386
+ cleanupLease: cleanupLease(leaseToken),
1387
+ canIssueStatement
1388
+ });
1389
+ if (deletedAbandoned === selected.abandonedIds.length) completedTargets.add("abandoned");
1390
+ }
1391
+ const hasIncompleteTargets = selected.entries.some((entry) => entry.target !== null && !completedTargets.has(entry.target));
1392
+ sweepComplete = !scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;
1393
+ nextCursor = sweepComplete ? null : cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);
1394
+ }
1395
+ const durationMs = elapsedSince(startedMs);
1396
+ return {
1397
+ status: await repo.completeMediaUsageCleanup({
1398
+ leaseToken,
1399
+ nextCursor,
1400
+ sweepComplete,
1401
+ candidateCount: candidateRows,
1402
+ deletedOrphans,
1403
+ deletedStale,
1404
+ deletedAbandoned,
1405
+ deletedWriteLeases,
1406
+ backlogLowerBound,
1407
+ scanHasMore,
1408
+ durationMs
1409
+ }) ? "completed" : "skipped",
1410
+ candidateRows,
1411
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
1412
+ deletedOrphans,
1413
+ deletedStale,
1414
+ deletedAbandoned,
1415
+ deletedWriteLeases,
1416
+ backlogLowerBound,
1417
+ scanHasMore,
1418
+ durationMs
1419
+ };
1420
+ } catch (error) {
1421
+ const durationMs = elapsedSince(startedMs);
1422
+ const failures = Math.min(claim.consecutiveFailures + 1, 5);
1423
+ try {
1424
+ await repo.failMediaUsageCleanup({
1425
+ leaseToken,
1426
+ retryDelaySeconds: failureDelayMs(failures) / 1e3,
1427
+ consecutiveFailures: failures,
1428
+ durationMs,
1429
+ errorCode: "MEDIA_USAGE_CLEANUP_FAILED"
1430
+ });
1431
+ } catch (failureError) {
1432
+ console.error("[media-usage-cleanup] Failed to record cleanup failure:", failureError);
1433
+ }
1434
+ console.error("[media-usage-cleanup] Cleanup failed:", error);
1435
+ return {
1436
+ ...emptyResult("failed", durationMs),
1437
+ candidateRows,
1438
+ deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
1439
+ deletedOrphans,
1440
+ deletedStale,
1441
+ deletedAbandoned,
1442
+ deletedWriteLeases,
1443
+ backlogLowerBound,
1444
+ scanHasMore
1445
+ };
1446
+ }
1447
+ }
1448
+ function selectCleanupCandidates(candidates, activeLeaseAt) {
1449
+ const orphanIds = [];
1450
+ const staleIds = [];
1451
+ const abandonedIds = [];
1452
+ const entries = [];
1453
+ for (const candidate of candidates) {
1454
+ if (hasActiveWriteLease(candidate, activeLeaseAt)) {
1455
+ entries.push({
1456
+ candidate,
1457
+ target: null
1458
+ });
1459
+ continue;
1460
+ }
1461
+ const target = cleanupTarget(candidate);
1462
+ if (target === null) {
1463
+ entries.push({
1464
+ candidate,
1465
+ target: null
1466
+ });
1467
+ continue;
1468
+ }
1469
+ if (orphanIds.length + staleIds.length + abandonedIds.length >= MEDIA_USAGE_CLEANUP_DELETE_LIMIT) break;
1470
+ if (target === "orphan") orphanIds.push(candidate.id);
1471
+ if (target === "stale") staleIds.push(candidate.id);
1472
+ if (target === "abandoned") abandonedIds.push(candidate.id);
1473
+ entries.push({
1474
+ candidate,
1475
+ target
1476
+ });
1477
+ }
178
1478
  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)
1479
+ orphanIds,
1480
+ staleIds,
1481
+ abandonedIds,
1482
+ entries
182
1483
  };
183
1484
  }
1485
+ function cleanupTarget(candidate) {
1486
+ if (candidate.currentGeneration === null) return "orphan";
1487
+ if (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null) return null;
1488
+ return candidate.createdAt < candidate.indexedAt ? "stale" : "abandoned";
1489
+ }
1490
+ function hasActiveWriteLease(candidate, activeLeaseAt) {
1491
+ return candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;
1492
+ }
1493
+ function cursorFor(candidate) {
1494
+ return {
1495
+ createdAt: candidate.createdAt,
1496
+ id: candidate.id
1497
+ };
1498
+ }
1499
+ function cursorAfterCompletedCandidates(selected, completedTargets, priorCursor) {
1500
+ let cursor = priorCursor;
1501
+ for (const entry of selected.entries) {
1502
+ if (entry.target !== null && !completedTargets.has(entry.target)) break;
1503
+ cursor = cursorFor(entry.candidate);
1504
+ }
1505
+ return cursor;
1506
+ }
1507
+ function emptyResult(status, durationMs) {
1508
+ return {
1509
+ status,
1510
+ candidateRows: 0,
1511
+ deletedRows: 0,
1512
+ deletedOrphans: 0,
1513
+ deletedStale: 0,
1514
+ deletedAbandoned: 0,
1515
+ deletedWriteLeases: 0,
1516
+ backlogLowerBound: 0,
1517
+ scanHasMore: false,
1518
+ durationMs
1519
+ };
1520
+ }
1521
+ function withinBudget(startedMs) {
1522
+ return elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;
1523
+ }
1524
+ function elapsedSince(startedMs) {
1525
+ return Math.max(0, Date.now() - startedMs);
1526
+ }
1527
+ function failureDelayMs(consecutiveFailures) {
1528
+ return Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;
1529
+ }
1530
+ function cleanupLease(leaseToken) {
1531
+ return { leaseToken };
1532
+ }
184
1533
 
185
1534
  //#endregion
186
1535
  //#region src/cleanup.ts
@@ -215,9 +1564,7 @@ async function runSystemCleanup(db, storage) {
215
1564
  pendingUploadFiles: -1,
216
1565
  uploadAttempts: -1,
217
1566
  revisionsPruned: -1,
218
- mediaUsageStaleGenerations: -1,
219
- mediaUsageAbandonedGenerations: -1,
220
- mediaUsageOrphanOccurrences: -1
1567
+ mediaUsage: -1
221
1568
  };
222
1569
  try {
223
1570
  result.challenges = await cleanupExpiredChallenges(db);
@@ -265,12 +1612,10 @@ async function runSystemCleanup(db, storage) {
265
1612
  console.error("[cleanup] Failed to prune revisions:", error);
266
1613
  }
267
1614
  try {
268
- const usage = await cleanupMediaUsageGenerations(db);
269
- result.mediaUsageStaleGenerations = usage.staleGenerations;
270
- result.mediaUsageAbandonedGenerations = usage.abandonedGenerations;
271
- result.mediaUsageOrphanOccurrences = usage.orphanOccurrences;
1615
+ const mediaUsage = await cleanupMediaUsage(db);
1616
+ result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
272
1617
  } catch (error) {
273
- console.error("[cleanup] Failed to clean media-usage generations:", error);
1618
+ console.error("[cleanup] Failed to clean media usage:", error);
274
1619
  }
275
1620
  return result;
276
1621
  }
@@ -469,6 +1814,15 @@ const DRAFT_ONLY_UPDATE_KEYS = new Set([
469
1814
  "skipRevision"
470
1815
  ]);
471
1816
  const MAX_DRAFT_STAGE_ATTEMPTS = 32;
1817
+ const LIST_COLUMN_FIELD_TYPES = new Set([
1818
+ "string",
1819
+ "number",
1820
+ "integer",
1821
+ "boolean",
1822
+ "datetime",
1823
+ "select",
1824
+ "multiSelect"
1825
+ ]);
472
1826
  /**
473
1827
  * Convert a ContentItem to Record<string, unknown> for hook consumption.
474
1828
  * Hooks receive the full item as a flat record.
@@ -552,6 +1906,49 @@ const marketplaceManifestCache = /* @__PURE__ */ new Map();
552
1906
  /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
553
1907
  const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
554
1908
  let sandboxRunner = null;
1909
+ const MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS = Object.freeze({
1910
+ entryWork: MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob,
1911
+ collectionDeletion: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick,
1912
+ reconciliation: MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick,
1913
+ maxClassQueries: Math.max(MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick, MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick),
1914
+ eventCeiling: 40
1915
+ });
1916
+ async function runScheduledMediaUsageLane(db) {
1917
+ if ((getRequestContext()?.metrics?.dbCount ?? 0) + 1 + MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.maxClassQueries > MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.eventCeiling) return {
1918
+ outcome: "admission_closed",
1919
+ taskClass: null,
1920
+ turn: null
1921
+ };
1922
+ 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();
1923
+ if (!activation) return {
1924
+ outcome: "inactive",
1925
+ taskClass: null,
1926
+ turn: null
1927
+ };
1928
+ const turn = activation.media_usage_maintenance_turn;
1929
+ if (turn === 0) {
1930
+ await processDueMediaUsageWork(db);
1931
+ return {
1932
+ outcome: "processed",
1933
+ taskClass: "entry_work",
1934
+ turn
1935
+ };
1936
+ }
1937
+ if (turn === 1) {
1938
+ await processDueMediaUsageCollectionDeletions(db);
1939
+ return {
1940
+ outcome: "processed",
1941
+ taskClass: "collection_deletion",
1942
+ turn
1943
+ };
1944
+ }
1945
+ await processDueMediaUsageReconciliation(db);
1946
+ return {
1947
+ outcome: "processed",
1948
+ taskClass: "reconciliation",
1949
+ turn
1950
+ };
1951
+ }
555
1952
  /**
556
1953
  * EmDashRuntime - singleton per worker
557
1954
  */
@@ -663,7 +2060,14 @@ var EmDashRuntime = class EmDashRuntime {
663
2060
  * Returns the items promoted so callers can invalidate their cache tags.
664
2061
  */
665
2062
  async publishScheduled() {
666
- return publishDueContent(this.db, { publish: (collection, id, options) => this.handleContentPublish(collection, id, options) });
2063
+ return this.publishScheduledWithFence();
2064
+ }
2065
+ async publishScheduledWithFence(onPublished) {
2066
+ await assertMediaUsageActivationWriteAllowed(this.db);
2067
+ return publishDueContent(this.db, {
2068
+ publish: (collection, id, options) => this.handleContentPublish(collection, id, options),
2069
+ onPublished
2070
+ });
667
2071
  }
668
2072
  /**
669
2073
  * Run the full scheduled-maintenance batch: cron tasks, scheduled
@@ -694,10 +2098,7 @@ var EmDashRuntime = class EmDashRuntime {
694
2098
  }
695
2099
  let published = [];
696
2100
  try {
697
- published = await publishDueContent(this.db, {
698
- publish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),
699
- onPublished: options.onPublished
700
- });
2101
+ published = await this.publishScheduledWithFence(options.onPublished);
701
2102
  } catch (error) {
702
2103
  console.error("[scheduled-publish] Sweep failed:", error);
703
2104
  }
@@ -712,8 +2113,12 @@ var EmDashRuntime = class EmDashRuntime {
712
2113
  console.error("[plugins] Storage index sync failed:", error);
713
2114
  }
714
2115
  await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
2116
+ await recordSchedulerHeartbeatSafely(this.db);
715
2117
  return { published };
716
2118
  }
2119
+ async runScheduledMediaUsageTasks() {
2120
+ return runScheduledMediaUsageLane(this.db);
2121
+ }
717
2122
  /**
718
2123
  * Materialize plugin-declared storage indexes, once per process.
719
2124
  *
@@ -1013,7 +2418,7 @@ var EmDashRuntime = class EmDashRuntime {
1013
2418
  });
1014
2419
  }
1015
2420
  };
1016
- const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
2421
+ const db = await phase("rt.db", "DB init + migration policy", () => EmDashRuntime.getDatabase(deps));
1017
2422
  const resolveDb = () => {
1018
2423
  return getRequestContext()?.db ?? db;
1019
2424
  };
@@ -1032,6 +2437,14 @@ var EmDashRuntime = class EmDashRuntime {
1032
2437
  const ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);
1033
2438
  let readDb = db;
1034
2439
  let readDbDisposable;
2440
+ const disposeReadDb = async () => {
2441
+ const disposable = readDbDisposable;
2442
+ readDbDisposable = void 0;
2443
+ if (!disposable) return;
2444
+ try {
2445
+ await disposable.destroy();
2446
+ } catch {}
2447
+ };
1035
2448
  if (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) try {
1036
2449
  const dialect = deps.createCoalescingDialect(deps.config.database.config);
1037
2450
  if (dialect) {
@@ -1045,6 +2458,10 @@ var EmDashRuntime = class EmDashRuntime {
1045
2458
  readDb = db;
1046
2459
  }
1047
2460
  const optionsRepo = new OptionsRepository(readDb);
2461
+ let missingManualSchemaError;
2462
+ const captureMissingManualSchema = (error) => {
2463
+ if ((deps.migrationMode ?? "auto") === "manual" && isMissingTableError(error)) missingManualSchemaError ??= error;
2464
+ };
1048
2465
  const readSiteInfo = async () => {
1049
2466
  const siteOpts = await optionsRepo.getMany([
1050
2467
  "emdash:site_title",
@@ -1064,11 +2481,15 @@ var EmDashRuntime = class EmDashRuntime {
1064
2481
  try {
1065
2482
  const states = await readDb.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
1066
2483
  pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
1067
- } catch {}
2484
+ } catch (error) {
2485
+ captureMissingManualSchema(error);
2486
+ }
1068
2487
  }), phase("rt.site", "Site info options", async () => {
1069
2488
  try {
1070
2489
  siteInfo = await readSiteInfo();
1071
- } catch {}
2490
+ } catch (error) {
2491
+ captureMissingManualSchema(error);
2492
+ }
1072
2493
  })];
1073
2494
  if (ownsConfiguredDb) coldStartReads.push(phase("rt.seedcheck", "Auto-seed gate", async () => {
1074
2495
  try {
@@ -1084,9 +2505,15 @@ var EmDashRuntime = class EmDashRuntime {
1084
2505
  collectionCount: collectionCount.count,
1085
2506
  setupDone
1086
2507
  };
1087
- } catch {}
2508
+ } catch (error) {
2509
+ captureMissingManualSchema(error);
2510
+ }
1088
2511
  }));
1089
2512
  await Promise.all(coldStartReads);
2513
+ if (missingManualSchemaError) {
2514
+ await disposeReadDb();
2515
+ throw missingManualSchemaError;
2516
+ }
1090
2517
  if (localeCasingRepairVersion && (configuredLocales.some((locale) => locale.includes("-") || locale !== locale.toLowerCase()) || storedLocaleCasingRepairVersion !== void 0) && storedLocaleCasingRepairVersion !== localeCasingRepairVersion) await phase("rt.locale", "Repair locale casing", async () => {
1091
2518
  await repairLocaleCasing(db, configuredLocales);
1092
2519
  await new OptionsRepository(db).set(LOCALE_CASING_REPAIR_OPTION, localeCasingRepairVersion);
@@ -1096,9 +2523,9 @@ var EmDashRuntime = class EmDashRuntime {
1096
2523
  const seedHolder = getSeedHolder();
1097
2524
  try {
1098
2525
  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);
2526
+ const { applySeed } = await import("../apply-kUDt80-G.mjs").then((n) => n.n);
2527
+ const { loadSeed } = await import("../load-YJ2xl3ra.mjs").then((n) => n.r);
2528
+ const { validateSeed } = await import("../validate-BMLBvxOC.mjs").then((n) => n.n);
1102
2529
  const seed = await loadSeed();
1103
2530
  if (validateSeed(seed).valid) {
1104
2531
  await applySeed(db, seed, { onConflict: "skip" });
@@ -1115,9 +2542,7 @@ var EmDashRuntime = class EmDashRuntime {
1115
2542
  } catch {}
1116
2543
  } catch {}
1117
2544
  }
1118
- if (readDbDisposable) try {
1119
- await readDbDisposable.destroy();
1120
- } catch {}
2545
+ await disposeReadDb();
1121
2546
  const enabledPlugins = /* @__PURE__ */ new Set();
1122
2547
  for (const plugin of deps.plugins) {
1123
2548
  const status = pluginStates.get(plugin.id);
@@ -1179,6 +2604,7 @@ var EmDashRuntime = class EmDashRuntime {
1179
2604
  const pipelineFactoryOptions = {
1180
2605
  db,
1181
2606
  getDb: resolveDb,
2607
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
1182
2608
  storage: storage ?? void 0,
1183
2609
  siteInfo
1184
2610
  };
@@ -1229,10 +2655,19 @@ var EmDashRuntime = class EmDashRuntime {
1229
2655
  if (deps.createScheduler) {
1230
2656
  const scheduler = deps.createScheduler(cronExecutor);
1231
2657
  cronScheduler = scheduler;
2658
+ const runMediaUsageMaintenance = async () => {
2659
+ const runtime = runtimeRef.current;
2660
+ if (runtime) await runtime.runScheduledMediaUsageTasks();
2661
+ else await runScheduledMediaUsageLane(db);
2662
+ };
1232
2663
  scheduler.setSystemCleanup(async () => {
1233
2664
  try {
1234
2665
  const runtime = runtimeRef.current;
1235
- await publishDueContent(db, { publish: runtime ? (collection, id, options) => runtime.handleContentPublish(collection, id, options) : void 0 });
2666
+ if (runtime) await runtime.publishScheduled();
2667
+ else {
2668
+ await assertMediaUsageActivationWriteAllowed(db);
2669
+ await publishDueContent(db);
2670
+ }
1236
2671
  } catch (error) {
1237
2672
  console.error("[scheduled-publish] Sweep failed:", error);
1238
2673
  }
@@ -1247,7 +2682,14 @@ var EmDashRuntime = class EmDashRuntime {
1247
2682
  console.error("[plugins] Storage index sync failed:", error);
1248
2683
  }
1249
2684
  await maybeRunScheduledBackup(db, storage ?? void 0);
2685
+ await recordSchedulerHeartbeatSafely(db);
2686
+ if (!scheduler.setMediaUsageMaintenance) try {
2687
+ await runMediaUsageMaintenance();
2688
+ } catch (error) {
2689
+ console.error("[media-usage] Scheduled maintenance failed:", error);
2690
+ }
1250
2691
  });
2692
+ scheduler.setMediaUsageMaintenance?.(runMediaUsageMaintenance);
1251
2693
  scheduler.start();
1252
2694
  }
1253
2695
  } catch (error) {
@@ -1322,9 +2764,9 @@ var EmDashRuntime = class EmDashRuntime {
1322
2764
  log: kyselyLogOption()
1323
2765
  });
1324
2766
  try {
1325
- await runMigrations(db);
2767
+ await enforceRuntimeMigrationPolicy(db, deps.migrationMode ?? "auto");
1326
2768
  } catch (error) {
1327
- if (!(error instanceof ConcurrentMigrationTimeoutError)) holder.failures.set(cacheKey, {
2769
+ if (!(error instanceof ConcurrentMigrationTimeoutError) && !(error instanceof PendingMigrationsError)) holder.failures.set(cacheKey, {
1328
2770
  at: Date.now(),
1329
2771
  message: error instanceof Error ? error.message : String(error)
1330
2772
  });
@@ -1407,6 +2849,7 @@ var EmDashRuntime = class EmDashRuntime {
1407
2849
  if (!deps.sandboxEnabled) return sandboxedPluginCache;
1408
2850
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1409
2851
  db,
2852
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1410
2853
  mediaStorage: mediaStorage ? {
1411
2854
  upload: (opts) => mediaStorage.upload({
1412
2855
  key: opts.key,
@@ -1472,6 +2915,7 @@ var EmDashRuntime = class EmDashRuntime {
1472
2915
  static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
1473
2916
  if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
1474
2917
  db,
2918
+ beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
1475
2919
  mediaStorage: {
1476
2920
  upload: (opts) => storage.upload({
1477
2921
  key: opts.key,
@@ -1673,13 +3117,33 @@ var EmDashRuntime = class EmDashRuntime {
1673
3117
  if ((field.type === "repeater" || field.type === "file" || field.type === "image") && field.validation) entry.validation = { ...field.validation };
1674
3118
  fields[field.slug] = entry;
1675
3119
  }
3120
+ const configuredListColumns = collection.admin?.listColumns ?? [];
3121
+ const fieldTypes = new Map(collection.fields.map((field) => [field.slug, field.type]));
3122
+ const listColumns = [];
3123
+ for (const slug of configuredListColumns) {
3124
+ if (listColumns.includes(slug)) continue;
3125
+ const fieldType = fieldTypes.get(slug);
3126
+ if (!fieldType || !LIST_COLUMN_FIELD_TYPES.has(fieldType)) {
3127
+ console.warn(`EmDash: Ignoring unsupported or unknown list column "${slug}" in collection "${collection.slug}".`);
3128
+ continue;
3129
+ }
3130
+ if (listColumns.length >= MAX_COLLECTION_LIST_COLUMNS) {
3131
+ console.warn(`EmDash: Collection "${collection.slug}" declares more than ${MAX_COLLECTION_LIST_COLUMNS} list columns; extra columns are ignored.`);
3132
+ break;
3133
+ }
3134
+ listColumns.push(slug);
3135
+ }
1676
3136
  manifestCollections[collection.slug] = {
1677
3137
  label: collection.label,
1678
3138
  labelSingular: collection.labelSingular || collection.label,
1679
3139
  supports: collection.supports || [],
1680
3140
  hasSeo: collection.hasSeo,
1681
3141
  urlPattern: collection.urlPattern,
3142
+ routable: collection.routable !== false,
3143
+ titleField: collection.titleField,
3144
+ dateField: collection.dateField,
1682
3145
  ...collection.hidden ? { hidden: true } : {},
3146
+ listColumns: listColumns.length > 0 ? listColumns : void 0,
1683
3147
  fields
1684
3148
  };
1685
3149
  }
@@ -1739,21 +3203,33 @@ var EmDashRuntime = class EmDashRuntime {
1739
3203
  };
1740
3204
  }
1741
3205
  let manifestTaxonomies = [];
3206
+ let taxonomyDefinitionLocales = [];
1742
3207
  try {
1743
- manifestTaxonomies = (await this.db.selectFrom("_emdash_taxonomy_defs").selectAll().orderBy("name").execute()).map((row) => ({
3208
+ const rows = await this.db.selectFrom("_emdash_taxonomy_defs").selectAll().orderBy("name").execute();
3209
+ taxonomyDefinitionLocales = rows.map((row) => row.locale);
3210
+ manifestTaxonomies = rows.map((row) => ({
3211
+ id: row.id,
1744
3212
  name: row.name,
1745
3213
  label: row.label,
1746
3214
  labelSingular: row.label_singular ?? void 0,
1747
3215
  hierarchical: row.hierarchical === 1,
1748
- collections: parseStringArray(row.collections).toSorted()
3216
+ collections: parseStringArray(row.collections).toSorted(),
3217
+ locale: row.locale,
3218
+ translationGroup: row.translation_group ?? row.id
1749
3219
  }));
1750
3220
  } catch (error) {
1751
3221
  console.debug("EmDash: Could not load taxonomy definitions:", error);
1752
3222
  }
3223
+ try {
3224
+ const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
3225
+ await warnAboutUnconfiguredTaxonomyLocales(this.db, configuredLocales, taxonomyDefinitionLocales);
3226
+ } catch (error) {
3227
+ console.warn("[i18n] taxonomy locale diagnostic failed:", error);
3228
+ }
1753
3229
  const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
1754
3230
  const authMode = getAuthMode(this.config);
1755
3231
  const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
1756
- const i18nConfig = virtualConfig?.i18n;
3232
+ const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
1757
3233
  const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
1758
3234
  defaultLocale: i18nConfig.defaultLocale,
1759
3235
  locales: i18nConfig.locales
@@ -1769,6 +3245,10 @@ var EmDashRuntime = class EmDashRuntime {
1769
3245
  taxonomies: manifestTaxonomies,
1770
3246
  authMode: authModeValue,
1771
3247
  i18n,
3248
+ contentLocale: {
3249
+ defaultLocale: i18nConfig?.defaultLocale ?? "en",
3250
+ implicit: i18nConfig === null
3251
+ },
1772
3252
  marketplace: !!this.config.marketplace,
1773
3253
  registry
1774
3254
  };
@@ -1874,7 +3354,7 @@ var EmDashRuntime = class EmDashRuntime {
1874
3354
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
1875
3355
  processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
1876
3356
  processedData = await this.normalizeMediaFields(collection, processedData);
1877
- const { validateContentData } = await import("../validation-BgpucsvF.mjs");
3357
+ const { validateContentData } = await import("../validation-npvENUCB.mjs");
1878
3358
  const validation = await validateContentData(this.db, collection, processedData, { partial: false });
1879
3359
  if (!validation.ok) return {
1880
3360
  success: false,
@@ -1917,7 +3397,7 @@ var EmDashRuntime = class EmDashRuntime {
1917
3397
  if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
1918
3398
  processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
1919
3399
  processedData = await this.normalizeMediaFields(collection, processedData);
1920
- const { validateContentData } = await import("../validation-BgpucsvF.mjs");
3400
+ const { validateContentData } = await import("../validation-npvENUCB.mjs");
1921
3401
  const validation = await validateContentData(this.db, collection, processedData, { partial: true });
1922
3402
  if (!validation.ok) return {
1923
3403
  success: false,
@@ -2233,13 +3713,16 @@ var EmDashRuntime = class EmDashRuntime {
2233
3713
  }
2234
3714
  async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
2235
3715
  for (const contentId of new Set(contentIds)) try {
3716
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2236
3717
  await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
2237
3718
  } catch (error) {
2238
3719
  console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
3720
+ return;
2239
3721
  }
2240
3722
  }
2241
3723
  async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
2242
3724
  try {
3725
+ if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
2243
3726
  const result = await deleteContentMediaUsage(this.db, collection, contentId);
2244
3727
  if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
2245
3728
  } catch (error) {
@@ -2285,7 +3768,7 @@ var EmDashRuntime = class EmDashRuntime {
2285
3768
  if (!meta) return null;
2286
3769
  return meta.admin?.settingsSchema ?? {};
2287
3770
  }
2288
- async handlePluginApiRoute(pluginId, _method, path, request) {
3771
+ async handlePluginApiRoute(pluginId, _method, path, request, user) {
2289
3772
  if (!this.isPluginEnabled(pluginId)) return {
2290
3773
  success: false,
2291
3774
  error: {
@@ -2293,6 +3776,7 @@ var EmDashRuntime = class EmDashRuntime {
2293
3776
  message: `Plugin not enabled: ${pluginId}`
2294
3777
  }
2295
3778
  };
3779
+ const caller = user ? toRouteCallerInfo(user) : void 0;
2296
3780
  const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
2297
3781
  if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
2298
3782
  const routeRegistry = new PluginRouteRegistry({
@@ -2306,11 +3790,12 @@ var EmDashRuntime = class EmDashRuntime {
2306
3790
  const body = await parseRouteInput(request);
2307
3791
  return routeRegistry.invoke(pluginId, routeKey, {
2308
3792
  request,
2309
- body
3793
+ body,
3794
+ user: caller
2310
3795
  });
2311
3796
  }
2312
3797
  const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
2313
- if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
3798
+ if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request, caller);
2314
3799
  return {
2315
3800
  success: false,
2316
3801
  error: {
@@ -2385,7 +3870,7 @@ var EmDashRuntime = class EmDashRuntime {
2385
3870
  ...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
2386
3871
  })).toSorted((a, b) => a.name.localeCompare(b.name)));
2387
3872
  }
2388
- async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request) {
3873
+ async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller) {
2389
3874
  const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
2390
3875
  const audit = new AuditRepository(this.db);
2391
3876
  const headers = new Headers(request.headers);
@@ -2396,7 +3881,7 @@ var EmDashRuntime = class EmDashRuntime {
2396
3881
  headers,
2397
3882
  body: JSON.stringify(input)
2398
3883
  });
2399
- const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest);
3884
+ const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller);
2400
3885
  await audit.log({
2401
3886
  actorId,
2402
3887
  actorIp: requestMeta.ip ?? void 0,
@@ -2625,7 +4110,7 @@ var EmDashRuntime = class EmDashRuntime {
2625
4110
  runAfterUnscheduleHooks(content, collection) {
2626
4111
  this.runDeferredContentHook("content:afterUnschedule", content, collection);
2627
4112
  }
2628
- async handleSandboxedRoute(plugin, path, request) {
4113
+ async handleSandboxedRoute(plugin, path, request, user) {
2629
4114
  const routeName = path.replace(LEADING_SLASH_PATTERN, "");
2630
4115
  const body = await parseRouteInput(request);
2631
4116
  try {
@@ -2637,11 +4122,21 @@ var EmDashRuntime = class EmDashRuntime {
2637
4122
  url: request.url,
2638
4123
  method: request.method,
2639
4124
  headers,
2640
- meta
4125
+ meta,
4126
+ user
2641
4127
  })
2642
4128
  };
2643
4129
  } catch (error) {
2644
4130
  console.error(`EmDash: Sandboxed plugin route error:`, error);
4131
+ const sandboxRouteError = getSandboxRouteErrorDetails(error);
4132
+ if (sandboxRouteError) return {
4133
+ success: false,
4134
+ status: sandboxRouteError.status,
4135
+ error: {
4136
+ code: sandboxRouteError.code,
4137
+ message: sandboxRouteError.message
4138
+ }
4139
+ };
2645
4140
  return {
2646
4141
  success: false,
2647
4142
  error: {
@@ -2760,6 +4255,21 @@ function createPublicMediaUrlResolver(storage) {
2760
4255
  * middleware.ts.
2761
4256
  */
2762
4257
  const ASTRO_COOKIES_SYMBOL = Symbol.for("astro.cookies");
4258
+ /**
4259
+ * Whether the request ended authenticated, for the db adapter's `commit()`.
4260
+ * `isAuthenticated` is captured before the route runs, so it misses
4261
+ * login/signup/invite requests that create the Astro session mid-request;
4262
+ * `session.set()` writes the `astro-session` cookie into the jar synchronously,
4263
+ * so scanning the outgoing cookies detects them. Only outgoing cookies count —
4264
+ * a stale `astro-session` cookie arriving on an anonymous render must not
4265
+ * qualify, or its replica-read bookmark would overwrite a fresher one from an
4266
+ * earlier authenticated write.
4267
+ */
4268
+ function requestEndedAuthenticated(isAuthenticated, cookies) {
4269
+ if (isAuthenticated) return true;
4270
+ for (const header of cookies.headers()) if (header.startsWith("astro-session=")) return true;
4271
+ return false;
4272
+ }
2763
4273
  /** Hold the real adapter close behind both response and deferred-task completion. */
2764
4274
  function coordinateScopedDbLifecycle(scoped) {
2765
4275
  if (!scoped.close) return { lifecycle: scoped };
@@ -3021,12 +4531,6 @@ function createPublicPluginApiRouteHandler(runtime) {
3021
4531
  //#endregion
3022
4532
  //#region src/astro/middleware.ts
3023
4533
  /**
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
4534
  * Runtime init lock reclaim deadline. Must be strictly larger than the db
3031
4535
  * init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →
3032
4536
  * the db init lock, and equal deadlines would let this outer lock reclaim
@@ -3112,10 +4616,11 @@ function getPlugins() {
3112
4616
  /**
3113
4617
  * Build runtime dependencies from virtual modules
3114
4618
  */
3115
- function buildDependencies(config) {
4619
+ function buildDependencies(config, migrationMode) {
3116
4620
  const sandboxModule = virtualSandboxRunnerModule;
3117
4621
  return {
3118
4622
  config,
4623
+ migrationMode,
3119
4624
  plugins: getPlugins(),
3120
4625
  createDialect,
3121
4626
  createCoalescingDialect,
@@ -3136,10 +4641,10 @@ function buildDependencies(config) {
3136
4641
  * cached instance) push nothing — callers should treat an empty array
3137
4642
  * as "warm, nothing to report".
3138
4643
  */
3139
- async function getRuntime(config, initTimings) {
4644
+ async function getRuntime(config, migrationMode, initTimings) {
3140
4645
  const holder = getRuntimeHolder();
3141
4646
  return initWithLock(holder.lock, () => holder.instance, async (isCurrentClaim) => {
3142
- const deps = buildDependencies(config);
4647
+ const deps = buildDependencies(config, migrationMode);
3143
4648
  const runtime = await EmDashRuntime.create(deps, initTimings);
3144
4649
  if (isCurrentClaim()) holder.instance = runtime;
3145
4650
  else runtime.stopCron().catch((error) => {
@@ -3170,6 +4675,15 @@ async function runScheduledTasks(options = {}) {
3170
4675
  if (!config) return { published: [] };
3171
4676
  return runOutsideRequest(config, (runtime) => runtime.runScheduledTasks(options));
3172
4677
  }
4678
+ async function runScheduledMediaUsageTasks() {
4679
+ const config = getConfig();
4680
+ if (!config) return {
4681
+ outcome: "inactive",
4682
+ taskClass: null,
4683
+ turn: null
4684
+ };
4685
+ return runOutsideRequest(config, (runtime) => runtime.runScheduledMediaUsageTasks());
4686
+ }
3173
4687
  /**
3174
4688
  * Run a callback against the EmDash runtime outside any HTTP request — from a
3175
4689
  * Cloudflare Queue consumer, a `scheduled()` handler, or any other
@@ -3213,19 +4727,37 @@ async function withEmDashRuntime(run) {
3213
4727
  /**
3214
4728
  * Shared plumbing for request-free entry points (`runScheduledTasks`,
3215
4729
  * `withEmDashRuntime`): resolve the runtime singleton, then run the callback
3216
- * under an event-scoped db connection when the adapter needs one.
4730
+ * under an event-scoped db when the adapter needs one.
3217
4731
  *
3218
4732
  * Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse
3219
4733
  * the per-isolate singleton from a platform event: its socket belongs to the
3220
4734
  * request that opened it, and workerd rejects cross-event I/O. Open an
3221
4735
  * event-scoped connection and run the callback under it in ALS — the
3222
4736
  * 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.
4737
+ * the connection from ALS — then close it when required. Stateless adapters
4738
+ * that need primary routing can return a close-less scope; adapters with no
4739
+ * event scoping return null and keep using the singleton.
3226
4740
  */
3227
4741
  async function runOutsideRequest(config, fn) {
3228
- const runtime = await getRuntime(config);
4742
+ const migrationMode = resolveConfiguredMigrationMode(config);
4743
+ if (getRequestContext()) return runOutsideRequestWithRuntime(config, await getRuntime(config, migrationMode), fn);
4744
+ const deferredTasks = createDeferredTaskTracker(() => {});
4745
+ return runWithContext({
4746
+ editMode: false,
4747
+ metrics: createRequestMetrics(performance.now()),
4748
+ deferredTasks
4749
+ }, async () => {
4750
+ return runOutsideRequestWithRuntime(config, await (async () => {
4751
+ try {
4752
+ return await getRuntime(config, migrationMode);
4753
+ } finally {
4754
+ deferredTasks.settle();
4755
+ await deferredTasks.settled;
4756
+ }
4757
+ })(), fn);
4758
+ });
4759
+ }
4760
+ async function runOutsideRequestWithRuntime(config, runtime, fn) {
3229
4761
  const scoped = createRequestScopedDb$1({
3230
4762
  config: config.database?.config,
3231
4763
  isAuthenticated: false,
@@ -3234,7 +4766,7 @@ async function runOutsideRequest(config, fn) {
3234
4766
  cookies: NOOP_COOKIE_JAR,
3235
4767
  url: CRON_EVENT_URL
3236
4768
  });
3237
- if (!scoped?.close) return fn(runtime);
4769
+ if (!scoped) return fn(runtime);
3238
4770
  const { closed, deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
3239
4771
  const parent = getRequestContext();
3240
4772
  const ctx = parent ? {
@@ -3280,6 +4812,24 @@ const NOOP_COOKIE_JAR = {
3280
4812
  * that path, so the value is never used — it exists to satisfy the contract.
3281
4813
  */
3282
4814
  const CRON_EVENT_URL = new URL("https://cron.emdash.internal/");
4815
+ function resolveConfiguredMigrationMode(config) {
4816
+ const processOverride = typeof process !== "undefined" && process.env ? process.env.EMDASH_MIGRATIONS_MODE : void 0;
4817
+ const importMetaOverride = import.meta.env.EMDASH_MIGRATIONS_MODE;
4818
+ return resolveRuntimeMigrationMode(config.migrations, {
4819
+ dev: import.meta.env.DEV,
4820
+ override: processOverride ?? importMetaOverride
4821
+ });
4822
+ }
4823
+ function pendingMigrationsResponse(error) {
4824
+ console.error("[emdash] database migrations are pending:", error.pending.join(", "));
4825
+ return migrationRequiredResponse();
4826
+ }
4827
+ function migrationRequiredResponse() {
4828
+ return new Response("Database migrations are required. Apply the deployment migration manifest and retry.", {
4829
+ status: 503,
4830
+ headers: { "Retry-After": "60" }
4831
+ });
4832
+ }
3283
4833
  /**
3284
4834
  * Baseline security headers applied to all responses.
3285
4835
  * Admin routes get additional headers (strict CSP) from auth middleware.
@@ -3359,15 +4909,44 @@ function createRequestScopedDb$1(opts) {
3359
4909
  if (typeof createRequestScopedDb !== "function") return null;
3360
4910
  return createRequestScopedDb(opts);
3361
4911
  }
4912
+ const buildDate = buildTime ? new Date(buildTime) : null;
4913
+ /**
4914
+ * Fold the build timestamp into the route cache validator.
4915
+ *
4916
+ * `CacheHint.lastModified` describes the content, but the response also depends
4917
+ * on the build: `/_astro/*` names are content-hashed, and a deployment only
4918
+ * serves its own. Without the build dimension a code-only deploy answers a
4919
+ * returning visitor's conditional request with 304, leaving them on HTML whose
4920
+ * assets 404.
4921
+ *
4922
+ * Prerendered pages are served by the host's static layer, which manages its
4923
+ * own validators — only on-demand responses need the build dimension.
4924
+ *
4925
+ * Only forward moves are covered. `Last-Modified` expresses newer, not
4926
+ * different, so after a rollback the earlier build still answers a conditional
4927
+ * request with 304 and the browser stays on the newer build's HTML.
4928
+ *
4929
+ * Must run before next(): Astro keeps the later of two dates, so a route's own
4930
+ * hint still wins when content is newer, and a route that opts out with
4931
+ * `Astro.cache.set(false)` stays opted out — calling set() afterwards would
4932
+ * clear that opt-out.
4933
+ */
4934
+ function applyBuildValidator(context) {
4935
+ if (context.isPrerendered || !buildDate || !context.cache?.enabled) return;
4936
+ context.cache.set({ lastModified: buildDate });
4937
+ }
3362
4938
  const onRequest = defineMiddleware(async (context, next) => {
3363
4939
  const { request, locals, cookies } = context;
3364
4940
  const url = context.url;
3365
4941
  if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
3366
4942
  if (virtualConfig.authProviders.some((p) => p.routes?.some((r) => r.pattern && url.pathname === r.pattern))) return finalizeResponse(await next());
3367
4943
  }
4944
+ applyBuildValidator(context);
3368
4945
  const queryRecorder = isInstrumentationEnabled() ? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default") : void 0;
3369
4946
  const metrics = createRequestMetrics(performance.now());
3370
4947
  const run = async () => {
4948
+ const config = getConfig();
4949
+ const migrationMode = config ? resolveConfiguredMigrationMode(config) : "auto";
3371
4950
  const isEmDashRoute = url.pathname.startsWith("/_emdash");
3372
4951
  const isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname) || SITEMAP_COLLECTION_RE.test(url.pathname);
3373
4952
  const hasEditCookie = cookies.get("emdash-edit-mode")?.value === "true";
@@ -3378,15 +4957,16 @@ const onRequest = defineMiddleware(async (context, next) => {
3378
4957
  const hasBearerAuth = (request.headers.get("authorization") ?? "").toLowerCase().startsWith("bearer ");
3379
4958
  const isWrite = request.method !== "GET" && request.method !== "HEAD";
3380
4959
  const isAuthenticated = !!sessionUser || hasBearerAuth;
4960
+ const endedAuthenticated = () => requestEndedAuthenticated(isAuthenticated, cookies);
3381
4961
  const canUseCachedBinding = !isAuthenticated && !isWrite && !playgroundDb && !isEmDashRoute && !hasEditCookie && !hasPreviewToken;
3382
4962
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
3383
4963
  if (!sessionUser && !playgroundDb) {
3384
4964
  const timings = [];
3385
4965
  const mwStart = performance.now();
3386
- if (!isSetupVerified() && !context.isPrerendered) {
4966
+ if (migrationMode === "auto" && !isSetupVerified() && !context.isPrerendered) {
3387
4967
  const t0 = performance.now();
3388
4968
  try {
3389
- const { getDb } = await import("../loader-CGugY7Zl.mjs").then((n) => n.s);
4969
+ const { getDb } = await import("../loader-CdG_Ci3H.mjs").then((n) => n.c);
3390
4970
  await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
3391
4971
  markSetupVerified();
3392
4972
  } catch (error) {
@@ -3399,12 +4979,11 @@ const onRequest = defineMiddleware(async (context, next) => {
3399
4979
  desc: "Setup probe"
3400
4980
  });
3401
4981
  }
3402
- const config = getConfig();
3403
4982
  if (config) {
3404
4983
  const initSubTimings = [];
3405
4984
  const t0 = performance.now();
3406
4985
  try {
3407
- const runtime = await getRuntime(config, initSubTimings);
4986
+ const runtime = await getRuntime(config, migrationMode, initSubTimings);
3408
4987
  markSetupVerified();
3409
4988
  locals.emdash = {
3410
4989
  handlePublicPluginApiRoute: createPublicPluginApiRouteHandler(runtime),
@@ -3414,6 +4993,11 @@ const onRequest = defineMiddleware(async (context, next) => {
3414
4993
  storage: runtime.storage
3415
4994
  };
3416
4995
  } catch (error) {
4996
+ if (error instanceof PendingMigrationsError) return pendingMigrationsResponse(error);
4997
+ if (migrationMode === "manual" && isMissingTableError(error)) {
4998
+ console.error("[emdash] database schema is unavailable in manual migration mode:", error);
4999
+ return migrationRequiredResponse();
5000
+ }
3417
5001
  if (Date.now() - lastRuntimeInitErrorLogAt >= RUNTIME_INIT_ERROR_LOG_INTERVAL_MS) {
3418
5002
  lastRuntimeInitErrorLogAt = Date.now();
3419
5003
  console.error("[emdash] runtime init failed (page renders without CMS data):", error);
@@ -3430,6 +5014,7 @@ const onRequest = defineMiddleware(async (context, next) => {
3430
5014
  const anonScoped = createRequestScopedDb$1({
3431
5015
  config: config?.database?.config,
3432
5016
  isAuthenticated,
5017
+ endedAuthenticated,
3433
5018
  isWrite,
3434
5019
  canUseCachedBinding,
3435
5020
  cookies,
@@ -3474,7 +5059,6 @@ const onRequest = defineMiddleware(async (context, next) => {
3474
5059
  return runAnon();
3475
5060
  }
3476
5061
  }
3477
- const config = getConfig();
3478
5062
  if (!config) {
3479
5063
  console.error("EmDash: No configuration found");
3480
5064
  return finalizeResponse(await next());
@@ -3485,7 +5069,7 @@ const onRequest = defineMiddleware(async (context, next) => {
3485
5069
  try {
3486
5070
  const initSubTimings = [];
3487
5071
  let t0 = performance.now();
3488
- const runtime = await getRuntime(config, initSubTimings);
5072
+ const runtime = await getRuntime(config, migrationMode, initSubTimings);
3489
5073
  timings.push({
3490
5074
  name: "rt",
3491
5075
  dur: performance.now() - t0,
@@ -3554,12 +5138,18 @@ const onRequest = defineMiddleware(async (context, next) => {
3554
5138
  setPluginStatus: runtime.setPluginStatus.bind(runtime)
3555
5139
  };
3556
5140
  } catch (error) {
5141
+ if (error instanceof PendingMigrationsError) return pendingMigrationsResponse(error);
5142
+ if (migrationMode === "manual" && isMissingTableError(error)) {
5143
+ console.error("[emdash] database schema is unavailable in manual migration mode:", error);
5144
+ return migrationRequiredResponse();
5145
+ }
3557
5146
  console.error("EmDash middleware error:", error);
3558
5147
  }
3559
5148
  const lastContentWriteAt = canUseCachedBinding && config?.database?.needsLastContentWriteAt ? await getLastContentWriteAt() : void 0;
3560
5149
  const scoped = createRequestScopedDb$1({
3561
5150
  config: config?.database?.config,
3562
5151
  isAuthenticated,
5152
+ endedAuthenticated,
3563
5153
  isWrite,
3564
5154
  canUseCachedBinding,
3565
5155
  cookies: context.cookies,
@@ -3627,5 +5217,5 @@ const onRequest = defineMiddleware(async (context, next) => {
3627
5217
  });
3628
5218
 
3629
5219
  //#endregion
3630
- export { onRequest as default, onRequest, runScheduledTasks, withEmDashRuntime };
5220
+ export { onRequest as default, onRequest, runScheduledMediaUsageTasks, runScheduledTasks, withEmDashRuntime };
3631
5221
  //# sourceMappingURL=middleware.mjs.map