emdash 0.35.0 → 0.36.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 (763) hide show
  1. package/dist/activation-K0kGBWuJ.mjs +382 -0
  2. package/dist/activation-K0kGBWuJ.mjs.map +1 -0
  3. package/dist/{adapters-Bb1vG41D.d.mts → adapters-CzL3H752.d.mts} +3 -3
  4. package/dist/{adapters-Bb1vG41D.d.mts.map → adapters-CzL3H752.d.mts.map} +1 -1
  5. package/dist/{allowed-origins-DW7sYyqg.mjs → allowed-origins-maylGaHv.mjs} +2 -2
  6. package/dist/{allowed-origins-DW7sYyqg.mjs.map → allowed-origins-maylGaHv.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -3
  8. package/dist/api/route-utils.mjs +22 -20
  9. package/dist/api/route-utils.mjs.map +1 -1
  10. package/dist/api/schemas/index.d.mts +2 -2
  11. package/dist/api/schemas/index.mjs +5 -5
  12. package/dist/api-B0w3uBiG.mjs +2269 -0
  13. package/dist/api-B0w3uBiG.mjs.map +1 -0
  14. package/dist/{api-tokens-KO24G1Fd.mjs → api-tokens-9YrrCoE6.mjs} +2 -2
  15. package/dist/{api-tokens-KO24G1Fd.mjs.map → api-tokens-9YrrCoE6.mjs.map} +1 -1
  16. package/dist/{apply-kUDt80-G.mjs → apply-B_VMpJ2P.mjs} +34 -35
  17. package/dist/{apply-kUDt80-G.mjs.map → apply-B_VMpJ2P.mjs.map} +1 -1
  18. package/dist/astro/index.d.mts +12 -12
  19. package/dist/astro/index.mjs +26 -15
  20. package/dist/astro/index.mjs.map +1 -1
  21. package/dist/astro/middleware/auth.d.mts +10 -10
  22. package/dist/astro/middleware/auth.d.mts.map +1 -1
  23. package/dist/astro/middleware/auth.mjs +10 -7
  24. package/dist/astro/middleware/auth.mjs.map +1 -1
  25. package/dist/astro/middleware/media-usage-write-fence.mjs +2 -2
  26. package/dist/astro/middleware/redirect.mjs +4 -4
  27. package/dist/astro/middleware/request-context.mjs +3 -3
  28. package/dist/astro/middleware/setup.mjs +1 -1
  29. package/dist/astro/middleware.d.mts +11 -12
  30. package/dist/astro/middleware.d.mts.map +1 -1
  31. package/dist/astro/middleware.mjs +168 -1305
  32. package/dist/astro/middleware.mjs.map +1 -1
  33. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  34. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  35. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
  36. package/dist/astro/routes/api/admin/api-tokens/index.mjs +4 -4
  37. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
  38. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +9 -9
  39. package/dist/astro/routes/api/admin/byline-fields/index.mjs +9 -9
  40. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +9 -9
  41. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +15 -15
  42. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +15 -15
  43. package/dist/astro/routes/api/admin/bylines/index.mjs +15 -15
  44. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
  45. package/dist/astro/routes/api/admin/comments/_id_.mjs +4 -4
  46. package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
  47. package/dist/astro/routes/api/admin/comments/counts.mjs +4 -4
  48. package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
  49. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  50. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  51. package/dist/astro/routes/api/admin/media-usage/activation.d.mts +9 -0
  52. package/dist/astro/routes/api/admin/media-usage/activation.d.mts.map +1 -0
  53. package/dist/astro/routes/api/admin/media-usage/activation.mjs +46 -0
  54. package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -0
  55. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +14 -13
  56. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +14 -13
  58. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/media-usage/progress.d.mts +9 -0
  60. package/dist/astro/routes/api/admin/media-usage/progress.d.mts.map +1 -0
  61. package/dist/astro/routes/api/admin/media-usage/progress.mjs +44 -0
  62. package/dist/astro/routes/api/admin/media-usage/progress.mjs.map +1 -0
  63. package/dist/astro/routes/api/admin/media-usage/repair.mjs +16 -11
  64. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  65. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +14 -13
  66. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +14 -13
  68. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
  69. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  70. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  71. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +54 -48
  72. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +54 -48
  74. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
  76. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
  78. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
  79. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +53 -47
  81. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +53 -47
  83. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
  85. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  87. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
  88. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +53 -47
  90. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
  92. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +53 -47
  94. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +54 -48
  96. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  97. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +172 -133
  99. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +54 -48
  101. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  102. package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
  103. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  104. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
  105. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  107. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
  108. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  109. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  110. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  111. package/dist/astro/routes/api/admin/users/_id_/index.mjs +7 -7
  112. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  113. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  114. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  115. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  116. package/dist/astro/routes/api/auth/invite/complete.mjs +11 -11
  117. package/dist/astro/routes/api/auth/invite/index.mjs +8 -8
  118. package/dist/astro/routes/api/auth/invite/register-options.mjs +10 -10
  119. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  120. package/dist/astro/routes/api/auth/magic-link/send.mjs +10 -10
  121. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  122. package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
  123. package/dist/astro/routes/api/auth/me.mjs +19 -9
  124. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  125. package/dist/astro/routes/api/auth/mode.mjs +2 -2
  126. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  127. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  128. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  129. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  130. package/dist/astro/routes/api/auth/passkey/options.mjs +12 -12
  131. package/dist/astro/routes/api/auth/passkey/register/options.mjs +10 -10
  132. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +11 -11
  133. package/dist/astro/routes/api/auth/passkey/verify.mjs +11 -11
  134. package/dist/astro/routes/api/auth/signup/complete.mjs +11 -11
  135. package/dist/astro/routes/api/auth/signup/request.mjs +10 -10
  136. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  137. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
  138. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
  139. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  140. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  141. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  142. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  143. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +11 -11
  144. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +8 -8
  145. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +16 -14
  146. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  147. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +16 -14
  148. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  149. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  150. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  151. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -8
  152. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
  153. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  154. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  155. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
  156. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  157. package/dist/astro/routes/api/content/_collection_/index.mjs +8 -8
  158. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  159. package/dist/astro/routes/api/dashboard.mjs +7 -7
  160. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  161. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  162. package/dist/astro/routes/api/import/probe.mjs +16 -16
  163. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  164. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  165. package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -12
  166. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  167. package/dist/astro/routes/api/import/wordpress/media.mjs +11 -11
  168. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  169. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  170. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +12 -10
  171. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  172. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  173. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +16 -16
  174. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  175. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +26 -26
  176. package/dist/astro/routes/api/manifest.mjs +4 -4
  177. package/dist/astro/routes/api/mcp.mjs +42 -42
  178. package/dist/astro/routes/api/media/_id_/confirm.mjs +11 -11
  179. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  180. package/dist/astro/routes/api/media/_id_/usage.mjs +18 -13
  181. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  182. package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
  183. package/dist/astro/routes/api/media/_id_.mjs +21 -13
  184. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  185. package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
  186. package/dist/astro/routes/api/media/folders/_id_.d.mts +10 -0
  187. package/dist/astro/routes/api/media/folders/_id_.d.mts.map +1 -0
  188. package/dist/astro/routes/api/media/folders/_id_.mjs +52 -0
  189. package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -0
  190. package/dist/astro/routes/api/media/folders/index.d.mts +9 -0
  191. package/dist/astro/routes/api/media/folders/index.d.mts.map +1 -0
  192. package/dist/astro/routes/api/media/folders/index.mjs +41 -0
  193. package/dist/astro/routes/api/media/folders/index.mjs.map +1 -0
  194. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  195. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  196. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  197. package/dist/astro/routes/api/media/upload-url.mjs +9 -9
  198. package/dist/astro/routes/api/media.d.mts.map +1 -1
  199. package/dist/astro/routes/api/media.mjs +28 -19
  200. package/dist/astro/routes/api/media.mjs.map +1 -1
  201. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
  202. package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
  203. package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
  204. package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
  205. package/dist/astro/routes/api/menus/_name_.mjs +8 -8
  206. package/dist/astro/routes/api/menus/index.mjs +8 -8
  207. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  208. package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
  209. package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
  210. package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
  211. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  212. package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
  213. package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
  214. package/dist/astro/routes/api/oauth/token.mjs +5 -5
  215. package/dist/astro/routes/api/openapi.json.mjs +153 -6
  216. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  217. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  218. package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
  219. package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
  220. package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
  221. package/dist/astro/routes/api/redirects/index.mjs +11 -11
  222. package/dist/astro/routes/api/relations/_id_/index.mjs +16 -14
  223. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  224. package/dist/astro/routes/api/relations/_id_/translations.mjs +11 -9
  225. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  226. package/dist/astro/routes/api/relations/index.mjs +16 -14
  227. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  229. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  230. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
  231. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  232. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
  233. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  234. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
  235. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  236. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
  237. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  238. package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
  239. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  240. package/dist/astro/routes/api/schema/collections/reorder.mjs +52 -46
  241. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  242. package/dist/astro/routes/api/schema/index.mjs +10 -8
  243. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  244. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
  245. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  246. package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
  247. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  248. package/dist/astro/routes/api/search/enable.mjs +10 -10
  249. package/dist/astro/routes/api/search/index.mjs +9 -9
  250. package/dist/astro/routes/api/search/rebuild.mjs +10 -10
  251. package/dist/astro/routes/api/search/stats.mjs +5 -5
  252. package/dist/astro/routes/api/search/suggest.mjs +9 -9
  253. package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
  254. package/dist/astro/routes/api/sections/index.mjs +9 -9
  255. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +6 -6
  256. package/dist/astro/routes/api/settings/backups/archives/index.mjs +5 -5
  257. package/dist/astro/routes/api/settings/backups/export.mjs +6 -6
  258. package/dist/astro/routes/api/settings/backups/index.mjs +7 -7
  259. package/dist/astro/routes/api/settings/email.mjs +4 -4
  260. package/dist/astro/routes/api/settings.mjs +13 -13
  261. package/dist/astro/routes/api/setup/admin-verify.mjs +12 -12
  262. package/dist/astro/routes/api/setup/admin.mjs +11 -11
  263. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  264. package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -27
  265. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  266. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  267. package/dist/astro/routes/api/setup/index.mjs +31 -29
  268. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/setup/status.mjs +3 -3
  270. package/dist/astro/routes/api/snapshot.mjs +7 -7
  271. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +13 -13
  272. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
  273. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
  274. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
  275. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +13 -13
  276. package/dist/astro/routes/api/taxonomies/_name_.mjs +13 -13
  277. package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
  278. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  279. package/dist/astro/routes/api/typegen.mjs +4 -4
  280. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  281. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  282. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  283. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
  284. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
  285. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
  286. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  287. package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
  288. package/dist/astro/routes/api/widget-components.mjs +2 -2
  289. package/dist/astro/routes/robots.txt.mjs +6 -6
  290. package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
  291. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  292. package/dist/astro/types.d.mts +19 -14
  293. package/dist/astro/types.d.mts.map +1 -1
  294. package/dist/auth/providers/github.d.mts +1 -1
  295. package/dist/auth/providers/google.d.mts +1 -1
  296. package/dist/{authorize-DUElxgOQ.mjs → authorize-ByYs-Uxs.mjs} +2 -2
  297. package/dist/{authorize-DUElxgOQ.mjs.map → authorize-ByYs-Uxs.mjs.map} +1 -1
  298. package/dist/{backup-DWYpJlLj.mjs → backup-ChyHfIoX.mjs} +5 -5
  299. package/dist/{backup-DWYpJlLj.mjs.map → backup-ChyHfIoX.mjs.map} +1 -1
  300. package/dist/{bundle-vn56sLos.mjs → bundle-DZFhFurR.mjs} +4 -4
  301. package/dist/{bundle-vn56sLos.mjs.map → bundle-DZFhFurR.mjs.map} +1 -1
  302. package/dist/{byline-yjvWq4ip.mjs → byline-Cpnwrje1.mjs} +6 -6
  303. package/dist/{byline-yjvWq4ip.mjs.map → byline-Cpnwrje1.mjs.map} +1 -1
  304. package/dist/{byline-fields-txaLGoME.mjs → byline-fields-CAyA_Umd.mjs} +2 -2
  305. package/dist/{byline-fields-txaLGoME.mjs.map → byline-fields-CAyA_Umd.mjs.map} +1 -1
  306. package/dist/{byline-registry-DDun8sxh.mjs → byline-registry-Bpu9-MVs.mjs} +2 -2
  307. package/dist/{byline-registry-DDun8sxh.mjs.map → byline-registry-Bpu9-MVs.mjs.map} +1 -1
  308. package/dist/{bylines-DjXS5p0P.mjs → bylines-BSo9B9RX.mjs} +7 -7
  309. package/dist/{bylines-DjXS5p0P.mjs.map → bylines-BSo9B9RX.mjs.map} +1 -1
  310. package/dist/{bylines-Bu7pchQ4.mjs → bylines-CHjvehVX.mjs} +3 -3
  311. package/dist/{bylines-Bu7pchQ4.mjs.map → bylines-CHjvehVX.mjs.map} +1 -1
  312. package/dist/{cache-C46-3Irl.mjs → cache-5VirJ2Cn.mjs} +4 -4
  313. package/dist/{cache-C46-3Irl.mjs.map → cache-5VirJ2Cn.mjs.map} +1 -1
  314. package/dist/capture-triggers-CNW1q3HK.mjs +362 -0
  315. package/dist/capture-triggers-CNW1q3HK.mjs.map +1 -0
  316. package/dist/{challenge-store-SIL5DsF2.mjs → challenge-store-D5pSY2S1.mjs} +1 -1
  317. package/dist/{challenge-store-SIL5DsF2.mjs.map → challenge-store-D5pSY2S1.mjs.map} +1 -1
  318. package/dist/{chunks-fiZ8G9RJ.mjs → chunks-bNiX6_DR.mjs} +2 -2
  319. package/dist/{chunks-fiZ8G9RJ.mjs.map → chunks-bNiX6_DR.mjs.map} +1 -1
  320. package/dist/cli/index.mjs +36 -52
  321. package/dist/cli/index.mjs.map +1 -1
  322. package/dist/client/cf-access.d.mts +1 -1
  323. package/dist/client/index.d.mts +74 -4
  324. package/dist/client/index.d.mts.map +1 -1
  325. package/dist/client/index.mjs +54 -2
  326. package/dist/client/index.mjs.map +1 -1
  327. package/dist/{collection-deletion-DQPvMD8R.mjs → collection-deletion-m_oi2c0N.mjs} +13 -356
  328. package/dist/collection-deletion-m_oi2c0N.mjs.map +1 -0
  329. package/dist/{comment-Dxlep6cE.mjs → comment-DhvLEdhQ.mjs} +1 -1
  330. package/dist/{comment-Dxlep6cE.mjs.map → comment-DhvLEdhQ.mjs.map} +1 -1
  331. package/dist/{comment-reaction-D-2J84UE.mjs → comment-reaction-CRuItEjt.mjs} +2 -2
  332. package/dist/{comment-reaction-D-2J84UE.mjs.map → comment-reaction-CRuItEjt.mjs.map} +1 -1
  333. package/dist/{comments-CTfZtLmM.mjs → comments-CLj4bi8I.mjs} +2 -2
  334. package/dist/{comments-CTfZtLmM.mjs.map → comments-CLj4bi8I.mjs.map} +1 -1
  335. package/dist/{components-CQy3lh9x.mjs → components-v4U4DT2y.mjs} +1 -1
  336. package/dist/{components-CQy3lh9x.mjs.map → components-v4U4DT2y.mjs.map} +1 -1
  337. package/dist/{config-COQzTe5Q.d.mts → config-DrfghTPe.d.mts} +1 -1
  338. package/dist/{config-COQzTe5Q.d.mts.map → config-DrfghTPe.d.mts.map} +1 -1
  339. package/dist/{content-ZHCc1ngN.mjs → content-CP-SeXNa.mjs} +5 -4
  340. package/dist/content-CP-SeXNa.mjs.map +1 -0
  341. package/dist/{content-list-query-wJ2WN8Vp.d.mts → content-list-query-BhejKVqb.d.mts} +1 -1
  342. package/dist/{content-list-query-wJ2WN8Vp.d.mts.map → content-list-query-BhejKVqb.d.mts.map} +1 -1
  343. package/dist/{content-refresh-B_8SJzuU.mjs → content-refresh-DLOfIhYc.mjs} +1094 -93
  344. package/dist/content-refresh-DLOfIhYc.mjs.map +1 -0
  345. package/dist/{media-usage-BZCydfvu.mjs → content-repair-BCxzy0r4.mjs} +5 -196
  346. package/dist/content-repair-BCxzy0r4.mjs.map +1 -0
  347. package/dist/{context-ZldbZcle.mjs → context-BR5hRWiK.mjs} +13 -13
  348. package/dist/{context-ZldbZcle.mjs.map → context-BR5hRWiK.mjs.map} +1 -1
  349. package/dist/{cron-Y9aLtgrA.mjs → cron-CWfWrWKG.mjs} +1 -1
  350. package/dist/{cron-Y9aLtgrA.mjs.map → cron-CWfWrWKG.mjs.map} +1 -1
  351. package/dist/{dashboard-DvHbk_cv.mjs → dashboard-CkIV2rnQ.mjs} +5 -5
  352. package/dist/{dashboard-DvHbk_cv.mjs.map → dashboard-CkIV2rnQ.mjs.map} +1 -1
  353. package/dist/db/index.d.mts +3 -3
  354. package/dist/db/index.mjs +3 -3
  355. package/dist/db/index.mjs.map +1 -1
  356. package/dist/db/libsql-migrations.d.mts +1 -1
  357. package/dist/db/libsql-migrations.mjs +4 -4
  358. package/dist/db/libsql.d.mts +1 -1
  359. package/dist/db/postgres-migrations.d.mts +1 -1
  360. package/dist/db/postgres-migrations.mjs +4 -4
  361. package/dist/db/postgres.d.mts +1 -1
  362. package/dist/db/sqlite-migrations.d.mts +1 -1
  363. package/dist/db/sqlite-migrations.mjs +4 -4
  364. package/dist/db/sqlite.d.mts +1 -1
  365. package/dist/db/sqlite.mjs +3 -3
  366. package/dist/db/sqlite.mjs.map +1 -1
  367. package/dist/{default-DdDl3fxv.mjs → default-YMEfcQUx.mjs} +1 -1
  368. package/dist/{default-DdDl3fxv.mjs.map → default-YMEfcQUx.mjs.map} +1 -1
  369. package/dist/{deferred-tasks-SDWxrgzJ.d.mts → deferred-tasks-DfqxH5Xf.d.mts} +1 -1
  370. package/dist/{deferred-tasks-SDWxrgzJ.d.mts.map → deferred-tasks-DfqxH5Xf.d.mts.map} +1 -1
  371. package/dist/{device-flow-C0GdD_z8.mjs → device-flow-CojjSBwb.mjs} +5 -5
  372. package/dist/{device-flow-C0GdD_z8.mjs.map → device-flow-CojjSBwb.mjs.map} +1 -1
  373. package/dist/{direct-executor-CDXeJCSs.mjs → direct-executor-BWeWiRYB.mjs} +3 -3
  374. package/dist/{direct-executor-CDXeJCSs.mjs.map → direct-executor-BWeWiRYB.mjs.map} +1 -1
  375. package/dist/{dist-D_6CKR8z.mjs → dist-89xCrGqi.mjs} +1 -1
  376. package/dist/{dist-D_6CKR8z.mjs.map → dist-89xCrGqi.mjs.map} +1 -1
  377. package/dist/{email-console-BHNjIwiB.mjs → email-console-CEJgR_LB.mjs} +1 -1
  378. package/dist/{email-console-BHNjIwiB.mjs.map → email-console-CEJgR_LB.mjs.map} +1 -1
  379. package/dist/{enrich-_ySMEvEE.mjs → enrich-BX0IDp4z.mjs} +3 -3
  380. package/dist/{enrich-_ySMEvEE.mjs.map → enrich-BX0IDp4z.mjs.map} +1 -1
  381. package/dist/{error-DupyJZE2.mjs → error-D9w94fjV.mjs} +2 -2
  382. package/dist/{error-DupyJZE2.mjs.map → error-D9w94fjV.mjs.map} +1 -1
  383. package/dist/{errors-CLUPm7aD.mjs → errors-n2j49uNf.mjs} +22 -2
  384. package/dist/errors-n2j49uNf.mjs.map +1 -0
  385. package/dist/{escape-DUY2r2Zs.mjs → escape-DN4Qmjsl.mjs} +1 -1
  386. package/dist/{escape-DUY2r2Zs.mjs.map → escape-DN4Qmjsl.mjs.map} +1 -1
  387. package/dist/{field-defs-cache-BzwBlJrR.mjs → field-defs-cache-Bh4q6oar.mjs} +5 -5
  388. package/dist/{field-defs-cache-BzwBlJrR.mjs.map → field-defs-cache-Bh4q6oar.mjs.map} +1 -1
  389. package/dist/focal-point-BeWFo6Xx.mjs +23 -0
  390. package/dist/focal-point-BeWFo6Xx.mjs.map +1 -0
  391. package/dist/{fts-manager-DyZEohZC.mjs → fts-manager-BhOdW0bi.mjs} +3 -3
  392. package/dist/{fts-manager-DyZEohZC.mjs.map → fts-manager-BhOdW0bi.mjs.map} +1 -1
  393. package/dist/{hash-Cg0TXJPV.mjs → hash-CTcngWvj.mjs} +1 -1
  394. package/dist/{hash-Cg0TXJPV.mjs.map → hash-CTcngWvj.mjs.map} +1 -1
  395. package/dist/{hreflang-DwT2E0Sa.d.mts → hreflang-7pTHsLZJ.d.mts} +2 -2
  396. package/dist/{hreflang-DwT2E0Sa.d.mts.map → hreflang-7pTHsLZJ.d.mts.map} +1 -1
  397. package/dist/{hreflang-BBe83kKa.mjs → hreflang-Huc6O5wa.mjs} +7 -7
  398. package/dist/{hreflang-BBe83kKa.mjs.map → hreflang-Huc6O5wa.mjs.map} +1 -1
  399. package/dist/{identity-Ys6Dlc9C.mjs → identity-DXmb4aVp.mjs} +3 -3
  400. package/dist/{identity-Ys6Dlc9C.mjs.map → identity-DXmb4aVp.mjs.map} +1 -1
  401. package/dist/{import-DPkLmeXO.mjs → import-C4_O3KXU.mjs} +5 -5
  402. package/dist/{import-DPkLmeXO.mjs.map → import-C4_O3KXU.mjs.map} +1 -1
  403. package/dist/{index-C9KuQyO-.d.mts → index-6bRee8zb.d.mts} +3 -3
  404. package/dist/{index-C9KuQyO-.d.mts.map → index-6bRee8zb.d.mts.map} +1 -1
  405. package/dist/{index-BVBZj4-7.d.mts → index-DWbJrsAg.d.mts} +128 -52
  406. package/dist/index-DWbJrsAg.d.mts.map +1 -0
  407. package/dist/index.d.mts +21 -21
  408. package/dist/index.mjs +78 -72
  409. package/dist/{init-lock-6b309ZrF.mjs → init-lock-DJkX6Hto.mjs} +1 -1
  410. package/dist/{init-lock-6b309ZrF.mjs.map → init-lock-DJkX6Hto.mjs.map} +1 -1
  411. package/dist/{load-YJ2xl3ra.mjs → load-Bo7fZAbg.mjs} +2 -2
  412. package/dist/{load-YJ2xl3ra.mjs.map → load-Bo7fZAbg.mjs.map} +1 -1
  413. package/dist/{loader-CdG_Ci3H.mjs → loader-ZsjE4paL.mjs} +2 -2
  414. package/dist/{loader-CdG_Ci3H.mjs.map → loader-ZsjE4paL.mjs.map} +1 -1
  415. package/dist/{manifest-builder-B252yQUf.mjs → manifest-builder-QXuqvVMP.mjs} +2 -2
  416. package/dist/{manifest-builder-B252yQUf.mjs.map → manifest-builder-QXuqvVMP.mjs.map} +1 -1
  417. package/dist/{manifest-schema-BXbUAHod.mjs → manifest-schema-CUYPfCEH.mjs} +1 -1
  418. package/dist/{manifest-schema-BXbUAHod.mjs.map → manifest-schema-CUYPfCEH.mjs.map} +1 -1
  419. package/dist/media/image-endpoint.mjs +1 -1
  420. package/dist/media/index.d.mts +1 -1
  421. package/dist/media/index.mjs +13 -3
  422. package/dist/media/index.mjs.map +1 -1
  423. package/dist/media/local-runtime.d.mts +14 -12
  424. package/dist/media/local-runtime.d.mts.map +1 -1
  425. package/dist/media/local-runtime.mjs +11 -5
  426. package/dist/media/local-runtime.mjs.map +1 -1
  427. package/dist/{media-BWWWb8kg.mjs → media-BjwNuLEu.mjs} +100 -6
  428. package/dist/media-BjwNuLEu.mjs.map +1 -0
  429. package/dist/{media-CzhAn2--.mjs → media-D6SeAfBl.mjs} +41 -12
  430. package/dist/media-D6SeAfBl.mjs.map +1 -0
  431. package/dist/{media-allowlist-D6x-Us02.mjs → media-allowlist-D35v7bmL.mjs} +3 -2
  432. package/dist/media-allowlist-D35v7bmL.mjs.map +1 -0
  433. package/dist/media-folders-D-0CmRZj.mjs +210 -0
  434. package/dist/media-folders-D-0CmRZj.mjs.map +1 -0
  435. package/dist/{media-upload-CHBVn6m-.mjs → media-upload-C7bLXGcI.mjs} +9 -9
  436. package/dist/{media-upload-CHBVn6m-.mjs.map → media-upload-C7bLXGcI.mjs.map} +1 -1
  437. package/dist/{media-url-BvoX88FA.mjs → media-url-DH7giMx_.mjs} +1 -1
  438. package/dist/{media-url-BvoX88FA.mjs.map → media-url-DH7giMx_.mjs.map} +1 -1
  439. package/dist/media-usage-BTRJvS6V.mjs +1833 -0
  440. package/dist/media-usage-BTRJvS6V.mjs.map +1 -0
  441. package/dist/media-usage-activation-Dx0kPV6b.mjs +78 -0
  442. package/dist/media-usage-activation-Dx0kPV6b.mjs.map +1 -0
  443. package/dist/{media-usage-work-DrCbqBcl.mjs → media-usage-work-C-fhM30c.mjs} +221 -59
  444. package/dist/media-usage-work-C-fhM30c.mjs.map +1 -0
  445. package/dist/{media-usage-work-CLo_6tct.mjs → media-usage-work-DWb2XPMI.mjs} +4 -4
  446. package/dist/{media-usage-work-CLo_6tct.mjs.map → media-usage-work-DWb2XPMI.mjs.map} +1 -1
  447. package/dist/{media-usage-write-fence-aHEgN1zB.mjs → media-usage-write-fence-BwoJVRDC.mjs} +2 -2
  448. package/dist/{media-usage-write-fence-aHEgN1zB.mjs.map → media-usage-write-fence-BwoJVRDC.mjs.map} +1 -1
  449. package/dist/{menus-TCNSd2vp.mjs → menus-CD7EFLwz.mjs} +3 -3
  450. package/dist/{menus-TCNSd2vp.mjs.map → menus-CD7EFLwz.mjs.map} +1 -1
  451. package/dist/{menus-DSo5QeW0.mjs → menus-CNg0QlDt.mjs} +42 -26
  452. package/dist/menus-CNg0QlDt.mjs.map +1 -0
  453. package/dist/migrations/index.d.mts +1 -1
  454. package/dist/migrations/index.mjs +3 -3
  455. package/dist/{mime-DfIVjNkr.mjs → mime-CUIG6x84.mjs} +1 -1
  456. package/dist/mime-CUIG6x84.mjs.map +1 -0
  457. package/dist/{mode-BDoH_WcF.mjs → mode-DaUM_MB2.mjs} +1 -1
  458. package/dist/{mode-BDoH_WcF.mjs.map → mode-DaUM_MB2.mjs.map} +1 -1
  459. package/dist/node-sqlite-compat-Udb4Y-rs.mjs +46 -0
  460. package/dist/node-sqlite-compat-Udb4Y-rs.mjs.map +1 -0
  461. package/dist/{normalize-Bc-s_r-a.mjs → normalize-CSxdKBVa.mjs} +22 -1
  462. package/dist/normalize-CSxdKBVa.mjs.map +1 -0
  463. package/dist/{oauth-authorization-iOikmrJd.mjs → oauth-authorization-Dpsb-fMJ.mjs} +5 -5
  464. package/dist/{oauth-authorization-iOikmrJd.mjs.map → oauth-authorization-Dpsb-fMJ.mjs.map} +1 -1
  465. package/dist/{oauth-clients-CvI0nHMJ.mjs → oauth-clients-Bxnoe_67.mjs} +1 -1
  466. package/dist/{oauth-clients-CvI0nHMJ.mjs.map → oauth-clients-Bxnoe_67.mjs.map} +1 -1
  467. package/dist/{oauth-state-store-Ba8KZlwW.mjs → oauth-state-store-0C4s0K6w.mjs} +1 -1
  468. package/dist/{oauth-state-store-Ba8KZlwW.mjs.map → oauth-state-store-0C4s0K6w.mjs.map} +1 -1
  469. package/dist/{oauth-user-lookup-C5SQwGYC.mjs → oauth-user-lookup-wlGYSeqX.mjs} +1 -1
  470. package/dist/{oauth-user-lookup-C5SQwGYC.mjs.map → oauth-user-lookup-wlGYSeqX.mjs.map} +1 -1
  471. package/dist/object-cache/memory.d.mts +1 -1
  472. package/dist/{options-CBXIHUoT.mjs → options-BHPeIbBd.mjs} +1 -1
  473. package/dist/{options-CBXIHUoT.mjs.map → options-BHPeIbBd.mjs.map} +1 -1
  474. package/dist/{options--hhz-jZ1.d.mts → options-BhZIeFY1.d.mts} +3 -3
  475. package/dist/{options--hhz-jZ1.d.mts.map → options-BhZIeFY1.d.mts.map} +1 -1
  476. package/dist/page/index.d.mts +2 -2
  477. package/dist/{parse-DFbuc-Fh.mjs → parse-BRJg1kz8.mjs} +2 -2
  478. package/dist/{parse-DFbuc-Fh.mjs.map → parse-BRJg1kz8.mjs.map} +1 -1
  479. package/dist/{passkey-config-DFAmUyuE.mjs → passkey-config-B3P0nHRR.mjs} +1 -1
  480. package/dist/{passkey-config-DFAmUyuE.mjs.map → passkey-config-B3P0nHRR.mjs.map} +1 -1
  481. package/dist/{patterns-CiyXeDgr.mjs → patterns-D9iDO3j8.mjs} +1 -1
  482. package/dist/{patterns-CiyXeDgr.mjs.map → patterns-D9iDO3j8.mjs.map} +1 -1
  483. package/dist/{placeholder-Dp0K3RkD.d.mts → placeholder-BR4ZhVJl.d.mts} +7 -1
  484. package/dist/placeholder-BR4ZhVJl.d.mts.map +1 -0
  485. package/dist/plugin-types.d.mts +1 -1
  486. package/dist/plugin-utils.d.mts +10 -10
  487. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  488. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  489. package/dist/{policy-TluUIxuu.mjs → policy-BVRganJQ.mjs} +2 -2
  490. package/dist/{policy-TluUIxuu.mjs.map → policy-BVRganJQ.mjs.map} +1 -1
  491. package/dist/{portable-text-imBW5cHK.mjs → portable-text-C8Tvp4_g.mjs} +1 -1
  492. package/dist/{portable-text-imBW5cHK.mjs.map → portable-text-C8Tvp4_g.mjs.map} +1 -1
  493. package/dist/{preview-PW-nOnq4.mjs → preview-dn2dUyM0.mjs} +2 -2
  494. package/dist/{preview-PW-nOnq4.mjs.map → preview-dn2dUyM0.mjs.map} +1 -1
  495. package/dist/{protocol-DjsJHTlK.d.mts → protocol-CuBEsqgE.d.mts} +2 -2
  496. package/dist/{protocol-DjsJHTlK.d.mts.map → protocol-CuBEsqgE.d.mts.map} +1 -1
  497. package/dist/{public-url-CWoFhQT8.mjs → public-url-DRz_AinV.mjs} +1 -1
  498. package/dist/{public-url-CWoFhQT8.mjs.map → public-url-DRz_AinV.mjs.map} +1 -1
  499. package/dist/{query-zTVAR1Sl.mjs → query-D18vTdha.mjs} +14 -14
  500. package/dist/{query-zTVAR1Sl.mjs.map → query-D18vTdha.mjs.map} +1 -1
  501. package/dist/{query-BWs7O5Il.mjs → query-tZDEdtCN.mjs} +4 -4
  502. package/dist/{query-BWs7O5Il.mjs.map → query-tZDEdtCN.mjs.map} +1 -1
  503. package/dist/{rate-limit-Bx4g9m3p.mjs → rate-limit-on7KPo-v.mjs} +2 -2
  504. package/dist/{rate-limit-Bx4g9m3p.mjs.map → rate-limit-on7KPo-v.mjs.map} +1 -1
  505. package/dist/{redirect-COsS6SWJ.mjs → redirect-CZkbozu9.mjs} +2 -2
  506. package/dist/{redirect-COsS6SWJ.mjs.map → redirect-CZkbozu9.mjs.map} +1 -1
  507. package/dist/{redirect-BgAOGHDq.mjs → redirect-DrbEmU9a.mjs} +1 -1
  508. package/dist/{redirect-BgAOGHDq.mjs.map → redirect-DrbEmU9a.mjs.map} +1 -1
  509. package/dist/{redirects-DyChwDkG.mjs → redirects-BahWiu8Z.mjs} +5 -5
  510. package/dist/{redirects-DyChwDkG.mjs.map → redirects-BahWiu8Z.mjs.map} +1 -1
  511. package/dist/{redirects-DTDHWH31.mjs → redirects-CRIM55KH.mjs} +6 -6
  512. package/dist/{redirects-DTDHWH31.mjs.map → redirects-CRIM55KH.mjs.map} +1 -1
  513. package/dist/{api-BeBglVxU.mjs → registry-BiO5WwPq.mjs} +483 -2462
  514. package/dist/registry-BiO5WwPq.mjs.map +1 -0
  515. package/dist/{registry-Cqa2I749.mjs → registry-Cx-jEJOS.mjs} +9 -123
  516. package/dist/registry-Cx-jEJOS.mjs.map +1 -0
  517. package/dist/{relation-BnHB0T4J.mjs → relation-A6wLF_DS.mjs} +2 -2
  518. package/dist/{relation-BnHB0T4J.mjs.map → relation-A6wLF_DS.mjs.map} +1 -1
  519. package/dist/{relations-BBApI4e2.mjs → relations-BTPM2saV.mjs} +2 -2
  520. package/dist/{relations-BBApI4e2.mjs.map → relations-BTPM2saV.mjs.map} +1 -1
  521. package/dist/{relations-BHo5A8Zf.d.mts → relations-C7B46bcU.d.mts} +192 -33
  522. package/dist/relations-C7B46bcU.d.mts.map +1 -0
  523. package/dist/{relations-D5sM5fW0.mjs → relations-Cj5hZ6Zg.mjs} +4 -4
  524. package/dist/{relations-D5sM5fW0.mjs.map → relations-Cj5hZ6Zg.mjs.map} +1 -1
  525. package/dist/{request-cache-BSUptuJR.mjs → request-cache-DH9zdCOD.mjs} +1 -1
  526. package/dist/{request-cache-BSUptuJR.mjs.map → request-cache-DH9zdCOD.mjs.map} +1 -1
  527. package/dist/request-context.d.mts +1 -1
  528. package/dist/{request-meta-DgZJh3zG.mjs → request-meta-DRp5ENd_.mjs} +2 -2
  529. package/dist/{request-meta-DgZJh3zG.mjs.map → request-meta-DRp5ENd_.mjs.map} +1 -1
  530. package/dist/{resolve-DOhVEvty.mjs → resolve-DxUpTMmE.mjs} +1 -1
  531. package/dist/{resolve-DOhVEvty.mjs.map → resolve-DxUpTMmE.mjs.map} +1 -1
  532. package/dist/{runner-C1T11CfC.mjs → runner-77GOZCnH.mjs} +410 -286
  533. package/dist/runner-77GOZCnH.mjs.map +1 -0
  534. package/dist/{runner-BYHZn3Jy.d.mts → runner-zgqA_CqP.d.mts} +2 -2
  535. package/dist/{runner-BYHZn3Jy.d.mts.map → runner-zgqA_CqP.d.mts.map} +1 -1
  536. package/dist/runtime.d.mts +11 -11
  537. package/dist/runtime.mjs +1 -1
  538. package/dist/{schema-DZ5YVVZj.mjs → schema-BY-oVntF.mjs} +3 -3
  539. package/dist/{schema-DZ5YVVZj.mjs.map → schema-BY-oVntF.mjs.map} +1 -1
  540. package/dist/{schema-2AOsSn2v.mjs → schema-Cw9kVK0I.mjs} +35 -5
  541. package/dist/{schema-2AOsSn2v.mjs.map → schema-Cw9kVK0I.mjs.map} +1 -1
  542. package/dist/{scopes-8ClPF2bC.mjs → scopes-vMR1uIbt.mjs} +3 -3
  543. package/dist/{scopes-8ClPF2bC.mjs.map → scopes-vMR1uIbt.mjs.map} +1 -1
  544. package/dist/{search-u1QYdOCG.mjs → search-CfT3KxO5.mjs} +4 -4
  545. package/dist/{search-u1QYdOCG.mjs.map → search-CfT3KxO5.mjs.map} +1 -1
  546. package/dist/{secrets-BEgsAQEU.mjs → secrets-BH2Qow1-.mjs} +3 -3
  547. package/dist/{secrets-BEgsAQEU.mjs.map → secrets-BH2Qow1-.mjs.map} +1 -1
  548. package/dist/{sections-DmHviGyM.mjs → sections-DTfjyf1z.mjs} +2 -2
  549. package/dist/{sections-DmHviGyM.mjs.map → sections-DTfjyf1z.mjs.map} +1 -1
  550. package/dist/seed/index.d.mts +2 -2
  551. package/dist/seed/index.mjs +24 -22
  552. package/dist/seo/index.d.mts +3 -3
  553. package/dist/seo/index.mjs +16 -14
  554. package/dist/seo/index.mjs.map +1 -1
  555. package/dist/{seo-AFpVvTqr.mjs → seo-BSiTIXzd.mjs} +1 -1
  556. package/dist/{seo-AFpVvTqr.mjs.map → seo-BSiTIXzd.mjs.map} +1 -1
  557. package/dist/{seo-Dq4G-RrO.mjs → seo-Cm-dtvgk.mjs} +2 -2
  558. package/dist/{seo-Dq4G-RrO.mjs.map → seo-Cm-dtvgk.mjs.map} +1 -1
  559. package/dist/{service-BpArayPr.mjs → service-DYsDHEKe.mjs} +3 -3
  560. package/dist/{service-BpArayPr.mjs.map → service-DYsDHEKe.mjs.map} +1 -1
  561. package/dist/{session-user-BEorKVT-.mjs → session-user-CNAfvsdw.mjs} +1 -1
  562. package/dist/{session-user-BEorKVT-.mjs.map → session-user-CNAfvsdw.mjs.map} +1 -1
  563. package/dist/{settings-xRdoUZr0.mjs → settings-BgN-0DT-.mjs} +6 -6
  564. package/dist/{settings-xRdoUZr0.mjs.map → settings-BgN-0DT-.mjs.map} +1 -1
  565. package/dist/{settings-DE9xtzqd.mjs → settings-DjtkUo__.mjs} +3 -3
  566. package/dist/{settings-DE9xtzqd.mjs.map → settings-DjtkUo__.mjs.map} +1 -1
  567. package/dist/{setup-complete-BnT8pq_C.mjs → setup-complete-CcpXaPU7.mjs} +2 -2
  568. package/dist/{setup-complete-BnT8pq_C.mjs.map → setup-complete-CcpXaPU7.mjs.map} +1 -1
  569. package/dist/{setup-nonce-CBitf_oQ.mjs → setup-nonce-Reyw30jg.mjs} +1 -1
  570. package/dist/{setup-nonce-CBitf_oQ.mjs.map → setup-nonce-Reyw30jg.mjs.map} +1 -1
  571. package/dist/{single-flight-cache-B0eLKS6K.mjs → single-flight-cache-BPuGPdcN.mjs} +2 -2
  572. package/dist/{single-flight-cache-B0eLKS6K.mjs.map → single-flight-cache-BPuGPdcN.mjs.map} +1 -1
  573. package/dist/{site-url-DjpIXMXJ.mjs → site-url-Cs_V9n3V.mjs} +3 -3
  574. package/dist/{site-url-DjpIXMXJ.mjs.map → site-url-Cs_V9n3V.mjs.map} +1 -1
  575. package/dist/{snapshot-BBlGkb2q.mjs → snapshot-DSB8P7Lm.mjs} +1 -1
  576. package/dist/{snapshot-BBlGkb2q.mjs.map → snapshot-DSB8P7Lm.mjs.map} +1 -1
  577. package/dist/{ssrf-Cpvg_n3A.mjs → ssrf-DqebVE34.mjs} +21 -5
  578. package/dist/{ssrf-Cpvg_n3A.mjs.map → ssrf-DqebVE34.mjs.map} +1 -1
  579. package/dist/{state-DYwPue40.mjs → state-BJfU9Uzw.mjs} +12 -4
  580. package/dist/state-BJfU9Uzw.mjs.map +1 -0
  581. package/dist/{status-CWVYpbD8.mjs → status-DDu8HIQl.mjs} +1 -1
  582. package/dist/{status-CWVYpbD8.mjs.map → status-DDu8HIQl.mjs.map} +1 -1
  583. package/dist/storage/local.d.mts +1 -1
  584. package/dist/storage/local.mjs +1 -1
  585. package/dist/storage/s3.d.mts +1 -1
  586. package/dist/storage/s3.mjs +1 -1
  587. package/dist/{target-DixqqV5i.mjs → target-D0o26Azo.mjs} +1 -1
  588. package/dist/{target-DixqqV5i.mjs.map → target-D0o26Azo.mjs.map} +1 -1
  589. package/dist/{taxonomies-OHUIiIpI.mjs → taxonomies-CPGxo-bn.mjs} +5 -5
  590. package/dist/{taxonomies-OHUIiIpI.mjs.map → taxonomies-CPGxo-bn.mjs.map} +1 -1
  591. package/dist/{taxonomies-BlrncHi9.mjs → taxonomies-kmyudNGP.mjs} +7 -7
  592. package/dist/{taxonomies-BlrncHi9.mjs.map → taxonomies-kmyudNGP.mjs.map} +1 -1
  593. package/dist/{taxonomy-BqCZ1TdU.mjs → taxonomy-8X09dLN5.mjs} +4 -4
  594. package/dist/{taxonomy-BqCZ1TdU.mjs.map → taxonomy-8X09dLN5.mjs.map} +1 -1
  595. package/dist/{tokens-C39klX8R.mjs → tokens-gTS-IVUp.mjs} +1 -1
  596. package/dist/{tokens-C39klX8R.mjs.map → tokens-gTS-IVUp.mjs.map} +1 -1
  597. package/dist/{transaction-NQj4VJ7Z.mjs → transaction-0KOpOyMu.mjs} +2 -1
  598. package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-0KOpOyMu.mjs.map} +1 -1
  599. package/dist/{transport-RQE36APG.d.mts → transport-_6R1ub7t.d.mts} +1 -1
  600. package/dist/{transport-RQE36APG.d.mts.map → transport-_6R1ub7t.d.mts.map} +1 -1
  601. package/dist/{transport-DBJwa-Si.mjs → transport-fX59xaJz.mjs} +4 -3
  602. package/dist/{transport-DBJwa-Si.mjs.map → transport-fX59xaJz.mjs.map} +1 -1
  603. package/dist/{trusted-proxy-CEVekpby.mjs → trusted-proxy-fe6nEgR4.mjs} +1 -1
  604. package/dist/{trusted-proxy-CEVekpby.mjs.map → trusted-proxy-fe6nEgR4.mjs.map} +1 -1
  605. package/dist/types-75KIqzL9.mjs.map +1 -1
  606. package/dist/{types-BW1syrlw.mjs → types-BXUmsINL.mjs} +1 -1
  607. package/dist/{types-BW1syrlw.mjs.map → types-BXUmsINL.mjs.map} +1 -1
  608. package/dist/{types-BntycPTF.d.mts → types-Blb-MRC3.d.mts} +1 -1
  609. package/dist/{types-BntycPTF.d.mts.map → types-Blb-MRC3.d.mts.map} +1 -1
  610. package/dist/{types-C2pMvO62.d.mts → types-Br-ERaxh.d.mts} +2 -1
  611. package/dist/types-Br-ERaxh.d.mts.map +1 -0
  612. package/dist/{types-DWRhk9xZ.d.mts → types-BzBd3PeX.d.mts} +1 -1
  613. package/dist/{types-DWRhk9xZ.d.mts.map → types-BzBd3PeX.d.mts.map} +1 -1
  614. package/dist/{types-CXnOzDe1.d.mts → types-CLqvnd05.d.mts} +1 -1
  615. package/dist/{types-CXnOzDe1.d.mts.map → types-CLqvnd05.d.mts.map} +1 -1
  616. package/dist/{types-Dfk8KI5R.d.mts → types-D5VVLr7J.d.mts} +1 -1
  617. package/dist/{types-Dfk8KI5R.d.mts.map → types-D5VVLr7J.d.mts.map} +1 -1
  618. package/dist/{types-DQIDRKF2.d.mts → types-DHxzBjbD.d.mts} +10 -1
  619. package/dist/types-DHxzBjbD.d.mts.map +1 -0
  620. package/dist/{types-DN64_zzZ.d.mts → types-DJQxln7q.d.mts} +3 -3
  621. package/dist/{types-DN64_zzZ.d.mts.map → types-DJQxln7q.d.mts.map} +1 -1
  622. package/dist/{types-CtzytVXn.mjs → types-DM3vioBY.mjs} +1 -1
  623. package/dist/{types-CtzytVXn.mjs.map → types-DM3vioBY.mjs.map} +1 -1
  624. package/dist/{types-BcqdcT-r.d.mts → types-D_DWe7k9.d.mts} +1 -1
  625. package/dist/{types-BcqdcT-r.d.mts.map → types-D_DWe7k9.d.mts.map} +1 -1
  626. package/dist/{types-DrJrVCT1.d.mts → types-Vg6f5i3s.d.mts} +3 -3
  627. package/dist/{types-DrJrVCT1.d.mts.map → types-Vg6f5i3s.d.mts.map} +1 -1
  628. package/dist/{upload-attempts-D4FS8LT6.mjs → upload-attempts-CNyGSepg.mjs} +1 -1
  629. package/dist/{upload-attempts-D4FS8LT6.mjs.map → upload-attempts-CNyGSepg.mjs.map} +1 -1
  630. package/dist/{url-pattern-BHS0V5Ng.mjs → url-pattern-K-KdXcSe.mjs} +1 -1
  631. package/dist/{url-pattern-BHS0V5Ng.mjs.map → url-pattern-K-KdXcSe.mjs.map} +1 -1
  632. package/dist/{user-DJZfUJVE.mjs → user-CtYY0tp2.mjs} +2 -2
  633. package/dist/{user-DJZfUJVE.mjs.map → user-CtYY0tp2.mjs.map} +1 -1
  634. package/dist/{utils-D4vD_Y98.mjs → utils-HztC73hq.mjs} +1 -1
  635. package/dist/{utils-D4vD_Y98.mjs.map → utils-HztC73hq.mjs.map} +1 -1
  636. package/dist/{validate-BMLBvxOC.mjs → validate-Dd-IUjnZ.mjs} +2 -2
  637. package/dist/{validate-BMLBvxOC.mjs.map → validate-Dd-IUjnZ.mjs.map} +1 -1
  638. package/dist/{validate-CTqt5nuE.d.mts → validate-DlqdOZXB.d.mts} +5 -5
  639. package/dist/{validate-CTqt5nuE.d.mts.map → validate-DlqdOZXB.d.mts.map} +1 -1
  640. package/dist/{validation-npvENUCB.mjs → validation-D83n7Npi.mjs} +10 -8
  641. package/dist/{validation-npvENUCB.mjs.map → validation-D83n7Npi.mjs.map} +1 -1
  642. package/dist/version-ri-sCyAp.mjs +7 -0
  643. package/dist/{version-DB_fqCSI.mjs.map → version-ri-sCyAp.mjs.map} +1 -1
  644. package/dist/{widgets-BD3ho7Rq.mjs → widgets-CpmZd9Q-.mjs} +4 -4
  645. package/dist/{widgets-BD3ho7Rq.mjs.map → widgets-CpmZd9Q-.mjs.map} +1 -1
  646. package/dist/{wxr-taxonomies-Ca68766D.d.mts → wxr-taxonomies-BFd0y72_.d.mts} +1 -1
  647. package/dist/{wxr-taxonomies-Ca68766D.d.mts.map → wxr-taxonomies-BFd0y72_.d.mts.map} +1 -1
  648. package/dist/{wxr-taxonomies-B1R3vBoD.mjs → wxr-taxonomies-Df4vxWcU.mjs} +4 -4
  649. package/dist/{wxr-taxonomies-B1R3vBoD.mjs.map → wxr-taxonomies-Df4vxWcU.mjs.map} +1 -1
  650. package/dist/{zod-generator-LC-on_Vb.mjs → zod-generator-DylzusHh.mjs} +24 -18
  651. package/dist/zod-generator-DylzusHh.mjs.map +1 -0
  652. package/package.json +16 -8
  653. package/src/api/errors.ts +20 -0
  654. package/src/api/handlers/index.ts +17 -0
  655. package/src/api/handlers/media-allowlist.ts +1 -0
  656. package/src/api/handlers/media-folders.ts +130 -0
  657. package/src/api/handlers/media-usage-activation.ts +102 -0
  658. package/src/api/handlers/media-usage.ts +108 -0
  659. package/src/api/handlers/media.ts +62 -2
  660. package/src/api/handlers/registry.ts +134 -316
  661. package/src/api/openapi/document.ts +192 -2
  662. package/src/api/schemas/media-usage.ts +77 -0
  663. package/src/api/schemas/media.ts +69 -0
  664. package/src/astro/integration/routes.ts +18 -0
  665. package/src/astro/integration/vite-config.ts +2 -7
  666. package/src/astro/middleware/auth.ts +4 -0
  667. package/src/astro/middleware.ts +0 -7
  668. package/src/astro/routes/api/admin/media-usage/activation.ts +42 -0
  669. package/src/astro/routes/api/admin/media-usage/progress.ts +37 -0
  670. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +246 -96
  671. package/src/astro/routes/api/auth/me.ts +14 -2
  672. package/src/astro/routes/api/media/[id].ts +3 -0
  673. package/src/astro/routes/api/media/folders/[id].ts +55 -0
  674. package/src/astro/routes/api/media/folders/index.ts +37 -0
  675. package/src/astro/routes/api/media.ts +12 -2
  676. package/src/astro/routes/api/setup/dev-bypass.ts +12 -0
  677. package/src/astro/types.ts +11 -1
  678. package/src/client/index.ts +140 -2
  679. package/src/client/transport.ts +6 -2
  680. package/src/components/EmDashImage.astro +154 -50
  681. package/src/components/Gallery.astro +7 -0
  682. package/src/components/InlinePortableTextEditor.tsx +130 -1
  683. package/src/components/LiveSearch.astro +23 -0
  684. package/src/components/inline-code-block.tsx +180 -49
  685. package/src/content/converters/gallery.ts +2 -0
  686. package/src/content/converters/types.ts +2 -0
  687. package/src/database/connection.ts +2 -31
  688. package/src/database/index.ts +1 -1
  689. package/src/database/json-recordset.ts +16 -0
  690. package/src/database/migrations/019_i18n.ts +1 -1
  691. package/src/database/migrations/071_restore_content_bylines_table.ts +43 -0
  692. package/src/database/migrations/072_media_folders.ts +72 -0
  693. package/src/database/migrations/073_media_focal_point.ts +57 -0
  694. package/src/database/migrations/runner.ts +8 -2
  695. package/src/database/repositories/content.ts +5 -4
  696. package/src/database/repositories/media-folders.ts +115 -0
  697. package/src/database/repositories/media-usage-work.ts +395 -111
  698. package/src/database/repositories/media-usage.ts +1038 -10
  699. package/src/database/repositories/media.ts +97 -29
  700. package/src/database/transaction.ts +1 -0
  701. package/src/database/types.ts +13 -0
  702. package/src/db/adapters.ts +2 -2
  703. package/src/db/node-sqlite-compat.ts +78 -0
  704. package/src/db/sqlite.ts +3 -3
  705. package/src/emdash-runtime.ts +110 -105
  706. package/src/fields/image.ts +7 -1
  707. package/src/fields/types.ts +6 -2
  708. package/src/index.ts +5 -0
  709. package/src/media/focal-point.ts +37 -0
  710. package/src/media/local-runtime.ts +8 -0
  711. package/src/media/mime.ts +1 -0
  712. package/src/media/normalize.ts +10 -0
  713. package/src/media/types.ts +10 -0
  714. package/src/media/usage/activation.ts +162 -35
  715. package/src/media/usage/capture-triggers.ts +19 -3
  716. package/src/media/usage/collection-deletion-processor.ts +7 -3
  717. package/src/media/usage/collection-deletion.ts +10 -0
  718. package/src/media/usage/content-refresh.ts +258 -39
  719. package/src/media/usage/content-snapshots.ts +128 -31
  720. package/src/media/usage/extractor.ts +32 -13
  721. package/src/media/usage/maintenance-engine.ts +166 -0
  722. package/src/media/usage/reconciliation-processor.ts +106 -29
  723. package/src/media/usage/reconciliation.ts +90 -7
  724. package/src/media/usage/work-processor.ts +342 -101
  725. package/src/plugins/index.ts +1 -0
  726. package/src/plugins/save-rejection.ts +20 -0
  727. package/src/plugins/scheduler/node.ts +1 -13
  728. package/src/plugins/scheduler/types.ts +0 -2
  729. package/src/plugins/state.ts +13 -3
  730. package/src/registry/artifact-fetch.ts +530 -0
  731. package/src/registry/config.ts +24 -6
  732. package/src/registry/types.ts +12 -15
  733. package/src/schema/types.ts +1 -0
  734. package/src/schema/zod-generator.ts +12 -4
  735. package/src/security/ssrf.ts +21 -2
  736. package/src/seed/apply.ts +4 -4
  737. package/src/ui.ts +2 -0
  738. package/dist/api-BeBglVxU.mjs.map +0 -1
  739. package/dist/collection-deletion-DQPvMD8R.mjs.map +0 -1
  740. package/dist/content-ZHCc1ngN.mjs.map +0 -1
  741. package/dist/content-refresh-B_8SJzuU.mjs.map +0 -1
  742. package/dist/errors-CLUPm7aD.mjs.map +0 -1
  743. package/dist/index-BVBZj4-7.d.mts.map +0 -1
  744. package/dist/media-BWWWb8kg.mjs.map +0 -1
  745. package/dist/media-CzhAn2--.mjs.map +0 -1
  746. package/dist/media-allowlist-D6x-Us02.mjs.map +0 -1
  747. package/dist/media-usage-BZCydfvu.mjs.map +0 -1
  748. package/dist/media-usage-work-DrCbqBcl.mjs.map +0 -1
  749. package/dist/menus-DSo5QeW0.mjs.map +0 -1
  750. package/dist/mime-DfIVjNkr.mjs.map +0 -1
  751. package/dist/normalize-Bc-s_r-a.mjs.map +0 -1
  752. package/dist/placeholder-Dp0K3RkD.d.mts.map +0 -1
  753. package/dist/registry-Cqa2I749.mjs.map +0 -1
  754. package/dist/relations-BHo5A8Zf.d.mts.map +0 -1
  755. package/dist/runner-C1T11CfC.mjs.map +0 -1
  756. package/dist/state-DYwPue40.mjs.map +0 -1
  757. package/dist/types-C2pMvO62.d.mts.map +0 -1
  758. package/dist/types-DQIDRKF2.d.mts.map +0 -1
  759. package/dist/version-DB_fqCSI.mjs +0 -7
  760. package/dist/zod-generator-LC-on_Vb.mjs.map +0 -1
  761. /package/dist/{api-tokens-rIXC6LdE.mjs → api-tokens-B5NHLNUN.mjs} +0 -0
  762. /package/dist/{ssrf-DpXgKsna.mjs → ssrf-BPKzkm6O.mjs} +0 -0
  763. /package/dist/{types-CP1CflWf.mjs → types-D6OcIp2P.mjs} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-B0w3uBiG.mjs","names":[],"sources":["../src/api/rev.ts","../src/api/handlers/validate-media-fields.ts","../src/api/handlers/content.ts","../src/api/handlers/manifest.ts","../src/api/handlers/revision.ts","../src/api/handlers/media.ts","../src/api/handlers/plugins.ts","../src/api/handlers/plugin-settings.ts"],"sourcesContent":["/**\n * Opaque _rev token generation and validation.\n *\n * Format: base64(\"version:updated_at\")\n * Stateless — server decodes and checks both components.\n *\n * Rules:\n * - No _rev sent → blind write (backwards-compatible)\n * - _rev matches → write proceeds, new _rev returned\n * - _rev mismatch → 409 Conflict\n */\n\nimport type { ContentItem } from \"../database/repositories/types.js\";\nimport { encodeBase64, decodeBase64 } from \"../utils/base64.js\";\n\n/**\n * Generate a _rev token from a content item's version and updatedAt.\n */\nexport function encodeRev(item: ContentItem): string {\n\treturn encodeBase64(`${item.version}:${item.updatedAt}`);\n}\n\n/**\n * Decode a _rev token into its components.\n * Returns null if the token is malformed.\n */\nexport function decodeRev(rev: string): { version: number; updatedAt: string } | null {\n\ttry {\n\t\tconst decoded = decodeBase64(rev);\n\t\tconst colonIdx = decoded.indexOf(\":\");\n\t\tif (colonIdx === -1) return null;\n\n\t\tconst version = parseInt(decoded.slice(0, colonIdx), 10);\n\t\tconst updatedAt = decoded.slice(colonIdx + 1);\n\n\t\tif (isNaN(version) || !updatedAt) return null;\n\t\treturn { version, updatedAt };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Validate a _rev token against a content item.\n * Returns null if valid (or if no _rev provided), or an error message if invalid.\n */\nexport function validateRev(\n\trev: string | undefined,\n\titem: ContentItem,\n): { valid: true } | { valid: false; message: string } {\n\t// No _rev = blind write (backwards-compatible)\n\tif (!rev) return { valid: true };\n\n\tconst decoded = decodeRev(rev);\n\tif (!decoded) {\n\t\treturn { valid: false, message: \"Malformed _rev token\" };\n\t}\n\n\tif (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tmessage: \"Content has been modified since last read (version conflict)\",\n\t\t};\n\t}\n\n\treturn { valid: true };\n}\n","import type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { matchesMimeAllowlist, parseAllowedMimeTypes } from \"../../media/mime.js\";\nimport { requestCached } from \"../../request-cache.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { ApiResult } from \"../types.js\";\n\ninterface FieldRow {\n\tslug: string;\n\ttype: string;\n\tallowedMimeTypes: string[];\n}\n\ninterface MediaRefValue {\n\tid?: unknown;\n\tprovider?: unknown;\n\tmimeType?: unknown;\n}\n\nfunction asMediaRef(value: unknown): MediaRefValue | null {\n\tif (value === null || value === undefined) return null;\n\tif (typeof value !== \"object\" || Array.isArray(value)) return null;\n\treturn value;\n}\n\nfunction fail(message: string): ApiResult<never> {\n\treturn { success: false, error: { code: \"INVALID_MIME_FOR_FIELD\", message } };\n}\n\nasync function loadMediaFieldsForCollection(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n): Promise<FieldRow[]> {\n\tconst rows = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.innerJoin(\"_emdash_collections\", \"_emdash_collections.id\", \"_emdash_fields.collection_id\")\n\t\t.select([\"_emdash_fields.slug\", \"_emdash_fields.type\", \"_emdash_fields.validation\"])\n\t\t.where(\"_emdash_collections.slug\", \"=\", collectionSlug)\n\t\t.where(\"_emdash_fields.type\", \"in\", [\"file\", \"image\"])\n\t\t.execute();\n\n\tconst out: FieldRow[] = [];\n\tfor (const row of rows) {\n\t\tconst list = parseAllowedMimeTypes(row.validation);\n\t\tif (!list) continue;\n\t\tout.push({ slug: row.slug, type: row.type, allowedMimeTypes: list });\n\t}\n\treturn out;\n}\n\nexport async function validateMediaFields(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tdata: Record<string, unknown>,\n): Promise<ApiResult<true>> {\n\t// Cache is keyed on slug only. If a handler creates/modifies a field and\n\t// then writes content in the same request (e.g. bulk import), the cached\n\t// list will be stale for that request. This is an edge case in normal use.\n\tconst fields = await requestCached(`mediaFields:${collectionSlug}`, () =>\n\t\tloadMediaFieldsForCollection(db, collectionSlug),\n\t);\n\tif (fields.length === 0) return { success: true, data: true };\n\n\t// Collect local media ids that need a MIME lookup\n\tconst localIds = new Set<string>();\n\tfor (const field of fields) {\n\t\tconst ref = asMediaRef(data[field.slug]);\n\t\tif (!ref) continue;\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\t\tif (provider === \"local\" && typeof ref.id === \"string\") {\n\t\t\tlocalIds.add(ref.id);\n\t\t}\n\t}\n\n\t// Batch-load local media MIMEs\n\tconst idList = [...localIds];\n\tconst mimeById = new Map<string, string>();\n\tif (idList.length > 0) {\n\t\tfor (const batch of chunks(idList, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await db\n\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t.select([\"id\", \"mime_type\"])\n\t\t\t\t.where(\"id\", \"in\", batch)\n\t\t\t\t.execute();\n\t\t\tfor (const r of rows) mimeById.set(r.id, r.mime_type);\n\t\t}\n\t}\n\n\tfor (const field of fields) {\n\t\tconst value = data[field.slug];\n\t\tif (value === null || value === undefined) continue;\n\t\tconst ref = asMediaRef(value);\n\t\tif (!ref) continue;\n\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\n\t\t// External providers carry mimeType in the ref; trust it as-is.\n\t\t// Local media: look up the stored mimeType by id.\n\t\tlet mime: string | undefined;\n\t\tif (provider === \"local\") {\n\t\t\tif (typeof ref.id !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with an invalid id`);\n\t\t\t}\n\t\t\tmime = mimeById.get(ref.id);\n\t\t\tif (!mime) {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with unknown MIME type`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (typeof ref.mimeType !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);\n\t\t\t}\n\t\t\t// TODO: long-term, consider a server-side HEAD probe or provider-vouched\n\t\t\t// MIMEs for non-local refs; for now the constraint is only as strong as\n\t\t\t// the client that constructed the ref.\n\t\t\tmime = ref.mimeType;\n\t\t}\n\n\t\tif (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) {\n\t\t\treturn fail(`Field '${field.slug}' does not accept ${mime}`);\n\t\t}\n\t}\n\n\treturn { success: true, data: true };\n}\n","/**\n * Content CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport type { ContentFieldFilters } from \"../../content-list-query.js\";\nimport { isSqlite } from \"../../database/dialect-helpers.js\";\nimport { BylineRepository } from \"../../database/repositories/byline.js\";\nimport type { ContentBylineInput } from \"../../database/repositories/byline.js\";\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport { ContentRepository, isSystemOrderField } from \"../../database/repositories/content.js\";\nimport { RedirectRepository } from \"../../database/repositories/redirect.js\";\nimport { RevisionRepository } from \"../../database/repositories/revision.js\";\nimport { SeoRepository } from \"../../database/repositories/seo.js\";\nimport { TaxonomyRepository } from \"../../database/repositories/taxonomy.js\";\nimport {\n\tContentCollectionNotFoundError,\n\tContentMutationConflictError,\n\tEmDashValidationError,\n\tScheduledNotDueError,\n\tInvalidCursorError,\n\ttype BylineSummary,\n\ttype ContentBylineCredit,\n\ttype ContentBylineFilter,\n\ttype ContentDateField,\n\ttype ContentItem,\n\ttype ContentSeo,\n\ttype ContentSeoInput,\n\ttype FindManyOptions,\n} from \"../../database/repositories/types.js\";\nimport { UserRepository } from \"../../database/repositories/user.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { getI18nConfig, isI18nEnabled, resolveConfiguredLocale } from \"../../i18n/config.js\";\nimport { invalidateRedirectCache } from \"../../redirects/cache.js\";\nimport { FTSManager } from \"../../search/fts-manager.js\";\nimport { invalidateTermCache } from \"../../taxonomies/index.js\";\nimport { isMissingColumnError, isMissingTableError } from \"../../utils/db-errors.js\";\nimport { encodeRev, validateRev } from \"../rev.js\";\nimport type { ApiResult, ContentListResponse, ContentResponse } from \"../types.js\";\nimport { validateMediaFields } from \"./validate-media-fields.js\";\n\n/**\n * Narrow a caught error to one carrying a structured `apiError` discriminant.\n * Used by transaction callbacks that want to surface a specific error code\n * through the standard Error throwing path.\n */\nfunction hasApiError(error: unknown): error is Error & { apiError: { code: string } } {\n\tif (!(error instanceof Error) || !(\"apiError\" in error)) return false;\n\tconst { apiError } = error;\n\treturn (\n\t\ttypeof apiError === \"object\" &&\n\t\tapiError !== null &&\n\t\t\"code\" in apiError &&\n\t\ttypeof apiError.code === \"string\"\n\t);\n}\n\n/**\n * Extract a slug source (title or name) from content data.\n * Returns null if no suitable string field is found.\n */\nfunction getSlugSource(data: Record<string, unknown>): string | null {\n\tif (typeof data.title === \"string\" && data.title.length > 0) return data.title;\n\tif (typeof data.name === \"string\" && data.name.length > 0) return data.name;\n\treturn null;\n}\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Check if a collection has SEO enabled.\n */\nasync function collectionHasSeo(db: Kysely<Database>, collection: string): Promise<boolean> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"has_seo\")\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\treturn row?.has_seo === 1;\n}\n\nasync function getCollectionPublishConfig(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<{ supportsRevisions: boolean; routable: boolean }> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"supports\", \"routable\"])\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\tconst supports: unknown = row?.supports ? JSON.parse(row.supports) : [];\n\treturn {\n\t\tsupportsRevisions: Array.isArray(supports) && supports.includes(\"revisions\"),\n\t\troutable: row?.routable !== 0,\n\t};\n}\n\nfunction requireRoutablePublishSlug(routable: boolean, slug: string | null | undefined): void {\n\tif (routable && !slug?.trim()) {\n\t\tthrow new EmDashValidationError(\"Cannot publish routable content without a slug\");\n\t}\n}\n\n/**\n * Hydrate SEO data on a single content item if the collection has SEO enabled.\n */\nasync function hydrateSeo(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo) return;\n\tconst seoRepo = new SeoRepository(db);\n\titem.seo = await seoRepo.get(collection, item.id);\n}\n\n/**\n * Hydrate SEO data on multiple content items using a single batch query.\n */\nasync function hydrateSeoMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo || items.length === 0) return;\n\tconst seoRepo = new SeoRepository(db);\n\tconst seoMap = await seoRepo.getMany(\n\t\tcollection,\n\t\titems.map((i) => i.id),\n\t);\n\tfor (const item of items) {\n\t\titem.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };\n\t}\n}\n\nasync function hydrateBylines(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n): Promise<void> {\n\tconst bylineRepo = new BylineRepository(db);\n\t// Strict per-locale (migration 040): a credit at locale X renders iff a\n\t// byline row exists at locale X in the credited translation_group. The\n\t// junction itself spans translations; rendering does not fall back.\n\tconst localeOpt = item.locale ? { locale: item.locale } : undefined;\n\tconst bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);\n\n\tif (bylines.length > 0) {\n\t\titem.bylines = bylines.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\titem.byline = bylines[0]?.byline ?? null;\n\t\treturn;\n\t}\n\n\t// `primaryBylineId` is set iff junction rows exist; non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tif (item.primaryBylineId) {\n\t\titem.bylines = [];\n\t\titem.byline = null;\n\t\treturn;\n\t}\n\n\tif (item.authorId) {\n\t\t// Same strict-locale rule as explicit credits: a user-linked byline\n\t\t// renders on the entry only when a sibling exists at the entry's\n\t\t// locale. Without this we'd silently surface the default-locale\n\t\t// row, which contradicts the per-locale model.\n\t\tconst fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);\n\t\tif (fallback) {\n\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\titem.byline = fallback;\n\t\t\treturn;\n\t\t}\n\t}\n\n\titem.bylines = [];\n\titem.byline = null;\n}\n\n/**\n * Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.\n *\n * Items may live at different locales (e.g. a list endpoint returning the\n * translations of an entry). Group by `item.locale` and call the strict\n * per-locale repo method once per group so each item resolves against its\n * own locale's byline rows.\n */\nasync function hydrateBylinesMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n): Promise<void> {\n\tif (items.length === 0) return;\n\n\tconst bylineRepo = new BylineRepository(db);\n\n\t// 1. Bucket items by locale so we can call the strict-locale repo\n\t// once per bucket. Items with a null/undefined locale (pre-i18n\n\t// rows on a single-locale install) share an \"unscoped\" bucket.\n\tconst localeBuckets = new Map<string | null, ContentItem[]>();\n\tfor (const item of items) {\n\t\tconst key = item.locale ?? null;\n\t\tconst bucket = localeBuckets.get(key);\n\t\tif (bucket) bucket.push(item);\n\t\telse localeBuckets.set(key, [item]);\n\t}\n\n\t// 2. Per-locale: fetch explicit credits. Items whose credits don't\n\t// resolve at this locale go through a locale-agnostic \"has any\n\t// junction\" check before being considered for author inference —\n\t// explicit editorial intent at any locale beats inferred fallback.\n\tconst bylinesByItem = new Map<string, ContentBylineCredit[]>();\n\tconst itemsNeedingAuthorCheck: ContentItem[] = [];\n\tfor (const [locale, bucket] of localeBuckets) {\n\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\tconst ids = bucket.map((i) => i.id);\n\t\tconst credits = await bylineRepo.getContentBylinesMany(collection, ids, localeOpt);\n\t\tfor (const [id, list] of credits) bylinesByItem.set(id, list);\n\n\t\tfor (const item of bucket) {\n\t\t\tif (credits.has(item.id) && credits.get(item.id)!.length > 0) continue;\n\t\t\tif (item.authorId) itemsNeedingAuthorCheck.push(item);\n\t\t}\n\t}\n\n\t// 3. Author fallback applies only when no explicit credit exists\n\t// (primaryBylineId null).\n\tconst fallbackByItem = new Map<string, BylineSummary>();\n\tif (itemsNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, ContentItem[]>();\n\t\tfor (const item of itemsNeedingAuthorCheck) {\n\t\t\tif (item.primaryBylineId) continue;\n\t\t\tconst key = item.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(item);\n\t\t\telse authorBuckets.set(key, [item]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\t\tconst authorIds = bucket.map((i) => i.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\tconst authorMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const item of bucket) {\n\t\t\t\tif (!item.authorId) continue;\n\t\t\t\tconst f = authorMap.get(item.authorId);\n\t\t\t\tif (f) fallbackByItem.set(item.id, f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// 4. Assign to each item.\n\tfor (const item of items) {\n\t\tconst explicit = bylinesByItem.get(item.id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\titem.bylines = explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\t\titem.byline = explicit[0]?.byline ?? null;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fallback = fallbackByItem.get(item.id);\n\t\tif (fallback) {\n\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\titem.byline = fallback;\n\t\t\tcontinue;\n\t\t}\n\n\t\titem.bylines = [];\n\t\titem.byline = null;\n\t}\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID.\n * Returns the ID if found, null if not found.\n * When locale is provided, slug lookups are scoped to that locale.\n */\nasync function resolveId(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlug(\n\t\tcollection,\n\t\tidentifier,\n\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t);\n\treturn item?.id ?? null;\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID,\n * including trashed (soft-deleted) items.\n */\nasync function resolveIdIncludingTrashed(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlugIncludingTrashed(\n\t\tcollection,\n\t\tidentifier,\n\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t);\n\treturn item?.id ?? null;\n}\n\n/**\n * Trashed content item with deletion timestamp\n */\nexport interface TrashedContentItem {\n\tid: string;\n\ttype: string;\n\tslug: string | null;\n\tstatus: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tpublishedAt: string | null;\n\tdeletedAt: string;\n}\n\n/**\n * Resolve the columns a content-list search should match against. Always\n * includes `slug` (a standard column), adds the configured `titleField` plus\n * the `title`/`name` display fields when the collection actually defines them,\n * mirroring the admin's item-title resolution (titleField -> title -> name ->\n * slug), and includes every field explicitly marked searchable. Returning only\n * schema-backed columns avoids \"no such column\" errors.\n */\nasync function resolveSearchColumns(db: Kysely<Database>, collection: string): Promise<string[]> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"id\", \"title_field\"])\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\tif (!row) return [\"slug\"];\n\n\tconst fields = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select([\"slug\", \"searchable\"])\n\t\t.where(\"collection_id\", \"=\", row.id)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\tconst columns = new Set([\"slug\"]);\n\tconst fieldSlugs = new Set(fields.map((f) => f.slug));\n\n\t// A configured titleField takes precedence, then the conventional\n\t// title/name fields. A null title_field falls through to those defaults.\n\tif (row.title_field && fieldSlugs.has(row.title_field)) columns.add(row.title_field);\n\tfor (const candidate of [\"title\", \"name\"]) {\n\t\tif (fieldSlugs.has(candidate)) columns.add(candidate);\n\t}\n\tfor (const field of fields) {\n\t\tif (field.searchable === 1) columns.add(field.slug);\n\t}\n\treturn [...columns];\n}\n\n/**\n * Decide whether the content-list `q` filter can be served from the\n * collection's FTS5 index instead of a full-scan substring LIKE (#1517).\n *\n * Requires SQLite (FTS5 is SQLite-only), search enabled on the collection,\n * every non-slug display column present in the searchable-field set (or the\n * index would miss matches the LIKE finds), and the index table actually\n * existing.\n */\nasync function canUseFtsForListFilter(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tsearchColumns: string[],\n): Promise<boolean> {\n\tif (!isSqlite(db)) return false;\n\tconst ftsManager = new FTSManager(db);\n\tconst config = await ftsManager.getSearchConfig(collection);\n\tif (!config?.enabled) return false;\n\tconst searchable = new Set(await ftsManager.getSearchableFields(collection));\n\tconst covered = searchColumns.every((col) => col === \"slug\" || searchable.has(col));\n\tif (!covered) return false;\n\treturn ftsManager.ftsTableExists(collection);\n}\n\n/**\n * Create a 301 auto-redirect from an entry's old URL to its new one after a\n * slug change, using the collection's URL pattern. Shared by\n * handleContentUpdate (direct slug edits) and handleContentPublish (slug edits\n * staged as `_slug` in a draft revision, which only land on publish).\n */\nasync function createSlugChangeRedirect(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\toldSlug: string,\n\tnewSlug: string,\n\tcontentId: string,\n): Promise<void> {\n\t// A URL pattern has no locale token, so every locale variant of an entry\n\t// generates the same URL, and slugs are unique per (slug, locale) — a\n\t// translation may still hold the old slug. Redirecting away from a URL\n\t// another row still answers on would take that page down: the redirect\n\t// middleware runs `order: \"pre\"`, so routing never gets a chance.\n\t// Any surviving row counts, published or not: a draft that publishes later\n\t// would otherwise be shadowed by the redirect.\n\tif (await slugStillTaken(db, collection, oldSlug, contentId)) return;\n\n\tconst collectionRow = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"url_pattern\")\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\n\tconst redirectRepo = new RedirectRepository(db);\n\tawait redirectRepo.createAutoRedirect(\n\t\tcollection,\n\t\toldSlug,\n\t\tnewSlug,\n\t\tcontentId,\n\t\tcollectionRow?.url_pattern ?? null,\n\t);\n\tinvalidateRedirectCache();\n}\n\n/** Whether a row other than `contentId` still holds `slug` in this collection. */\nasync function slugStillTaken(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tslug: string,\n\tcontentId: string,\n): Promise<boolean> {\n\tvalidateIdentifier(collection, \"collection slug\");\n\tconst result = await sql<{ id: string }>`\n\t\tSELECT id FROM ${sql.ref(`ec_${collection}`)}\n\t\tWHERE slug = ${slug}\n\t\tAND id != ${contentId}\n\t\tAND deleted_at IS NULL\n\t\tLIMIT 1\n\t`.execute(db);\n\treturn result.rows.length > 0;\n}\n\n/** Matches a date-only `YYYY-MM-DD` bound (no time component). */\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Normalize a date-range bound to an ISO datetime for lexicographic comparison\n * against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the\n * appropriate UTC day boundary so the range stays inclusive: a `start` bound\n * becomes the start of the day and an `end` bound the end of the day.\n * Otherwise a date-only upper bound would exclude every same-day row (since\n * `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass\n * through unchanged.\n */\nfunction normalizeDateBound(value: string | undefined, edge: \"start\" | \"end\"): string | undefined {\n\tif (!value) return undefined;\n\tif (!DATE_ONLY_RE.test(value)) return value;\n\treturn edge === \"start\" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;\n}\n\n/**\n * Build the repository's byline filter from the wire params.\n *\n * `locale` is the locale the list is scoped to, which is the locale an\n * inferred credit has to resolve at — the admin list is always scoped to the\n * locale picked in its switcher.\n */\nfunction resolveBylineFilter(\n\tparams: { bylines?: string[]; bylinesNone?: boolean; includeInferredBylines?: boolean },\n\tlocale: string | undefined,\n): ContentBylineFilter | undefined {\n\tconst includeInferred = params.includeInferredBylines === true;\n\n\tif (params.bylinesNone) return { mode: \"none\", includeInferred, locale };\n\n\tconst bylineIds = params.bylines ?? [];\n\tif (bylineIds.length === 0) return undefined;\n\n\treturn { mode: \"any\", bylineIds, includeInferred, locale };\n}\n\n/**\n * Create content list handler\n */\nexport async function handleContentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tparams: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tstatus?: string;\n\t\torderBy?: string;\n\t\torder?: \"asc\" | \"desc\";\n\t\tlocale?: string;\n\t\tq?: string;\n\t\tauthorId?: string;\n\t\tdateField?: ContentDateField;\n\t\tdateFrom?: string;\n\t\tdateTo?: string;\n\t\tbylines?: string[];\n\t\tbylinesNone?: boolean;\n\t\tincludeInferredBylines?: boolean;\n\t\tfieldFilters?: ContentFieldFilters;\n\t},\n): Promise<ApiResult<ContentListResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst where: FindManyOptions[\"where\"] = {};\n\t\tif (params.status) where.status = params.status;\n\t\tconst locale = params.locale ? resolveConfiguredLocale(params.locale) : undefined;\n\t\tif (locale) where.locale = locale;\n\t\tif (params.authorId) where.authorId = params.authorId;\n\t\tif (params.fieldFilters && Object.keys(params.fieldFilters).length > 0) {\n\t\t\twhere.fieldFilters = params.fieldFilters;\n\t\t}\n\n\t\tconst bylineFilter = resolveBylineFilter(params, locale);\n\t\tif (bylineFilter) where.bylineFilter = bylineFilter;\n\n\t\t// A date range requires a target column; ignore stray from/to without\n\t\t// a field so a half-specified filter doesn't silently drop all rows.\n\t\tif (params.dateField && (params.dateFrom || params.dateTo)) {\n\t\t\twhere.dateFilter = {\n\t\t\t\tfield: params.dateField,\n\t\t\t\tfrom: normalizeDateBound(params.dateFrom, \"start\"),\n\t\t\t\tto: normalizeDateBound(params.dateTo, \"end\"),\n\t\t\t};\n\t\t}\n\n\t\tconst q = params.q?.trim();\n\t\tif (q) {\n\t\t\twhere.q = q;\n\t\t\twhere.searchColumns = await resolveSearchColumns(db, collection);\n\t\t\twhere.useFts = await canUseFtsForListFilter(db, collection, where.searchColumns);\n\t\t}\n\n\t\t// Sorting by a non-system field (a collection's titleField/dateField)\n\t\t// needs the collection's *actual* sort fields resolved server-side,\n\t\t// so the orderBy set stays closed. Only query when it's not a system field.\n\t\tlet sortableExtras: string[] | undefined;\n\t\tif (params.orderBy && !isSystemOrderField(params.orderBy)) {\n\t\t\tconst coll = await db\n\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t.select([\"title_field\", \"date_field\"])\n\t\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t\t.executeTakeFirst();\n\t\t\tsortableExtras = [coll?.title_field, coll?.date_field].filter(\n\t\t\t\t(slug): slug is string => !!slug,\n\t\t\t);\n\t\t}\n\n\t\tconst result = await repo.findMany(collection, {\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: params.limit || 50,\n\t\t\twhere: Object.keys(where).length > 0 ? where : undefined,\n\t\t\torderBy: params.orderBy\n\t\t\t\t? { field: params.orderBy, direction: params.order || \"desc\" }\n\t\t\t\t: undefined,\n\t\t\tsortableExtras,\n\t\t});\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeoMany(db, collection, result.items, hasSeo);\n\t\tawait hydrateBylinesMany(db, collection, result.items);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t\ttotal: result.total,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (error instanceof ContentCollectionNotFoundError || isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (isMissingColumnError(error, \"deleted_at\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_SCHEMA_MISMATCH\",\n\t\t\t\t\tmessage: `Collection '${collection}' backing table is missing the 'deleted_at' column`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\t// e.g. invalid orderBy field\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** A content author option for the admin author filter. */\nexport interface ContentAuthor {\n\tid: string;\n\tname: string | null;\n\temail: string;\n\tavatarUrl: string | null;\n}\n\n/**\n * List the distinct authors of a collection's live content.\n *\n * Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN\n * only), this is gated on `content:read`, so any editor can filter by author.\n * Returns only users who have authored at least one non-trashed entry, sorted\n * by display name then email for a stable dropdown order.\n */\nexport async function handleContentAuthors(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ items: ContentAuthor[] }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst authorIds = await repo.findDistinctAuthorIds(collection);\n\t\tif (authorIds.length === 0) {\n\t\t\treturn { success: true, data: { items: [] } };\n\t\t}\n\n\t\tconst userRepo = new UserRepository(db);\n\t\tconst users = await userRepo.findByIds(authorIds);\n\n\t\tconst items: ContentAuthor[] = users\n\t\t\t.map((u) => ({ id: u.id, name: u.name, email: u.email, avatarUrl: u.avatarUrl }))\n\t\t\t.toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email));\n\n\t\treturn { success: true, data: { items } };\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content authors error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_AUTHORS_ERROR\",\n\t\t\t\tmessage: \"Failed to list content authors\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single content item\n */\nexport async function handleContentGet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t\t);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a content item by id, including trashed items.\n * Used by restore endpoint for ownership checks on soft-deleted items.\n */\nexport async function handleContentGetIncludingTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlugIncludingTrashed(\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t\t);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create content item.\n *\n * Content + SEO writes are wrapped in a transaction so either both succeed\n * or neither does. If `body.seo` is provided for a non-SEO collection, the\n * API returns a validation error rather than silently dropping it.\n */\nexport async function handleContentCreate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tbody: {\n\t\tdata: Record<string, unknown>;\n\t\tslug?: string | null;\n\t\tstatus?: string;\n\t\tauthorId?: string;\n\t\tbylines?: ContentBylineInput[];\n\t\tlocale?: string;\n\t\ttranslationOf?: string;\n\t\tseo?: ContentSeoInput;\n\t\ttaxonomies?: Record<string, string[]>;\n\t\tcreatedAt?: string | null;\n\t\tpublishedAt?: string | null;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\tif (!mimeCheck.success) return mimeCheck;\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Default to the configured site locale rather than the repo's\n\t\t\t// hard-coded \"en\" — otherwise non-English default-locale sites\n\t\t\t// silently create entries in a locale the editor never chose.\n\t\t\tconst effectiveLocale = body.locale\n\t\t\t\t? resolveConfiguredLocale(body.locale)\n\t\t\t\t: getI18nConfig()?.defaultLocale;\n\n\t\t\tlet slug: string | null | undefined = body.slug;\n\t\t\tif (!slug) {\n\t\t\t\tconst slugSource = getSlugSource(body.data);\n\t\t\t\tif (slugSource) {\n\t\t\t\t\tslug = await repo.generateUniqueSlug(collection, slugSource, effectiveLocale);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (body.status === \"published\") {\n\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, slug);\n\t\t\t}\n\n\t\t\tconst created = await repo.create({\n\t\t\t\ttype: collection,\n\t\t\t\tslug,\n\t\t\t\tdata: body.data,\n\t\t\t\tstatus: body.status || \"draft\",\n\t\t\t\tauthorId: body.authorId,\n\t\t\t\tlocale: effectiveLocale,\n\t\t\t\ttranslationOf: body.translationOf,\n\t\t\t\tcreatedAt: body.createdAt,\n\t\t\t\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tconst credits = await bylineRepo.setContentBylines(collection, created.id, body.bylines);\n\t\t\t\t// `setContentBylines` translates wire row ids to their\n\t\t\t\t// `translation_group` before writing. The response-shape\n\t\t\t\t// `primaryBylineId` must match what's now in the DB, so read\n\t\t\t\t// it from the returned credit (whose `byline` came from a\n\t\t\t\t// hydration round-trip).\n\t\t\t\tcreated.primaryBylineId = credits[0]?.byline.translationGroup ?? null;\n\t\t\t}\n\n\t\t\t// Taxonomy assignments already belong to the content translation\n\t\t\t// group. Byline credits remain per content row and need copying.\n\t\t\t// Explicit `body.bylines` wins — `copyContentBylines` no-ops\n\t\t\t// when the target already has credits, but the cleaner guard\n\t\t\t// is to skip the call entirely.\n\t\t\tif (body.translationOf) {\n\t\t\t\tif (body.bylines === undefined) {\n\t\t\t\t\tawait bylineRepo.copyContentBylines(collection, body.translationOf, created.id);\n\t\t\t\t\t// `copyContentBylines` writes the source's primary\n\t\t\t\t\t// pointer onto the new row; reflect it in-memory so the\n\t\t\t\t\t// response includes it before hydrateBylines runs.\n\t\t\t\t\tconst source = await repo.findById(collection, body.translationOf);\n\t\t\t\t\tif (source) created.primaryBylineId = source.primaryBylineId;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, created);\n\n\t\t\t// Side-write SEO data if provided\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tcreated.seo = await seoRepo.upsert(collection, created.id, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\t// Assign defaults in-memory — no DB round-trip needed\n\t\t\t\tcreated.seo = { ...SEO_DEFAULTS };\n\t\t\t}\n\n\t\t\t// Attach taxonomy terms in the same transaction. The MCP tool\n\t\t\t// (and the REST create body) previously accepted a `taxonomies`\n\t\t\t// field on `content_create` without doing anything with it, so\n\t\t\t// agents publishing a categorized/tagged entry had to make N\n\t\t\t// follow-up REST calls per taxonomy. This resolves each slug in\n\t\t\t// the entry's locale and pipes it through the same\n\t\t\t// `setTermsForEntry` path the `.../terms/{taxonomy}` REST route\n\t\t\t// uses, so the two entry points can't drift.\n\t\t\tif (body.taxonomies) {\n\t\t\t\tawait assignTaxonomies(trx, collection, created.id, effectiveLocale, body.taxonomies);\n\t\t\t}\n\n\t\t\treturn created;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\t// SQLite UNIQUE constraint OR Postgres unique_violation — slug\n\t\t// collisions and any other unique violations land here. Match\n\t\t// specifically on \"unique constraint failed\" / \"duplicate key\" so we\n\t\t// don't false-positive on NOT NULL or CHECK violations whose\n\t\t// messages also contain \"constraint failed\".\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\t// Detect slug-specific collisions by message fingerprint\n\t\t\tif (message.includes(\"slug\")) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? \"(auto-generated)\"}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content create error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update content item.\n * If `_rev` is provided, validates it against the current version before writing.\n * No `_rev` = blind write (backwards-compatible for admin UI).\n *\n * Content + SEO writes are wrapped in a transaction for atomicity.\n */\nexport async function handleContentUpdate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tbody: {\n\t\tdata?: Record<string, unknown>;\n\t\tslug?: string | null;\n\t\tstatus?: string;\n\t\tauthorId?: string | null;\n\t\tbylines?: ContentBylineInput[];\n\t\tlocale?: string;\n\t\t_rev?: string;\n\t\tseo?: ContentSeoInput;\n\t\ttaxonomies?: Record<string, string[]>;\n\t\tpublishedAt?: string | null;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (body.data) {\n\t\t\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\t\tif (!mimeCheck.success) return mimeCheck;\n\t\t}\n\n\t\tconst repo = new ContentRepository(db);\n\n\t\t// Resolve slug → ID if needed\n\t\tconst resolvedId = (await resolveId(repo, collection, id, body.locale)) ?? id;\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity.\n\t\t// The _rev check is inside the transaction so the read-then-write\n\t\t// is atomic -- no concurrent write can slip between the check and update.\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Read existing item once for both _rev check and old slug capture\n\t\t\tconst existing =\n\t\t\t\tbody._rev || body.slug !== undefined || body.status === \"published\"\n\t\t\t\t\t? await trxRepo.findById(collection, resolvedId)\n\t\t\t\t\t: null;\n\n\t\t\t// Validate _rev if provided (optimistic concurrency)\n\t\t\tif (body._rev) {\n\t\t\t\tif (!existing) {\n\t\t\t\t\tthrow Object.assign(new Error(`Content item not found: ${id}`), {\n\t\t\t\t\t\tapiError: { code: \"NOT_FOUND\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst revCheck = validateRev(body._rev, existing);\n\t\t\t\tif (!revCheck.valid) {\n\t\t\t\t\tthrow Object.assign(new Error(revCheck.message), {\n\t\t\t\t\t\tapiError: { code: \"CONFLICT\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Capture old slug before update for auto-redirect\n\t\t\tlet oldSlug: string | undefined;\n\t\t\tif (body.slug && existing?.slug && existing.slug !== body.slug) {\n\t\t\t\toldSlug = existing.slug;\n\t\t\t}\n\n\t\t\tconst resultingStatus = body.status ?? existing?.status;\n\t\t\tif (resultingStatus === \"published\") {\n\t\t\t\tif (!existing) {\n\t\t\t\t\tthrow Object.assign(new Error(`Content item not found: ${id}`), {\n\t\t\t\t\t\tapiError: { code: \"NOT_FOUND\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\tconst intendedSlug = body.slug !== undefined ? body.slug : existing.slug;\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, intendedSlug);\n\t\t\t}\n\n\t\t\tconst updated = await trxRepo.update(collection, resolvedId, {\n\t\t\t\tdata: body.data,\n\t\t\t\tslug: body.slug,\n\t\t\t\tstatus: body.status,\n\t\t\t\tauthorId: body.authorId,\n\t\t\t\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tconst credits = await bylineRepo.setContentBylines(collection, resolvedId, body.bylines);\n\t\t\t\t// `setContentBylines` translates wire row ids to their\n\t\t\t\t// `translation_group` before writing. Read the in-memory\n\t\t\t\t// pointer from the persisted credit so the response shape\n\t\t\t\t// matches the DB. See the matching block in handleContentCreate.\n\t\t\t\tupdated.primaryBylineId = credits[0]?.byline.translationGroup ?? null;\n\t\t\t}\n\n\t\t\t// Create auto-redirect when slug changes\n\t\t\tif (oldSlug && body.slug) {\n\t\t\t\tawait createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId);\n\t\t\t}\n\n\t\t\t// Sync non-translatable fields to sibling locales in the same\n\t\t\t// translation group. Only runs when i18n is enabled, data was updated,\n\t\t\t// and the item belongs to a translation group with siblings.\n\t\t\tif (isI18nEnabled() && body.data && updated.translationGroup) {\n\t\t\t\tawait syncNonTranslatableFields(\n\t\t\t\t\ttrx,\n\t\t\t\t\tcollection,\n\t\t\t\t\tupdated.id,\n\t\t\t\t\tupdated.translationGroup,\n\t\t\t\t\tbody.data,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Side-write SEO data if provided, always hydrate for SEO-enabled collections\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.upsert(collection, resolvedId, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.get(collection, resolvedId);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, updated);\n\n\t\t\t// Replace taxonomy assignments in the same transaction. Uses the\n\t\t\t// entry's own locale (post-update) to resolve slugs so an update\n\t\t\t// that also changes locale still lands on the correct term\n\t\t\t// variants. See handleContentCreate for rationale.\n\t\t\tif (body.taxonomies) {\n\t\t\t\tawait assignTaxonomies(\n\t\t\t\t\ttrx,\n\t\t\t\t\tcollection,\n\t\t\t\t\tresolvedId,\n\t\t\t\t\tupdated.locale ?? body.locale,\n\t\t\t\t\tbody.taxonomies,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn updated;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\t// Handle structured errors thrown from inside the transaction\n\t\t// (rev check failures, not-found)\n\t\tif (hasApiError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: error.apiError.code, message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\tif (message.includes(\"slug\")) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content update error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Duplicate content item.\n *\n * Only copies SEO data if the collection has SEO enabled.\n * Always returns consistent `seo` shape for SEO-enabled collections.\n */\nexport async function handleContentDuplicate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tauthorId?: string,\n): Promise<ApiResult<{ item: ContentItem }>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Wrap duplicate + SEO copy in a transaction for atomicity\n\t\tconst duplicate = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\tconst dup = await repo.duplicate(collection, resolvedId, authorId);\n\n\t\t\tconst existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);\n\t\t\tif (existingBylines.length > 0) {\n\t\t\t\tawait bylineRepo.setContentBylines(\n\t\t\t\t\tcollection,\n\t\t\t\t\tdup.id,\n\t\t\t\t\texistingBylines.map((entry) => ({\n\t\t\t\t\t\tbylineId: entry.byline.id,\n\t\t\t\t\t\troleLabel: entry.roleLabel,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (hasSeo) {\n\t\t\t\t// Copy SEO data from the original (clears canonical)\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.copyForDuplicate(collection, resolvedId, dup.id);\n\t\t\t\t// Always hydrate SEO for consistent response shape\n\t\t\t\tdup.seo = await seoRepo.get(collection, dup.id);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, dup);\n\n\t\t\treturn dup;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item: duplicate },\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: err.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content duplicate error:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DUPLICATE_ERROR\",\n\t\t\t\tmessage: \"Failed to duplicate content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete content item (soft delete - moves to trash)\n */\nexport async function handleContentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true; id: string }>> {\n\ttry {\n\t\tconst result = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn {\n\t\t\t\tid: resolvedId,\n\t\t\t\tdeleted: await repo.delete(collection, resolvedId),\n\t\t\t};\n\t\t});\n\n\t\tif (!result.deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true, id: result.id },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore content item from trash\n */\nexport async function handleContentRestore(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ restored: true; item: ContentItem }>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\t\t\treturn repo.restore(collection, resolvedId);\n\t\t});\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Trashed content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { restored: true, item },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content restore error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Permanently delete content item (cannot be undone).\n * Also cleans up associated SEO data.\n */\nexport async function handleContentPermanentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true; id: string }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\n\t\t// Wrap content delete + SEO/comment cleanup in a transaction\n\t\tconst deleted = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst wasDeleted = await trxRepo.permanentDelete(collection, resolvedId);\n\n\t\t\tif (wasDeleted) {\n\t\t\t\t// Clean up SEO data for permanently deleted content\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.delete(collection, resolvedId);\n\t\t\t\t// Clean up comments for permanently deleted content\n\t\t\t\tconst commentRepo = new CommentRepository(trx);\n\t\t\t\tawait commentRepo.deleteByContent(collection, resolvedId);\n\t\t\t\t// Clean up revisions for permanently deleted content\n\t\t\t\tconst revisionRepo = new RevisionRepository(trx);\n\t\t\t\tawait revisionRepo.deleteByEntry(collection, resolvedId);\n\t\t\t}\n\n\t\t\treturn wasDeleted;\n\t\t});\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true, id: resolvedId },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content permanent delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to permanently delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List trashed content items\n */\nexport async function handleContentListTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\toptions: { limit?: number; cursor?: string } = {},\n): Promise<ApiResult<{ items: TrashedContentItem[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst result = await repo.findTrashed(collection, {\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items.map((item) => ({\n\t\t\t\t\tid: item.id,\n\t\t\t\t\ttype: item.type,\n\t\t\t\t\tslug: item.slug,\n\t\t\t\t\tstatus: item.status,\n\t\t\t\t\tdata: item.data,\n\t\t\t\t\tauthorId: item.authorId,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t\tdeletedAt: item.deletedAt,\n\t\t\t\t})),\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content list trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count trashed content items\n */\nexport async function handleContentCountTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countTrashed(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Schedule content for future publishing\n */\nexport async function handleContentSchedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tscheduledAt: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst existing = await repo.findByIdOrSlug(collection, id);\n\t\t\tconst resolvedId = existing?.id ?? id;\n\t\t\tif (existing) {\n\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, existing.slug);\n\t\t\t}\n\t\t\treturn repo.schedule(collection, resolvedId, scheduledAt);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content schedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_SCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to schedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unschedule content (revert to draft)\n */\nexport async function handleContentUnschedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unschedule(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content unschedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNSCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to unschedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Publish content immediately.\n *\n * Publication is one atomic content-row statement. On databases that support\n * transactions, the existing slug-redirect side write remains grouped with it.\n */\nexport async function handleContentPublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\toptions: {\n\t\tpublishedAt?: string;\n\t\trequireScheduledDue?: boolean;\n\t\texpectedScheduledAt?: string;\n\t} = {},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\n\t\t\t// Capture the pre-publish state. For revision-supporting collections a\n\t\t\t// slug edit is staged as `_slug` in the draft revision and only lands\n\t\t\t// on the live `slug` column here, inside `repo.publish()` — it never\n\t\t\t// passes through handleContentUpdate, where slug-change auto-redirects\n\t\t\t// are normally created.\n\t\t\tconst existing = await repo.findById(collection, resolvedId);\n\n\t\t\tconst published = await repo.publish(\n\t\t\t\tcollection,\n\t\t\t\tresolvedId,\n\t\t\t\toptions.publishedAt,\n\t\t\t\toptions.requireScheduledDue,\n\t\t\t\toptions.expectedScheduledAt,\n\t\t\t\tpublishConfig.supportsRevisions,\n\t\t\t\tpublishConfig.routable,\n\t\t\t);\n\n\t\t\t// Leave a 301 behind when publishing changed the slug of an entry that\n\t\t\t// was already published — its old URL was live and may be indexed or\n\t\t\t// linked. A first publish is excluded: a draft's URL was never public.\n\t\t\tif (\n\t\t\t\texisting?.status === \"published\" &&\n\t\t\t\texisting.slug &&\n\t\t\t\tpublished.slug &&\n\t\t\t\texisting.slug !== published.slug\n\t\t\t) {\n\t\t\t\tawait createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId);\n\t\t\t}\n\n\t\t\treturn published;\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof ContentMutationConflictError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\t// The scheduled sweep gates publish on the row still being due; a row\n\t\t// unscheduled in the meantime is a silent skip, not a failure.\n\t\tif (error instanceof ScheduledNotDueError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_DUE\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\t// The staged-slug pre-check tags its error so it maps to the same\n\t\t\t// 409 SLUG_CONFLICT as direct slug edits in create/update.\n\t\t\tconst details: unknown = error.details;\n\t\t\tconst isSlugConflict =\n\t\t\t\ttypeof details === \"object\" &&\n\t\t\t\tdetails !== null &&\n\t\t\t\t\"code\" in details &&\n\t\t\t\tdetails.code === \"SLUG_CONFLICT\";\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: isSlugConflict ? \"SLUG_CONFLICT\" : \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\t// Backstop for the pre-check inside repo.publish(): a concurrent write\n\t\t// can still take the slug between the check and the UPDATE, in which\n\t\t// case the `(slug, locale)` unique constraint fires. Same fingerprint\n\t\t// mapping as create/update — never a raw SQLite error to the client.\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (\n\t\t\t(message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) &&\n\t\t\tmessage.includes(\"slug\")\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\tmessage: `The staged slug is already used by another entry in collection '${collection}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content publish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_PUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to publish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unpublish content (revert to draft).\n *\n * Wrapped in a transaction — unpublish may create a draft revision\n * from the live version then update the status, which is multi-step.\n */\nexport async function handleContentUnpublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unpublish(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content unpublish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNPUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to unpublish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count scheduled content items\n */\nexport async function handleContentCountScheduled(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countScheduled(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count scheduled error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count scheduled content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Discard draft changes (revert to live version)\n */\nexport async function handleContentDiscardDraft(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.discardDraft(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content discard draft error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DISCARD_DRAFT_ERROR\",\n\t\t\t\tmessage: \"Failed to discard draft\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Compare live and draft revisions\n */\nexport async function handleContentCompare(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\thasChanges: boolean;\n\t\tlive: Record<string, unknown> | null;\n\t\tdraft: Record<string, unknown> | null;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst entry = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!entry) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(db);\n\n\t\tconst live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;\n\t\tconst draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\thasChanges:\n\t\t\t\t\tentry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,\n\t\t\t\tlive: live?.data ?? null,\n\t\t\t\tdraft: draft?.data ?? null,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content compare error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COMPARE_ERROR\",\n\t\t\t\tmessage: \"Failed to compare revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get all translations for a content item.\n * Returns the item's translation group members with locale and status info.\n */\nexport async function handleContentTranslations(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\ttranslationGroup: string;\n\t\ttranslations: Array<{\n\t\t\tid: string;\n\t\t\tlocale: string | null;\n\t\t\tslug: string | null;\n\t\t\tstatus: string;\n\t\t\tupdatedAt: string;\n\t\t}>;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (!item.translationGroup) {\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\ttranslationGroup: item.id,\n\t\t\t\t\ttranslations: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\t\t\tslug: item.slug,\n\t\t\t\t\t\t\tstatus: item.status,\n\t\t\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst translations = await repo.findTranslations(collection, item.translationGroup);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: item.translationGroup,\n\t\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\t\tid: t.id,\n\t\t\t\t\tlocale: t.locale,\n\t\t\t\t\tslug: t.slug,\n\t\t\t\t\tstatus: t.status,\n\t\t\t\t\tupdatedAt: t.updatedAt,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tconsole.error(\"Content translations error:\", error);\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_TRANSLATIONS_ERROR\",\n\t\t\t\tmessage: \"Failed to get translations\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Non-translatable field sync\n// ---------------------------------------------------------------------------\n\n/**\n * Sync non-translatable fields to sibling locales.\n *\n * When a content item is updated and it belongs to a translation group,\n * any non-translatable fields in the update data are written to all other\n * rows in the same translation group within the same transaction.\n *\n * Non-translatable fields are **copied, not linked** — each row owns its\n * own data. This keeps queries simple and avoids cross-row joins.\n */\nasync function syncNonTranslatableFields(\n\ttrx: Kysely<Database>,\n\tcollectionSlug: string,\n\tupdatedItemId: string,\n\ttranslationGroup: string,\n\tdata: Record<string, unknown>,\n): Promise<void> {\n\t// Get the collection to find its fields\n\tconst collection = await trx\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"id\")\n\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t.executeTakeFirst();\n\n\tif (!collection) return;\n\n\t// Find non-translatable fields that are present in the update data\n\tconst fields = await trx\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select(\"slug\")\n\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t.where(\"translatable\", \"=\", 0)\n\t\t.execute();\n\n\tconst nonTranslatableSlugs = fields.map((f) => f.slug);\n\tif (nonTranslatableSlugs.length === 0) return;\n\n\t// Filter to only the non-translatable fields present in this update\n\tconst syncData: Record<string, unknown> = {};\n\tfor (const slug of nonTranslatableSlugs) {\n\t\tif (slug in data) {\n\t\t\tsyncData[slug] = data[slug];\n\t\t}\n\t}\n\tif (Object.keys(syncData).length === 0) return;\n\n\t// Build the SET clause for sibling rows\n\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\tconst tableName = `ec_${collectionSlug}`;\n\n\t// Update all sibling rows (same translation_group, different id)\n\tconst setClauses = Object.entries(syncData).map(([key, value]) => {\n\t\tvalidateIdentifier(key, \"field slug\");\n\t\tconst serialized = typeof value === \"object\" && value !== null ? JSON.stringify(value) : value;\n\t\treturn sql`${sql.ref(key)} = ${serialized}`;\n\t});\n\n\tawait sql`\n\t\tUPDATE ${sql.ref(tableName)}\n\t\tSET ${sql.join(setClauses, sql`, `)}\n\t\tWHERE translation_group = ${translationGroup}\n\t\tAND id != ${updatedItemId}\n\t`.execute(trx);\n}\n\n/**\n * Resolve a `{ taxonomyName: [slug, ...] }` map to term IDs and replace the\n * entry's assignments for each named taxonomy.\n *\n * Shared by handleContentCreate and handleContentUpdate so both MCP entry\n * points behave identically. Slug resolution is scoped to `locale`; passing\n * `undefined` lets `findBySlug` fall back to its default (lowest locale code)\n * so callers on single-locale sites don't need to know the site's default.\n *\n * Throws EmDashValidationError on unknown slug or wrong shape; the calling\n * handler translates that into a VALIDATION_ERROR response.\n */\nasync function assignTaxonomies(\n\ttrx: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tlocale: string | undefined,\n\ttaxonomies: Record<string, string[]>,\n): Promise<void> {\n\tconst taxRepo = new TaxonomyRepository(trx);\n\tlet anyChange = false;\n\n\tfor (const [taxonomyName, slugs] of Object.entries(taxonomies)) {\n\t\tif (!Array.isArray(slugs)) {\n\t\t\tthrow new EmDashValidationError(`taxonomies.${taxonomyName} must be an array of term slugs`);\n\t\t}\n\n\t\tconst termIds: string[] = [];\n\t\tfor (const slug of slugs) {\n\t\t\tif (typeof slug !== \"string\" || slug.length === 0) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`taxonomies.${taxonomyName} contains a non-string or empty slug`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst term = await taxRepo.findBySlug(taxonomyName, slug, locale);\n\t\t\tif (!term) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Unknown taxonomy term: ${taxonomyName}='${slug}'${\n\t\t\t\t\t\tlocale ? ` (locale '${locale}')` : \"\"\n\t\t\t\t\t}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\ttermIds.push(term.id);\n\t\t}\n\n\t\tawait taxRepo.setTermsForEntry(collection, entryId, taxonomyName, termIds);\n\t\tanyChange = true;\n\t}\n\n\t// Match the REST route's behaviour: taxonomy term assignments changed,\n\t// so invalidate the taxonomy object cache used during hydration.\n\tif (anyChange) invalidateTermCache();\n}\n","/**\n * Manifest generation handlers\n */\n\nimport { hashString } from \"../../utils/hash.js\";\nimport type { ManifestResponse, FieldDescriptor } from \"../types.js\";\n\n/** Pattern to add spaces before capital letters */\nconst CAMEL_CASE_PATTERN = /([A-Z])/g;\nconst FIRST_CHAR_PATTERN = /^./;\n\n// Collection definition shape for manifest generation\ninterface CollectionDefinition {\n\tschema: {\n\t\t_def?: { shape?: () => Record<string, unknown> };\n\t\tshape?: Record<string, unknown>;\n\t};\n\tadmin: {\n\t\tlabel: string;\n\t\tlabelSingular?: string;\n\t\tsupports?: string[];\n\t\troutable?: boolean;\n\t};\n}\ntype CollectionMap = Record<string, CollectionDefinition>;\n\n/**\n * Generate admin manifest from collections\n */\nexport async function generateManifest(\n\tcollections: CollectionMap,\n\tplugins: Record<\n\t\tstring,\n\t\t{\n\t\t\tadminPages?: Array<{ path: string; component: string }>;\n\t\t\twidgets?: string[];\n\t\t}\n\t> = {},\n): Promise<ManifestResponse> {\n\tconst manifestCollections: ManifestResponse[\"collections\"] = {};\n\n\tfor (const [name, definition] of Object.entries(collections)) {\n\t\t// Extract field descriptors from Zod schema\n\t\tconst fields = extractFieldDescriptors(definition.schema);\n\n\t\tmanifestCollections[name] = {\n\t\t\tlabel: definition.admin.label,\n\t\t\tlabelSingular: definition.admin.labelSingular || definition.admin.label,\n\t\t\tsupports: definition.admin.supports || [],\n\t\t\troutable: definition.admin.routable ?? true,\n\t\t\tfields,\n\t\t};\n\t}\n\n\t// Generate hash from collections (for cache invalidation)\n\tconst hash = await hashString(JSON.stringify(manifestCollections));\n\n\treturn {\n\t\tversion: \"0.1.0\",\n\t\thash,\n\t\tcollections: manifestCollections,\n\t\tplugins,\n\t};\n}\n\n/**\n * Extract field descriptors from Zod schema\n * Note: This is a simplified implementation that handles common types\n */\nfunction extractFieldDescriptors(schema: {\n\t_def?: { shape?: () => Record<string, unknown> };\n\tshape?: Record<string, unknown>;\n}): Record<string, FieldDescriptor> {\n\tconst fields: Record<string, FieldDescriptor> = {};\n\n\t// Handle Zod object schema\n\tconst shape = typeof schema._def?.shape === \"function\" ? schema._def.shape() : schema.shape || {};\n\n\tfor (const [name, fieldSchema] of Object.entries(shape)) {\n\t\tfields[name] = extractFieldType(name, fieldSchema);\n\t}\n\n\treturn fields;\n}\n\n/**\n * Extract field type from Zod schema\n */\n/** Type guard: check if a value is a non-null object */\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction extractFieldType(name: string, schema: unknown): FieldDescriptor {\n\tif (!isObject(schema)) {\n\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n\n\t// Check for custom field markers\n\tif (schema.isPortableText) {\n\t\treturn { kind: \"portableText\", label: formatLabel(name) };\n\t}\n\tif (schema.isImage) {\n\t\treturn { kind: \"image\", label: formatLabel(name) };\n\t}\n\tif (schema.isReference) {\n\t\treturn { kind: \"reference\", label: formatLabel(name) };\n\t}\n\n\t// Handle standard Zod types\n\tconst def = isObject(schema._def) ? schema._def : undefined;\n\tconst typeName = typeof def?.typeName === \"string\" ? def.typeName : undefined;\n\n\tswitch (typeName) {\n\t\tcase \"ZodString\":\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tcase \"ZodNumber\":\n\t\t\treturn { kind: \"number\", label: formatLabel(name) };\n\t\tcase \"ZodBoolean\":\n\t\t\treturn { kind: \"boolean\", label: formatLabel(name) };\n\t\tcase \"ZodDate\":\n\t\t\treturn { kind: \"datetime\", label: formatLabel(name) };\n\t\tcase \"ZodEnum\": {\n\t\t\tconst values = Array.isArray(def?.values) ? def.values : [];\n\t\t\treturn {\n\t\t\t\tkind: \"select\",\n\t\t\t\tlabel: formatLabel(name),\n\t\t\t\toptions: values\n\t\t\t\t\t.filter((v): v is string => typeof v === \"string\")\n\t\t\t\t\t.map((v) => ({\n\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t})),\n\t\t\t};\n\t\t}\n\t\tcase \"ZodArray\":\n\t\t\treturn { kind: \"array\", label: formatLabel(name) };\n\t\tcase \"ZodObject\":\n\t\t\treturn { kind: \"object\", label: formatLabel(name) };\n\t\tcase \"ZodOptional\":\n\t\tcase \"ZodDefault\":\n\t\t\t// Unwrap optional/default types\n\t\t\tif (def?.innerType) {\n\t\t\t\treturn extractFieldType(name, def.innerType);\n\t\t\t}\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tdefault:\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n}\n\n/**\n * Format field name as label\n */\nfunction formatLabel(name: string): string {\n\treturn name\n\t\t.replace(CAMEL_CASE_PATTERN, \" $1\")\n\t\t.replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase())\n\t\t.trim();\n}\n","/**\n * Revision history handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../../after.js\";\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RevisionRepository, type Revision } from \"../../database/repositories/revision.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult, ContentResponse } from \"../types.js\";\n\nexport interface RevisionListResponse {\n\titems: Revision[];\n\ttotal: number;\n}\n\nexport interface RevisionResponse {\n\titem: Revision;\n}\n\n/**\n * List revisions for a content entry\n */\nexport async function handleRevisionList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tparams: { limit?: number } = {},\n): Promise<ApiResult<RevisionListResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst [items, total] = await Promise.all([\n\t\t\trepo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }),\n\t\t\trepo.countByEntry(collection, entryId),\n\t\t]);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items, total },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a specific revision\n */\nexport async function handleRevisionGet(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n): Promise<ApiResult<RevisionResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst item = await repo.findById(revisionId);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore a revision (updates content to this revision's data and creates new revision)\n */\nexport async function handleRevisionRestore(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n\tcallerUserId: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst revisionRepo = new RevisionRepository(db);\n\n\t\t// Get the revision\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Extract _slug from revision data (stored as metadata, not a real column)\n\t\tconst { _slug, ...fieldData } = revision.data;\n\n\t\t// Atomically update content and create a new revision to record the restore.\n\t\t// If either operation fails, neither is committed (on engines that support\n\t\t// transactions; on D1, withTransaction falls back to sequential execution).\n\t\tconst { item, queuedRevisionId } = await withTransaction(db, async (trx) => {\n\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\tconst trxRevisionRepo = new RevisionRepository(trx);\n\n\t\t\tconst updated = await trxContentRepo.update(revision.collection, revision.entryId, {\n\t\t\t\tdata: fieldData,\n\t\t\t\tslug: typeof _slug === \"string\" ? _slug : undefined,\n\t\t\t});\n\n\t\t\tconst queuedRevision = await trxRevisionRepo.create({\n\t\t\t\tcollection: revision.collection,\n\t\t\t\tentryId: revision.entryId,\n\t\t\t\tdata: revision.data,\n\t\t\t\tauthorId: callerUserId,\n\t\t\t});\n\n\t\t\treturn { item: updated, queuedRevisionId: queuedRevision.id };\n\t\t});\n\n\t\tconst pruneRepo = new RevisionRepository(db);\n\t\tafter(async () => {\n\t\t\ttry {\n\t\t\t\tawait pruneRepo.pruneQueuedEntry(\n\t\t\t\t\trevision.collection,\n\t\t\t\t\trevision.entryId,\n\t\t\t\t\tqueuedRevisionId,\n\t\t\t\t\t50,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Media CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { isValidFocalPointUpdate, type FocalPointUpdate } from \"../../media/focal-point.js\";\nimport type { ApiResult } from \"../types.js\";\n\nconst FOREIGN_KEY_VIOLATION_RE = /foreign key constraint failed/i;\n\nexport interface MediaListResponse {\n\titems: MediaItem[];\n\tnextCursor?: string;\n\ttotalCount?: number;\n}\n\nexport interface MediaResponse {\n\titem: MediaItem;\n}\n\n/**\n * List media items\n */\nexport async function handleMediaList(\n\tdb: Kysely<Database>,\n\tparams: {\n\t\tcursor?: string;\n\t\tpage?: number;\n\t\tlimit?: number;\n\t\tmimeType?: string | readonly string[];\n\t\tq?: string;\n\t\tfolderId?: string | null;\n\t},\n): Promise<ApiResult<MediaListResponse>> {\n\ttry {\n\t\tif (params.page !== undefined) {\n\t\t\tconst limit = Math.min(params.limit || 50, 100);\n\t\t\tconst offset = (params.page - 1) * limit;\n\t\t\tif (\n\t\t\t\tparams.cursor !== undefined ||\n\t\t\t\t!Number.isSafeInteger(params.page) ||\n\t\t\t\tparams.page < 1 ||\n\t\t\t\t!Number.isSafeInteger(offset)\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: \"Invalid media page\" },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst repo = new MediaRepository(db);\n\t\t\tconst result = await repo.findPage({\n\t\t\t\tpage: params.page,\n\t\t\t\tlimit,\n\t\t\t\tmimeType: params.mimeType,\n\t\t\t\tq: params.q,\n\t\t\t\tfolderId: params.folderId,\n\t\t\t});\n\t\t\treturn { success: true, data: result };\n\t\t}\n\n\t\tconst repo = new MediaRepository(db);\n\t\tconst result = await repo.findMany({\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: Math.min(params.limit || 50, 100),\n\t\t\tmimeType: params.mimeType,\n\t\t\tq: params.q,\n\t\t\tfolderId: params.folderId,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single media item\n */\nexport async function handleMediaGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.findById(id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create media item (after file upload)\n */\nexport async function handleMediaCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\talt?: string;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t\tauthorId?: string;\n\t},\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.create(input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update media metadata\n */\nexport async function handleMediaUpdate(\n\tdb: Kysely<Database>,\n\tid: string,\n\tinput: {\n\t\talt?: string;\n\t\tcaption?: string;\n\t\twidth?: number;\n\t\theight?: number;\n\t\tfolderId?: string | null;\n\t} & FocalPointUpdate,\n): Promise<ApiResult<MediaResponse>> {\n\tif (!isValidFocalPointUpdate(input)) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\tmessage: \"focalX and focalY must both be valid numbers or both be null\",\n\t\t\t},\n\t\t};\n\t}\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.update(id, input);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (isForeignKeyViolation(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Media folder not found\" },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\nfunction isForeignKeyViolation(error: unknown): boolean {\n\tif (error && typeof error === \"object\") {\n\t\tif (\"code\" in error && error.code === \"23503\") return true;\n\t}\n\tconst message = error instanceof Error ? error.message : \"\";\n\tif (FOREIGN_KEY_VIOLATION_RE.test(message)) return true;\n\treturn Boolean(\n\t\terror && typeof error === \"object\" && \"cause\" in error && isForeignKeyViolation(error.cause),\n\t);\n}\n\n/**\n * Delete media item\n */\nexport async function handleMediaDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true; storageKey: string }>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst storageKey = await repo.deleteWithStorageKey(id);\n\n\t\tif (!storageKey) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true, storageKey },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete media\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Plugin management handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport type { SandboxedPluginEntry } from \"../../emdash-runtime.js\";\nimport { PluginStateRepository, type PluginState, type PluginStatus } from \"../../plugins/state.js\";\nimport type { ResolvedPlugin } from \"../../plugins/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface PluginInfo {\n\tid: string;\n\tname: string;\n\tversion: string;\n\tpackage?: string;\n\tenabled: boolean;\n\tstatus: PluginStatus;\n\tsource?: \"config\" | \"marketplace\" | \"registry\";\n\t/** True for statically-sandboxed plugins (registered via `sandboxed: []`) */\n\tsandboxed?: boolean;\n\tmarketplaceVersion?: string;\n\t/** Publisher DID, for registry-source plugins */\n\tregistryPublisherDid?: string;\n\t/** Publisher slug, for registry-source plugins */\n\tregistrySlug?: string;\n\tcapabilities: string[];\n\thasAdminPages: boolean;\n\thasDashboardWidgets: boolean;\n\thasHooks: boolean;\n\t/** True when the plugin declares `admin.settingsSchema` (auto-generated settings form) */\n\thasSettings: boolean;\n\tinstalledAt?: string;\n\tactivatedAt?: string;\n\tdeactivatedAt?: string;\n\t/** Description of what the plugin does */\n\tdescription?: string;\n\t/** URL to the plugin icon on the marketplace */\n\ticonUrl?: string;\n\tmcpToolsEnabled: boolean;\n\tmcpTools: Array<{\n\t\tname: string;\n\t\tdescription: string;\n\t\troute: string;\n\t\tpermission: string;\n\t\tdestructive: boolean;\n\t}>;\n}\n\nexport interface PluginListResponse {\n\titems: PluginInfo[];\n}\n\nexport interface PluginResponse {\n\titem: PluginInfo;\n}\n\nfunction marketplaceIconUrl(marketplaceUrl: string, pluginId: string): string {\n\treturn `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;\n}\n\n/**\n * Get plugin info from configured plugin and database state\n */\nfunction buildPluginInfo(\n\tplugin: ResolvedPlugin,\n\tstate: PluginState | null,\n\tmarketplaceUrl?: string,\n): PluginInfo {\n\t// If no state exists, plugin is considered active (default on first run)\n\tconst status = state?.status ?? \"active\";\n\tconst enabled = status === \"active\";\n\tconst isMarketplace = (state?.source ?? \"config\") === \"marketplace\";\n\n\treturn {\n\t\tid: plugin.id,\n\t\tname: state?.displayName || plugin.id,\n\t\tversion: plugin.version,\n\t\tpackage: undefined, // v2 doesn't have package field\n\t\tenabled,\n\t\tstatus,\n\t\tsource: state?.source ?? \"config\",\n\t\tmarketplaceVersion: state?.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state?.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state?.registrySlug ?? undefined,\n\t\tcapabilities: plugin.capabilities,\n\t\thasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,\n\t\thasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,\n\t\thasHooks: Object.keys(plugin.hooks ?? {}).length > 0,\n\t\thasSettings: Object.keys(plugin.admin.settingsSchema ?? {}).length > 0,\n\t\tinstalledAt: state?.installedAt?.toISOString(),\n\t\tactivatedAt: state?.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state?.description ?? undefined,\n\t\ticonUrl:\n\t\t\tisMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : undefined,\n\t\tmcpToolsEnabled: state?.mcpToolsEnabled ?? false,\n\t\tmcpTools: Object.entries(plugin.mcp?.tools ?? {}).flatMap(([name, tool]) => {\n\t\t\tconst permission = plugin.routes[tool.route]?.permission;\n\t\t\treturn permission\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\t\troute: tool.route,\n\t\t\t\t\t\t\tpermission,\n\t\t\t\t\t\t\tdestructive: tool.destructive ?? false,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\t}),\n\t};\n}\n\n/**\n * Build plugin info for a statically-sandboxed plugin entry\n */\nfunction buildSandboxedPluginInfo(\n\tentry: SandboxedPluginEntry,\n\tstate: PluginState | null,\n): PluginInfo {\n\tconst status = state?.status ?? \"active\";\n\tconst enabled = status === \"active\";\n\n\treturn {\n\t\tid: entry.id,\n\t\tname: state?.displayName || entry.id,\n\t\tversion: entry.version,\n\t\tpackage: undefined, // v2 doesn't have package field\n\t\tenabled,\n\t\tstatus,\n\t\tsource: \"config\",\n\t\tsandboxed: true,\n\t\tcapabilities: entry.capabilities,\n\t\thasAdminPages: (entry.adminPages?.length ?? 0) > 0,\n\t\thasDashboardWidgets: (entry.adminWidgets?.length ?? 0) > 0,\n\t\thasHooks: false,\n\t\thasSettings: Object.keys(entry.settingsSchema ?? {}).length > 0,\n\t\tinstalledAt: state?.installedAt?.toISOString(),\n\t\tactivatedAt: state?.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state?.description ?? undefined,\n\t\tmcpToolsEnabled: state?.mcpToolsEnabled ?? false,\n\t\tmcpTools: entry.mcp?.tools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool) ?? [],\n\t};\n}\n\n/**\n * List all configured plugins with their state\n */\nexport async function handlePluginList(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tmarketplaceUrl?: string,\n\t/**\n\t * Settings-schema lookup for runtime-installed (marketplace/registry)\n\t * plugins, which aren't in either build-time list. Typically\n\t * `EmDashRuntime.getRuntimePluginSettingsSchema`.\n\t */\n\truntimeSettingsSchemaLookup?: (pluginId: string) => Record<string, unknown> | null,\n): Promise<ApiResult<PluginListResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst allStates = await stateRepo.getAll();\n\t\tconst stateMap = new Map(allStates.map((s) => [s.pluginId, s]));\n\n\t\tconst configuredIds = new Set(configuredPlugins.map((p) => p.id));\n\n\t\tconst items = configuredPlugins.map((plugin) => {\n\t\t\tconst state = stateMap.get(plugin.id) ?? null;\n\t\t\treturn buildPluginInfo(plugin, state, marketplaceUrl);\n\t\t});\n\n\t\t// Include statically-sandboxed plugins (registered via `sandboxed: []`\n\t\t// in astro.config.mjs).\n\t\tfor (const entry of sandboxedPluginEntries) {\n\t\t\tif (configuredIds.has(entry.id)) continue;\n\t\t\tconfiguredIds.add(entry.id);\n\t\t\titems.push(buildSandboxedPluginInfo(entry, stateMap.get(entry.id) ?? null));\n\t\t}\n\n\t\t// Include runtime-installed plugins (marketplace or registry) that\n\t\t// aren't in the configured plugins list.\n\t\tfor (const state of allStates) {\n\t\t\tif (state.source !== \"marketplace\" && state.source !== \"registry\") continue;\n\t\t\tif (configuredIds.has(state.pluginId)) continue;\n\n\t\t\titems.push({\n\t\t\t\tid: state.pluginId,\n\t\t\t\tname: state.displayName || state.pluginId,\n\t\t\t\tversion: state.marketplaceVersion ?? state.version,\n\t\t\t\tenabled: state.status === \"active\",\n\t\t\t\tstatus: state.status,\n\t\t\t\tsource: state.source,\n\t\t\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\t\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\t\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\t\t\tcapabilities: [],\n\t\t\t\thasAdminPages: false,\n\t\t\t\thasDashboardWidgets: false,\n\t\t\t\thasHooks: false,\n\t\t\t\thasSettings: Object.keys(runtimeSettingsSchemaLookup?.(state.pluginId) ?? {}).length > 0,\n\t\t\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\t\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\t\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\t\t\tdescription: state.description ?? undefined,\n\t\t\t\ticonUrl:\n\t\t\t\t\tstate.source === \"marketplace\" && marketplaceUrl\n\t\t\t\t\t\t? marketplaceIconUrl(marketplaceUrl, state.pluginId)\n\t\t\t\t\t\t: undefined,\n\t\t\t\tmcpToolsEnabled: state.mcpToolsEnabled,\n\t\t\t\tmcpTools: [],\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list plugins\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a single plugin's info\n */\nexport async function handlePluginGet(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n\tmarketplaceUrl?: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.get(pluginId);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { item: buildPluginInfo(plugin, state, marketplaceUrl) },\n\t\t\t};\n\t\t}\n\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.get(pluginId);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { item: buildSandboxedPluginInfo(sandboxed, state) },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\tmessage: `Plugin not found: ${pluginId}`,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Build a minimal `PluginInfo` for a plugin that exists only as a\n * `_plugin_state` row (marketplace or registry install), with no\n * matching `configuredPlugins` entry. Runtime-installed plugins don't\n * have ResolvedPlugin metadata until they're loaded into the sandbox,\n * so the enable/disable response surfaces the state-row view as a\n * stable shape the admin UI already understands.\n */\nfunction buildStateOnlyPluginInfo(\n\tstate: NonNullable<Awaited<ReturnType<PluginStateRepository[\"get\"]>>>,\n): PluginInfo {\n\treturn {\n\t\tid: state.pluginId,\n\t\tname: state.displayName || state.pluginId,\n\t\tversion: state.marketplaceVersion ?? state.version,\n\t\tenabled: state.status === \"active\",\n\t\tstatus: state.status,\n\t\tsource: state.source,\n\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\tcapabilities: [],\n\t\thasAdminPages: false,\n\t\thasDashboardWidgets: false,\n\t\thasHooks: false,\n\t\thasSettings: false,\n\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state.description ?? undefined,\n\t\tmcpToolsEnabled: state.mcpToolsEnabled,\n\t\tmcpTools: [],\n\t};\n}\n\n/**\n * Enable a plugin\n */\nexport async function handlePluginEnable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\t// Configured plugin: use its version as the source of truth.\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.enable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\t// Statically-sandboxed plugin: addressable via its build-time entry.\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.enable(pluginId, sandboxed.version);\n\t\t\treturn { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };\n\t\t}\n\n\t\t// Runtime-installed plugin (marketplace or registry): only\n\t\t// addressable through the state row. Fall back to the existing\n\t\t// version recorded there.\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tconst enabled = await stateRepo.enable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(enabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_ENABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to enable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Disable a plugin\n */\nexport async function handlePluginDisable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.disable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.disable(pluginId, sandboxed.version);\n\t\t\treturn { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };\n\t\t}\n\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tconst disabled = await stateRepo.disable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(disabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_DISABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to disable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Plugin settings handlers\n *\n * Auto-generated settings UI backend for plugins that declare\n * `admin.settingsSchema`. Values are stored in the options table under\n * `plugin:{pluginId}:settings:{key}` — the same keys the plugin itself\n * reads via `ctx.kv.get(\"settings:{key}\")`.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { SandboxedPluginEntry } from \"../../emdash-runtime.js\";\nimport type { ResolvedPlugin, SettingField } from \"../../plugins/types.js\";\nimport { ErrorCode } from \"../errors.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface PluginSettingsResponse {\n\t/** The plugin's declared settings schema, keyed by setting name */\n\tschema: Record<string, SettingField>;\n\t/**\n\t * Current values keyed by setting name. Secret fields are never\n\t * included here — check `secretsSet` instead.\n\t */\n\tvalues: Record<string, unknown>;\n\t/** For secret fields: whether a value is currently stored */\n\tsecretsSet: Record<string, boolean>;\n}\n\nfunction settingsKey(pluginId: string, key: string): string {\n\treturn `plugin:${pluginId}:settings:${key}`;\n}\n\n/**\n * Resolve a plugin's settings schema from either the configured\n * (in-process) plugin list or the statically-sandboxed entries.\n * Returns null when the plugin doesn't exist, undefined-equivalent\n * empty object when it declares no schema.\n */\nexport function getPluginSettingsSchema(\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Record<string, SettingField> | null {\n\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\tif (plugin) return plugin.admin.settingsSchema ?? {};\n\n\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\tif (sandboxed) return sandboxed.settingsSchema ?? {};\n\n\treturn null;\n}\n\n/**\n * Validate a single value against its schema field.\n * Returns an error message, or null when valid.\n */\nfunction validateValue(key: string, field: SettingField, value: unknown): string | null {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"secret\":\n\t\tcase \"url\":\n\t\tcase \"email\":\n\t\t\tif (typeof value !== \"string\") return `Setting \"${key}\" must be a string`;\n\t\t\tif (field.type === \"url\" && value !== \"\" && !URL.canParse(value)) {\n\t\t\t\treturn `Setting \"${key}\" must be a valid URL`;\n\t\t\t}\n\t\t\tif (field.type === \"email\" && value !== \"\" && !value.includes(\"@\")) {\n\t\t\t\treturn `Setting \"${key}\" must be a valid email address`;\n\t\t\t}\n\t\t\treturn null;\n\t\tcase \"number\": {\n\t\t\tif (typeof value !== \"number\" || Number.isNaN(value)) {\n\t\t\t\treturn `Setting \"${key}\" must be a number`;\n\t\t\t}\n\t\t\tif (field.min !== undefined && value < field.min) {\n\t\t\t\treturn `Setting \"${key}\" must be at least ${field.min}`;\n\t\t\t}\n\t\t\tif (field.max !== undefined && value > field.max) {\n\t\t\t\treturn `Setting \"${key}\" must be at most ${field.max}`;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\tcase \"boolean\":\n\t\t\treturn typeof value === \"boolean\" ? null : `Setting \"${key}\" must be a boolean`;\n\t\tcase \"select\":\n\t\t\tif (typeof value !== \"string\" || !field.options.some((o) => o.value === value)) {\n\t\t\t\treturn `Setting \"${key}\" must be one of the defined options`;\n\t\t\t}\n\t\t\treturn null;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = field;\n\t\t\treturn `Setting \"${key}\" has an unknown field type`;\n\t\t}\n\t}\n}\n\nasync function buildSettingsResponse(\n\toptionsRepo: OptionsRepository,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n): Promise<PluginSettingsResponse> {\n\tconst keys = Object.keys(schema);\n\tconst stored = await optionsRepo.getMany(keys.map((key) => settingsKey(pluginId, key)));\n\n\tconst values: Record<string, unknown> = {};\n\tconst secretsSet: Record<string, boolean> = {};\n\n\tfor (const key of keys) {\n\t\tconst field = schema[key];\n\t\tif (!field) continue;\n\t\tconst storedValue = stored.get(settingsKey(pluginId, key));\n\n\t\tif (field.type === \"secret\") {\n\t\t\tsecretsSet[key] = typeof storedValue === \"string\" && storedValue.length > 0;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (storedValue !== undefined && storedValue !== null) {\n\t\t\tvalues[key] = storedValue;\n\t\t} else if (\"default\" in field && field.default !== undefined) {\n\t\t\tvalues[key] = field.default;\n\t\t} else {\n\t\t\tvalues[key] = null;\n\t\t}\n\t}\n\n\treturn { schema, values, secretsSet };\n}\n\n/**\n * Get a plugin's settings (schema + current values, secrets masked)\n */\nexport async function handlePluginSettingsGet(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n): Promise<ApiResult<PluginSettingsResponse>> {\n\ttry {\n\t\tconst optionsRepo = new OptionsRepository(db);\n\t\treturn { success: true, data: await buildSettingsResponse(optionsRepo, pluginId, schema) };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: ErrorCode.PLUGIN_SETTINGS_READ_ERROR,\n\t\t\t\tmessage: \"Failed to read plugin settings\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update a plugin's settings.\n *\n * Only keys present in `updates` are written. A `null` value deletes the\n * stored value (reverting to the schema default). Secret fields are\n * write-only: they accept a new string value or `null` to clear, and the\n * response never echoes them back.\n */\nexport async function handlePluginSettingsUpdate(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n\tupdates: Record<string, unknown>,\n): Promise<ApiResult<PluginSettingsResponse>> {\n\ttry {\n\t\t// Validate everything before writing anything.\n\t\tfor (const [key, value] of Object.entries(updates)) {\n\t\t\tconst field = schema[key];\n\t\t\tif (!field) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: ErrorCode.VALIDATION_ERROR,\n\t\t\t\t\t\tmessage: `Unknown setting \"${key}\" for plugin \"${pluginId}\"`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (value === null) continue;\n\t\t\tconst error = validateValue(key, field, value);\n\t\t\tif (error) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { code: ErrorCode.VALIDATION_ERROR, message: error },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Wrap the writes + read-back in a transaction so a partial failure\n\t\t// can't leave some settings updated and others not. On D1\n\t\t// withTransaction degrades to running the callback directly — D1 is\n\t\t// single-writer, so per-statement atomicity still holds.\n\t\tconst data = await withTransaction(db, async (trx) => {\n\t\t\tconst txRepo = new OptionsRepository(trx);\n\t\t\tfor (const [key, value] of Object.entries(updates)) {\n\t\t\t\tif (value === null) {\n\t\t\t\t\tawait txRepo.delete(settingsKey(pluginId, key));\n\t\t\t\t} else {\n\t\t\t\t\tawait txRepo.set(settingsKey(pluginId, key), value);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn buildSettingsResponse(txRepo, pluginId, schema);\n\t\t});\n\n\t\treturn { success: true, data };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: ErrorCode.PLUGIN_SETTINGS_UPDATE_ERROR,\n\t\t\t\tmessage: \"Failed to update plugin settings\",\n\t\t\t},\n\t\t};\n\t}\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as hashApiToken, r as generatePrefixedToken } from "./api-tokens-rIXC6LdE.mjs";
