emdash 0.36.0 → 0.37.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 (809) hide show
  1. package/dist/{adapters-CzL3H752.d.mts → adapters-BDQwg236.d.mts} +4 -2
  2. package/dist/{adapters-CzL3H752.d.mts.map → adapters-BDQwg236.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-maylGaHv.mjs → allowed-origins-BGglilcs.mjs} +2 -2
  4. package/dist/{allowed-origins-maylGaHv.mjs.map → allowed-origins-BGglilcs.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +17 -17
  7. package/dist/api/schemas/index.d.mts +2 -2
  8. package/dist/api/schemas/index.mjs +5 -5
  9. package/dist/api-Dz6rgoa3.mjs +10040 -0
  10. package/dist/api-Dz6rgoa3.mjs.map +1 -0
  11. package/dist/{api-tokens-9YrrCoE6.mjs → api-tokens-BSsQACNe.mjs} +2 -2
  12. package/dist/{api-tokens-9YrrCoE6.mjs.map → api-tokens-BSsQACNe.mjs.map} +1 -1
  13. package/dist/{apply-B_VMpJ2P.mjs → apply-B4SIOxR1.mjs} +18 -18
  14. package/dist/{apply-B_VMpJ2P.mjs.map → apply-B4SIOxR1.mjs.map} +1 -1
  15. package/dist/astro/image-endpoint.mjs +2 -2
  16. package/dist/astro/image-endpoint.mjs.map +1 -1
  17. package/dist/astro/index.d.mts +12 -12
  18. package/dist/astro/index.d.mts.map +1 -1
  19. package/dist/astro/index.mjs +110 -37
  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.mjs +6 -6
  23. package/dist/astro/middleware/media-usage-write-fence.mjs +2 -2
  24. package/dist/astro/middleware/redirect.mjs +3 -3
  25. package/dist/astro/middleware/request-context.mjs +2 -2
  26. package/dist/astro/middleware/setup.mjs +1 -1
  27. package/dist/astro/middleware.d.mts +10 -10
  28. package/dist/astro/middleware.d.mts.map +1 -1
  29. package/dist/astro/middleware.mjs +157 -195
  30. package/dist/astro/middleware.mjs.map +1 -1
  31. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  32. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  33. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
  34. package/dist/astro/routes/api/admin/api-tokens/index.d.mts.map +1 -1
  35. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  36. package/dist/astro/routes/api/admin/api-tokens/index.mjs.map +1 -1
  37. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  38. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
  39. package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
  40. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
  41. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -14
  42. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +14 -14
  43. package/dist/astro/routes/api/admin/bylines/index.mjs +14 -14
  44. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
  45. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  46. package/dist/astro/routes/api/admin/comments/bulk.mjs +10 -10
  47. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  48. package/dist/astro/routes/api/admin/comments/index.mjs +10 -10
  49. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +3 -3
  50. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +2 -2
  51. package/dist/astro/routes/api/admin/media-usage/activation.mjs +10 -10
  52. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +13 -13
  53. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +13 -13
  54. package/dist/astro/routes/api/admin/media-usage/progress.mjs +11 -11
  55. package/dist/astro/routes/api/admin/media-usage/repair.mjs +14 -14
  56. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +13 -13
  57. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +13 -13
  58. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  59. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs.map +1 -1
  60. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  61. package/dist/astro/routes/api/admin/oauth-clients/index.mjs.map +1 -1
  62. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +49 -49
  63. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +49 -49
  64. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +47 -47
  65. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
  66. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +47 -47
  67. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +48 -48
  68. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +48 -48
  69. package/dist/astro/routes/api/admin/plugins/index.mjs +47 -47
  70. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  71. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +47 -47
  72. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +48 -48
  73. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +47 -47
  74. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +48 -48
  75. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  76. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +54 -50
  77. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  78. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +17 -16
  79. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  81. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +55 -51
  82. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/plugins/registry/verify.d.mts +8 -0
  84. package/dist/astro/routes/api/admin/plugins/registry/verify.d.mts.map +1 -0
  85. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +93 -0
  86. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -0
  87. package/dist/astro/routes/api/admin/plugins/updates.mjs +47 -47
  88. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +47 -47
  89. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  90. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +47 -47
  91. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
  92. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  93. package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
  94. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  95. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  96. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  97. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  98. package/dist/astro/routes/api/auth/invite/complete.mjs +10 -10
  99. package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
  100. package/dist/astro/routes/api/auth/invite/register-options.mjs +9 -9
  101. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  102. package/dist/astro/routes/api/auth/magic-link/send.mjs +9 -9
  103. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  104. package/dist/astro/routes/api/auth/me.mjs +7 -7
  105. package/dist/astro/routes/api/auth/mode.mjs +2 -2
  106. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  107. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  108. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  109. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  110. package/dist/astro/routes/api/auth/passkey/options.mjs +11 -11
  111. package/dist/astro/routes/api/auth/passkey/register/options.mjs +9 -9
  112. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +10 -10
  113. package/dist/astro/routes/api/auth/passkey/verify.mjs +10 -10
  114. package/dist/astro/routes/api/auth/signup/complete.mjs +10 -10
  115. package/dist/astro/routes/api/auth/signup/request.mjs +9 -9
  116. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  117. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
  118. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
  119. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  120. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts.map +1 -1
  121. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +14 -5
  122. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  123. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  124. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  125. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +10 -10
  126. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +13 -10
  127. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  128. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +15 -15
  129. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +15 -15
  130. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  131. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  132. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -8
  133. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
  134. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  135. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts.map +1 -1
  136. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +14 -5
  137. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  138. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
  139. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  140. package/dist/astro/routes/api/content/_collection_/index.mjs +8 -8
  141. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  142. package/dist/astro/routes/api/dashboard.mjs +7 -7
  143. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  144. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  145. package/dist/astro/routes/api/import/probe.mjs +15 -15
  146. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  147. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  148. package/dist/astro/routes/api/import/wordpress/execute.mjs +11 -11
  149. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  150. package/dist/astro/routes/api/import/wordpress/media.mjs +9 -9
  151. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  152. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  153. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +10 -10
  154. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  155. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +15 -15
  156. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  157. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +22 -22
  158. package/dist/astro/routes/api/manifest.mjs +3 -3
  159. package/dist/astro/routes/api/manifest.mjs.map +1 -1
  160. package/dist/astro/routes/api/mcp.mjs +188 -65
  161. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  162. package/dist/astro/routes/api/media/_id_/confirm.mjs +9 -9
  163. package/dist/astro/routes/api/media/_id_/replace.d.mts +8 -0
  164. package/dist/astro/routes/api/media/_id_/replace.d.mts.map +1 -0
  165. package/dist/astro/routes/api/media/_id_/replace.mjs +91 -0
  166. package/dist/astro/routes/api/media/_id_/replace.mjs.map +1 -0
  167. package/dist/astro/routes/api/media/_id_/upload.mjs +5 -5
  168. package/dist/astro/routes/api/media/_id_/usage.mjs +16 -16
  169. package/dist/astro/routes/api/media/_id_.mjs +15 -15
  170. package/dist/astro/routes/api/media/file/_...key_.d.mts.map +1 -1
  171. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -2
  172. package/dist/astro/routes/api/media/file/_...key_.mjs.map +1 -1
  173. package/dist/astro/routes/api/media/folders/_id_.mjs +8 -8
  174. package/dist/astro/routes/api/media/folders/index.mjs +8 -8
  175. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  176. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  177. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  178. package/dist/astro/routes/api/media/upload-url.mjs +15 -13
  179. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  180. package/dist/astro/routes/api/media.d.mts.map +1 -1
  181. package/dist/astro/routes/api/media.mjs +47 -24
  182. package/dist/astro/routes/api/media.mjs.map +1 -1
  183. package/dist/astro/routes/api/menus/_name_/items/_id_.d.mts.map +1 -1
  184. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +20 -13
  185. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
  186. package/dist/astro/routes/api/menus/_name_/items.d.mts.map +1 -1
  187. package/dist/astro/routes/api/menus/_name_/items.mjs +15 -11
  188. package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
  189. package/dist/astro/routes/api/menus/_name_/reorder.d.mts.map +1 -1
  190. package/dist/astro/routes/api/menus/_name_/reorder.mjs +15 -11
  191. package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
  192. package/dist/astro/routes/api/menus/_name_/translations.d.mts.map +1 -1
  193. package/dist/astro/routes/api/menus/_name_/translations.mjs +16 -12
  194. package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
  195. package/dist/astro/routes/api/menus/_name_.d.mts.map +1 -1
  196. package/dist/astro/routes/api/menus/_name_.mjs +21 -14
  197. package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
  198. package/dist/astro/routes/api/menus/index.d.mts.map +1 -1
  199. package/dist/astro/routes/api/menus/index.mjs +15 -11
  200. package/dist/astro/routes/api/menus/index.mjs.map +1 -1
  201. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  202. package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
  203. package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
  204. package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
  205. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  206. package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
  207. package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
  208. package/dist/astro/routes/api/oauth/token.mjs +5 -5
  209. package/dist/astro/routes/api/openapi.json.mjs +104 -38
  210. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  211. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  212. package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
  213. package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
  214. package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
  215. package/dist/astro/routes/api/redirects/index.mjs +9 -9
  216. package/dist/astro/routes/api/relations/_id_/index.mjs +15 -15
  217. package/dist/astro/routes/api/relations/_id_/translations.mjs +10 -10
  218. package/dist/astro/routes/api/relations/index.mjs +15 -15
  219. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  220. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  221. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +47 -47
  222. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +47 -47
  223. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +47 -47
  224. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +47 -47
  225. package/dist/astro/routes/api/schema/collections/index.mjs +47 -47
  226. package/dist/astro/routes/api/schema/collections/reorder.mjs +47 -47
  227. package/dist/astro/routes/api/schema/index.mjs +8 -8
  228. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +47 -47
  229. package/dist/astro/routes/api/schema/orphans/index.mjs +47 -47
  230. package/dist/astro/routes/api/search/enable.mjs +10 -10
  231. package/dist/astro/routes/api/search/index.mjs +9 -9
  232. package/dist/astro/routes/api/search/rebuild.mjs +10 -10
  233. package/dist/astro/routes/api/search/stats.mjs +5 -5
  234. package/dist/astro/routes/api/search/suggest.mjs +9 -9
  235. package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
  236. package/dist/astro/routes/api/sections/index.mjs +9 -9
  237. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +5 -5
  238. package/dist/astro/routes/api/settings/backups/archives/index.mjs +4 -4
  239. package/dist/astro/routes/api/settings/backups/export.mjs +5 -5
  240. package/dist/astro/routes/api/settings/backups/index.mjs +6 -6
  241. package/dist/astro/routes/api/settings/email.mjs +4 -4
  242. package/dist/astro/routes/api/settings/email.mjs.map +1 -1
  243. package/dist/astro/routes/api/settings.d.mts.map +1 -1
  244. package/dist/astro/routes/api/settings.mjs +18 -14
  245. package/dist/astro/routes/api/settings.mjs.map +1 -1
  246. package/dist/astro/routes/api/setup/admin-verify.mjs +11 -11
  247. package/dist/astro/routes/api/setup/admin.mjs +10 -10
  248. package/dist/astro/routes/api/setup/dev-bypass.mjs +25 -25
  249. package/dist/astro/routes/api/setup/dev-reset.mjs +1 -1
  250. package/dist/astro/routes/api/setup/index.mjs +25 -25
  251. package/dist/astro/routes/api/setup/status.mjs +3 -3
  252. package/dist/astro/routes/api/snapshot.mjs +6 -6
  253. package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -1
  254. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +19 -15
  255. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
  256. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.d.mts.map +1 -1
  257. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +20 -16
  258. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  259. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.d.mts.map +1 -1
  260. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +24 -17
  261. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  262. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  263. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +19 -15
  264. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  265. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +13 -13
  266. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -1
  267. package/dist/astro/routes/api/taxonomies/_name_.mjs +25 -18
  268. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -1
  269. package/dist/astro/routes/api/taxonomies/index.d.mts.map +1 -1
  270. package/dist/astro/routes/api/taxonomies/index.mjs +19 -15
  271. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  272. package/dist/astro/routes/api/themes/preview.mjs +4 -4
  273. package/dist/astro/routes/api/typegen.mjs +4 -4
  274. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  275. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  276. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  277. package/dist/astro/routes/api/widget-areas/_name_/reorder.d.mts.map +1 -1
  278. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -8
  279. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  280. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.d.mts.map +1 -1
  281. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +16 -12
  282. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  283. package/dist/astro/routes/api/widget-areas/_name_/widgets.d.mts.map +1 -1
  284. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +14 -11
  285. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  286. package/dist/astro/routes/api/widget-areas/_name_.d.mts.map +1 -1
  287. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -5
  288. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  289. package/dist/astro/routes/api/widget-areas/index.d.mts.map +1 -1
  290. package/dist/astro/routes/api/widget-areas/index.mjs +14 -11
  291. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  292. package/dist/astro/routes/api/widget-components.mjs +2 -2
  293. package/dist/astro/routes/robots.txt.mjs +5 -5
  294. package/dist/astro/routes/sitemap-_collection_.xml.mjs +8 -8
  295. package/dist/astro/routes/sitemap.xml.mjs +6 -6
  296. package/dist/astro/types.d.mts +32 -17
  297. package/dist/astro/types.d.mts.map +1 -1
  298. package/dist/auth/providers/github.d.mts +1 -1
  299. package/dist/auth/providers/google.d.mts +1 -1
  300. package/dist/{authorize-ByYs-Uxs.mjs → authorize-Bkf_UHhD.mjs} +2 -2
  301. package/dist/{authorize-ByYs-Uxs.mjs.map → authorize-Bkf_UHhD.mjs.map} +1 -1
  302. package/dist/{backup-ChyHfIoX.mjs → backup-rwVAl9NI.mjs} +4 -4
  303. package/dist/{backup-ChyHfIoX.mjs.map → backup-rwVAl9NI.mjs.map} +1 -1
  304. package/dist/{bundle-DZFhFurR.mjs → bundle-C8S2Zl6U.mjs} +3 -3
  305. package/dist/{bundle-DZFhFurR.mjs.map → bundle-C8S2Zl6U.mjs.map} +1 -1
  306. package/dist/{byline-Cpnwrje1.mjs → byline-DuhF7NWl.mjs} +25 -12
  307. package/dist/byline-DuhF7NWl.mjs.map +1 -0
  308. package/dist/{byline-fields-CAyA_Umd.mjs → byline-fields-BovY4KYM.mjs} +1 -1
  309. package/dist/{byline-fields-CAyA_Umd.mjs.map → byline-fields-BovY4KYM.mjs.map} +1 -1
  310. package/dist/{bylines-BSo9B9RX.mjs → bylines-CJ5nd6Lw.mjs} +6 -6
  311. package/dist/{bylines-BSo9B9RX.mjs.map → bylines-CJ5nd6Lw.mjs.map} +1 -1
  312. package/dist/{bylines-CHjvehVX.mjs → bylines-RwBAeOF1.mjs} +3 -3
  313. package/dist/{bylines-CHjvehVX.mjs.map → bylines-RwBAeOF1.mjs.map} +1 -1
  314. package/dist/{cache-5VirJ2Cn.mjs → cache-BF5Kvn59.mjs} +2 -2
  315. package/dist/{cache-5VirJ2Cn.mjs.map → cache-BF5Kvn59.mjs.map} +1 -1
  316. package/dist/{challenge-store-D5pSY2S1.mjs → challenge-store-DQN_Yrck.mjs} +5 -3
  317. package/dist/challenge-store-DQN_Yrck.mjs.map +1 -0
  318. package/dist/chrome-tags-D8fiZG94.mjs +24 -0
  319. package/dist/chrome-tags-D8fiZG94.mjs.map +1 -0
  320. package/dist/{chunks-bNiX6_DR.mjs → chunks-C4szrD8o.mjs} +2 -2
  321. package/dist/{chunks-bNiX6_DR.mjs.map → chunks-C4szrD8o.mjs.map} +1 -1
  322. package/dist/cli/index.mjs +26 -26
  323. package/dist/client/cf-access.d.mts +1 -1
  324. package/dist/client/index.d.mts +2 -2
  325. package/dist/client/index.mjs +2 -2
  326. package/dist/{collection-deletion-m_oi2c0N.mjs → collection-deletion-Bj07NHbj.mjs} +2 -2
  327. package/dist/{collection-deletion-m_oi2c0N.mjs.map → collection-deletion-Bj07NHbj.mjs.map} +1 -1
  328. package/dist/{comment-reaction-CRuItEjt.mjs → comment-reaction-BhrvSHml.mjs} +2 -2
  329. package/dist/{comment-reaction-CRuItEjt.mjs.map → comment-reaction-BhrvSHml.mjs.map} +1 -1
  330. package/dist/{comment-DhvLEdhQ.mjs → comment-vxOzWbZN.mjs} +2 -2
  331. package/dist/{comment-DhvLEdhQ.mjs.map → comment-vxOzWbZN.mjs.map} +1 -1
  332. package/dist/{comments-CLj4bi8I.mjs → comments-Ct4ensnf.mjs} +2 -2
  333. package/dist/{comments-CLj4bi8I.mjs.map → comments-Ct4ensnf.mjs.map} +1 -1
  334. package/dist/{components-v4U4DT2y.mjs → components-DJbo1z0S.mjs} +1 -1
  335. package/dist/{components-v4U4DT2y.mjs.map → components-DJbo1z0S.mjs.map} +1 -1
  336. package/dist/{config-DrfghTPe.d.mts → config-B9eZ340f.d.mts} +1 -1
  337. package/dist/{config-DrfghTPe.d.mts.map → config-B9eZ340f.d.mts.map} +1 -1
  338. package/dist/config-it29yDLA.mjs +1163 -0
  339. package/dist/config-it29yDLA.mjs.map +1 -0
  340. package/dist/{content-CP-SeXNa.mjs → content-DjvM0T92.mjs} +84 -44
  341. package/dist/content-DjvM0T92.mjs.map +1 -0
  342. package/dist/{content-list-query-BhejKVqb.d.mts → content-list-query-Btqd4uV2.d.mts} +1 -1
  343. package/dist/{content-list-query-BhejKVqb.d.mts.map → content-list-query-Btqd4uV2.d.mts.map} +1 -1
  344. package/dist/{content-refresh-DLOfIhYc.mjs → content-refresh-Bwp-8yGy.mjs} +48 -21
  345. package/dist/content-refresh-Bwp-8yGy.mjs.map +1 -0
  346. package/dist/{content-repair-BCxzy0r4.mjs → content-repair-Czdp7jz5.mjs} +4 -4
  347. package/dist/{content-repair-BCxzy0r4.mjs.map → content-repair-Czdp7jz5.mjs.map} +1 -1
  348. package/dist/{context-BR5hRWiK.mjs → context-CX6gxOLW.mjs} +52 -17
  349. package/dist/context-CX6gxOLW.mjs.map +1 -0
  350. package/dist/{cron-CWfWrWKG.mjs → cron-C6o7VppJ.mjs} +1 -1
  351. package/dist/{cron-CWfWrWKG.mjs.map → cron-C6o7VppJ.mjs.map} +1 -1
  352. package/dist/{dashboard-CkIV2rnQ.mjs → dashboard-C2kTeeRI.mjs} +4 -4
  353. package/dist/{dashboard-CkIV2rnQ.mjs.map → dashboard-C2kTeeRI.mjs.map} +1 -1
  354. package/dist/db/index.d.mts +3 -3
  355. package/dist/db/index.mjs +1 -1
  356. package/dist/db/index.mjs.map +1 -1
  357. package/dist/db/libsql-migrations.d.mts +1 -1
  358. package/dist/db/libsql-migrations.mjs +4 -4
  359. package/dist/db/libsql.d.mts +1 -1
  360. package/dist/db/postgres-migrations.d.mts +1 -1
  361. package/dist/db/postgres-migrations.mjs +4 -4
  362. package/dist/db/postgres.d.mts +1 -1
  363. package/dist/db/postgres.d.mts.map +1 -1
  364. package/dist/db/postgres.mjs +19 -3
  365. package/dist/db/postgres.mjs.map +1 -1
  366. package/dist/db/sqlite-migrations.d.mts +1 -1
  367. package/dist/db/sqlite-migrations.mjs +4 -4
  368. package/dist/db/sqlite.d.mts +1 -1
  369. package/dist/db/sqlite.mjs +1 -1
  370. package/dist/{default-YMEfcQUx.mjs → default-Clra32mw.mjs} +1 -1
  371. package/dist/{default-YMEfcQUx.mjs.map → default-Clra32mw.mjs.map} +1 -1
  372. package/dist/{deferred-tasks-DfqxH5Xf.d.mts → deferred-tasks-CvDETTJO.d.mts} +1 -1
  373. package/dist/{deferred-tasks-DfqxH5Xf.d.mts.map → deferred-tasks-CvDETTJO.d.mts.map} +1 -1
  374. package/dist/dev-typegen-DzOPfsNj.mjs +29 -0
  375. package/dist/dev-typegen-DzOPfsNj.mjs.map +1 -0
  376. package/dist/{device-flow-CojjSBwb.mjs → device-flow-D2oCWvLJ.mjs} +4 -4
  377. package/dist/{device-flow-CojjSBwb.mjs.map → device-flow-D2oCWvLJ.mjs.map} +1 -1
  378. package/dist/{direct-executor-BWeWiRYB.mjs → direct-executor-Bfa5_ToE.mjs} +3 -3
  379. package/dist/{direct-executor-BWeWiRYB.mjs.map → direct-executor-Bfa5_ToE.mjs.map} +1 -1
  380. package/dist/{email-console-CEJgR_LB.mjs → email-console-Dzb6uGYa.mjs} +1 -1
  381. package/dist/{email-console-CEJgR_LB.mjs.map → email-console-Dzb6uGYa.mjs.map} +1 -1
  382. package/dist/{error-D9w94fjV.mjs → error-DFrQjppn.mjs} +2 -2
  383. package/dist/{error-D9w94fjV.mjs.map → error-DFrQjppn.mjs.map} +1 -1
  384. package/dist/{errors-n2j49uNf.mjs → errors-DsVoDUjJ.mjs} +3 -1
  385. package/dist/errors-DsVoDUjJ.mjs.map +1 -0
  386. package/dist/{escape-DN4Qmjsl.mjs → escape-HmHTC1Is.mjs} +1 -1
  387. package/dist/{escape-DN4Qmjsl.mjs.map → escape-HmHTC1Is.mjs.map} +1 -1
  388. package/dist/{field-defs-cache-Bh4q6oar.mjs → field-defs-cache-BM4SIRqH.mjs} +2 -2
  389. package/dist/{field-defs-cache-Bh4q6oar.mjs.map → field-defs-cache-BM4SIRqH.mjs.map} +1 -1
  390. package/dist/{fts-manager-BhOdW0bi.mjs → fts-manager-Do1BlW2r.mjs} +2 -2
  391. package/dist/{fts-manager-BhOdW0bi.mjs.map → fts-manager-Do1BlW2r.mjs.map} +1 -1
  392. package/dist/{hash-CTcngWvj.mjs → hash-3JJPlHF2.mjs} +1 -1
  393. package/dist/{hash-CTcngWvj.mjs.map → hash-3JJPlHF2.mjs.map} +1 -1
  394. package/dist/hook-result-DCh2WUpe.d.mts +31 -0
  395. package/dist/hook-result-DCh2WUpe.d.mts.map +1 -0
  396. package/dist/{hreflang-7pTHsLZJ.d.mts → hreflang-B6hbAuuO.d.mts} +2 -2
  397. package/dist/{hreflang-7pTHsLZJ.d.mts.map → hreflang-B6hbAuuO.d.mts.map} +1 -1
  398. package/dist/{hreflang-Huc6O5wa.mjs → hreflang-DQbPZliA.mjs} +6 -6
  399. package/dist/{hreflang-Huc6O5wa.mjs.map → hreflang-DQbPZliA.mjs.map} +1 -1
  400. package/dist/{identity-DXmb4aVp.mjs → identity-DksvEazf.mjs} +3 -3
  401. package/dist/{identity-DXmb4aVp.mjs.map → identity-DksvEazf.mjs.map} +1 -1
  402. package/dist/{import-C4_O3KXU.mjs → import-CJpaBBBT.mjs} +5 -5
  403. package/dist/{import-C4_O3KXU.mjs.map → import-CJpaBBBT.mjs.map} +1 -1
  404. package/dist/{index-DWbJrsAg.d.mts → index-BJNMCWmu.d.mts} +158 -49
  405. package/dist/index-BJNMCWmu.d.mts.map +1 -0
  406. package/dist/{index-6bRee8zb.d.mts → index-Du8K7bWa.d.mts} +3 -3
  407. package/dist/{index-6bRee8zb.d.mts.map → index-Du8K7bWa.d.mts.map} +1 -1
  408. package/dist/index.d.mts +22 -21
  409. package/dist/index.mjs +70 -70
  410. package/dist/{load-Bo7fZAbg.mjs → load-CawHjTOH.mjs} +2 -2
  411. package/dist/{load-Bo7fZAbg.mjs.map → load-CawHjTOH.mjs.map} +1 -1
  412. package/dist/{loader-ZsjE4paL.mjs → loader-Z8vl7zTq.mjs} +48 -13
  413. package/dist/loader-Z8vl7zTq.mjs.map +1 -0
  414. package/dist/{manifest-builder-QXuqvVMP.mjs → manifest-builder-COtzuca2.mjs} +2 -2
  415. package/dist/{manifest-builder-QXuqvVMP.mjs.map → manifest-builder-COtzuca2.mjs.map} +1 -1
  416. package/dist/{manifest-schema-CUYPfCEH.mjs → manifest-schema-BNWwR8Vd.mjs} +5 -5
  417. package/dist/manifest-schema-BNWwR8Vd.mjs.map +1 -0
  418. package/dist/media/image-endpoint.d.mts +4 -2
  419. package/dist/media/image-endpoint.d.mts.map +1 -1
  420. package/dist/media/image-endpoint.mjs +6 -4
  421. package/dist/media/image-endpoint.mjs.map +1 -1
  422. package/dist/media/index.d.mts +1 -1
  423. package/dist/media/index.mjs +1 -1
  424. package/dist/media/local-runtime.d.mts +12 -12
  425. package/dist/media/local-runtime.mjs +4 -4
  426. package/dist/{media-D6SeAfBl.mjs → media-BBm8Wg3f.mjs} +30 -2
  427. package/dist/media-BBm8Wg3f.mjs.map +1 -0
  428. package/dist/{media-BjwNuLEu.mjs → media-BHjus86Y.mjs} +35 -5
  429. package/dist/media-BHjus86Y.mjs.map +1 -0
  430. package/dist/{media-allowlist-D35v7bmL.mjs → media-allowlist-CoTBWoDp.mjs} +1 -1
  431. package/dist/{media-allowlist-D35v7bmL.mjs.map → media-allowlist-CoTBWoDp.mjs.map} +1 -1
  432. package/dist/{media-folders-D-0CmRZj.mjs → media-folders-BGkATs8n.mjs} +1 -1
  433. package/dist/{media-folders-D-0CmRZj.mjs.map → media-folders-BGkATs8n.mjs.map} +1 -1
  434. package/dist/{media-upload-C7bLXGcI.mjs → media-upload-BCidR-EY.mjs} +6 -6
  435. package/dist/{media-upload-C7bLXGcI.mjs.map → media-upload-BCidR-EY.mjs.map} +1 -1
  436. package/dist/{media-url-DH7giMx_.mjs → media-url-DHoPr0hq.mjs} +1 -1
  437. package/dist/{media-url-DH7giMx_.mjs.map → media-url-DHoPr0hq.mjs.map} +1 -1
  438. package/dist/{media-usage-BTRJvS6V.mjs → media-usage-CdY5jhSR.mjs} +8 -8
  439. package/dist/{media-usage-BTRJvS6V.mjs.map → media-usage-CdY5jhSR.mjs.map} +1 -1
  440. package/dist/{media-usage-activation-Dx0kPV6b.mjs → media-usage-activation-BgeMtK5C.mjs} +2 -2
  441. package/dist/{media-usage-activation-Dx0kPV6b.mjs.map → media-usage-activation-BgeMtK5C.mjs.map} +1 -1
  442. package/dist/{media-usage-work-DWb2XPMI.mjs → media-usage-work-CnGCdHxx.mjs} +4 -4
  443. package/dist/{media-usage-work-DWb2XPMI.mjs.map → media-usage-work-CnGCdHxx.mjs.map} +1 -1
  444. package/dist/{media-usage-work-C-fhM30c.mjs → media-usage-work-D6D9sMCA.mjs} +1 -1
  445. package/dist/{media-usage-work-C-fhM30c.mjs.map → media-usage-work-D6D9sMCA.mjs.map} +1 -1
  446. package/dist/{media-usage-write-fence-BwoJVRDC.mjs → media-usage-write-fence-hjeD4u7s.mjs} +2 -2
  447. package/dist/{media-usage-write-fence-BwoJVRDC.mjs.map → media-usage-write-fence-hjeD4u7s.mjs.map} +1 -1
  448. package/dist/{menus-CNg0QlDt.mjs → menus-6qSwTNxG.mjs} +346 -94
  449. package/dist/menus-6qSwTNxG.mjs.map +1 -0
  450. package/dist/{menus-CD7EFLwz.mjs → menus-uvPLXMZf.mjs} +3 -3
  451. package/dist/{menus-CD7EFLwz.mjs.map → menus-uvPLXMZf.mjs.map} +1 -1
  452. package/dist/migrations/index.d.mts +1 -1
  453. package/dist/migrations/index.mjs +3 -3
  454. package/dist/{mode-DaUM_MB2.mjs → mode-DN_WneRx.mjs} +1 -1
  455. package/dist/{mode-DaUM_MB2.mjs.map → mode-DN_WneRx.mjs.map} +1 -1
  456. package/dist/{node-sqlite-compat-Udb4Y-rs.mjs → node-sqlite-compat-BuJXIeMz.mjs} +1 -1
  457. package/dist/{node-sqlite-compat-Udb4Y-rs.mjs.map → node-sqlite-compat-BuJXIeMz.mjs.map} +1 -1
  458. package/dist/{normalize-CSxdKBVa.mjs → normalize-C2xNjTSG.mjs} +15 -5
  459. package/dist/normalize-C2xNjTSG.mjs.map +1 -0
  460. package/dist/{oauth-authorization-Dpsb-fMJ.mjs → oauth-authorization-Dj6Kwc21.mjs} +4 -4
  461. package/dist/{oauth-authorization-Dpsb-fMJ.mjs.map → oauth-authorization-Dj6Kwc21.mjs.map} +1 -1
  462. package/dist/{oauth-clients-Bxnoe_67.mjs → oauth-clients-FD7I0J8m.mjs} +1 -1
  463. package/dist/{oauth-clients-Bxnoe_67.mjs.map → oauth-clients-FD7I0J8m.mjs.map} +1 -1
  464. package/dist/{oauth-state-store-0C4s0K6w.mjs → oauth-state-store-DkmDOmHs.mjs} +1 -1
  465. package/dist/{oauth-state-store-0C4s0K6w.mjs.map → oauth-state-store-DkmDOmHs.mjs.map} +1 -1
  466. package/dist/{oauth-user-lookup-wlGYSeqX.mjs → oauth-user-lookup-CvIwGpYd.mjs} +1 -1
  467. package/dist/{oauth-user-lookup-wlGYSeqX.mjs.map → oauth-user-lookup-CvIwGpYd.mjs.map} +1 -1
  468. package/dist/object-cache/memory.d.mts +1 -1
  469. package/dist/{object-cache-71tPfV4A.mjs → object-cache-DVKnyOfy.mjs} +19 -3
  470. package/dist/object-cache-DVKnyOfy.mjs.map +1 -0
  471. package/dist/{options-BhZIeFY1.d.mts → options-CYb4y7hM.d.mts} +24 -11
  472. package/dist/options-CYb4y7hM.d.mts.map +1 -0
  473. package/dist/page/index.d.mts +2 -2
  474. package/dist/{parse-BRJg1kz8.mjs → parse-Ckpg-Qfp.mjs} +2 -2
  475. package/dist/{parse-BRJg1kz8.mjs.map → parse-Ckpg-Qfp.mjs.map} +1 -1
  476. package/dist/{passkey-config-B3P0nHRR.mjs → passkey-config-O_lj3y0k.mjs} +1 -1
  477. package/dist/{passkey-config-B3P0nHRR.mjs.map → passkey-config-O_lj3y0k.mjs.map} +1 -1
  478. package/dist/{placeholder-BR4ZhVJl.d.mts → placeholder-4X9BrRrp.d.mts} +1 -1
  479. package/dist/{placeholder-BR4ZhVJl.d.mts.map → placeholder-4X9BrRrp.d.mts.map} +1 -1
  480. package/dist/plugin-types.d.mts +8 -3
  481. package/dist/plugin-types.d.mts.map +1 -1
  482. package/dist/plugin-utils.d.mts +10 -10
  483. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  484. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  485. package/dist/{policy-BVRganJQ.mjs → policy-Dhtwntrt.mjs} +2 -2
  486. package/dist/{policy-BVRganJQ.mjs.map → policy-Dhtwntrt.mjs.map} +1 -1
  487. package/dist/{portable-text-C8Tvp4_g.mjs → portable-text-U85hOafW.mjs} +1 -1
  488. package/dist/{portable-text-C8Tvp4_g.mjs.map → portable-text-U85hOafW.mjs.map} +1 -1
  489. package/dist/{preview-dn2dUyM0.mjs → preview-B_vJLvf8.mjs} +2 -2
  490. package/dist/{preview-dn2dUyM0.mjs.map → preview-B_vJLvf8.mjs.map} +1 -1
  491. package/dist/{protocol-CuBEsqgE.d.mts → protocol-JdD2YhY9.d.mts} +2 -2
  492. package/dist/{protocol-CuBEsqgE.d.mts.map → protocol-JdD2YhY9.d.mts.map} +1 -1
  493. package/dist/{public-url-DRz_AinV.mjs → public-url-CarhZJgf.mjs} +1 -1
  494. package/dist/{public-url-DRz_AinV.mjs.map → public-url-CarhZJgf.mjs.map} +1 -1
  495. package/dist/{query-D18vTdha.mjs → query-BZKZcG5b.mjs} +14 -14
  496. package/dist/{query-D18vTdha.mjs.map → query-BZKZcG5b.mjs.map} +1 -1
  497. package/dist/{query-tZDEdtCN.mjs → query-BndaFxxp.mjs} +4 -4
  498. package/dist/{query-tZDEdtCN.mjs.map → query-BndaFxxp.mjs.map} +1 -1
  499. package/dist/{rate-limit-on7KPo-v.mjs → rate-limit-BZmATC6W.mjs} +2 -2
  500. package/dist/{rate-limit-on7KPo-v.mjs.map → rate-limit-BZmATC6W.mjs.map} +1 -1
  501. package/dist/{redirect-DrbEmU9a.mjs → redirect-CzqnLxK-.mjs} +1 -1
  502. package/dist/{redirect-DrbEmU9a.mjs.map → redirect-CzqnLxK-.mjs.map} +1 -1
  503. package/dist/{redirects-CRIM55KH.mjs → redirects-BLNOWQTn.mjs} +63 -28
  504. package/dist/redirects-BLNOWQTn.mjs.map +1 -0
  505. package/dist/{redirects-BahWiu8Z.mjs → redirects-CqzNM0KM.mjs} +2 -2
  506. package/dist/{redirects-BahWiu8Z.mjs.map → redirects-CqzNM0KM.mjs.map} +1 -1
  507. package/dist/{registry-Cx-jEJOS.mjs → registry-AZAsRsor.mjs} +27 -9
  508. package/dist/registry-AZAsRsor.mjs.map +1 -0
  509. package/dist/{relation-A6wLF_DS.mjs → relation-04DfKRTO.mjs} +2 -2
  510. package/dist/{relation-A6wLF_DS.mjs.map → relation-04DfKRTO.mjs.map} +1 -1
  511. package/dist/{relations-BTPM2saV.mjs → relations-C4htk5sb.mjs} +7 -7
  512. package/dist/relations-C4htk5sb.mjs.map +1 -0
  513. package/dist/{relations-Cj5hZ6Zg.mjs → relations-CcFwDJdJ.mjs} +4 -4
  514. package/dist/{relations-Cj5hZ6Zg.mjs.map → relations-CcFwDJdJ.mjs.map} +1 -1
  515. package/dist/{relations-C7B46bcU.d.mts → relations-DnRK4YIT.d.mts} +175 -41
  516. package/dist/relations-DnRK4YIT.d.mts.map +1 -0
  517. package/dist/request-context.d.mts +1 -1
  518. package/dist/{request-meta-DRp5ENd_.mjs → request-meta-DYxLlnmz.mjs} +2 -2
  519. package/dist/{request-meta-DRp5ENd_.mjs.map → request-meta-DYxLlnmz.mjs.map} +1 -1
  520. package/dist/{resolve-DxUpTMmE.mjs → resolve-prZ3mP7l.mjs} +1 -1
  521. package/dist/{resolve-DxUpTMmE.mjs.map → resolve-prZ3mP7l.mjs.map} +1 -1
  522. package/dist/{runner-77GOZCnH.mjs → runner-BLIDKpmA.mjs} +351 -294
  523. package/dist/runner-BLIDKpmA.mjs.map +1 -0
  524. package/dist/{runner-zgqA_CqP.d.mts → runner-YKfIMnaF.d.mts} +3 -2
  525. package/dist/runner-YKfIMnaF.d.mts.map +1 -0
  526. package/dist/runtime.d.mts +11 -11
  527. package/dist/runtime.mjs +1 -1
  528. package/dist/{schema-BY-oVntF.mjs → schema-CGnBeLiW.mjs} +3 -3
  529. package/dist/{schema-BY-oVntF.mjs.map → schema-CGnBeLiW.mjs.map} +1 -1
  530. package/dist/{schema-Cw9kVK0I.mjs → schema-DYr-Vd-L.mjs} +5 -5
  531. package/dist/{schema-Cw9kVK0I.mjs.map → schema-DYr-Vd-L.mjs.map} +1 -1
  532. package/dist/{scopes-vMR1uIbt.mjs → scopes-BhWvBavj.mjs} +3 -3
  533. package/dist/{scopes-vMR1uIbt.mjs.map → scopes-BhWvBavj.mjs.map} +1 -1
  534. package/dist/{search-CfT3KxO5.mjs → search-C-qDk4kx.mjs} +4 -4
  535. package/dist/{search-CfT3KxO5.mjs.map → search-C-qDk4kx.mjs.map} +1 -1
  536. package/dist/{secrets-BH2Qow1-.mjs → secrets-rEtHt-6z.mjs} +1 -1
  537. package/dist/{secrets-BH2Qow1-.mjs.map → secrets-rEtHt-6z.mjs.map} +1 -1
  538. package/dist/{sections-DTfjyf1z.mjs → sections-B_nYvlwn.mjs} +2 -2
  539. package/dist/{sections-DTfjyf1z.mjs.map → sections-B_nYvlwn.mjs.map} +1 -1
  540. package/dist/seed/index.d.mts +2 -2
  541. package/dist/seed/index.mjs +18 -18
  542. package/dist/seo/index.d.mts +3 -3
  543. package/dist/seo/index.mjs +13 -13
  544. package/dist/{seo-Cm-dtvgk.mjs → seo-926NRIgt.mjs} +3 -3
  545. package/dist/{seo-Cm-dtvgk.mjs.map → seo-926NRIgt.mjs.map} +1 -1
  546. package/dist/{seo-BSiTIXzd.mjs → seo-CVWkdv6x.mjs} +1 -1
  547. package/dist/{seo-BSiTIXzd.mjs.map → seo-CVWkdv6x.mjs.map} +1 -1
  548. package/dist/{service-DYsDHEKe.mjs → service-CJl3ZAYs.mjs} +3 -3
  549. package/dist/{service-DYsDHEKe.mjs.map → service-CJl3ZAYs.mjs.map} +1 -1
  550. package/dist/{session-user-CNAfvsdw.mjs → session-user-BlMgqCHS.mjs} +1 -1
  551. package/dist/{session-user-CNAfvsdw.mjs.map → session-user-BlMgqCHS.mjs.map} +1 -1
  552. package/dist/{settings-BgN-0DT-.mjs → settings--wobrkkC.mjs} +18 -5
  553. package/dist/settings--wobrkkC.mjs.map +1 -0
  554. package/dist/{settings-DjtkUo__.mjs → settings-Cwm6o2jY.mjs} +3 -3
  555. package/dist/{settings-DjtkUo__.mjs.map → settings-Cwm6o2jY.mjs.map} +1 -1
  556. package/dist/{setup-complete-CcpXaPU7.mjs → setup-complete-DQbClIN2.mjs} +1 -1
  557. package/dist/{setup-complete-CcpXaPU7.mjs.map → setup-complete-DQbClIN2.mjs.map} +1 -1
  558. package/dist/{setup-nonce-Reyw30jg.mjs → setup-nonce-DZCFS2sn.mjs} +1 -1
  559. package/dist/{setup-nonce-Reyw30jg.mjs.map → setup-nonce-DZCFS2sn.mjs.map} +1 -1
  560. package/dist/{site-url-Cs_V9n3V.mjs → site-url-BrxA_E9H.mjs} +2 -2
  561. package/dist/{site-url-Cs_V9n3V.mjs.map → site-url-BrxA_E9H.mjs.map} +1 -1
  562. package/dist/{snapshot-DSB8P7Lm.mjs → snapshot--OCDTqdb.mjs} +1 -1
  563. package/dist/{snapshot-DSB8P7Lm.mjs.map → snapshot--OCDTqdb.mjs.map} +1 -1
  564. package/dist/{ssrf-DqebVE34.mjs → ssrf-5sIM5jzl.mjs} +4 -3
  565. package/dist/{ssrf-DqebVE34.mjs.map → ssrf-5sIM5jzl.mjs.map} +1 -1
  566. package/dist/{state-BJfU9Uzw.mjs → state-BpFVsJZB.mjs} +1 -1
  567. package/dist/{state-BJfU9Uzw.mjs.map → state-BpFVsJZB.mjs.map} +1 -1
  568. package/dist/{status-DDu8HIQl.mjs → status-B777-dXz.mjs} +1 -1
  569. package/dist/{status-DDu8HIQl.mjs.map → status-B777-dXz.mjs.map} +1 -1
  570. package/dist/storage/local.d.mts +2 -1
  571. package/dist/storage/local.d.mts.map +1 -1
  572. package/dist/storage/local.mjs +1 -1
  573. package/dist/storage/local.mjs.map +1 -1
  574. package/dist/storage/s3.d.mts +2 -1
  575. package/dist/storage/s3.d.mts.map +1 -1
  576. package/dist/storage/s3.mjs +3 -2
  577. package/dist/storage/s3.mjs.map +1 -1
  578. package/dist/{target-D0o26Azo.mjs → target-BmR0iTwz.mjs} +1 -1
  579. package/dist/{target-D0o26Azo.mjs.map → target-BmR0iTwz.mjs.map} +1 -1
  580. package/dist/{taxonomies-CPGxo-bn.mjs → taxonomies-DkAkMupr.mjs} +16 -13
  581. package/dist/taxonomies-DkAkMupr.mjs.map +1 -0
  582. package/dist/{taxonomies-kmyudNGP.mjs → taxonomies-Wfy_Lg12.mjs} +42 -16
  583. package/dist/taxonomies-Wfy_Lg12.mjs.map +1 -0
  584. package/dist/{taxonomy-8X09dLN5.mjs → taxonomy-tJepplLL.mjs} +4 -4
  585. package/dist/{taxonomy-8X09dLN5.mjs.map → taxonomy-tJepplLL.mjs.map} +1 -1
  586. package/dist/{tokens-gTS-IVUp.mjs → tokens-BCO1oruF.mjs} +1 -1
  587. package/dist/{tokens-gTS-IVUp.mjs.map → tokens-BCO1oruF.mjs.map} +1 -1
  588. package/dist/{transport-fX59xaJz.mjs → transport-Dr3qtwXC.mjs} +1 -1
  589. package/dist/{transport-fX59xaJz.mjs.map → transport-Dr3qtwXC.mjs.map} +1 -1
  590. package/dist/{transport-_6R1ub7t.d.mts → transport-xh6YKW2G.d.mts} +1 -1
  591. package/dist/{transport-_6R1ub7t.d.mts.map → transport-xh6YKW2G.d.mts.map} +1 -1
  592. package/dist/{trusted-proxy-fe6nEgR4.mjs → trusted-proxy-BR5F1Otp.mjs} +1 -1
  593. package/dist/{trusted-proxy-fe6nEgR4.mjs.map → trusted-proxy-BR5F1Otp.mjs.map} +1 -1
  594. package/dist/{types-BXUmsINL.mjs → types-3qNFPlci.mjs} +1 -1
  595. package/dist/{types-BXUmsINL.mjs.map → types-3qNFPlci.mjs.map} +1 -1
  596. package/dist/{types-Vg6f5i3s.d.mts → types-B6GrPic4.d.mts} +9 -3
  597. package/dist/{types-Vg6f5i3s.d.mts.map → types-B6GrPic4.d.mts.map} +1 -1
  598. package/dist/{types-DJQxln7q.d.mts → types-BuHW4xDE.d.mts} +11 -10
  599. package/dist/types-BuHW4xDE.d.mts.map +1 -0
  600. package/dist/{types-BzBd3PeX.d.mts → types-C00oyCZp.d.mts} +1 -1
  601. package/dist/{types-BzBd3PeX.d.mts.map → types-C00oyCZp.d.mts.map} +1 -1
  602. package/dist/{types-Blb-MRC3.d.mts → types-CDtkNSFQ.d.mts} +1 -1
  603. package/dist/{types-Blb-MRC3.d.mts.map → types-CDtkNSFQ.d.mts.map} +1 -1
  604. package/dist/{types-D5VVLr7J.d.mts → types-CPeXo1wF.d.mts} +1 -1
  605. package/dist/{types-D5VVLr7J.d.mts.map → types-CPeXo1wF.d.mts.map} +1 -1
  606. package/dist/types-CQPWhPxE.mjs.map +1 -1
  607. package/dist/{types-D_DWe7k9.d.mts → types-CXFkwEGC.d.mts} +3 -2
  608. package/dist/types-CXFkwEGC.d.mts.map +1 -0
  609. package/dist/{types-CLqvnd05.d.mts → types-DczELMop.d.mts} +1 -1
  610. package/dist/{types-CLqvnd05.d.mts.map → types-DczELMop.d.mts.map} +1 -1
  611. package/dist/types-DfuTR6dL.mjs +3 -0
  612. package/dist/{types-DHxzBjbD.d.mts → types-FV23hEoW.d.mts} +1 -1
  613. package/dist/{types-DHxzBjbD.d.mts.map → types-FV23hEoW.d.mts.map} +1 -1
  614. package/dist/{types-Br-ERaxh.d.mts → types-xDMtUFnO.d.mts} +1 -1
  615. package/dist/{types-Br-ERaxh.d.mts.map → types-xDMtUFnO.d.mts.map} +1 -1
  616. package/dist/{upload-attempts-CNyGSepg.mjs → upload-attempts-BsqoQ1fn.mjs} +1 -1
  617. package/dist/{upload-attempts-CNyGSepg.mjs.map → upload-attempts-BsqoQ1fn.mjs.map} +1 -1
  618. package/dist/{url-pattern-K-KdXcSe.mjs → url-pattern-D4c2czpJ.mjs} +1 -1
  619. package/dist/{url-pattern-K-KdXcSe.mjs.map → url-pattern-D4c2czpJ.mjs.map} +1 -1
  620. package/dist/{user-CtYY0tp2.mjs → user-BTZ40aq6.mjs} +2 -2
  621. package/dist/{user-CtYY0tp2.mjs.map → user-BTZ40aq6.mjs.map} +1 -1
  622. package/dist/{utils-HztC73hq.mjs → utils-BKvNjjhY.mjs} +1 -1
  623. package/dist/{utils-HztC73hq.mjs.map → utils-BKvNjjhY.mjs.map} +1 -1
  624. package/dist/{validate-Dd-IUjnZ.mjs → validate-DE_LRwo9.mjs} +2 -2
  625. package/dist/{validate-Dd-IUjnZ.mjs.map → validate-DE_LRwo9.mjs.map} +1 -1
  626. package/dist/{validate-DlqdOZXB.d.mts → validate-DUP27OYD.d.mts} +5 -5
  627. package/dist/{validate-DlqdOZXB.d.mts.map → validate-DUP27OYD.d.mts.map} +1 -1
  628. package/dist/{validation-D83n7Npi.mjs → validation-BMImFJ11.mjs} +8 -8
  629. package/dist/{validation-D83n7Npi.mjs.map → validation-BMImFJ11.mjs.map} +1 -1
  630. package/dist/version-Bii3suaO.mjs +7 -0
  631. package/dist/{version-ri-sCyAp.mjs.map → version-Bii3suaO.mjs.map} +1 -1
  632. package/dist/{widgets-CpmZd9Q-.mjs → widgets-D3gJ10xU.mjs} +17 -4
  633. package/dist/widgets-D3gJ10xU.mjs.map +1 -0
  634. package/dist/{wxr-taxonomies-BFd0y72_.d.mts → wxr-taxonomies-B2Og_6rU.d.mts} +1 -1
  635. package/dist/{wxr-taxonomies-BFd0y72_.d.mts.map → wxr-taxonomies-B2Og_6rU.d.mts.map} +1 -1
  636. package/dist/{wxr-taxonomies-Df4vxWcU.mjs → wxr-taxonomies-Cxngsjwo.mjs} +4 -4
  637. package/dist/{wxr-taxonomies-Df4vxWcU.mjs.map → wxr-taxonomies-Cxngsjwo.mjs.map} +1 -1
  638. package/dist/{zod-generator-DylzusHh.mjs → zod-generator-BuYSQJB7.mjs} +11 -7
  639. package/dist/zod-generator-BuYSQJB7.mjs.map +1 -0
  640. package/package.json +12 -9
  641. package/src/api/errors.ts +2 -0
  642. package/src/api/handlers/content.ts +49 -11
  643. package/src/api/handlers/index.ts +1 -0
  644. package/src/api/handlers/manifest.ts +161 -12
  645. package/src/api/handlers/media.ts +30 -0
  646. package/src/api/handlers/registry.ts +393 -289
  647. package/src/api/handlers/taxonomies.ts +16 -9
  648. package/src/api/openapi/document.ts +85 -5
  649. package/src/api/parse.ts +1 -1
  650. package/src/api/schemas/auth.ts +4 -4
  651. package/src/api/schemas/bylines.ts +3 -3
  652. package/src/api/schemas/comments.ts +1 -1
  653. package/src/api/schemas/common.ts +3 -2
  654. package/src/api/schemas/content.ts +78 -22
  655. package/src/api/schemas/media.ts +52 -0
  656. package/src/api/schemas/redirects.ts +6 -1
  657. package/src/api/schemas/schema.ts +4 -0
  658. package/src/api/schemas/setup.ts +1 -1
  659. package/src/api/schemas/taxonomies.ts +2 -2
  660. package/src/api/schemas/users.ts +1 -1
  661. package/src/api/types.ts +23 -10
  662. package/src/astro/dev-typegen.ts +35 -0
  663. package/src/astro/image-endpoint.ts +2 -2
  664. package/src/astro/integration/dev-typegen.ts +91 -0
  665. package/src/astro/integration/index.ts +9 -44
  666. package/src/astro/integration/routes.ts +5 -0
  667. package/src/astro/integration/vite-config.ts +21 -2
  668. package/src/astro/middleware.ts +5 -0
  669. package/src/astro/routes/api/admin/api-tokens/index.ts +4 -1
  670. package/src/astro/routes/api/admin/oauth-clients/[id].ts +1 -1
  671. package/src/astro/routes/api/admin/oauth-clients/index.ts +1 -1
  672. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  673. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +3 -7
  674. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  675. package/src/astro/routes/api/admin/plugins/registry/verify.ts +60 -0
  676. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +8 -2
  677. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +2 -1
  678. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +6 -2
  679. package/src/astro/routes/api/manifest.ts +1 -1
  680. package/src/astro/routes/api/media/[id]/replace.ts +114 -0
  681. package/src/astro/routes/api/media/file/[...key].ts +4 -1
  682. package/src/astro/routes/api/media/upload-url.ts +8 -4
  683. package/src/astro/routes/api/media.ts +54 -8
  684. package/src/astro/routes/api/menus/[name]/items/[id].ts +7 -2
  685. package/src/astro/routes/api/menus/[name]/items.ts +4 -1
  686. package/src/astro/routes/api/menus/[name]/reorder.ts +4 -1
  687. package/src/astro/routes/api/menus/[name]/translations.ts +4 -1
  688. package/src/astro/routes/api/menus/[name].ts +7 -2
  689. package/src/astro/routes/api/menus/index.ts +4 -1
  690. package/src/astro/routes/api/settings/email.ts +1 -1
  691. package/src/astro/routes/api/settings.ts +4 -1
  692. package/src/astro/routes/api/taxonomies/[name]/reorder.ts +4 -1
  693. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +4 -1
  694. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +7 -2
  695. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +4 -1
  696. package/src/astro/routes/api/taxonomies/[name].ts +7 -2
  697. package/src/astro/routes/api/taxonomies/index.ts +4 -1
  698. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +3 -1
  699. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +5 -2
  700. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +3 -1
  701. package/src/astro/routes/api/widget-areas/[name].ts +3 -1
  702. package/src/astro/routes/api/widget-areas/index.ts +3 -1
  703. package/src/astro/types.ts +23 -5
  704. package/src/auth/challenge-store.ts +3 -1
  705. package/src/aws-sdk.d.ts +1 -0
  706. package/src/cache/chrome-tags.ts +24 -0
  707. package/src/components/EmDashImage.astro +2 -1
  708. package/src/components/EmDashMedia.astro +2 -1
  709. package/src/components/InlinePortableTextEditor.tsx +6 -1
  710. package/src/components/widgets/Archives.astro +2 -37
  711. package/src/content/converters/index.ts +2 -0
  712. package/src/content/converters/mark-safety.ts +1 -0
  713. package/src/content/converters/portable-text-identity.ts +139 -0
  714. package/src/content/converters/portable-text-to-prosemirror.ts +202 -69
  715. package/src/content/converters/prosemirror-to-portable-text.ts +96 -25
  716. package/src/content/converters/types.ts +1 -0
  717. package/src/database/migrations/017_authorization_codes.ts +14 -4
  718. package/src/database/migrations/074_content_deleted_scheduled_index.ts +55 -0
  719. package/src/database/migrations/runner.ts +3 -1
  720. package/src/database/repositories/byline.ts +32 -16
  721. package/src/database/repositories/content.ts +114 -47
  722. package/src/database/repositories/media-usage.ts +66 -37
  723. package/src/database/repositories/media.ts +60 -1
  724. package/src/database/repositories/plugin-storage.ts +74 -9
  725. package/src/database/repositories/types.ts +8 -7
  726. package/src/db/adapters.ts +8 -1
  727. package/src/db/postgres.ts +23 -0
  728. package/src/emdash-runtime.ts +80 -150
  729. package/src/fields/boolean.ts +1 -1
  730. package/src/fields/datetime.ts +1 -1
  731. package/src/fields/file.ts +1 -1
  732. package/src/fields/integer.ts +1 -1
  733. package/src/fields/json.ts +1 -1
  734. package/src/fields/multiselect.ts +1 -1
  735. package/src/fields/number.ts +1 -1
  736. package/src/fields/portable-text.ts +1 -1
  737. package/src/fields/richtext.ts +1 -1
  738. package/src/fields/select.ts +1 -1
  739. package/src/fields/slug.ts +1 -1
  740. package/src/fields/text.ts +1 -1
  741. package/src/fields/textarea.ts +1 -1
  742. package/src/fields/types.ts +2 -2
  743. package/src/index.ts +25 -3
  744. package/src/loader.ts +63 -10
  745. package/src/mcp/server.ts +228 -24
  746. package/src/media/image-endpoint.ts +5 -2
  747. package/src/media/normalize.ts +15 -4
  748. package/src/media/usage/cleanup.ts +9 -8
  749. package/src/menus/index.ts +16 -0
  750. package/src/object-cache/index.ts +44 -2
  751. package/src/plugin-types.ts +18 -1
  752. package/src/plugins/hooks.ts +4 -1
  753. package/src/plugins/index.ts +6 -0
  754. package/src/plugins/manager.ts +2 -1
  755. package/src/plugins/manifest-schema.ts +1 -1
  756. package/src/plugins/routes.ts +3 -1
  757. package/src/plugins/sandbox/hook-result.ts +52 -0
  758. package/src/plugins/sandbox/index.ts +10 -0
  759. package/src/plugins/types.ts +6 -0
  760. package/src/registry/artifact-checksum.ts +15 -0
  761. package/src/registry/artifact-verification.ts +85 -0
  762. package/src/registry/authoritative-records.ts +180 -0
  763. package/src/registry/record-labels.ts +28 -0
  764. package/src/schema/registry.ts +26 -3
  765. package/src/schema/zod-generator.ts +16 -13
  766. package/src/security/ssrf.ts +5 -2
  767. package/src/settings/index.ts +16 -0
  768. package/src/storage/local.ts +1 -0
  769. package/src/storage/s3.ts +2 -0
  770. package/src/storage/types.ts +2 -0
  771. package/src/taxonomies/index.ts +40 -20
  772. package/src/widgets/archives.ts +67 -0
  773. package/src/widgets/index.ts +16 -0
  774. package/dist/api-B0w3uBiG.mjs +0 -2269
  775. package/dist/api-B0w3uBiG.mjs.map +0 -1
  776. package/dist/byline-Cpnwrje1.mjs.map +0 -1
  777. package/dist/challenge-store-D5pSY2S1.mjs.map +0 -1
  778. package/dist/content-CP-SeXNa.mjs.map +0 -1
  779. package/dist/content-refresh-DLOfIhYc.mjs.map +0 -1
  780. package/dist/context-BR5hRWiK.mjs.map +0 -1
  781. package/dist/errors-n2j49uNf.mjs.map +0 -1
  782. package/dist/index-DWbJrsAg.d.mts.map +0 -1
  783. package/dist/loader-ZsjE4paL.mjs.map +0 -1
  784. package/dist/manifest-schema-CUYPfCEH.mjs.map +0 -1
  785. package/dist/media-BjwNuLEu.mjs.map +0 -1
  786. package/dist/media-D6SeAfBl.mjs.map +0 -1
  787. package/dist/menus-CNg0QlDt.mjs.map +0 -1
  788. package/dist/normalize-CSxdKBVa.mjs.map +0 -1
  789. package/dist/object-cache-71tPfV4A.mjs.map +0 -1
  790. package/dist/options-BhZIeFY1.d.mts.map +0 -1
  791. package/dist/redirects-CRIM55KH.mjs.map +0 -1
  792. package/dist/registry-BiO5WwPq.mjs +0 -2716
  793. package/dist/registry-BiO5WwPq.mjs.map +0 -1
  794. package/dist/registry-Cx-jEJOS.mjs.map +0 -1
  795. package/dist/relations-BTPM2saV.mjs.map +0 -1
  796. package/dist/relations-C7B46bcU.d.mts.map +0 -1
  797. package/dist/runner-77GOZCnH.mjs.map +0 -1
  798. package/dist/runner-zgqA_CqP.d.mts.map +0 -1
  799. package/dist/settings-BgN-0DT-.mjs.map +0 -1
  800. package/dist/taxonomies-CPGxo-bn.mjs.map +0 -1
  801. package/dist/taxonomies-kmyudNGP.mjs.map +0 -1
  802. package/dist/types-D6OcIp2P.mjs +0 -3
  803. package/dist/types-DJQxln7q.d.mts.map +0 -1
  804. package/dist/types-D_DWe7k9.d.mts.map +0 -1
  805. package/dist/version-ri-sCyAp.mjs +0 -7
  806. package/dist/widgets-CpmZd9Q-.mjs.map +0 -1
  807. package/dist/zod-generator-DylzusHh.mjs.map +0 -1
  808. /package/dist/{api-tokens-B5NHLNUN.mjs → api-tokens-BfYts-sq.mjs} +0 -0
  809. /package/dist/{ssrf-BPKzkm6O.mjs → ssrf-Dg3cIT-b.mjs} +0 -0
@@ -1 +0,0 @@
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 +0,0 @@
1
- {"version":3,"file":"byline-Cpnwrje1.mjs","names":[],"sources":["../src/database/repositories/byline.ts"],"sourcesContent":["import { sql, type Kysely, type Selectable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { getBylineFieldDefs } from \"../../bylines/field-defs-cache.js\";\nimport {\n\tinvalidateBylineObjectCache,\n\tinvalidateCollectionCache,\n} from \"../../object-cache/index.js\";\nimport {\n\tclearRequestCacheEntry,\n\tpeekRequestCache,\n\tsetRequestCacheEntry,\n} from \"../../request-cache.js\";\nimport type { BylineFieldDefinition, CustomFieldValue } from \"../../schema/types.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { listTablesLike } from \"../dialect-helpers.js\";\nimport { withTransaction } from \"../transaction.js\";\nimport type { BylineTable, Database } from \"../types.js\";\nimport { validateIdentifier } from \"../validate.js\";\nimport {\n\tdecodeCursor,\n\tEmDashValidationError,\n\tencodeCursor,\n\ttype BylineSummary,\n\ttype ContentBylineCredit,\n\ttype FindManyResult,\n} from \"./types.js\";\n\ntype BylineRow = Selectable<BylineTable>;\n\n/**\n * A byline row optionally augmented with the avatar's media columns, folded in\n * by the `LEFT JOIN media` in the content-credit hydration queries. The plain\n * `selectAll()` finders produce rows without these keys, so they're optional\n * and `rowToByline` defaults them to null.\n */\ntype BylineRowWithAvatar = BylineRow & {\n\tavatar_storage_key?: string | null;\n\tavatar_alt?: string | null;\n\tavatar_blurhash?: string | null;\n\tavatar_dominant_color?: string | null;\n};\n\nexport interface CreateBylineInput {\n\tslug: string;\n\tdisplayName: string;\n\tbio?: string | null;\n\tavatarMediaId?: string | null;\n\twebsiteUrl?: string | null;\n\tuserId?: string | null;\n\tisGuest?: boolean;\n\t/**\n\t * Locale this byline row belongs to. When omitted, the DB DEFAULT (the\n\t * configured `defaultLocale` after migration 040) is used. Keeps behaviour\n\t * consistent with `TaxonomyRepository.create`.\n\t */\n\tlocale?: string;\n\t/**\n\t * When set, the new row joins the source byline's translation_group rather\n\t * than minting a fresh one. The source must exist; otherwise the create\n\t * throws. Mirrors `TaxonomyRepository.create`.\n\t */\n\ttranslationOf?: string;\n\t/**\n\t * Byline custom-field values to seed on the new row (Phase 6 of\n\t * Discussion #1174). Same semantics as `UpdateBylineInput.customFields`:\n\t * keys must match registered slugs in `_emdash_byline_fields`, values\n\t * are validated against the field's type, and writes route to\n\t * `_emdash_byline_field_values` (translatable) or\n\t * `_emdash_byline_field_group_values` (group-shared). Validation runs\n\t * before the row insert so a bad value can't leave a bare byline behind.\n\t */\n\tcustomFields?: Record<string, unknown>;\n}\n\nexport interface UpdateBylineInput {\n\tslug?: string;\n\tdisplayName?: string;\n\tbio?: string | null;\n\tavatarMediaId?: string | null;\n\twebsiteUrl?: string | null;\n\tuserId?: string | null;\n\tisGuest?: boolean;\n\t/**\n\t * Byline custom-field values to write (Phase 3 of Discussion #1174).\n\t *\n\t * Each key must match a registered slug in `_emdash_byline_fields`;\n\t * unknown keys throw `EmDashValidationError`. Per-field writes route\n\t * to `_emdash_byline_field_values` (when the field's `translatable`\n\t * flag is true) or `_emdash_byline_field_group_values` (when false).\n\t * A value of `null` clears the row.\n\t *\n\t * Values are validated against the field's type:\n\t * - `string` / `text` / `url` accept a `string`\n\t * - `boolean` accepts a `boolean`\n\t * - `select` accepts a `string` that appears in `validation.options`\n\t *\n\t * Writes are idempotent (`INSERT … ON CONFLICT DO UPDATE`), so\n\t * retrying the same update produces the same DB state.\n\t */\n\tcustomFields?: Record<string, unknown>;\n}\n\nexport interface ContentBylineInput {\n\tbylineId: string;\n\troleLabel?: string | null;\n}\n\nfunction rowToByline(row: BylineRowWithAvatar): BylineSummary {\n\treturn {\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\tdisplayName: row.display_name,\n\t\tbio: row.bio,\n\t\tavatarMediaId: row.avatar_media_id,\n\t\tavatarStorageKey: row.avatar_storage_key ?? null,\n\t\tavatarAlt: row.avatar_alt ?? null,\n\t\tavatarBlurhash: row.avatar_blurhash ?? null,\n\t\tavatarDominantColor: row.avatar_dominant_color ?? null,\n\t\twebsiteUrl: row.website_url,\n\t\tuserId: row.user_id,\n\t\tisGuest: row.is_guest === 1,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Merge a single decoded value into a `BylineSummary.customFields` map.\n * Centralised so the merge semantics (null storage, JSON.parse failure\n * handling) live in one place across both translatable and group-shared\n * paths.\n *\n * A stored row with `value = NULL` (representing an explicit null) is\n * surfaced as `null` in `customFields`. A row with a malformed JSON\n * payload is dropped silently with a `console.warn` — a corrupted\n * payload shouldn't break the entire byline hydration; the field-defs\n * cache will let admins replace the value, and the warning makes the\n * issue debuggable. (Storage path uses `JSON.stringify`, so the only\n * way to get malformed JSON is direct DB tampering or a future\n * migration bug.)\n */\nfunction assignCustomFieldValue(\n\tsummary: BylineSummary,\n\tfield: BylineFieldDefinition,\n\tstored: string | null,\n): void {\n\tconst target = summary.customFields ?? {};\n\tif (stored === null) {\n\t\ttarget[field.slug] = null;\n\t} else {\n\t\ttry {\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- coerceFieldValue ran at write time, see field-defs-cache.ts\n\t\t\ttarget[field.slug] = JSON.parse(stored) as CustomFieldValue;\n\t\t} catch {\n\t\t\tconsole.warn(\n\t\t\t\t`[BylineRepository] dropping malformed JSON for byline=${summary.id} ` +\n\t\t\t\t\t`field=${field.slug}: ${stored.slice(0, 60)}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t}\n\tsummary.customFields = target;\n}\n\n/**\n * Coerce a raw write-path value to `CustomFieldValue`, throwing\n * `EmDashValidationError` on type mismatch. `null` clears the field\n * (DELETE in the write path).\n *\n * TODO: `field.required` is not enforced. The admin UI exposes the\n * toggle but the backend accepts missing values; design pass needed\n * on the enforcement model.\n */\nfunction coerceFieldValue(field: BylineFieldDefinition, raw: unknown): CustomFieldValue {\n\tif (raw === null) return null;\n\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\": {\n\t\t\tif (typeof raw !== \"string\") {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" expects a string value (received ${typeof raw})`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: typeof raw },\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn raw;\n\t\t}\n\t\tcase \"url\": {\n\t\t\tif (typeof raw !== \"string\") {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" expects a string value (received ${typeof raw})`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: typeof raw },\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Empty string round-trips as a clear from the admin UI; any\n\t\t\t// non-empty value must be a valid http(s) URL. The scheme\n\t\t\t// allowlist mirrors `httpUrl` in `api/schemas/common.ts` —\n\t\t\t// `new URL` alone would accept `javascript:`/`data:` etc.\n\t\t\tif (raw === \"\") return raw;\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(raw);\n\t\t\t} catch {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" expects a valid URL (received \"${raw}\")`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: raw },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" must use http or https scheme (received \"${parsed.protocol}\")`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: raw, protocol: parsed.protocol },\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn raw;\n\t\t}\n\t\tcase \"boolean\": {\n\t\t\tif (typeof raw !== \"boolean\") {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" expects a boolean value (received ${typeof raw})`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: typeof raw },\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn raw;\n\t\t}\n\t\tcase \"select\": {\n\t\t\tif (typeof raw !== \"string\") {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" expects a string value (received ${typeof raw})`,\n\t\t\t\t\t{ slug: field.slug, type: field.type, received: typeof raw },\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst options = field.validation?.options ?? [];\n\t\t\tif (!options.includes(raw)) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Byline field \"${field.slug}\" value \"${raw}\" is not one of the registered choices`,\n\t\t\t\t\t{ slug: field.slug, value: raw, options },\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn raw;\n\t\t}\n\t}\n}\n\n/**\n * Byline repository for content credits.\n *\n * Bylines are per-locale (migration 040). Translations of the same byline\n * share a `translation_group` ULID. `_emdash_content_bylines.byline_id` and\n * `ec_*.primary_byline_id` store the translation_group (not a row id) so a\n * single credit spans every locale variant of a byline.\n *\n * The repository does not resolve locale fallbacks on its own — callers\n * supply the locale they want. Hydration is strict per locale: a credit at\n * locale X renders iff a byline row exists at locale X within the credited\n * translation group. This mirrors `TaxonomyRepository.getTermsForEntry` and\n * the convention established by PR #916.\n *\n * Runtime helpers in `packages/core/src/bylines/index.ts` may layer fallback\n * resolution on top for the \"look up one byline by slug\" path, but the\n * relation-hydration methods on this class are always strict.\n */\nexport class BylineRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// ============================================\n\t// Custom-field hydration (Phase 3 of #1174)\n\t// ============================================\n\n\t/**\n\t * Merge `customFields` onto each `BylineSummary` produced from the\n\t * given rows. Two batched queries total — one against\n\t * `_emdash_byline_field_values` (keyed by `byline_id`), one against\n\t * `_emdash_byline_field_group_values` (keyed by `translation_group`)\n\t * — both chunked at `SQL_BATCH_SIZE` for D1's bound-parameter cap.\n\t *\n\t * When zero fields are registered, every row gets `customFields = {}`\n\t * with no value-table reads (the field-defs cache returns `[]`).\n\t * Group-shared values are looked up via the row's `translation_group`,\n\t * so every locale sibling of the same byline identity sees the same\n\t * non-translatable value without re-reading per row.\n\t *\n\t * **Duplicate-row handling.** Callers (notably `getContentBylinesMany`\n\t * for list views with repeated authors) can pass the same byline row\n\t * multiple times. We assign values by *iterating both `rows` and\n\t * `summaries` in lockstep by index*, not by deduping into a Map keyed\n\t * on byline id. A Map approach silently drops earlier duplicates' merge\n\t * step (last writer wins, earlier instances keep their initial `{}`).\n\t * Iterating by index gives every duplicate its own merged copy.\n\t *\n\t * Hydration is *strict per row* — values are merged onto whichever\n\t * `BylineRow` produced them. Fallback semantics (e.g. \"if no value\n\t * for this locale, show the default-locale value\") are not the\n\t * repository's concern; consumers layer them on top if wanted, the\n\t * same way `BylineRepository` doesn't resolve locale fallback for\n\t * the base byline lookup.\n\t */\n\tprivate async withCustomFields(rows: BylineRow[]): Promise<BylineSummary[]> {\n\t\tconst summaries = rows.map(rowToByline);\n\t\t// Always populate `customFields = {}` (PR plan AC #6) — even when\n\t\t// no fields are registered, every BylineSummary carries the empty\n\t\t// object. A fresh object per summary so duplicate rows don't share\n\t\t// state.\n\t\tfor (const summary of summaries) {\n\t\t\tsummary.customFields = {};\n\t\t}\n\t\tawait this.applyCustomFieldsTo(summaries);\n\t\treturn summaries;\n\t}\n\n\tprivate async withCustomFieldsOne(row: BylineRow | undefined): Promise<BylineSummary | null> {\n\t\tif (!row) return null;\n\t\tconst [result] = await this.withCustomFields([row]);\n\t\treturn result ?? null;\n\t}\n\n\t/**\n\t * Hydrate `customFields` on each `BylineSummary`, mutating in place.\n\t *\n\t * The public entry point for callers that fetch byline rows in\n\t * multiple passes (e.g. `getBylinesForEntries`, which buckets by\n\t * locale and calls `getContentBylinesMany` per bucket) and want a\n\t * single batched hydration over the union of bylines, not one per\n\t * pass. Use with the `skipHydration` option on the read methods to\n\t * defer customFields work to a single call here.\n\t *\n\t * Two batched queries total (translatable + group-shared) regardless\n\t * of how many bylines, locales, or translation_groups are in the\n\t * input — meets the Phase 3 query-count envelope for mixed-locale\n\t * list views even when sibling locales reference disjoint\n\t * translation_groups.\n\t *\n\t * Replaces any existing `customFields` on each summary with a freshly\n\t * fetched map. Callers that want to merge rather than replace should\n\t * not use this entry point.\n\t */\n\tasync hydrateBylineCustomFields(summaries: BylineSummary[]): Promise<void> {\n\t\tfor (const summary of summaries) {\n\t\t\tsummary.customFields = {};\n\t\t}\n\t\tawait this.applyCustomFieldsTo(summaries);\n\t}\n\n\t/**\n\t * Shared merge engine for `withCustomFields` and\n\t * `hydrateBylineCustomFields`. Reads field defs (cached), batches the\n\t * translatable + group-shared fetches, and walks `summaries` directly\n\t * to apply values.\n\t *\n\t * Iterates `summaries` (not a `summaryById` map) so duplicate\n\t * `BylineSummary` objects sharing the same `id` — e.g. the same\n\t * author credited to multiple entries — each get their own merged\n\t * values. The previous Map-based dedup silently dropped earlier\n\t * duplicates' merge step.\n\t */\n\tprivate async applyCustomFieldsTo(summaries: BylineSummary[]): Promise<void> {\n\t\tif (summaries.length === 0) return;\n\n\t\tconst defs = await getBylineFieldDefs(this.db);\n\t\tif (defs.length === 0) return;\n\n\t\tconst fieldById = new Map(defs.map((d) => [d.id, d]));\n\n\t\t// Translatable values, batched by byline_id (unique per locale, so\n\t\t// IDs across different locale buckets don't collide — one batched\n\t\t// query covers everything).\n\t\tconst translatableByByline = new Map<string, Map<string, string | null>>();\n\t\tconst bylineIds = [...new Set(summaries.map((s) => s.id))];\n\t\tfor (const chunk of chunks(bylineIds, SQL_BATCH_SIZE)) {\n\t\t\tconst trRows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_byline_field_values\")\n\t\t\t\t.select([\"byline_id\", \"field_id\", \"value\"])\n\t\t\t\t.where(\"byline_id\", \"in\", chunk)\n\t\t\t\t.execute();\n\t\t\tfor (const trRow of trRows) {\n\t\t\t\tlet fieldMap = translatableByByline.get(trRow.byline_id);\n\t\t\t\tif (!fieldMap) {\n\t\t\t\t\tfieldMap = new Map();\n\t\t\t\t\ttranslatableByByline.set(trRow.byline_id, fieldMap);\n\t\t\t\t}\n\t\t\t\tfieldMap.set(trRow.field_id, trRow.value);\n\t\t\t}\n\t\t}\n\n\t\t// Group-shared values, batched over the union of translation_groups,\n\t\t// with per-group request-cache priming so subsequent calls within\n\t\t// the same request share the lookup. Together with the\n\t\t// `hydrateBylineCustomFields` + `skipHydration` flow in\n\t\t// `getBylinesForEntries`, this keeps mixed-locale list views to\n\t\t// **one** group-shared query per request, even for disjoint\n\t\t// translation_groups across locale buckets.\n\t\tconst groups = [\n\t\t\t...new Set(\n\t\t\t\tsummaries\n\t\t\t\t\t.map((s) => s.translationGroup)\n\t\t\t\t\t.filter((g): g is string => typeof g === \"string\" && g.length > 0),\n\t\t\t),\n\t\t];\n\t\tconst groupByGroup = await this.loadGroupValuesByIds(groups);\n\n\t\t// Each loop gates on `field.translatable` so a row in the wrong\n\t\t// owner table (e.g. left over from a translatable flip) can't\n\t\t// leak into hydration.\n\t\tfor (const summary of summaries) {\n\t\t\tconst trValues = translatableByByline.get(summary.id);\n\t\t\tif (trValues) {\n\t\t\t\tfor (const [fieldId, value] of trValues) {\n\t\t\t\t\tconst field = fieldById.get(fieldId);\n\t\t\t\t\tif (!field || !field.translatable) continue;\n\t\t\t\t\tassignCustomFieldValue(summary, field, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (summary.translationGroup) {\n\t\t\t\tconst grpValues = groupByGroup.get(summary.translationGroup);\n\t\t\t\tif (grpValues) {\n\t\t\t\t\tfor (const [fieldId, value] of grpValues) {\n\t\t\t\t\t\tconst field = fieldById.get(fieldId);\n\t\t\t\t\t\tif (!field || field.translatable) continue;\n\t\t\t\t\t\tassignCustomFieldValue(summary, field, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the group-shared custom-field values for a set of\n\t * translation_groups, sharing work across hydration calls within the\n\t * same request via per-group `requestCached` entries.\n\t *\n\t * The non-translatable storage table (`_emdash_byline_field_group_values`)\n\t * is keyed by `translation_group`, which is locale-agnostic. Combining\n\t * this method with `skipHydration` on `getContentBylinesMany` and a\n\t * single `hydrateBylineCustomFields` call (see\n\t * `getBylinesForEntries`) keeps mixed-locale list hydration to **one**\n\t * batched group-shared SQL per request — even with disjoint\n\t * translation_groups across locale buckets. Solo callers (`findById`,\n\t * `findMany`, etc.) still get the same per-call batching they had\n\t * before; the cache simply means a second call in the same request\n\t * for an overlapping group is free.\n\t *\n\t * Cache key: `byline-field-group-values:${groupId}` — one entry per\n\t * group. Writes use `setRequestCacheEntry` (idempotent, doesn't\n\t * overwrite); `BylineRepository.update` calls `clearRequestCacheEntry`\n\t * after a group-shared write to keep the cache fresh within the same\n\t * request.\n\t */\n\tprivate async loadGroupValuesByIds(\n\t\tgroups: string[],\n\t): Promise<Map<string, Map<string, string | null>>> {\n\t\tconst result = new Map<string, Map<string, string | null>>();\n\t\tif (groups.length === 0) return result;\n\n\t\t// First pass: pull any already-cached groups from the request scope.\n\t\tconst missing: string[] = [];\n\t\tfor (const g of groups) {\n\t\t\tconst cached = peekRequestCache<Map<string, string | null>>(`byline-field-group-values:${g}`);\n\t\t\tif (cached) {\n\t\t\t\tresult.set(g, await cached);\n\t\t\t} else {\n\t\t\t\tmissing.push(g);\n\t\t\t}\n\t\t}\n\n\t\tif (missing.length === 0) return result;\n\n\t\t// Second pass: one batched SQL for the union of all missing groups\n\t\t// (chunked for D1's bound-parameter cap). Initialise empty maps for\n\t\t// missing groups so the primed cache covers \"this group has no\n\t\t// values\" — preventing a re-fetch on subsequent calls.\n\t\tconst fetched = new Map<string, Map<string, string | null>>();\n\t\tfor (const g of missing) fetched.set(g, new Map());\n\t\tfor (const chunk of chunks(missing, SQL_BATCH_SIZE)) {\n\t\t\tconst grpRows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_byline_field_group_values\")\n\t\t\t\t.select([\"translation_group\", \"field_id\", \"value\"])\n\t\t\t\t.where(\"translation_group\", \"in\", chunk)\n\t\t\t\t.execute();\n\t\t\tfor (const grpRow of grpRows) {\n\t\t\t\tconst fieldMap = fetched.get(grpRow.translation_group);\n\t\t\t\tif (!fieldMap) continue;\n\t\t\t\tfieldMap.set(grpRow.field_id, grpRow.value);\n\t\t\t}\n\t\t}\n\n\t\tfor (const g of missing) {\n\t\t\tconst m = fetched.get(g);\n\t\t\tif (!m) continue;\n\t\t\tsetRequestCacheEntry(`byline-field-group-values:${g}`, m);\n\t\t\tresult.set(g, m);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// ============================================\n\t// Reads\n\t// ============================================\n\n\tasync findById(id: string): Promise<BylineSummary | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_bylines\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn this.withCustomFieldsOne(row);\n\t}\n\n\t/**\n\t * Find a byline by slug. When `locale` is provided, filter by it strictly.\n\t * When omitted, returns the lowest-locale-code match (deterministic across\n\t * calls). Mirrors `TaxonomyRepository.findBySlug`.\n\t */\n\tasync findBySlug(slug: string, options?: { locale?: string }): Promise<BylineSummary | null> {\n\t\tlet query = this.db.selectFrom(\"_emdash_bylines\").selectAll().where(\"slug\", \"=\", slug);\n\t\tif (options?.locale !== undefined) query = query.where(\"locale\", \"=\", options.locale);\n\t\tconst row = await query.orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t\treturn this.withCustomFieldsOne(row);\n\t}\n\n\t/**\n\t * Find the byline linked to a CMS user. Post-migration 040 the partial\n\t * unique on user_id is `(user_id, locale)`, so `locale` is required to\n\t * disambiguate when multiple locale variants exist. When omitted, returns\n\t * the lowest-locale-code match.\n\t */\n\tasync findByUserId(userId: string, options?: { locale?: string }): Promise<BylineSummary | null> {\n\t\tlet query = this.db.selectFrom(\"_emdash_bylines\").selectAll().where(\"user_id\", \"=\", userId);\n\t\tif (options?.locale !== undefined) query = query.where(\"locale\", \"=\", options.locale);\n\t\tconst row = await query.orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t\treturn this.withCustomFieldsOne(row);\n\t}\n\n\tasync findMany(options?: {\n\t\tsearch?: string;\n\t\tisGuest?: boolean;\n\t\tuserId?: string;\n\t\tlocale?: string;\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t}): Promise<FindManyResult<BylineSummary>> {\n\t\tconst limit = Math.min(Math.max(options?.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_bylines\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (options?.search) {\n\t\t\tconst escaped = options.search\n\t\t\t\t.replaceAll(\"\\\\\", \"\\\\\\\\\")\n\t\t\t\t.replaceAll(\"%\", \"\\\\%\")\n\t\t\t\t.replaceAll(\"_\", \"\\\\_\");\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([eb(\"display_name\", \"like\", term), eb(\"slug\", \"like\", term)]),\n\t\t\t);\n\t\t}\n\n\t\tif (options?.isGuest !== undefined) {\n\t\t\tquery = query.where(\"is_guest\", \"=\", options.isGuest ? 1 : 0);\n\t\t}\n\n\t\tif (options?.userId !== undefined) {\n\t\t\tquery = query.where(\"user_id\", \"=\", options.userId);\n\t\t}\n\n\t\tif (options?.locale !== undefined) {\n\t\t\tquery = query.where(\"locale\", \"=\", options.locale);\n\t\t}\n\n\t\tif (options?.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst pageRows = rows.slice(0, limit);\n\t\tconst items = await this.withCustomFields(pageRows);\n\t\tconst result: FindManyResult<BylineSummary> = { items };\n\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1);\n\t\t\tif (last) {\n\t\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * List every sibling row in `translation_group`. Used by the admin\n\t * `TranslationsPanel` to render one entry per configured locale.\n\t */\n\tasync listTranslations(id: string): Promise<BylineSummary[]> {\n\t\tconst anchor = await this.findById(id);\n\t\tif (!anchor) return [];\n\t\tconst group = anchor.translationGroup ?? anchor.id;\n\t\treturn this.findByTranslationGroup(group);\n\t}\n\n\t/**\n\t * Direct lookup by `translation_group`. Returns every locale variant of a\n\t * byline, ordered by locale code (deterministic).\n\t */\n\tasync findByTranslationGroup(translationGroup: string): Promise<BylineSummary[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_bylines\")\n\t\t\t.selectAll()\n\t\t\t.where(\"translation_group\", \"=\", translationGroup)\n\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t.execute();\n\t\treturn this.withCustomFields(rows);\n\t}\n\n\t/**\n\t * Validate a `customFields` input map into a write list before any row\n\t * write — throws `EmDashValidationError` on unknown slugs, type\n\t * mismatches, or select-choice misses.\n\t */\n\tprivate async resolveCustomFieldWrites(\n\t\tcustomFields: Record<string, unknown> | undefined,\n\t): Promise<Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>> {\n\t\tif (!customFields || Object.keys(customFields).length === 0) return [];\n\t\tconst defs = await getBylineFieldDefs(this.db);\n\t\tconst bySlug = new Map(defs.map((d) => [d.slug, d]));\n\t\tconst writes: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }> = [];\n\t\tfor (const [slug, raw] of Object.entries(customFields)) {\n\t\t\tconst field = bySlug.get(slug);\n\t\t\tif (!field) {\n\t\t\t\tthrow new EmDashValidationError(`Unknown byline custom field \"${slug}\"`, {\n\t\t\t\t\tslug,\n\t\t\t\t\tregistered: defs.map((d) => d.slug),\n\t\t\t\t});\n\t\t\t}\n\t\t\twrites.push({ field, value: coerceFieldValue(field, raw) });\n\t\t}\n\t\treturn writes;\n\t}\n\n\t/**\n\t * Write a validated custom-field list against a byline row inside the\n\t * caller's transaction. Per-field writes route to\n\t * `_emdash_byline_field_values` (translatable) or\n\t * `_emdash_byline_field_group_values` (group-shared); `null` clears.\n\t * Returns `true` when any group-shared row was touched so the caller\n\t * can invalidate the per-request cache post-commit.\n\t */\n\tprivate async applyCustomFieldWritesInTrx(\n\t\ttrx: Kysely<Database>,\n\t\tbylineId: string,\n\t\ttranslationGroup: string,\n\t\twrites: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>,\n\t\tnow: string,\n\t): Promise<boolean> {\n\t\tif (writes.length === 0) return false;\n\t\tlet touchedGroupShared = false;\n\t\tfor (const { field, value } of writes) {\n\t\t\tif (!field.translatable) touchedGroupShared = true;\n\t\t\tif (field.translatable) {\n\t\t\t\tif (value === null) {\n\t\t\t\t\tawait trx\n\t\t\t\t\t\t.deleteFrom(\"_emdash_byline_field_values\")\n\t\t\t\t\t\t.where(\"byline_id\", \"=\", bylineId)\n\t\t\t\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t\t\t\t.execute();\n\t\t\t\t} else {\n\t\t\t\t\tconst encoded = JSON.stringify(value);\n\t\t\t\t\tawait trx\n\t\t\t\t\t\t.insertInto(\"_emdash_byline_field_values\")\n\t\t\t\t\t\t.values({\n\t\t\t\t\t\t\tbyline_id: bylineId,\n\t\t\t\t\t\t\tfield_id: field.id,\n\t\t\t\t\t\t\tvalue: encoded,\n\t\t\t\t\t\t\tcreated_at: now,\n\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\t\t\toc.columns([\"byline_id\", \"field_id\"]).doUpdateSet({\n\t\t\t\t\t\t\t\tvalue: encoded,\n\t\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.execute();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (value === null) {\n\t\t\t\t\tawait trx\n\t\t\t\t\t\t.deleteFrom(\"_emdash_byline_field_group_values\")\n\t\t\t\t\t\t.where(\"translation_group\", \"=\", translationGroup)\n\t\t\t\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t\t\t\t.execute();\n\t\t\t\t} else {\n\t\t\t\t\tconst encoded = JSON.stringify(value);\n\t\t\t\t\tawait trx\n\t\t\t\t\t\t.insertInto(\"_emdash_byline_field_group_values\")\n\t\t\t\t\t\t.values({\n\t\t\t\t\t\t\ttranslation_group: translationGroup,\n\t\t\t\t\t\t\tfield_id: field.id,\n\t\t\t\t\t\t\tvalue: encoded,\n\t\t\t\t\t\t\tcreated_at: now,\n\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\t\t\toc.columns([\"translation_group\", \"field_id\"]).doUpdateSet({\n\t\t\t\t\t\t\t\tvalue: encoded,\n\t\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.execute();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn touchedGroupShared;\n\t}\n\n\tasync create(input: CreateBylineInput): Promise<BylineSummary> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\t// Validate customFields before opening the transaction so a bad\n\t\t// value surfaces as VALIDATION_ERROR without aborting an insert.\n\t\tconst customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);\n\n\t\t// translationOf joins the source's group; otherwise mint a fresh\n\t\t// group = id (matches migration 040's backfill pattern).\n\t\tlet translationGroup: string = id;\n\t\tif (input.translationOf) {\n\t\t\tconst source = await this.findById(input.translationOf);\n\t\t\tif (!source) throw new Error(\"Source byline for translation not found\");\n\t\t\ttranslationGroup = source.translationGroup ?? source.id;\n\t\t}\n\n\t\t// Wrap insert + custom-field writes in one transaction so a\n\t\t// partial failure rolls both back on Node/PG. D1 still has its\n\t\t// own no-transactions limitation — recovery for that path lives\n\t\t// in `handleBylineCreate`.\n\t\tlet touchedGroupShared = false;\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_bylines\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug: input.slug,\n\t\t\t\t\tdisplay_name: input.displayName,\n\t\t\t\t\tbio: input.bio ?? null,\n\t\t\t\t\tavatar_media_id: input.avatarMediaId ?? null,\n\t\t\t\t\twebsite_url: input.websiteUrl ?? null,\n\t\t\t\t\tuser_id: input.userId ?? null,\n\t\t\t\t\tis_guest: input.isGuest ? 1 : 0,\n\t\t\t\t\tcreated_at: now,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t// Omit `locale` so the DB DEFAULT (configured defaultLocale)\n\t\t\t\t\t// applies — matches TaxonomyRepository.create.\n\t\t\t\t\t...(input.locale !== undefined ? { locale: input.locale } : {}),\n\t\t\t\t\ttranslation_group: translationGroup,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\ttouchedGroupShared = await this.applyCustomFieldWritesInTrx(\n\t\t\t\ttrx,\n\t\t\t\tid,\n\t\t\t\ttranslationGroup,\n\t\t\t\tcustomFieldWrites,\n\t\t\t\tnow,\n\t\t\t);\n\t\t});\n\n\t\tif (touchedGroupShared) {\n\t\t\tclearRequestCacheEntry(`byline-field-group-values:${translationGroup}`);\n\t\t}\n\t\tinvalidateBylineObjectCache();\n\n\t\tconst byline = await this.findById(id);\n\t\tif (!byline) {\n\t\t\tthrow new Error(\"Failed to create byline\");\n\t\t}\n\t\treturn byline;\n\t}\n\n\tasync update(id: string, input: UpdateBylineInput): Promise<BylineSummary | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\t// Validate customFields before opening the transaction so a bad\n\t\t// value surfaces as VALIDATION_ERROR without aborting an update.\n\t\tconst customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);\n\n\t\tconst now = new Date().toISOString();\n\t\tconst updates: Record<string, unknown> = { updated_at: now };\n\n\t\tif (input.slug !== undefined) updates.slug = input.slug;\n\t\tif (input.displayName !== undefined) updates.display_name = input.displayName;\n\t\tif (input.bio !== undefined) updates.bio = input.bio;\n\t\tif (input.avatarMediaId !== undefined) updates.avatar_media_id = input.avatarMediaId;\n\t\tif (input.websiteUrl !== undefined) updates.website_url = input.websiteUrl;\n\t\tif (input.userId !== undefined) updates.user_id = input.userId;\n\t\tif (input.isGuest !== undefined) updates.is_guest = input.isGuest ? 1 : 0;\n\n\t\tconst group = existing.translationGroup ?? existing.id;\n\t\t// Wrap row update + custom-field writes in one transaction so a\n\t\t// partial failure rolls both back on Node/PG. The post-commit\n\t\t// invalidation below clears the per-request cache that the\n\t\t// top-of-method `findById` populated for this group.\n\t\tlet touchedGroupShared = false;\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx.updateTable(\"_emdash_bylines\").set(updates).where(\"id\", \"=\", id).execute();\n\t\t\ttouchedGroupShared = await this.applyCustomFieldWritesInTrx(\n\t\t\t\ttrx,\n\t\t\t\tid,\n\t\t\t\tgroup,\n\t\t\t\tcustomFieldWrites,\n\t\t\t\tnow,\n\t\t\t);\n\t\t});\n\n\t\tif (touchedGroupShared) {\n\t\t\tclearRequestCacheEntry(`byline-field-group-values:${group}`);\n\t\t}\n\t\tinvalidateBylineObjectCache();\n\n\t\treturn await this.findById(id);\n\t}\n\n\t/**\n\t * Delete a byline row. When this row is the last sibling in its\n\t * translation group, also drops every junction row pointing at the group,\n\t * clears `primary_byline_id` references, and removes the byline's\n\t * non-translatable custom-field values. When other siblings remain in\n\t * the group, junctions, `primary_byline_id` pointers, and group-shared\n\t * custom-field values stay intact — the credit (and its shared metadata)\n\t * lives on at other locales.\n\t *\n\t * **Application-level cascade.** The byline domain has standardised on\n\t * app-level cascade rather than trusting FK ON DELETE CASCADE, partly\n\t * because migration 040 had to strip its own FK to support the\n\t * translation_group remap (#1021), and partly so cleanup doesn't\n\t * depend on `PRAGMA foreign_keys = ON` (set in production via\n\t * `connection.ts:60`, but easy to bypass in tests, scripts, and\n\t * one-off tools). Every byline-related deletion table is cleared\n\t * explicitly here:\n\t *\n\t * - `_emdash_byline_field_values` (per-byline translatable values) —\n\t * migration 041 declares FK ON DELETE CASCADE on `byline_id`; the\n\t * explicit DELETE removes the dependency on that pragma.\n\t * - `_emdash_content_bylines` — migration 040 dropped its FK.\n\t * - `ec_*.primary_byline_id` — never had an FK.\n\t * - `_emdash_byline_field_group_values` (translation-group-keyed) —\n\t * keyed by a text column with no FK to bylines, so app-level cleanup\n\t * is the only path.\n\t *\n\t * The FKs that remain (migration 041) serve as defense-in-depth.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return false;\n\n\t\tconst group = existing.translationGroup ?? existing.id;\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\t// Per-row translatable custom-field values. Done BEFORE the\n\t\t\t// byline row delete so the application-level cleanup is\n\t\t\t// observable in the transaction log even if FK enforcement is\n\t\t\t// off; migration 041's FK ON DELETE CASCADE would catch any\n\t\t\t// row we miss, but the explicit DELETE is what the rest of\n\t\t\t// the byline domain expects to see.\n\t\t\tawait trx.deleteFrom(\"_emdash_byline_field_values\").where(\"byline_id\", \"=\", id).execute();\n\n\t\t\tawait trx.deleteFrom(\"_emdash_bylines\").where(\"id\", \"=\", id).execute();\n\n\t\t\t// Count remaining siblings in the translation group. If none\n\t\t\t// remain, purge dependent rows; otherwise leave them intact so\n\t\t\t// the credit still resolves at other locales.\n\t\t\tconst remaining = await trx\n\t\t\t\t.selectFrom(\"_emdash_bylines\")\n\t\t\t\t.select(({ fn }) => [fn.count<number>(\"id\").as(\"count\")])\n\t\t\t\t.where(\"translation_group\", \"=\", group)\n\t\t\t\t.executeTakeFirst();\n\t\t\tconst remainingCount = Number(remaining?.count ?? 0);\n\t\t\tif (remainingCount > 0) return;\n\n\t\t\t// Last sibling gone: cascade in application code.\n\t\t\tawait trx.deleteFrom(\"_emdash_content_bylines\").where(\"byline_id\", \"=\", group).execute();\n\n\t\t\t// Group-shared custom-field values are keyed by translation_group\n\t\t\t// (no FK to bylines), so they don't cascade with the byline row.\n\t\t\t// Clean them up explicitly so deleting the last sibling of an\n\t\t\t// identity doesn't leave orphan group values pointing at a\n\t\t\t// vanished translation group. Per-row translatable values\n\t\t\t// (`_emdash_byline_field_values` keyed by byline_id) already\n\t\t\t// cascaded when each sibling row was deleted, so no extra\n\t\t\t// cleanup is needed for that table.\n\t\t\tawait trx\n\t\t\t\t.deleteFrom(\"_emdash_byline_field_group_values\")\n\t\t\t\t.where(\"translation_group\", \"=\", group)\n\t\t\t\t.execute();\n\n\t\t\tconst tableNames = await listTablesLike(trx, \"ec_%\");\n\t\t\tfor (const tableName of tableNames) {\n\t\t\t\tvalidateIdentifier(tableName, \"content table\");\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET primary_byline_id = NULL\n\t\t\t\t\tWHERE primary_byline_id = ${group}\n\t\t\t\t`.execute(trx);\n\t\t\t}\n\t\t});\n\n\t\tinvalidateBylineObjectCache();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Strict per-locale credit hydration. Joins `_emdash_content_bylines` to\n\t * `_emdash_bylines` on `translation_group = byline_id`, then filters to\n\t * the requested locale. Credits whose translation group lacks a row at\n\t * the requested locale are omitted — callers wanting fallback behaviour\n\t * apply it themselves. Mirrors `TaxonomyRepository.getTermsForEntry`.\n\t */\n\tasync getContentBylines(\n\t\tcollectionSlug: string,\n\t\tcontentId: string,\n\t\toptions?: { locale?: string },\n\t): Promise<ContentBylineCredit[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_content_bylines as cb\")\n\t\t\t.innerJoin(\"_emdash_bylines as b\", \"b.translation_group\", \"cb.byline_id\")\n\t\t\t.leftJoin(\"media as m\", \"m.id\", \"b.avatar_media_id\")\n\t\t\t.select([\n\t\t\t\t\"cb.sort_order as sort_order\",\n\t\t\t\t\"cb.role_label as role_label\",\n\t\t\t\t\"b.id as id\",\n\t\t\t\t\"b.slug as slug\",\n\t\t\t\t\"b.display_name as display_name\",\n\t\t\t\t\"b.bio as bio\",\n\t\t\t\t\"b.avatar_media_id as avatar_media_id\",\n\t\t\t\t\"m.storage_key as avatar_storage_key\",\n\t\t\t\t\"m.alt as avatar_alt\",\n\t\t\t\t\"m.blurhash as avatar_blurhash\",\n\t\t\t\t\"m.dominant_color as avatar_dominant_color\",\n\t\t\t\t\"b.website_url as website_url\",\n\t\t\t\t\"b.user_id as user_id\",\n\t\t\t\t\"b.is_guest as is_guest\",\n\t\t\t\t\"b.created_at as created_at\",\n\t\t\t\t\"b.updated_at as updated_at\",\n\t\t\t\t\"b.locale as locale\",\n\t\t\t\t\"b.translation_group as translation_group\",\n\t\t\t])\n\t\t\t.where(\"cb.collection_slug\", \"=\", collectionSlug)\n\t\t\t.where(\"cb.content_id\", \"=\", contentId)\n\t\t\t.orderBy(\"cb.sort_order\", \"asc\");\n\t\tif (options?.locale !== undefined) query = query.where(\"b.locale\", \"=\", options.locale);\n\n\t\tconst rows = await query.execute();\n\t\t// Reconstruct byline rows to feed `withCustomFields`. The JOIN selects\n\t\t// the `BylineRow` columns under the `b.` alias plus the avatar media\n\t\t// columns from the `media` LEFT JOIN; carry both through so\n\t\t// `rowToByline` can populate `avatarStorageKey`/`avatarAlt` (otherwise\n\t\t// the join runs but its values are dropped here).\n\t\tconst bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tslug: row.slug,\n\t\t\tdisplay_name: row.display_name,\n\t\t\tbio: row.bio,\n\t\t\tavatar_media_id: row.avatar_media_id,\n\t\t\tavatar_storage_key: row.avatar_storage_key,\n\t\t\tavatar_alt: row.avatar_alt,\n\t\t\tavatar_blurhash: row.avatar_blurhash,\n\t\t\tavatar_dominant_color: row.avatar_dominant_color,\n\t\t\twebsite_url: row.website_url,\n\t\t\tuser_id: row.user_id,\n\t\t\tis_guest: row.is_guest,\n\t\t\tcreated_at: row.created_at,\n\t\t\tupdated_at: row.updated_at,\n\t\t\tlocale: row.locale,\n\t\t\ttranslation_group: row.translation_group,\n\t\t}));\n\t\tconst hydrated = await this.withCustomFields(bylineRows);\n\t\treturn rows.map((row, i) => {\n\t\t\tconst byline = hydrated[i];\n\t\t\tif (!byline) {\n\t\t\t\t// Defensive: hydrated and rows are produced in lock-step;\n\t\t\t\t// this branch is unreachable unless `withCustomFields`\n\t\t\t\t// breaks its contract.\n\t\t\t\tthrow new Error(\"getContentBylines: hydration row count mismatch\");\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tbyline,\n\t\t\t\tsortOrder: row.sort_order,\n\t\t\t\troleLabel: row.role_label,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Does this entry have any explicit byline credits — at any locale?\n\t *\n\t * Used to disambiguate \"no credits exist\" (fall back to author-linked\n\t * byline) from \"credits exist but don't resolve at the requested locale\"\n\t * (strict per-locale model: render no byline). Without this check the\n\t * locale-strict hydration would silently turn a missing translation into\n\t * an author-inferred byline, contradicting editorial intent.\n\t */\n\tasync hasContentBylines(collectionSlug: string, contentId: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_content_bylines\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection_slug\", \"=\", collectionSlug)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.limit(1)\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t}\n\n\t/**\n\t * Batch variant of `hasContentBylines`. Returns the set of content IDs\n\t * that have at least one junction row (locale-agnostic).\n\t */\n\tasync hasContentBylinesMany(collectionSlug: string, contentIds: string[]): Promise<Set<string>> {\n\t\tconst result = new Set<string>();\n\t\tif (contentIds.length === 0) return result;\n\n\t\tconst uniqueContentIds = [...new Set(contentIds)];\n\t\tfor (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_content_bylines\")\n\t\t\t\t.select(\"content_id\")\n\t\t\t\t.distinct()\n\t\t\t\t.where(\"collection_slug\", \"=\", collectionSlug)\n\t\t\t\t.where(\"content_id\", \"in\", chunk)\n\t\t\t\t.execute();\n\t\t\tfor (const row of rows) result.add(row.content_id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch variant of `getContentBylines`. Same strict-per-locale semantics\n\t * applied to the requested locale (single value, not per-entry).\n\t *\n\t * When callers need per-entry-locale filtering (e.g. a list endpoint\n\t * returning entries at mixed locales), they should group the input ids by\n\t * the entry's locale and call this method once per group.\n\t *\n\t * When the caller will issue multiple `getContentBylinesMany` calls in\n\t * one request (e.g. per locale bucket) and wants a *single* batched\n\t * customFields hydration over the union of returned bylines, pass\n\t * `skipHydration: true` on each call and finish with\n\t * `hydrateBylineCustomFields(allBylines)`. The returned bylines carry\n\t * `customFields = {}` until that hydration call runs — matching the\n\t * \"always populated\" invariant from AC #6 — so callers that forget to\n\t * hydrate get an empty map rather than `undefined`.\n\t */\n\tasync getContentBylinesMany(\n\t\tcollectionSlug: string,\n\t\tcontentIds: string[],\n\t\toptions?: { locale?: string; skipHydration?: boolean },\n\t): Promise<Map<string, ContentBylineCredit[]>> {\n\t\tconst result = new Map<string, ContentBylineCredit[]>();\n\t\tif (contentIds.length === 0) return result;\n\n\t\tconst uniqueContentIds = [...new Set(contentIds)];\n\t\tfor (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {\n\t\t\tlet query = this.db\n\t\t\t\t.selectFrom(\"_emdash_content_bylines as cb\")\n\t\t\t\t.innerJoin(\"_emdash_bylines as b\", \"b.translation_group\", \"cb.byline_id\")\n\t\t\t\t.leftJoin(\"media as m\", \"m.id\", \"b.avatar_media_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"cb.content_id as content_id\",\n\t\t\t\t\t\"cb.sort_order as sort_order\",\n\t\t\t\t\t\"cb.role_label as role_label\",\n\t\t\t\t\t\"b.id as id\",\n\t\t\t\t\t\"b.slug as slug\",\n\t\t\t\t\t\"b.display_name as display_name\",\n\t\t\t\t\t\"b.bio as bio\",\n\t\t\t\t\t\"b.avatar_media_id as avatar_media_id\",\n\t\t\t\t\t\"m.storage_key as avatar_storage_key\",\n\t\t\t\t\t\"m.alt as avatar_alt\",\n\t\t\t\t\t\"m.blurhash as avatar_blurhash\",\n\t\t\t\t\t\"m.dominant_color as avatar_dominant_color\",\n\t\t\t\t\t\"b.website_url as website_url\",\n\t\t\t\t\t\"b.user_id as user_id\",\n\t\t\t\t\t\"b.is_guest as is_guest\",\n\t\t\t\t\t\"b.created_at as created_at\",\n\t\t\t\t\t\"b.updated_at as updated_at\",\n\t\t\t\t\t\"b.locale as locale\",\n\t\t\t\t\t\"b.translation_group as translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"cb.collection_slug\", \"=\", collectionSlug)\n\t\t\t\t.where(\"cb.content_id\", \"in\", chunk)\n\t\t\t\t.orderBy(\"cb.sort_order\", \"asc\");\n\t\t\tif (options?.locale !== undefined) query = query.where(\"b.locale\", \"=\", options.locale);\n\n\t\t\tconst rows = await query.execute();\n\t\t\t// Carry the avatar media columns from the LEFT JOIN through the\n\t\t\t// reshape so `rowToByline` can populate avatarStorageKey/avatarAlt.\n\t\t\tconst bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\tslug: row.slug,\n\t\t\t\tdisplay_name: row.display_name,\n\t\t\t\tbio: row.bio,\n\t\t\t\tavatar_media_id: row.avatar_media_id,\n\t\t\t\tavatar_storage_key: row.avatar_storage_key,\n\t\t\t\tavatar_alt: row.avatar_alt,\n\t\t\t\tavatar_blurhash: row.avatar_blurhash,\n\t\t\t\tavatar_dominant_color: row.avatar_dominant_color,\n\t\t\t\twebsite_url: row.website_url,\n\t\t\t\tuser_id: row.user_id,\n\t\t\t\tis_guest: row.is_guest,\n\t\t\t\tcreated_at: row.created_at,\n\t\t\t\tupdated_at: row.updated_at,\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslation_group: row.translation_group,\n\t\t\t}));\n\n\t\t\t// When `skipHydration` is set, return BylineSummary objects with\n\t\t\t// `customFields = {}`. The caller is responsible for batching\n\t\t\t// `hydrateBylineCustomFields` across multiple\n\t\t\t// `getContentBylinesMany` calls. Otherwise hydrate per-call —\n\t\t\t// the historical behaviour for solo callers.\n\t\t\tlet bylines: BylineSummary[];\n\t\t\tif (options?.skipHydration === true) {\n\t\t\t\tbylines = bylineRows.map(rowToByline);\n\t\t\t\tfor (const b of bylines) b.customFields = {};\n\t\t\t} else {\n\t\t\t\tbylines = await this.withCustomFields(bylineRows);\n\t\t\t}\n\n\t\t\tfor (let i = 0; i < rows.length; i++) {\n\t\t\t\tconst row = rows[i];\n\t\t\t\tconst byline = bylines[i];\n\t\t\t\tif (!row || !byline) continue;\n\t\t\t\tconst contentId = row.content_id;\n\t\t\t\tconst credit: ContentBylineCredit = {\n\t\t\t\t\tbyline,\n\t\t\t\t\tsortOrder: row.sort_order,\n\t\t\t\t\troleLabel: row.role_label,\n\t\t\t\t};\n\t\t\t\tconst existing = result.get(contentId);\n\t\t\t\tif (existing) {\n\t\t\t\t\texisting.push(credit);\n\t\t\t\t} else {\n\t\t\t\t\tresult.set(contentId, [credit]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch-fetch byline profiles linked to user IDs in a single query.\n\t * Strict-locale variant of `findByUserId`.\n\t *\n\t * `skipHydration: true` returns bylines with `customFields = {}` so\n\t * callers issuing multiple `findByUserIds` calls in one request (e.g.\n\t * the per-locale-bucket author-fallback path in `getBylinesForEntries`)\n\t * can defer customFields hydration to a single batched\n\t * `hydrateBylineCustomFields` call across the union — keeping the\n\t * Phase 3 query-count envelope at \"+1 group-shared query per\n\t * hydration pass\" even when buckets fetch disjoint author bylines.\n\t */\n\tasync findByUserIds(\n\t\tuserIds: string[],\n\t\toptions?: { locale?: string; skipHydration?: boolean },\n\t): Promise<Map<string, BylineSummary>> {\n\t\tconst result = new Map<string, BylineSummary>();\n\t\tif (userIds.length === 0) return result;\n\n\t\tfor (const chunk of chunks(userIds, SQL_BATCH_SIZE)) {\n\t\t\t// LEFT JOIN media so author-inferred bylines (the fallback path in\n\t\t\t// `getBylinesForEntries`) carry the same render-ready avatar storage\n\t\t\t// key as explicitly-credited bylines do.\n\t\t\tlet query = this.db\n\t\t\t\t.selectFrom(\"_emdash_bylines as b\")\n\t\t\t\t.leftJoin(\"media as m\", \"m.id\", \"b.avatar_media_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"b.id as id\",\n\t\t\t\t\t\"b.slug as slug\",\n\t\t\t\t\t\"b.display_name as display_name\",\n\t\t\t\t\t\"b.bio as bio\",\n\t\t\t\t\t\"b.avatar_media_id as avatar_media_id\",\n\t\t\t\t\t\"m.storage_key as avatar_storage_key\",\n\t\t\t\t\t\"m.alt as avatar_alt\",\n\t\t\t\t\t\"m.blurhash as avatar_blurhash\",\n\t\t\t\t\t\"m.dominant_color as avatar_dominant_color\",\n\t\t\t\t\t\"b.website_url as website_url\",\n\t\t\t\t\t\"b.user_id as user_id\",\n\t\t\t\t\t\"b.is_guest as is_guest\",\n\t\t\t\t\t\"b.created_at as created_at\",\n\t\t\t\t\t\"b.updated_at as updated_at\",\n\t\t\t\t\t\"b.locale as locale\",\n\t\t\t\t\t\"b.translation_group as translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"b.user_id\", \"in\", chunk);\n\t\t\tif (options?.locale !== undefined) query = query.where(\"b.locale\", \"=\", options.locale);\n\n\t\t\tconst rows = await query.execute();\n\t\t\tlet bylines: BylineSummary[];\n\t\t\tif (options?.skipHydration === true) {\n\t\t\t\tbylines = rows.map(rowToByline);\n\t\t\t\tfor (const b of bylines) b.customFields = {};\n\t\t\t} else {\n\t\t\t\tbylines = await this.withCustomFields(rows);\n\t\t\t}\n\n\t\t\tfor (let i = 0; i < rows.length; i++) {\n\t\t\t\tconst row = rows[i];\n\t\t\t\tconst summary = bylines[i];\n\t\t\t\tif (!row || !summary || !row.user_id) continue;\n\t\t\t\tresult.set(row.user_id, summary);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Clone every junction row from `sourceContentId` to `targetContentId`,\n\t * preserving `sort_order` and `role_label`. Used by the content\n\t * translation flow: a newly created translation inherits the source's\n\t * byline credits at the storage level. Because the junction stores\n\t * `translation_group` (not a row id), the copy is locale-agnostic — the\n\t * credits resolve to whichever locale variants of each byline exist when\n\t * the translated entry is hydrated.\n\t *\n\t * No-op when the source has no credits. Skips when the target already\n\t * has credits (idempotent for re-runs).\n\t */\n\tasync copyContentBylines(\n\t\tcollection: string,\n\t\tsourceContentId: string,\n\t\ttargetContentId: string,\n\t): Promise<void> {\n\t\tvalidateIdentifier(collection, \"collection slug\");\n\t\tconst tableName = `ec_${collection}`;\n\t\tvalidateIdentifier(tableName, \"content table\");\n\n\t\t// Like `setContentBylines`, this method is expected to be called\n\t\t// within a transaction context (content handlers wrap in\n\t\t// withTransaction). All operations use `this.db` directly so an\n\t\t// outer transaction can serialise the copy alongside the create.\n\t\tconst existing = await this.db\n\t\t\t.selectFrom(\"_emdash_content_bylines\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection_slug\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", targetContentId)\n\t\t\t.executeTakeFirst();\n\t\tif (existing) return;\n\n\t\tconst sourceRows = await this.db\n\t\t\t.selectFrom(\"_emdash_content_bylines\")\n\t\t\t.select([\"byline_id\", \"sort_order\", \"role_label\"])\n\t\t\t.where(\"collection_slug\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", sourceContentId)\n\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t.execute();\n\t\tif (sourceRows.length === 0) return;\n\n\t\tconst now = new Date().toISOString();\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_content_bylines\")\n\t\t\t.values(\n\t\t\t\tsourceRows.map((row) => ({\n\t\t\t\t\tid: ulid(),\n\t\t\t\t\tcollection_slug: collection,\n\t\t\t\t\tcontent_id: targetContentId,\n\t\t\t\t\tbyline_id: row.byline_id,\n\t\t\t\t\tsort_order: row.sort_order,\n\t\t\t\t\trole_label: row.role_label,\n\t\t\t\t\tcreated_at: now,\n\t\t\t\t})),\n\t\t\t)\n\t\t\t.execute();\n\n\t\t// Mirror primary_byline_id from source so the cached pointer on the\n\t\t// target row matches the junction state we just wrote.\n\t\tconst firstByline = sourceRows[0]?.byline_id ?? null;\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET primary_byline_id = ${firstByline}\n\t\t\tWHERE id = ${targetContentId}\n\t\t`.execute(this.db);\n\n\t\t// Byline credits are folded into the target entry's cached payload.\n\t\tinvalidateCollectionCache(collection);\n\t}\n\n\t/**\n\t * Replace the set of byline credits on a content entry. Accepts row ids\n\t * at the wire (consistent with how the admin sends them), translates\n\t * each to its `translation_group` on write, and stores the group in\n\t * `_emdash_content_bylines.byline_id` and `ec_*.primary_byline_id`.\n\t *\n\t * The returned credits are hydrated with strict-locale matching at the\n\t * locale of the rows the caller supplied (i.e. the locale of the byline\n\t * each `bylineId` resolves to) — adequate for the autosave round-trip,\n\t * which then re-hydrates the entry against its own locale separately.\n\t */\n\tasync setContentBylines(\n\t\tcollectionSlug: string,\n\t\tcontentId: string,\n\t\tinputBylines: ContentBylineInput[],\n\t): Promise<ContentBylineCredit[]> {\n\t\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\t\tconst tableName = `ec_${collectionSlug}`;\n\t\tvalidateIdentifier(tableName, \"content table\");\n\n\t\t// Resolve each wire row id to its translation_group up front so we\n\t\t// can (a) validate the rows exist and (b) dedupe by the value that\n\t\t// actually lands in the junction. Deduping by wire row id BEFORE\n\t\t// resolving would let two locale siblings of the same byline slip\n\t\t// through and trigger a UNIQUE(collection, content, byline_id)\n\t\t// failure at insert time. A single SELECT keeps this O(1) DB\n\t\t// calls regardless of how many credits are being set.\n\t\tconst idToGroup = new Map<string, string>();\n\t\tif (inputBylines.length > 0) {\n\t\t\tconst wireIds = [...new Set(inputBylines.map((item) => item.bylineId))];\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_bylines\")\n\t\t\t\t.select([\"id\", \"translation_group\"])\n\t\t\t\t.where(\"id\", \"in\", wireIds)\n\t\t\t\t.execute();\n\t\t\tif (rows.length !== wireIds.length) {\n\t\t\t\tthrow new Error(\"One or more byline IDs do not exist\");\n\t\t\t}\n\t\t\tfor (const row of rows) {\n\t\t\t\tidToGroup.set(row.id, row.translation_group ?? row.id);\n\t\t\t}\n\t\t}\n\n\t\t// Dedupe by translation_group. Preserves the order of first\n\t\t// occurrence so the editor's intent (which sibling appears first)\n\t\t// is honored. `roleLabel` follows the first occurrence too.\n\t\tconst seenGroups = new Set<string>();\n\t\tconst bylines: Array<ContentBylineInput & { group: string }> = [];\n\t\tfor (const item of inputBylines) {\n\t\t\tconst group = idToGroup.get(item.bylineId);\n\t\t\tif (!group) {\n\t\t\t\tthrow new Error(`Missing translation_group for byline ${item.bylineId}`);\n\t\t\t}\n\t\t\tif (seenGroups.has(group)) continue;\n\t\t\tseenGroups.add(group);\n\t\t\tbylines.push({ ...item, group });\n\t\t}\n\n\t\t// This method is expected to be called within a transaction context\n\t\t// (content handlers wrap in withTransaction, seed applies sequentially).\n\t\t// All operations use this.db directly -- callers are responsible for\n\t\t// wrapping in a transaction when atomicity is required.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_content_bylines\")\n\t\t\t.where(\"collection_slug\", \"=\", collectionSlug)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.execute();\n\n\t\tfor (let i = 0; i < bylines.length; i++) {\n\t\t\tconst item = bylines[i];\n\t\t\tif (!item) continue;\n\t\t\tawait this.db\n\t\t\t\t.insertInto(\"_emdash_content_bylines\")\n\t\t\t\t.values({\n\t\t\t\t\tid: ulid(),\n\t\t\t\t\tcollection_slug: collectionSlug,\n\t\t\t\t\tcontent_id: contentId,\n\t\t\t\t\tbyline_id: item.group,\n\t\t\t\t\tsort_order: i,\n\t\t\t\t\trole_label: item.roleLabel ?? null,\n\t\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t}\n\n\t\tconst primaryGroup = bylines[0]?.group ?? null;\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET primary_byline_id = ${primaryGroup}\n\t\t\tWHERE id = ${contentId}\n\t\t`.execute(this.db);\n\n\t\t// Byline credits are folded into this entry's cached payload.\n\t\tinvalidateCollectionCache(collectionSlug);\n\n\t\treturn await this.getContentBylines(collectionSlug, contentId);\n\t}\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"challenge-store-D5pSY2S1.mjs","names":[],"sources":["../src/auth/challenge-store.ts"],"sourcesContent":["/**\n * Challenge store for WebAuthn\n *\n * Stores WebAuthn challenges in a dedicated table with automatic expiration.\n */\n\nimport type { ChallengeStore, ChallengeData } from \"@emdash-cms/auth/passkey\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nexport function createChallengeStore(db: Kysely<Database>): ChallengeStore {\n\treturn {\n\t\tasync set(challenge: string, data: ChallengeData): Promise<void> {\n\t\t\tconst expiresAt = new Date(data.expiresAt).toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"auth_challenges\")\n\t\t\t\t.values({\n\t\t\t\t\tchallenge,\n\t\t\t\t\ttype: data.type,\n\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\tdata: null, // Could store additional context if needed\n\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc.column(\"challenge\").doUpdateSet({\n\t\t\t\t\t\ttype: data.type,\n\t\t\t\t\t\tuser_id: data.userId ?? null,\n\t\t\t\t\t\texpires_at: expiresAt,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync get(challenge: string): Promise<ChallengeData | null> {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"auth_challenges\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"challenge\", \"=\", challenge)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!row) return null;\n\n\t\t\tconst expiresAt = new Date(row.expires_at).getTime();\n\n\t\t\t// Check expiration\n\t\t\tif (expiresAt < Date.now()) {\n\t\t\t\t// Expired, delete and return null\n\t\t\t\tawait this.delete(challenge);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: row.type === \"registration\" ? \"registration\" : \"authentication\",\n\t\t\t\tuserId: row.user_id ?? undefined,\n\t\t\t\texpiresAt,\n\t\t\t};\n\t\t},\n\n\t\tasync delete(challenge: string): Promise<void> {\n\t\t\tawait db.deleteFrom(\"auth_challenges\").where(\"challenge\", \"=\", challenge).execute();\n\t\t},\n\t};\n}\n\n/**\n * Clean up expired challenges.\n * Should be called periodically (e.g., on startup, or via cron).\n */\nexport async function cleanupExpiredChallenges(db: Kysely<Database>): Promise<number> {\n\tconst now = new Date().toISOString();\n\n\tconst result = await db\n\t\t.deleteFrom(\"auth_challenges\")\n\t\t.where(\"expires_at\", \"<\", now)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n"],"mappings":";AAWA,SAAgB,qBAAqB,IAAsC;AAC1E,QAAO;EACN,MAAM,IAAI,WAAmB,MAAoC;GAChE,MAAM,YAAY,IAAI,KAAK,KAAK,UAAU,CAAC,aAAa;AAExD,SAAM,GACJ,WAAW,kBAAkB,CAC7B,OAAO;IACP;IACA,MAAM,KAAK;IACX,SAAS,KAAK,UAAU;IACxB,MAAM;IACN,YAAY;IACZ,CAAC,CACD,YAAY,OACZ,GAAG,OAAO,YAAY,CAAC,YAAY;IAClC,MAAM,KAAK;IACX,SAAS,KAAK,UAAU;IACxB,YAAY;IACZ,CAAC,CACF,CACA,SAAS;;EAGZ,MAAM,IAAI,WAAkD;GAC3D,MAAM,MAAM,MAAM,GAChB,WAAW,kBAAkB,CAC7B,WAAW,CACX,MAAM,aAAa,KAAK,UAAU,CAClC,kBAAkB;AAEpB,OAAI,CAAC,IAAK,QAAO;GAEjB,MAAM,YAAY,IAAI,KAAK,IAAI,WAAW,CAAC,SAAS;AAGpD,OAAI,YAAY,KAAK,KAAK,EAAE;AAE3B,UAAM,KAAK,OAAO,UAAU;AAC5B,WAAO;;AAGR,UAAO;IACN,MAAM,IAAI,SAAS,iBAAiB,iBAAiB;IACrD,QAAQ,IAAI,WAAW;IACvB;IACA;;EAGF,MAAM,OAAO,WAAkC;AAC9C,SAAM,GAAG,WAAW,kBAAkB,CAAC,MAAM,aAAa,KAAK,UAAU,CAAC,SAAS;;EAEpF;;;;;;AAOF,eAAsB,yBAAyB,IAAuC;CACrF,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;CAEpC,MAAM,SAAS,MAAM,GACnB,WAAW,kBAAkB,CAC7B,MAAM,cAAc,KAAK,IAAI,CAC7B,kBAAkB;AAEpB,QAAO,OAAO,OAAO,kBAAkB,EAAE"}