1
+ import { a as hashApiToken, r as generatePrefixedToken } from "./api-tokens-B5NHLNUN.mjs";
2
2
  import { ulid } from "ulidx";
3
3
 
4
4
  //#region src/api/handlers/api-tokens.ts
@@ -160,4 +160,4 @@ async function resolveOAuthToken(db, rawToken) {
160
160
 
161
161
  //#endregion
162
162
  export { resolveApiToken as a, handleApiTokenRevoke as i, handleApiTokenCreate as n, resolveOAuthToken as o, handleApiTokenList as r, deleteApiTokensByName as t };
163
- //# sourceMappingURL=api-tokens-KO24G1Fd.mjs.map
163
+ //# sourceMappingURL=api-tokens-9YrrCoE6.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-tokens-KO24G1Fd.mjs","names":[],"sources":["../src/api/handlers/api-tokens.ts"],"sourcesContent":["/**\n * API token management handlers.\n *\n * Creates, lists, and revokes Personal Access Tokens (PATs).\n * Token format: ec_pat_<base64url>\n * Only the SHA-256 hash is stored — raw token shown once at creation.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { hashApiToken, generatePrefixedToken } from \"../../auth/api-tokens.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ApiTokenInfo {\n\tid: string;\n\tname: string;\n\tprefix: string;\n\tscopes: string[];\n\tuserId: string;\n\texpiresAt: string | null;\n\tlastUsedAt: string | null;\n\tcreatedAt: string;\n}\n\nexport interface ApiTokenCreateResult {\n\t/** The raw token — shown once, never stored */\n\ttoken: string;\n\t/** Token metadata */\n\tinfo: ApiTokenInfo;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new API token for a user.\n */\nexport async function handleApiTokenCreate(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tinput: {\n\t\tname: string;\n\t\tscopes: string[];\n\t\texpiresAt?: string;\n\t},\n): Promise<ApiResult<ApiTokenCreateResult>> {\n\ttry {\n\t\tconst id = ulid();\n\t\tconst { raw, hash, prefix } = generatePrefixedToken(\"ec_pat_\");\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_api_tokens\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tname: input.name,\n\t\t\t\ttoken_hash: hash,\n\t\t\t\tprefix,\n\t\t\t\tuser_id: userId,\n\t\t\t\tscopes: JSON.stringify(input.scopes),\n\t\t\t\texpires_at: input.expiresAt ?? null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst info: ApiTokenInfo = {\n\t\t\tid,\n\t\t\tname: input.name,\n\t\t\tprefix,\n\t\t\tscopes: input.scopes,\n\t\t\tuserId,\n\t\t\texpiresAt: input.expiresAt ?? null,\n\t\t\tlastUsedAt: null,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t};\n\n\t\treturn { success: true, data: { token: raw, info } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create API token\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List all API tokens for a user (never returns the raw token or hash).\n */\nexport async function handleApiTokenList(\n\tdb: Kysely<Database>,\n\tuserId: string,\n): Promise<ApiResult<{ items: ApiTokenInfo[] }>> {\n\ttry {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_api_tokens\")\n\t\t\t.select([\n\t\t\t\t\"id\",\n\t\t\t\t\"name\",\n\t\t\t\t\"prefix\",\n\t\t\t\t\"scopes\",\n\t\t\t\t\"user_id\",\n\t\t\t\t\"expires_at\",\n\t\t\t\t\"last_used_at\",\n\t\t\t\t\"created_at\",\n\t\t\t])\n\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.execute();\n\n\t\tconst items: ApiTokenInfo[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tprefix: row.prefix,\n\t\t\tscopes: JSON.parse(row.scopes) as string[],\n\t\t\tuserId: row.user_id,\n\t\t\texpiresAt: row.expires_at,\n\t\t\tlastUsedAt: row.last_used_at,\n\t\t\tcreatedAt: row.created_at,\n\t\t}));\n\n\t\treturn { success: true, data: { items } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list API tokens\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Revoke (delete) an API token.\n */\nexport async function handleApiTokenRevoke(\n\tdb: Kysely<Database>,\n\ttokenId: string,\n\tuserId: string,\n): Promise<ApiResult<{ revoked: boolean }>> {\n\ttry {\n\t\tconst result = await db\n\t\t\t.deleteFrom(\"_emdash_api_tokens\")\n\t\t\t.where(\"id\", \"=\", tokenId)\n\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (result.numDeletedRows === 0n) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Token not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { revoked: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_REVOKE_ERROR\",\n\t\t\t\tmessage: \"Failed to revoke API token\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete every token with the given name for a user, returning how many were\n * removed. Token names aren't unique, so this is a bulk delete — used by the\n * dev-bypass flow to keep a single `dev-bypass-token` instead of minting a new\n * one on every reset.\n */\nexport async function deleteApiTokensByName(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tname: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_emdash_api_tokens\")\n\t\t.where(\"user_id\", \"=\", userId)\n\t\t.where(\"name\", \"=\", name)\n\t\t.executeTakeFirst();\n\treturn Number(result.numDeletedRows ?? 0n);\n}\n\n/**\n * Resolve a raw API token (ec_pat_...) to a user ID and scopes.\n * Updates last_used_at on successful lookup.\n * Returns null if the token is invalid or expired.\n */\nexport async function resolveApiToken(\n\tdb: Kysely<Database>,\n\trawToken: string,\n): Promise<{ userId: string; scopes: string[] } | null> {\n\tconst hash = hashApiToken(rawToken);\n\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_api_tokens\")\n\t\t.select([\"id\", \"user_id\", \"scopes\", \"expires_at\"])\n\t\t.where(\"token_hash\", \"=\", hash)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\t// Check expiry\n\tif (row.expires_at && new Date(row.expires_at) < new Date()) {\n\t\treturn null;\n\t}\n\n\t// Update last_used_at (fire-and-forget, don't block the request)\n\tdb.updateTable(\"_emdash_api_tokens\")\n\t\t.set({ last_used_at: new Date().toISOString() })\n\t\t.where(\"id\", \"=\", row.id)\n\t\t.execute()\n\t\t.catch(() => {}); // Non-critical, swallow errors\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tscopes: JSON.parse(row.scopes) as string[],\n\t};\n}\n\n/**\n * Resolve an OAuth access token (ec_oat_...) to a user ID and scopes.\n * Returns null if the token is invalid or expired.\n */\nexport async function resolveOAuthToken(\n\tdb: Kysely<Database>,\n\trawToken: string,\n): Promise<{ userId: string; scopes: string[] } | null> {\n\tconst hash = hashApiToken(rawToken);\n\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_oauth_tokens\")\n\t\t.select([\"user_id\", \"scopes\", \"expires_at\", \"token_type\"])\n\t\t.where(\"token_hash\", \"=\", hash)\n\t\t.where(\"token_type\", \"=\", \"access\")\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\t// Check expiry\n\tif (new Date(row.expires_at) < new Date()) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tscopes: JSON.parse(row.scopes) as string[],\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api-tokens-9YrrCoE6.mjs","names":[],"sources":["../src/api/handlers/api-tokens.ts"],"sourcesContent":["/**\n * API token management handlers.\n *\n * Creates, lists, and revokes Personal Access Tokens (PATs).\n * Token format: ec_pat_<base64url>\n * Only the SHA-256 hash is stored — raw token shown once at creation.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { hashApiToken, generatePrefixedToken } from \"../../auth/api-tokens.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ApiTokenInfo {\n\tid: string;\n\tname: string;\n\tprefix: string;\n\tscopes: string[];\n\tuserId: string;\n\texpiresAt: string | null;\n\tlastUsedAt: string | null;\n\tcreatedAt: string;\n}\n\nexport interface ApiTokenCreateResult {\n\t/** The raw token — shown once, never stored */\n\ttoken: string;\n\t/** Token metadata */\n\tinfo: ApiTokenInfo;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new API token for a user.\n */\nexport async function handleApiTokenCreate(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tinput: {\n\t\tname: string;\n\t\tscopes: string[];\n\t\texpiresAt?: string;\n\t},\n): Promise<ApiResult<ApiTokenCreateResult>> {\n\ttry {\n\t\tconst id = ulid();\n\t\tconst { raw, hash, prefix } = generatePrefixedToken(\"ec_pat_\");\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_api_tokens\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tname: input.name,\n\t\t\t\ttoken_hash: hash,\n\t\t\t\tprefix,\n\t\t\t\tuser_id: userId,\n\t\t\t\tscopes: JSON.stringify(input.scopes),\n\t\t\t\texpires_at: input.expiresAt ?? null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst info: ApiTokenInfo = {\n\t\t\tid,\n\t\t\tname: input.name,\n\t\t\tprefix,\n\t\t\tscopes: input.scopes,\n\t\t\tuserId,\n\t\t\texpiresAt: input.expiresAt ?? null,\n\t\t\tlastUsedAt: null,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t};\n\n\t\treturn { success: true, data: { token: raw, info } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create API token\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List all API tokens for a user (never returns the raw token or hash).\n */\nexport async function handleApiTokenList(\n\tdb: Kysely<Database>,\n\tuserId: string,\n): Promise<ApiResult<{ items: ApiTokenInfo[] }>> {\n\ttry {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_api_tokens\")\n\t\t\t.select([\n\t\t\t\t\"id\",\n\t\t\t\t\"name\",\n\t\t\t\t\"prefix\",\n\t\t\t\t\"scopes\",\n\t\t\t\t\"user_id\",\n\t\t\t\t\"expires_at\",\n\t\t\t\t\"last_used_at\",\n\t\t\t\t\"created_at\",\n\t\t\t])\n\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.execute();\n\n\t\tconst items: ApiTokenInfo[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tprefix: row.prefix,\n\t\t\tscopes: JSON.parse(row.scopes) as string[],\n\t\t\tuserId: row.user_id,\n\t\t\texpiresAt: row.expires_at,\n\t\t\tlastUsedAt: row.last_used_at,\n\t\t\tcreatedAt: row.created_at,\n\t\t}));\n\n\t\treturn { success: true, data: { items } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list API tokens\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Revoke (delete) an API token.\n */\nexport async function handleApiTokenRevoke(\n\tdb: Kysely<Database>,\n\ttokenId: string,\n\tuserId: string,\n): Promise<ApiResult<{ revoked: boolean }>> {\n\ttry {\n\t\tconst result = await db\n\t\t\t.deleteFrom(\"_emdash_api_tokens\")\n\t\t\t.where(\"id\", \"=\", tokenId)\n\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (result.numDeletedRows === 0n) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Token not found\" },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { revoked: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TOKEN_REVOKE_ERROR\",\n\t\t\t\tmessage: \"Failed to revoke API token\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete every token with the given name for a user, returning how many were\n * removed. Token names aren't unique, so this is a bulk delete — used by the\n * dev-bypass flow to keep a single `dev-bypass-token` instead of minting a new\n * one on every reset.\n */\nexport async function deleteApiTokensByName(\n\tdb: Kysely<Database>,\n\tuserId: string,\n\tname: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_emdash_api_tokens\")\n\t\t.where(\"user_id\", \"=\", userId)\n\t\t.where(\"name\", \"=\", name)\n\t\t.executeTakeFirst();\n\treturn Number(result.numDeletedRows ?? 0n);\n}\n\n/**\n * Resolve a raw API token (ec_pat_...) to a user ID and scopes.\n * Updates last_used_at on successful lookup.\n * Returns null if the token is invalid or expired.\n */\nexport async function resolveApiToken(\n\tdb: Kysely<Database>,\n\trawToken: string,\n): Promise<{ userId: string; scopes: string[] } | null> {\n\tconst hash = hashApiToken(rawToken);\n\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_api_tokens\")\n\t\t.select([\"id\", \"user_id\", \"scopes\", \"expires_at\"])\n\t\t.where(\"token_hash\", \"=\", hash)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\t// Check expiry\n\tif (row.expires_at && new Date(row.expires_at) < new Date()) {\n\t\treturn null;\n\t}\n\n\t// Update last_used_at (fire-and-forget, don't block the request)\n\tdb.updateTable(\"_emdash_api_tokens\")\n\t\t.set({ last_used_at: new Date().toISOString() })\n\t\t.where(\"id\", \"=\", row.id)\n\t\t.execute()\n\t\t.catch(() => {}); // Non-critical, swallow errors\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tscopes: JSON.parse(row.scopes) as string[],\n\t};\n}\n\n/**\n * Resolve an OAuth access token (ec_oat_...) to a user ID and scopes.\n * Returns null if the token is invalid or expired.\n */\nexport async function resolveOAuthToken(\n\tdb: Kysely<Database>,\n\trawToken: string,\n): Promise<{ userId: string; scopes: string[] } | null> {\n\tconst hash = hashApiToken(rawToken);\n\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_oauth_tokens\")\n\t\t.select([\"user_id\", \"scopes\", \"expires_at\", \"token_type\"])\n\t\t.where(\"token_hash\", \"=\", hash)\n\t\t.where(\"token_type\", \"=\", \"access\")\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\t// Check expiry\n\tif (new Date(row.expires_at) < new Date()) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tscopes: JSON.parse(row.scopes) as string[],\n\t};\n}\n"],"mappings":"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"}
@@ -1,18 +1,18 @@
1
- import { c as __exportAll } from "./runner-C1T11CfC.mjs";
1
+ import { c as __exportAll } from "./runner-77GOZCnH.mjs";
2
2
  import { a as resolveConfiguredLocale, r as getI18nConfig } from "./config-C3EBstHi.mjs";
3
- import { i as RevisionRepository, t as ContentRepository } from "./content-ZHCc1ngN.mjs";
4
- import { t as MediaRepository } from "./media-CzhAn2--.mjs";
5
- import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
6
- import { t as TaxonomyRepository } from "./taxonomy-BqCZ1TdU.mjs";
7
- import { t as RedirectRepository } from "./redirect-COsS6SWJ.mjs";
8
- import { t as BylineRepository } from "./byline-yjvWq4ip.mjs";
9
- import { t as imageSize } from "./dist-D_6CKR8z.mjs";
10
- import { t as FTSManager } from "./fts-manager-DyZEohZC.mjs";
11
- import { c as markContentMediaUsageCollectionStaleSafely } from "./content-refresh-B_8SJzuU.mjs";
12
- import { n as SchemaRegistry } from "./registry-Cqa2I749.mjs";
13
- import { s as setSiteSettings } from "./settings-xRdoUZr0.mjs";
14
- import { a as validateExternalUrl, r as ssrfSafeFetch } from "./ssrf-Cpvg_n3A.mjs";
15
- import { t as validateSeed } from "./validate-BMLBvxOC.mjs";
3
+ import { i as RevisionRepository, t as ContentRepository } from "./content-CP-SeXNa.mjs";
4
+ import { t as MediaRepository } from "./media-D6SeAfBl.mjs";
5
+ import { t as withTransaction } from "./transaction-0KOpOyMu.mjs";
6
+ import { t as TaxonomyRepository } from "./taxonomy-8X09dLN5.mjs";
7
+ import { t as RedirectRepository } from "./redirect-CZkbozu9.mjs";
8
+ import { t as BylineRepository } from "./byline-Cpnwrje1.mjs";
9
+ import { t as imageSize } from "./dist-89xCrGqi.mjs";
10
+ import { t as FTSManager } from "./fts-manager-BhOdW0bi.mjs";
11
+ import { l as markContentMediaUsageCollectionStaleSafely } from "./content-refresh-DLOfIhYc.mjs";
12
+ import { n as SchemaRegistry } from "./registry-Cx-jEJOS.mjs";
13
+ import { s as setSiteSettings } from "./settings-BgN-0DT-.mjs";
14
+ import { i as ssrfSafeFetch, o as validateExternalUrl } from "./ssrf-DqebVE34.mjs";
15
+ import { t as validateSeed } from "./validate-Dd-IUjnZ.mjs";
16
16
  import { ulid } from "ulidx";
17
17
  import mime from "mime/lite";
18
18
 
@@ -298,7 +298,7 @@ async function applySeed(db, seed, options = {}) {
298
298
  }
299
299
  }
300
300
  }
301
- const { invalidateTaxonomyDefsCache } = await import("./taxonomies-BlrncHi9.mjs").then((n) => n.d);
301
+ const { invalidateTaxonomyDefsCache } = await import("./taxonomies-kmyudNGP.mjs").then((n) => n.d);
302
302
  invalidateTaxonomyDefsCache();
303
303
  }
304
304
  if (includeContent && seed.bylines) {
@@ -589,9 +589,9 @@ async function applySeed(db, seed, options = {}) {
589
589
  }
590
590
  }
591
591
  }
592
- const { invalidateBylineCache } = await import("./bylines-DjXS5p0P.mjs").then((n) => n.t);
593
- const { invalidateRedirectCache } = await import("./cache-C46-3Irl.mjs").then((n) => n.t);
594
- const { invalidateUrlPatternCache } = await import("./query-zTVAR1Sl.mjs").then((n) => n.o);
592
+ const { invalidateBylineCache } = await import("./bylines-BSo9B9RX.mjs").then((n) => n.t);
593
+ const { invalidateRedirectCache } = await import("./cache-5VirJ2Cn.mjs").then((n) => n.t);
594
+ const { invalidateUrlPatternCache } = await import("./query-D18vTdha.mjs").then((n) => n.o);
595
595
  invalidateBylineCache();
596
596
  invalidateRedirectCache();
597
597
  invalidateUrlPatternCache();
@@ -677,7 +677,7 @@ async function applyContentTaxonomies(db, collectionSlug, contentId, entry, isUp
677
677
  if (isUpdate) await termRepo.clearEntryTerms(collectionSlug, contentId);
678
678
  if (!entry.taxonomies) {
679
679
  if (isUpdate) {
680
- const { invalidateTermCache } = await import("./taxonomies-BlrncHi9.mjs").then((n) => n.d);
680
+ const { invalidateTermCache } = await import("./taxonomies-kmyudNGP.mjs").then((n) => n.d);
681
681
  invalidateTermCache();
682
682
  }
683
683
  return;
@@ -686,7 +686,7 @@ async function applyContentTaxonomies(db, collectionSlug, contentId, entry, isUp
686
686
  const term = await termRepo.findBySlug(taxonomyName, termSlug);
687
687
  if (term) await termRepo.attachToEntry(collectionSlug, contentId, term.id);
688
688
  }
689
- const { invalidateTermCache } = await import("./taxonomies-BlrncHi9.mjs").then((n) => n.d);
689
+ const { invalidateTermCache } = await import("./taxonomies-kmyudNGP.mjs").then((n) => n.d);
690
690
  invalidateTermCache();
691
691
  }
692
692
  /**
@@ -882,9 +882,9 @@ async function resolveMedia(ref, ctx, result) {
882
882
  }
883
883
  const contentType = response.headers.get("content-type") || "application/octet-stream";
884
884
  const ext = getExtensionFromContentType(contentType) || getExtensionFromUrl(url) || ".bin";
885
- const id = ulid();
885
+ const storageId = ulid();
886
886
  const finalFilename = filename || generateFilename(url, ext);
887
- const storageKey = `${id}${ext}`;
887
+ const storageKey = `${storageId}${ext}`;
888
888
  const arrayBuffer = await response.arrayBuffer();
889
889
  const body = new Uint8Array(arrayBuffer);
890
890
  let width;
@@ -899,20 +899,19 @@ async function resolveMedia(ref, ctx, result) {
899
899
  body,
900
900
  contentType
901
901
  });
902
- await new MediaRepository(ctx.db).create({
903
- filename: finalFilename,
904
- mimeType: contentType,
905
- size: body.length,
906
- width,
907
- height,
908
- alt,
909
- caption,
910
- storageKey,
911
- status: "ready"
912
- });
913
902
  const mediaValue = {
914
903
  provider: "local",
915
- id,
904
+ id: (await new MediaRepository(ctx.db).create({
905
+ filename: finalFilename,
906
+ mimeType: contentType,
907
+ size: body.length,
908
+ width,
909
+ height,
910
+ alt,
911
+ caption,
912
+ storageKey,
913
+ status: "ready"
914
+ })).id,
916
915
  alt: alt ?? void 0,
917
916
  width,
918
917
  height,
@@ -978,4 +977,4 @@ function getImageDimensions(buffer) {
978
977
 
979
978
  //#endregion
980
979
  export { apply_exports as n, applySeed as t };
981
- //# sourceMappingURL=apply-kUDt80-G.mjs.map
980
+ //# sourceMappingURL=apply-B_VMpJ2P.mjs.map