emdash 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (763) hide show
  1. package/dist/activation-K0kGBWuJ.mjs +382 -0
  2. package/dist/activation-K0kGBWuJ.mjs.map +1 -0
  3. package/dist/{adapters-Bb1vG41D.d.mts → adapters-CzL3H752.d.mts} +3 -3
  4. package/dist/{adapters-Bb1vG41D.d.mts.map → adapters-CzL3H752.d.mts.map} +1 -1
  5. package/dist/{allowed-origins-DW7sYyqg.mjs → allowed-origins-maylGaHv.mjs} +2 -2
  6. package/dist/{allowed-origins-DW7sYyqg.mjs.map → allowed-origins-maylGaHv.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -3
  8. package/dist/api/route-utils.mjs +22 -20
  9. package/dist/api/route-utils.mjs.map +1 -1
  10. package/dist/api/schemas/index.d.mts +2 -2
  11. package/dist/api/schemas/index.mjs +5 -5
  12. package/dist/api-B0w3uBiG.mjs +2269 -0
  13. package/dist/api-B0w3uBiG.mjs.map +1 -0
  14. package/dist/{api-tokens-KO24G1Fd.mjs → api-tokens-9YrrCoE6.mjs} +2 -2
  15. package/dist/{api-tokens-KO24G1Fd.mjs.map → api-tokens-9YrrCoE6.mjs.map} +1 -1
  16. package/dist/{apply-kUDt80-G.mjs → apply-B_VMpJ2P.mjs} +34 -35
  17. package/dist/{apply-kUDt80-G.mjs.map → apply-B_VMpJ2P.mjs.map} +1 -1
  18. package/dist/astro/index.d.mts +12 -12
  19. package/dist/astro/index.mjs +26 -15
  20. package/dist/astro/index.mjs.map +1 -1
  21. package/dist/astro/middleware/auth.d.mts +10 -10
  22. package/dist/astro/middleware/auth.d.mts.map +1 -1
  23. package/dist/astro/middleware/auth.mjs +10 -7
  24. package/dist/astro/middleware/auth.mjs.map +1 -1
  25. package/dist/astro/middleware/media-usage-write-fence.mjs +2 -2
  26. package/dist/astro/middleware/redirect.mjs +4 -4
  27. package/dist/astro/middleware/request-context.mjs +3 -3
  28. package/dist/astro/middleware/setup.mjs +1 -1
  29. package/dist/astro/middleware.d.mts +11 -12
  30. package/dist/astro/middleware.d.mts.map +1 -1
  31. package/dist/astro/middleware.mjs +168 -1305
  32. package/dist/astro/middleware.mjs.map +1 -1
  33. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  34. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  35. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
  36. package/dist/astro/routes/api/admin/api-tokens/index.mjs +4 -4
  37. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
  38. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +9 -9
  39. package/dist/astro/routes/api/admin/byline-fields/index.mjs +9 -9
  40. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +9 -9
  41. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +15 -15
  42. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +15 -15
  43. package/dist/astro/routes/api/admin/bylines/index.mjs +15 -15
  44. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
  45. package/dist/astro/routes/api/admin/comments/_id_.mjs +4 -4
  46. package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
  47. package/dist/astro/routes/api/admin/comments/counts.mjs +4 -4
  48. package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
  49. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  50. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  51. package/dist/astro/routes/api/admin/media-usage/activation.d.mts +9 -0
  52. package/dist/astro/routes/api/admin/media-usage/activation.d.mts.map +1 -0
  53. package/dist/astro/routes/api/admin/media-usage/activation.mjs +46 -0
  54. package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -0
  55. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +14 -13
  56. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +14 -13
  58. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/media-usage/progress.d.mts +9 -0
  60. package/dist/astro/routes/api/admin/media-usage/progress.d.mts.map +1 -0
  61. package/dist/astro/routes/api/admin/media-usage/progress.mjs +44 -0
  62. package/dist/astro/routes/api/admin/media-usage/progress.mjs.map +1 -0
  63. package/dist/astro/routes/api/admin/media-usage/repair.mjs +16 -11
  64. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  65. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +14 -13
  66. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +14 -13
  68. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
  69. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  70. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  71. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +54 -48
  72. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +54 -48
  74. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
  76. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
  78. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
  79. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +53 -47
  81. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +53 -47
  83. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
  85. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  87. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
  88. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +53 -47
  90. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
  92. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +53 -47
  94. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +54 -48
  96. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  97. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +172 -133
  99. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +54 -48
  101. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  102. package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
  103. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  104. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
  105. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  107. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
  108. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  109. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  110. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  111. package/dist/astro/routes/api/admin/users/_id_/index.mjs +7 -7
  112. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  113. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  114. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  115. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  116. package/dist/astro/routes/api/auth/invite/complete.mjs +11 -11
  117. package/dist/astro/routes/api/auth/invite/index.mjs +8 -8
  118. package/dist/astro/routes/api/auth/invite/register-options.mjs +10 -10
  119. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  120. package/dist/astro/routes/api/auth/magic-link/send.mjs +10 -10
  121. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  122. package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
  123. package/dist/astro/routes/api/auth/me.mjs +19 -9
  124. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  125. package/dist/astro/routes/api/auth/mode.mjs +2 -2
  126. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  127. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  128. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  129. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  130. package/dist/astro/routes/api/auth/passkey/options.mjs +12 -12
  131. package/dist/astro/routes/api/auth/passkey/register/options.mjs +10 -10
  132. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +11 -11
  133. package/dist/astro/routes/api/auth/passkey/verify.mjs +11 -11
  134. package/dist/astro/routes/api/auth/signup/complete.mjs +11 -11
  135. package/dist/astro/routes/api/auth/signup/request.mjs +10 -10
  136. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  137. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
  138. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
  139. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  140. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  141. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  142. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  143. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +11 -11
  144. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +8 -8
  145. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +16 -14
  146. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  147. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +16 -14
  148. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  149. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  150. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  151. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -8
  152. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
  153. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  154. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  155. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
  156. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  157. package/dist/astro/routes/api/content/_collection_/index.mjs +8 -8
  158. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  159. package/dist/astro/routes/api/dashboard.mjs +7 -7
  160. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  161. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  162. package/dist/astro/routes/api/import/probe.mjs +16 -16
  163. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  164. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  165. package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -12
  166. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  167. package/dist/astro/routes/api/import/wordpress/media.mjs +11 -11
  168. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  169. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  170. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +12 -10
  171. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  172. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  173. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +16 -16
  174. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  175. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +26 -26
  176. package/dist/astro/routes/api/manifest.mjs +4 -4
  177. package/dist/astro/routes/api/mcp.mjs +42 -42
  178. package/dist/astro/routes/api/media/_id_/confirm.mjs +11 -11
  179. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  180. package/dist/astro/routes/api/media/_id_/usage.mjs +18 -13
  181. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  182. package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
  183. package/dist/astro/routes/api/media/_id_.mjs +21 -13
  184. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  185. package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
  186. package/dist/astro/routes/api/media/folders/_id_.d.mts +10 -0
  187. package/dist/astro/routes/api/media/folders/_id_.d.mts.map +1 -0
  188. package/dist/astro/routes/api/media/folders/_id_.mjs +52 -0
  189. package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -0
  190. package/dist/astro/routes/api/media/folders/index.d.mts +9 -0
  191. package/dist/astro/routes/api/media/folders/index.d.mts.map +1 -0
  192. package/dist/astro/routes/api/media/folders/index.mjs +41 -0
  193. package/dist/astro/routes/api/media/folders/index.mjs.map +1 -0
  194. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  195. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  196. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  197. package/dist/astro/routes/api/media/upload-url.mjs +9 -9
  198. package/dist/astro/routes/api/media.d.mts.map +1 -1
  199. package/dist/astro/routes/api/media.mjs +28 -19
  200. package/dist/astro/routes/api/media.mjs.map +1 -1
  201. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
  202. package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
  203. package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
  204. package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
  205. package/dist/astro/routes/api/menus/_name_.mjs +8 -8
  206. package/dist/astro/routes/api/menus/index.mjs +8 -8
  207. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  208. package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
  209. package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
  210. package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
  211. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  212. package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
  213. package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
  214. package/dist/astro/routes/api/oauth/token.mjs +5 -5
  215. package/dist/astro/routes/api/openapi.json.mjs +153 -6
  216. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  217. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  218. package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
  219. package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
  220. package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
  221. package/dist/astro/routes/api/redirects/index.mjs +11 -11
  222. package/dist/astro/routes/api/relations/_id_/index.mjs +16 -14
  223. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  224. package/dist/astro/routes/api/relations/_id_/translations.mjs +11 -9
  225. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  226. package/dist/astro/routes/api/relations/index.mjs +16 -14
  227. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  229. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  230. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
  231. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  232. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
  233. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  234. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
  235. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  236. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
  237. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  238. package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
  239. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  240. package/dist/astro/routes/api/schema/collections/reorder.mjs +52 -46
  241. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  242. package/dist/astro/routes/api/schema/index.mjs +10 -8
  243. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  244. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
  245. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  246. package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
  247. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  248. package/dist/astro/routes/api/search/enable.mjs +10 -10
  249. package/dist/astro/routes/api/search/index.mjs +9 -9
  250. package/dist/astro/routes/api/search/rebuild.mjs +10 -10
  251. package/dist/astro/routes/api/search/stats.mjs +5 -5
  252. package/dist/astro/routes/api/search/suggest.mjs +9 -9
  253. package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
  254. package/dist/astro/routes/api/sections/index.mjs +9 -9
  255. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +6 -6
  256. package/dist/astro/routes/api/settings/backups/archives/index.mjs +5 -5
  257. package/dist/astro/routes/api/settings/backups/export.mjs +6 -6
  258. package/dist/astro/routes/api/settings/backups/index.mjs +7 -7
  259. package/dist/astro/routes/api/settings/email.mjs +4 -4
  260. package/dist/astro/routes/api/settings.mjs +13 -13
  261. package/dist/astro/routes/api/setup/admin-verify.mjs +12 -12
  262. package/dist/astro/routes/api/setup/admin.mjs +11 -11
  263. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  264. package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -27
  265. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  266. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  267. package/dist/astro/routes/api/setup/index.mjs +31 -29
  268. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/setup/status.mjs +3 -3
  270. package/dist/astro/routes/api/snapshot.mjs +7 -7
  271. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +13 -13
  272. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
  273. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
  274. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
  275. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +13 -13
  276. package/dist/astro/routes/api/taxonomies/_name_.mjs +13 -13
  277. package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
  278. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  279. package/dist/astro/routes/api/typegen.mjs +4 -4
  280. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  281. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  282. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  283. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
  284. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
  285. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
  286. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  287. package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
  288. package/dist/astro/routes/api/widget-components.mjs +2 -2
  289. package/dist/astro/routes/robots.txt.mjs +6 -6
  290. package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
  291. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  292. package/dist/astro/types.d.mts +19 -14
  293. package/dist/astro/types.d.mts.map +1 -1
  294. package/dist/auth/providers/github.d.mts +1 -1
  295. package/dist/auth/providers/google.d.mts +1 -1
  296. package/dist/{authorize-DUElxgOQ.mjs → authorize-ByYs-Uxs.mjs} +2 -2
  297. package/dist/{authorize-DUElxgOQ.mjs.map → authorize-ByYs-Uxs.mjs.map} +1 -1
  298. package/dist/{backup-DWYpJlLj.mjs → backup-ChyHfIoX.mjs} +5 -5
  299. package/dist/{backup-DWYpJlLj.mjs.map → backup-ChyHfIoX.mjs.map} +1 -1
  300. package/dist/{bundle-vn56sLos.mjs → bundle-DZFhFurR.mjs} +4 -4
  301. package/dist/{bundle-vn56sLos.mjs.map → bundle-DZFhFurR.mjs.map} +1 -1
  302. package/dist/{byline-yjvWq4ip.mjs → byline-Cpnwrje1.mjs} +6 -6
  303. package/dist/{byline-yjvWq4ip.mjs.map → byline-Cpnwrje1.mjs.map} +1 -1
  304. package/dist/{byline-fields-txaLGoME.mjs → byline-fields-CAyA_Umd.mjs} +2 -2
  305. package/dist/{byline-fields-txaLGoME.mjs.map → byline-fields-CAyA_Umd.mjs.map} +1 -1
  306. package/dist/{byline-registry-DDun8sxh.mjs → byline-registry-Bpu9-MVs.mjs} +2 -2
  307. package/dist/{byline-registry-DDun8sxh.mjs.map → byline-registry-Bpu9-MVs.mjs.map} +1 -1
  308. package/dist/{bylines-DjXS5p0P.mjs → bylines-BSo9B9RX.mjs} +7 -7
  309. package/dist/{bylines-DjXS5p0P.mjs.map → bylines-BSo9B9RX.mjs.map} +1 -1
  310. package/dist/{bylines-Bu7pchQ4.mjs → bylines-CHjvehVX.mjs} +3 -3
  311. package/dist/{bylines-Bu7pchQ4.mjs.map → bylines-CHjvehVX.mjs.map} +1 -1
  312. package/dist/{cache-C46-3Irl.mjs → cache-5VirJ2Cn.mjs} +4 -4
  313. package/dist/{cache-C46-3Irl.mjs.map → cache-5VirJ2Cn.mjs.map} +1 -1
  314. package/dist/capture-triggers-CNW1q3HK.mjs +362 -0
  315. package/dist/capture-triggers-CNW1q3HK.mjs.map +1 -0
  316. package/dist/{challenge-store-SIL5DsF2.mjs → challenge-store-D5pSY2S1.mjs} +1 -1
  317. package/dist/{challenge-store-SIL5DsF2.mjs.map → challenge-store-D5pSY2S1.mjs.map} +1 -1
  318. package/dist/{chunks-fiZ8G9RJ.mjs → chunks-bNiX6_DR.mjs} +2 -2
  319. package/dist/{chunks-fiZ8G9RJ.mjs.map → chunks-bNiX6_DR.mjs.map} +1 -1
  320. package/dist/cli/index.mjs +36 -52
  321. package/dist/cli/index.mjs.map +1 -1
  322. package/dist/client/cf-access.d.mts +1 -1
  323. package/dist/client/index.d.mts +74 -4
  324. package/dist/client/index.d.mts.map +1 -1
  325. package/dist/client/index.mjs +54 -2
  326. package/dist/client/index.mjs.map +1 -1
  327. package/dist/{collection-deletion-DQPvMD8R.mjs → collection-deletion-m_oi2c0N.mjs} +13 -356
  328. package/dist/collection-deletion-m_oi2c0N.mjs.map +1 -0
  329. package/dist/{comment-Dxlep6cE.mjs → comment-DhvLEdhQ.mjs} +1 -1
  330. package/dist/{comment-Dxlep6cE.mjs.map → comment-DhvLEdhQ.mjs.map} +1 -1
  331. package/dist/{comment-reaction-D-2J84UE.mjs → comment-reaction-CRuItEjt.mjs} +2 -2
  332. package/dist/{comment-reaction-D-2J84UE.mjs.map → comment-reaction-CRuItEjt.mjs.map} +1 -1
  333. package/dist/{comments-CTfZtLmM.mjs → comments-CLj4bi8I.mjs} +2 -2
  334. package/dist/{comments-CTfZtLmM.mjs.map → comments-CLj4bi8I.mjs.map} +1 -1
  335. package/dist/{components-CQy3lh9x.mjs → components-v4U4DT2y.mjs} +1 -1
  336. package/dist/{components-CQy3lh9x.mjs.map → components-v4U4DT2y.mjs.map} +1 -1
  337. package/dist/{config-COQzTe5Q.d.mts → config-DrfghTPe.d.mts} +1 -1
  338. package/dist/{config-COQzTe5Q.d.mts.map → config-DrfghTPe.d.mts.map} +1 -1
  339. package/dist/{content-ZHCc1ngN.mjs → content-CP-SeXNa.mjs} +5 -4
  340. package/dist/content-CP-SeXNa.mjs.map +1 -0
  341. package/dist/{content-list-query-wJ2WN8Vp.d.mts → content-list-query-BhejKVqb.d.mts} +1 -1
  342. package/dist/{content-list-query-wJ2WN8Vp.d.mts.map → content-list-query-BhejKVqb.d.mts.map} +1 -1
  343. package/dist/{content-refresh-B_8SJzuU.mjs → content-refresh-DLOfIhYc.mjs} +1094 -93
  344. package/dist/content-refresh-DLOfIhYc.mjs.map +1 -0
  345. package/dist/{media-usage-BZCydfvu.mjs → content-repair-BCxzy0r4.mjs} +5 -196
  346. package/dist/content-repair-BCxzy0r4.mjs.map +1 -0
  347. package/dist/{context-ZldbZcle.mjs → context-BR5hRWiK.mjs} +13 -13
  348. package/dist/{context-ZldbZcle.mjs.map → context-BR5hRWiK.mjs.map} +1 -1
  349. package/dist/{cron-Y9aLtgrA.mjs → cron-CWfWrWKG.mjs} +1 -1
  350. package/dist/{cron-Y9aLtgrA.mjs.map → cron-CWfWrWKG.mjs.map} +1 -1
  351. package/dist/{dashboard-DvHbk_cv.mjs → dashboard-CkIV2rnQ.mjs} +5 -5
  352. package/dist/{dashboard-DvHbk_cv.mjs.map → dashboard-CkIV2rnQ.mjs.map} +1 -1
  353. package/dist/db/index.d.mts +3 -3
  354. package/dist/db/index.mjs +3 -3
  355. package/dist/db/index.mjs.map +1 -1
  356. package/dist/db/libsql-migrations.d.mts +1 -1
  357. package/dist/db/libsql-migrations.mjs +4 -4
  358. package/dist/db/libsql.d.mts +1 -1
  359. package/dist/db/postgres-migrations.d.mts +1 -1
  360. package/dist/db/postgres-migrations.mjs +4 -4
  361. package/dist/db/postgres.d.mts +1 -1
  362. package/dist/db/sqlite-migrations.d.mts +1 -1
  363. package/dist/db/sqlite-migrations.mjs +4 -4
  364. package/dist/db/sqlite.d.mts +1 -1
  365. package/dist/db/sqlite.mjs +3 -3
  366. package/dist/db/sqlite.mjs.map +1 -1
  367. package/dist/{default-DdDl3fxv.mjs → default-YMEfcQUx.mjs} +1 -1
  368. package/dist/{default-DdDl3fxv.mjs.map → default-YMEfcQUx.mjs.map} +1 -1
  369. package/dist/{deferred-tasks-SDWxrgzJ.d.mts → deferred-tasks-DfqxH5Xf.d.mts} +1 -1
  370. package/dist/{deferred-tasks-SDWxrgzJ.d.mts.map → deferred-tasks-DfqxH5Xf.d.mts.map} +1 -1
  371. package/dist/{device-flow-C0GdD_z8.mjs → device-flow-CojjSBwb.mjs} +5 -5
  372. package/dist/{device-flow-C0GdD_z8.mjs.map → device-flow-CojjSBwb.mjs.map} +1 -1
  373. package/dist/{direct-executor-CDXeJCSs.mjs → direct-executor-BWeWiRYB.mjs} +3 -3
  374. package/dist/{direct-executor-CDXeJCSs.mjs.map → direct-executor-BWeWiRYB.mjs.map} +1 -1
  375. package/dist/{dist-D_6CKR8z.mjs → dist-89xCrGqi.mjs} +1 -1
  376. package/dist/{dist-D_6CKR8z.mjs.map → dist-89xCrGqi.mjs.map} +1 -1
  377. package/dist/{email-console-BHNjIwiB.mjs → email-console-CEJgR_LB.mjs} +1 -1
  378. package/dist/{email-console-BHNjIwiB.mjs.map → email-console-CEJgR_LB.mjs.map} +1 -1
  379. package/dist/{enrich-_ySMEvEE.mjs → enrich-BX0IDp4z.mjs} +3 -3
  380. package/dist/{enrich-_ySMEvEE.mjs.map → enrich-BX0IDp4z.mjs.map} +1 -1
  381. package/dist/{error-DupyJZE2.mjs → error-D9w94fjV.mjs} +2 -2
  382. package/dist/{error-DupyJZE2.mjs.map → error-D9w94fjV.mjs.map} +1 -1
  383. package/dist/{errors-CLUPm7aD.mjs → errors-n2j49uNf.mjs} +22 -2
  384. package/dist/errors-n2j49uNf.mjs.map +1 -0
  385. package/dist/{escape-DUY2r2Zs.mjs → escape-DN4Qmjsl.mjs} +1 -1
  386. package/dist/{escape-DUY2r2Zs.mjs.map → escape-DN4Qmjsl.mjs.map} +1 -1
  387. package/dist/{field-defs-cache-BzwBlJrR.mjs → field-defs-cache-Bh4q6oar.mjs} +5 -5
  388. package/dist/{field-defs-cache-BzwBlJrR.mjs.map → field-defs-cache-Bh4q6oar.mjs.map} +1 -1
  389. package/dist/focal-point-BeWFo6Xx.mjs +23 -0
  390. package/dist/focal-point-BeWFo6Xx.mjs.map +1 -0
  391. package/dist/{fts-manager-DyZEohZC.mjs → fts-manager-BhOdW0bi.mjs} +3 -3
  392. package/dist/{fts-manager-DyZEohZC.mjs.map → fts-manager-BhOdW0bi.mjs.map} +1 -1
  393. package/dist/{hash-Cg0TXJPV.mjs → hash-CTcngWvj.mjs} +1 -1
  394. package/dist/{hash-Cg0TXJPV.mjs.map → hash-CTcngWvj.mjs.map} +1 -1
  395. package/dist/{hreflang-DwT2E0Sa.d.mts → hreflang-7pTHsLZJ.d.mts} +2 -2
  396. package/dist/{hreflang-DwT2E0Sa.d.mts.map → hreflang-7pTHsLZJ.d.mts.map} +1 -1
  397. package/dist/{hreflang-BBe83kKa.mjs → hreflang-Huc6O5wa.mjs} +7 -7
  398. package/dist/{hreflang-BBe83kKa.mjs.map → hreflang-Huc6O5wa.mjs.map} +1 -1
  399. package/dist/{identity-Ys6Dlc9C.mjs → identity-DXmb4aVp.mjs} +3 -3
  400. package/dist/{identity-Ys6Dlc9C.mjs.map → identity-DXmb4aVp.mjs.map} +1 -1
  401. package/dist/{import-DPkLmeXO.mjs → import-C4_O3KXU.mjs} +5 -5
  402. package/dist/{import-DPkLmeXO.mjs.map → import-C4_O3KXU.mjs.map} +1 -1
  403. package/dist/{index-C9KuQyO-.d.mts → index-6bRee8zb.d.mts} +3 -3
  404. package/dist/{index-C9KuQyO-.d.mts.map → index-6bRee8zb.d.mts.map} +1 -1
  405. package/dist/{index-BVBZj4-7.d.mts → index-DWbJrsAg.d.mts} +128 -52
  406. package/dist/index-DWbJrsAg.d.mts.map +1 -0
  407. package/dist/index.d.mts +21 -21
  408. package/dist/index.mjs +78 -72
  409. package/dist/{init-lock-6b309ZrF.mjs → init-lock-DJkX6Hto.mjs} +1 -1
  410. package/dist/{init-lock-6b309ZrF.mjs.map → init-lock-DJkX6Hto.mjs.map} +1 -1
  411. package/dist/{load-YJ2xl3ra.mjs → load-Bo7fZAbg.mjs} +2 -2
  412. package/dist/{load-YJ2xl3ra.mjs.map → load-Bo7fZAbg.mjs.map} +1 -1
  413. package/dist/{loader-CdG_Ci3H.mjs → loader-ZsjE4paL.mjs} +2 -2
  414. package/dist/{loader-CdG_Ci3H.mjs.map → loader-ZsjE4paL.mjs.map} +1 -1
  415. package/dist/{manifest-builder-B252yQUf.mjs → manifest-builder-QXuqvVMP.mjs} +2 -2
  416. package/dist/{manifest-builder-B252yQUf.mjs.map → manifest-builder-QXuqvVMP.mjs.map} +1 -1
  417. package/dist/{manifest-schema-BXbUAHod.mjs → manifest-schema-CUYPfCEH.mjs} +1 -1
  418. package/dist/{manifest-schema-BXbUAHod.mjs.map → manifest-schema-CUYPfCEH.mjs.map} +1 -1
  419. package/dist/media/image-endpoint.mjs +1 -1
  420. package/dist/media/index.d.mts +1 -1
  421. package/dist/media/index.mjs +13 -3
  422. package/dist/media/index.mjs.map +1 -1
  423. package/dist/media/local-runtime.d.mts +14 -12
  424. package/dist/media/local-runtime.d.mts.map +1 -1
  425. package/dist/media/local-runtime.mjs +11 -5
  426. package/dist/media/local-runtime.mjs.map +1 -1
  427. package/dist/{media-BWWWb8kg.mjs → media-BjwNuLEu.mjs} +100 -6
  428. package/dist/media-BjwNuLEu.mjs.map +1 -0
  429. package/dist/{media-CzhAn2--.mjs → media-D6SeAfBl.mjs} +41 -12
  430. package/dist/media-D6SeAfBl.mjs.map +1 -0
  431. package/dist/{media-allowlist-D6x-Us02.mjs → media-allowlist-D35v7bmL.mjs} +3 -2
  432. package/dist/media-allowlist-D35v7bmL.mjs.map +1 -0
  433. package/dist/media-folders-D-0CmRZj.mjs +210 -0
  434. package/dist/media-folders-D-0CmRZj.mjs.map +1 -0
  435. package/dist/{media-upload-CHBVn6m-.mjs → media-upload-C7bLXGcI.mjs} +9 -9
  436. package/dist/{media-upload-CHBVn6m-.mjs.map → media-upload-C7bLXGcI.mjs.map} +1 -1
  437. package/dist/{media-url-BvoX88FA.mjs → media-url-DH7giMx_.mjs} +1 -1
  438. package/dist/{media-url-BvoX88FA.mjs.map → media-url-DH7giMx_.mjs.map} +1 -1
  439. package/dist/media-usage-BTRJvS6V.mjs +1833 -0
  440. package/dist/media-usage-BTRJvS6V.mjs.map +1 -0
  441. package/dist/media-usage-activation-Dx0kPV6b.mjs +78 -0
  442. package/dist/media-usage-activation-Dx0kPV6b.mjs.map +1 -0
  443. package/dist/{media-usage-work-DrCbqBcl.mjs → media-usage-work-C-fhM30c.mjs} +221 -59
  444. package/dist/media-usage-work-C-fhM30c.mjs.map +1 -0
  445. package/dist/{media-usage-work-CLo_6tct.mjs → media-usage-work-DWb2XPMI.mjs} +4 -4
  446. package/dist/{media-usage-work-CLo_6tct.mjs.map → media-usage-work-DWb2XPMI.mjs.map} +1 -1
  447. package/dist/{media-usage-write-fence-aHEgN1zB.mjs → media-usage-write-fence-BwoJVRDC.mjs} +2 -2
  448. package/dist/{media-usage-write-fence-aHEgN1zB.mjs.map → media-usage-write-fence-BwoJVRDC.mjs.map} +1 -1
  449. package/dist/{menus-TCNSd2vp.mjs → menus-CD7EFLwz.mjs} +3 -3
  450. package/dist/{menus-TCNSd2vp.mjs.map → menus-CD7EFLwz.mjs.map} +1 -1
  451. package/dist/{menus-DSo5QeW0.mjs → menus-CNg0QlDt.mjs} +42 -26
  452. package/dist/menus-CNg0QlDt.mjs.map +1 -0
  453. package/dist/migrations/index.d.mts +1 -1
  454. package/dist/migrations/index.mjs +3 -3
  455. package/dist/{mime-DfIVjNkr.mjs → mime-CUIG6x84.mjs} +1 -1
  456. package/dist/mime-CUIG6x84.mjs.map +1 -0
  457. package/dist/{mode-BDoH_WcF.mjs → mode-DaUM_MB2.mjs} +1 -1
  458. package/dist/{mode-BDoH_WcF.mjs.map → mode-DaUM_MB2.mjs.map} +1 -1
  459. package/dist/node-sqlite-compat-Udb4Y-rs.mjs +46 -0
  460. package/dist/node-sqlite-compat-Udb4Y-rs.mjs.map +1 -0
  461. package/dist/{normalize-Bc-s_r-a.mjs → normalize-CSxdKBVa.mjs} +22 -1
  462. package/dist/normalize-CSxdKBVa.mjs.map +1 -0
  463. package/dist/{oauth-authorization-iOikmrJd.mjs → oauth-authorization-Dpsb-fMJ.mjs} +5 -5
  464. package/dist/{oauth-authorization-iOikmrJd.mjs.map → oauth-authorization-Dpsb-fMJ.mjs.map} +1 -1
  465. package/dist/{oauth-clients-CvI0nHMJ.mjs → oauth-clients-Bxnoe_67.mjs} +1 -1
  466. package/dist/{oauth-clients-CvI0nHMJ.mjs.map → oauth-clients-Bxnoe_67.mjs.map} +1 -1
  467. package/dist/{oauth-state-store-Ba8KZlwW.mjs → oauth-state-store-0C4s0K6w.mjs} +1 -1
  468. package/dist/{oauth-state-store-Ba8KZlwW.mjs.map → oauth-state-store-0C4s0K6w.mjs.map} +1 -1
  469. package/dist/{oauth-user-lookup-C5SQwGYC.mjs → oauth-user-lookup-wlGYSeqX.mjs} +1 -1
  470. package/dist/{oauth-user-lookup-C5SQwGYC.mjs.map → oauth-user-lookup-wlGYSeqX.mjs.map} +1 -1
  471. package/dist/object-cache/memory.d.mts +1 -1
  472. package/dist/{options-CBXIHUoT.mjs → options-BHPeIbBd.mjs} +1 -1
  473. package/dist/{options-CBXIHUoT.mjs.map → options-BHPeIbBd.mjs.map} +1 -1
  474. package/dist/{options--hhz-jZ1.d.mts → options-BhZIeFY1.d.mts} +3 -3
  475. package/dist/{options--hhz-jZ1.d.mts.map → options-BhZIeFY1.d.mts.map} +1 -1
  476. package/dist/page/index.d.mts +2 -2
  477. package/dist/{parse-DFbuc-Fh.mjs → parse-BRJg1kz8.mjs} +2 -2
  478. package/dist/{parse-DFbuc-Fh.mjs.map → parse-BRJg1kz8.mjs.map} +1 -1
  479. package/dist/{passkey-config-DFAmUyuE.mjs → passkey-config-B3P0nHRR.mjs} +1 -1
  480. package/dist/{passkey-config-DFAmUyuE.mjs.map → passkey-config-B3P0nHRR.mjs.map} +1 -1
  481. package/dist/{patterns-CiyXeDgr.mjs → patterns-D9iDO3j8.mjs} +1 -1
  482. package/dist/{patterns-CiyXeDgr.mjs.map → patterns-D9iDO3j8.mjs.map} +1 -1
  483. package/dist/{placeholder-Dp0K3RkD.d.mts → placeholder-BR4ZhVJl.d.mts} +7 -1
  484. package/dist/placeholder-BR4ZhVJl.d.mts.map +1 -0
  485. package/dist/plugin-types.d.mts +1 -1
  486. package/dist/plugin-utils.d.mts +10 -10
  487. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  488. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  489. package/dist/{policy-TluUIxuu.mjs → policy-BVRganJQ.mjs} +2 -2
  490. package/dist/{policy-TluUIxuu.mjs.map → policy-BVRganJQ.mjs.map} +1 -1
  491. package/dist/{portable-text-imBW5cHK.mjs → portable-text-C8Tvp4_g.mjs} +1 -1
  492. package/dist/{portable-text-imBW5cHK.mjs.map → portable-text-C8Tvp4_g.mjs.map} +1 -1
  493. package/dist/{preview-PW-nOnq4.mjs → preview-dn2dUyM0.mjs} +2 -2
  494. package/dist/{preview-PW-nOnq4.mjs.map → preview-dn2dUyM0.mjs.map} +1 -1
  495. package/dist/{protocol-DjsJHTlK.d.mts → protocol-CuBEsqgE.d.mts} +2 -2
  496. package/dist/{protocol-DjsJHTlK.d.mts.map → protocol-CuBEsqgE.d.mts.map} +1 -1
  497. package/dist/{public-url-CWoFhQT8.mjs → public-url-DRz_AinV.mjs} +1 -1
  498. package/dist/{public-url-CWoFhQT8.mjs.map → public-url-DRz_AinV.mjs.map} +1 -1
  499. package/dist/{query-zTVAR1Sl.mjs → query-D18vTdha.mjs} +14 -14
  500. package/dist/{query-zTVAR1Sl.mjs.map → query-D18vTdha.mjs.map} +1 -1
  501. package/dist/{query-BWs7O5Il.mjs → query-tZDEdtCN.mjs} +4 -4
  502. package/dist/{query-BWs7O5Il.mjs.map → query-tZDEdtCN.mjs.map} +1 -1
  503. package/dist/{rate-limit-Bx4g9m3p.mjs → rate-limit-on7KPo-v.mjs} +2 -2
  504. package/dist/{rate-limit-Bx4g9m3p.mjs.map → rate-limit-on7KPo-v.mjs.map} +1 -1
  505. package/dist/{redirect-COsS6SWJ.mjs → redirect-CZkbozu9.mjs} +2 -2
  506. package/dist/{redirect-COsS6SWJ.mjs.map → redirect-CZkbozu9.mjs.map} +1 -1
  507. package/dist/{redirect-BgAOGHDq.mjs → redirect-DrbEmU9a.mjs} +1 -1
  508. package/dist/{redirect-BgAOGHDq.mjs.map → redirect-DrbEmU9a.mjs.map} +1 -1
  509. package/dist/{redirects-DyChwDkG.mjs → redirects-BahWiu8Z.mjs} +5 -5
  510. package/dist/{redirects-DyChwDkG.mjs.map → redirects-BahWiu8Z.mjs.map} +1 -1
  511. package/dist/{redirects-DTDHWH31.mjs → redirects-CRIM55KH.mjs} +6 -6
  512. package/dist/{redirects-DTDHWH31.mjs.map → redirects-CRIM55KH.mjs.map} +1 -1
  513. package/dist/{api-BeBglVxU.mjs → registry-BiO5WwPq.mjs} +483 -2462
  514. package/dist/registry-BiO5WwPq.mjs.map +1 -0
  515. package/dist/{registry-Cqa2I749.mjs → registry-Cx-jEJOS.mjs} +9 -123
  516. package/dist/registry-Cx-jEJOS.mjs.map +1 -0
  517. package/dist/{relation-BnHB0T4J.mjs → relation-A6wLF_DS.mjs} +2 -2
  518. package/dist/{relation-BnHB0T4J.mjs.map → relation-A6wLF_DS.mjs.map} +1 -1
  519. package/dist/{relations-BBApI4e2.mjs → relations-BTPM2saV.mjs} +2 -2
  520. package/dist/{relations-BBApI4e2.mjs.map → relations-BTPM2saV.mjs.map} +1 -1
  521. package/dist/{relations-BHo5A8Zf.d.mts → relations-C7B46bcU.d.mts} +192 -33
  522. package/dist/relations-C7B46bcU.d.mts.map +1 -0
  523. package/dist/{relations-D5sM5fW0.mjs → relations-Cj5hZ6Zg.mjs} +4 -4
  524. package/dist/{relations-D5sM5fW0.mjs.map → relations-Cj5hZ6Zg.mjs.map} +1 -1
  525. package/dist/{request-cache-BSUptuJR.mjs → request-cache-DH9zdCOD.mjs} +1 -1
  526. package/dist/{request-cache-BSUptuJR.mjs.map → request-cache-DH9zdCOD.mjs.map} +1 -1
  527. package/dist/request-context.d.mts +1 -1
  528. package/dist/{request-meta-DgZJh3zG.mjs → request-meta-DRp5ENd_.mjs} +2 -2
  529. package/dist/{request-meta-DgZJh3zG.mjs.map → request-meta-DRp5ENd_.mjs.map} +1 -1
  530. package/dist/{resolve-DOhVEvty.mjs → resolve-DxUpTMmE.mjs} +1 -1
  531. package/dist/{resolve-DOhVEvty.mjs.map → resolve-DxUpTMmE.mjs.map} +1 -1
  532. package/dist/{runner-C1T11CfC.mjs → runner-77GOZCnH.mjs} +410 -286
  533. package/dist/runner-77GOZCnH.mjs.map +1 -0
  534. package/dist/{runner-BYHZn3Jy.d.mts → runner-zgqA_CqP.d.mts} +2 -2
  535. package/dist/{runner-BYHZn3Jy.d.mts.map → runner-zgqA_CqP.d.mts.map} +1 -1
  536. package/dist/runtime.d.mts +11 -11
  537. package/dist/runtime.mjs +1 -1
  538. package/dist/{schema-DZ5YVVZj.mjs → schema-BY-oVntF.mjs} +3 -3
  539. package/dist/{schema-DZ5YVVZj.mjs.map → schema-BY-oVntF.mjs.map} +1 -1
  540. package/dist/{schema-2AOsSn2v.mjs → schema-Cw9kVK0I.mjs} +35 -5
  541. package/dist/{schema-2AOsSn2v.mjs.map → schema-Cw9kVK0I.mjs.map} +1 -1
  542. package/dist/{scopes-8ClPF2bC.mjs → scopes-vMR1uIbt.mjs} +3 -3
  543. package/dist/{scopes-8ClPF2bC.mjs.map → scopes-vMR1uIbt.mjs.map} +1 -1
  544. package/dist/{search-u1QYdOCG.mjs → search-CfT3KxO5.mjs} +4 -4
  545. package/dist/{search-u1QYdOCG.mjs.map → search-CfT3KxO5.mjs.map} +1 -1
  546. package/dist/{secrets-BEgsAQEU.mjs → secrets-BH2Qow1-.mjs} +3 -3
  547. package/dist/{secrets-BEgsAQEU.mjs.map → secrets-BH2Qow1-.mjs.map} +1 -1
  548. package/dist/{sections-DmHviGyM.mjs → sections-DTfjyf1z.mjs} +2 -2
  549. package/dist/{sections-DmHviGyM.mjs.map → sections-DTfjyf1z.mjs.map} +1 -1
  550. package/dist/seed/index.d.mts +2 -2
  551. package/dist/seed/index.mjs +24 -22
  552. package/dist/seo/index.d.mts +3 -3
  553. package/dist/seo/index.mjs +16 -14
  554. package/dist/seo/index.mjs.map +1 -1
  555. package/dist/{seo-AFpVvTqr.mjs → seo-BSiTIXzd.mjs} +1 -1
  556. package/dist/{seo-AFpVvTqr.mjs.map → seo-BSiTIXzd.mjs.map} +1 -1
  557. package/dist/{seo-Dq4G-RrO.mjs → seo-Cm-dtvgk.mjs} +2 -2
  558. package/dist/{seo-Dq4G-RrO.mjs.map → seo-Cm-dtvgk.mjs.map} +1 -1
  559. package/dist/{service-BpArayPr.mjs → service-DYsDHEKe.mjs} +3 -3
  560. package/dist/{service-BpArayPr.mjs.map → service-DYsDHEKe.mjs.map} +1 -1
  561. package/dist/{session-user-BEorKVT-.mjs → session-user-CNAfvsdw.mjs} +1 -1
  562. package/dist/{session-user-BEorKVT-.mjs.map → session-user-CNAfvsdw.mjs.map} +1 -1
  563. package/dist/{settings-xRdoUZr0.mjs → settings-BgN-0DT-.mjs} +6 -6
  564. package/dist/{settings-xRdoUZr0.mjs.map → settings-BgN-0DT-.mjs.map} +1 -1
  565. package/dist/{settings-DE9xtzqd.mjs → settings-DjtkUo__.mjs} +3 -3
  566. package/dist/{settings-DE9xtzqd.mjs.map → settings-DjtkUo__.mjs.map} +1 -1
  567. package/dist/{setup-complete-BnT8pq_C.mjs → setup-complete-CcpXaPU7.mjs} +2 -2
  568. package/dist/{setup-complete-BnT8pq_C.mjs.map → setup-complete-CcpXaPU7.mjs.map} +1 -1
  569. package/dist/{setup-nonce-CBitf_oQ.mjs → setup-nonce-Reyw30jg.mjs} +1 -1
  570. package/dist/{setup-nonce-CBitf_oQ.mjs.map → setup-nonce-Reyw30jg.mjs.map} +1 -1
  571. package/dist/{single-flight-cache-B0eLKS6K.mjs → single-flight-cache-BPuGPdcN.mjs} +2 -2
  572. package/dist/{single-flight-cache-B0eLKS6K.mjs.map → single-flight-cache-BPuGPdcN.mjs.map} +1 -1
  573. package/dist/{site-url-DjpIXMXJ.mjs → site-url-Cs_V9n3V.mjs} +3 -3
  574. package/dist/{site-url-DjpIXMXJ.mjs.map → site-url-Cs_V9n3V.mjs.map} +1 -1
  575. package/dist/{snapshot-BBlGkb2q.mjs → snapshot-DSB8P7Lm.mjs} +1 -1
  576. package/dist/{snapshot-BBlGkb2q.mjs.map → snapshot-DSB8P7Lm.mjs.map} +1 -1
  577. package/dist/{ssrf-Cpvg_n3A.mjs → ssrf-DqebVE34.mjs} +21 -5
  578. package/dist/{ssrf-Cpvg_n3A.mjs.map → ssrf-DqebVE34.mjs.map} +1 -1
  579. package/dist/{state-DYwPue40.mjs → state-BJfU9Uzw.mjs} +12 -4
  580. package/dist/state-BJfU9Uzw.mjs.map +1 -0
  581. package/dist/{status-CWVYpbD8.mjs → status-DDu8HIQl.mjs} +1 -1
  582. package/dist/{status-CWVYpbD8.mjs.map → status-DDu8HIQl.mjs.map} +1 -1
  583. package/dist/storage/local.d.mts +1 -1
  584. package/dist/storage/local.mjs +1 -1
  585. package/dist/storage/s3.d.mts +1 -1
  586. package/dist/storage/s3.mjs +1 -1
  587. package/dist/{target-DixqqV5i.mjs → target-D0o26Azo.mjs} +1 -1
  588. package/dist/{target-DixqqV5i.mjs.map → target-D0o26Azo.mjs.map} +1 -1
  589. package/dist/{taxonomies-OHUIiIpI.mjs → taxonomies-CPGxo-bn.mjs} +5 -5
  590. package/dist/{taxonomies-OHUIiIpI.mjs.map → taxonomies-CPGxo-bn.mjs.map} +1 -1
  591. package/dist/{taxonomies-BlrncHi9.mjs → taxonomies-kmyudNGP.mjs} +7 -7
  592. package/dist/{taxonomies-BlrncHi9.mjs.map → taxonomies-kmyudNGP.mjs.map} +1 -1
  593. package/dist/{taxonomy-BqCZ1TdU.mjs → taxonomy-8X09dLN5.mjs} +4 -4
  594. package/dist/{taxonomy-BqCZ1TdU.mjs.map → taxonomy-8X09dLN5.mjs.map} +1 -1
  595. package/dist/{tokens-C39klX8R.mjs → tokens-gTS-IVUp.mjs} +1 -1
  596. package/dist/{tokens-C39klX8R.mjs.map → tokens-gTS-IVUp.mjs.map} +1 -1
  597. package/dist/{transaction-NQj4VJ7Z.mjs → transaction-0KOpOyMu.mjs} +2 -1
  598. package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-0KOpOyMu.mjs.map} +1 -1
  599. package/dist/{transport-RQE36APG.d.mts → transport-_6R1ub7t.d.mts} +1 -1
  600. package/dist/{transport-RQE36APG.d.mts.map → transport-_6R1ub7t.d.mts.map} +1 -1
  601. package/dist/{transport-DBJwa-Si.mjs → transport-fX59xaJz.mjs} +4 -3
  602. package/dist/{transport-DBJwa-Si.mjs.map → transport-fX59xaJz.mjs.map} +1 -1
  603. package/dist/{trusted-proxy-CEVekpby.mjs → trusted-proxy-fe6nEgR4.mjs} +1 -1
  604. package/dist/{trusted-proxy-CEVekpby.mjs.map → trusted-proxy-fe6nEgR4.mjs.map} +1 -1
  605. package/dist/types-75KIqzL9.mjs.map +1 -1
  606. package/dist/{types-BW1syrlw.mjs → types-BXUmsINL.mjs} +1 -1
  607. package/dist/{types-BW1syrlw.mjs.map → types-BXUmsINL.mjs.map} +1 -1
  608. package/dist/{types-BntycPTF.d.mts → types-Blb-MRC3.d.mts} +1 -1
  609. package/dist/{types-BntycPTF.d.mts.map → types-Blb-MRC3.d.mts.map} +1 -1
  610. package/dist/{types-C2pMvO62.d.mts → types-Br-ERaxh.d.mts} +2 -1
  611. package/dist/types-Br-ERaxh.d.mts.map +1 -0
  612. package/dist/{types-DWRhk9xZ.d.mts → types-BzBd3PeX.d.mts} +1 -1
  613. package/dist/{types-DWRhk9xZ.d.mts.map → types-BzBd3PeX.d.mts.map} +1 -1
  614. package/dist/{types-CXnOzDe1.d.mts → types-CLqvnd05.d.mts} +1 -1
  615. package/dist/{types-CXnOzDe1.d.mts.map → types-CLqvnd05.d.mts.map} +1 -1
  616. package/dist/{types-Dfk8KI5R.d.mts → types-D5VVLr7J.d.mts} +1 -1
  617. package/dist/{types-Dfk8KI5R.d.mts.map → types-D5VVLr7J.d.mts.map} +1 -1
  618. package/dist/{types-DQIDRKF2.d.mts → types-DHxzBjbD.d.mts} +10 -1
  619. package/dist/types-DHxzBjbD.d.mts.map +1 -0
  620. package/dist/{types-DN64_zzZ.d.mts → types-DJQxln7q.d.mts} +3 -3
  621. package/dist/{types-DN64_zzZ.d.mts.map → types-DJQxln7q.d.mts.map} +1 -1
  622. package/dist/{types-CtzytVXn.mjs → types-DM3vioBY.mjs} +1 -1
  623. package/dist/{types-CtzytVXn.mjs.map → types-DM3vioBY.mjs.map} +1 -1
  624. package/dist/{types-BcqdcT-r.d.mts → types-D_DWe7k9.d.mts} +1 -1
  625. package/dist/{types-BcqdcT-r.d.mts.map → types-D_DWe7k9.d.mts.map} +1 -1
  626. package/dist/{types-DrJrVCT1.d.mts → types-Vg6f5i3s.d.mts} +3 -3
  627. package/dist/{types-DrJrVCT1.d.mts.map → types-Vg6f5i3s.d.mts.map} +1 -1
  628. package/dist/{upload-attempts-D4FS8LT6.mjs → upload-attempts-CNyGSepg.mjs} +1 -1
  629. package/dist/{upload-attempts-D4FS8LT6.mjs.map → upload-attempts-CNyGSepg.mjs.map} +1 -1
  630. package/dist/{url-pattern-BHS0V5Ng.mjs → url-pattern-K-KdXcSe.mjs} +1 -1
  631. package/dist/{url-pattern-BHS0V5Ng.mjs.map → url-pattern-K-KdXcSe.mjs.map} +1 -1
  632. package/dist/{user-DJZfUJVE.mjs → user-CtYY0tp2.mjs} +2 -2
  633. package/dist/{user-DJZfUJVE.mjs.map → user-CtYY0tp2.mjs.map} +1 -1
  634. package/dist/{utils-D4vD_Y98.mjs → utils-HztC73hq.mjs} +1 -1
  635. package/dist/{utils-D4vD_Y98.mjs.map → utils-HztC73hq.mjs.map} +1 -1
  636. package/dist/{validate-BMLBvxOC.mjs → validate-Dd-IUjnZ.mjs} +2 -2
  637. package/dist/{validate-BMLBvxOC.mjs.map → validate-Dd-IUjnZ.mjs.map} +1 -1
  638. package/dist/{validate-CTqt5nuE.d.mts → validate-DlqdOZXB.d.mts} +5 -5
  639. package/dist/{validate-CTqt5nuE.d.mts.map → validate-DlqdOZXB.d.mts.map} +1 -1
  640. package/dist/{validation-npvENUCB.mjs → validation-D83n7Npi.mjs} +10 -8
  641. package/dist/{validation-npvENUCB.mjs.map → validation-D83n7Npi.mjs.map} +1 -1
  642. package/dist/version-ri-sCyAp.mjs +7 -0
  643. package/dist/{version-DB_fqCSI.mjs.map → version-ri-sCyAp.mjs.map} +1 -1
  644. package/dist/{widgets-BD3ho7Rq.mjs → widgets-CpmZd9Q-.mjs} +4 -4
  645. package/dist/{widgets-BD3ho7Rq.mjs.map → widgets-CpmZd9Q-.mjs.map} +1 -1
  646. package/dist/{wxr-taxonomies-Ca68766D.d.mts → wxr-taxonomies-BFd0y72_.d.mts} +1 -1
  647. package/dist/{wxr-taxonomies-Ca68766D.d.mts.map → wxr-taxonomies-BFd0y72_.d.mts.map} +1 -1
  648. package/dist/{wxr-taxonomies-B1R3vBoD.mjs → wxr-taxonomies-Df4vxWcU.mjs} +4 -4
  649. package/dist/{wxr-taxonomies-B1R3vBoD.mjs.map → wxr-taxonomies-Df4vxWcU.mjs.map} +1 -1
  650. package/dist/{zod-generator-LC-on_Vb.mjs → zod-generator-DylzusHh.mjs} +24 -18
  651. package/dist/zod-generator-DylzusHh.mjs.map +1 -0
  652. package/package.json +16 -8
  653. package/src/api/errors.ts +20 -0
  654. package/src/api/handlers/index.ts +17 -0
  655. package/src/api/handlers/media-allowlist.ts +1 -0
  656. package/src/api/handlers/media-folders.ts +130 -0
  657. package/src/api/handlers/media-usage-activation.ts +102 -0
  658. package/src/api/handlers/media-usage.ts +108 -0
  659. package/src/api/handlers/media.ts +62 -2
  660. package/src/api/handlers/registry.ts +134 -316
  661. package/src/api/openapi/document.ts +192 -2
  662. package/src/api/schemas/media-usage.ts +77 -0
  663. package/src/api/schemas/media.ts +69 -0
  664. package/src/astro/integration/routes.ts +18 -0
  665. package/src/astro/integration/vite-config.ts +2 -7
  666. package/src/astro/middleware/auth.ts +4 -0
  667. package/src/astro/middleware.ts +0 -7
  668. package/src/astro/routes/api/admin/media-usage/activation.ts +42 -0
  669. package/src/astro/routes/api/admin/media-usage/progress.ts +37 -0
  670. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +246 -96
  671. package/src/astro/routes/api/auth/me.ts +14 -2
  672. package/src/astro/routes/api/media/[id].ts +3 -0
  673. package/src/astro/routes/api/media/folders/[id].ts +55 -0
  674. package/src/astro/routes/api/media/folders/index.ts +37 -0
  675. package/src/astro/routes/api/media.ts +12 -2
  676. package/src/astro/routes/api/setup/dev-bypass.ts +12 -0
  677. package/src/astro/types.ts +11 -1
  678. package/src/client/index.ts +140 -2
  679. package/src/client/transport.ts +6 -2
  680. package/src/components/EmDashImage.astro +154 -50
  681. package/src/components/Gallery.astro +7 -0
  682. package/src/components/InlinePortableTextEditor.tsx +130 -1
  683. package/src/components/LiveSearch.astro +23 -0
  684. package/src/components/inline-code-block.tsx +180 -49
  685. package/src/content/converters/gallery.ts +2 -0
  686. package/src/content/converters/types.ts +2 -0
  687. package/src/database/connection.ts +2 -31
  688. package/src/database/index.ts +1 -1
  689. package/src/database/json-recordset.ts +16 -0
  690. package/src/database/migrations/019_i18n.ts +1 -1
  691. package/src/database/migrations/071_restore_content_bylines_table.ts +43 -0
  692. package/src/database/migrations/072_media_folders.ts +72 -0
  693. package/src/database/migrations/073_media_focal_point.ts +57 -0
  694. package/src/database/migrations/runner.ts +8 -2
  695. package/src/database/repositories/content.ts +5 -4
  696. package/src/database/repositories/media-folders.ts +115 -0
  697. package/src/database/repositories/media-usage-work.ts +395 -111
  698. package/src/database/repositories/media-usage.ts +1038 -10
  699. package/src/database/repositories/media.ts +97 -29
  700. package/src/database/transaction.ts +1 -0
  701. package/src/database/types.ts +13 -0
  702. package/src/db/adapters.ts +2 -2
  703. package/src/db/node-sqlite-compat.ts +78 -0
  704. package/src/db/sqlite.ts +3 -3
  705. package/src/emdash-runtime.ts +110 -105
  706. package/src/fields/image.ts +7 -1
  707. package/src/fields/types.ts +6 -2
  708. package/src/index.ts +5 -0
  709. package/src/media/focal-point.ts +37 -0
  710. package/src/media/local-runtime.ts +8 -0
  711. package/src/media/mime.ts +1 -0
  712. package/src/media/normalize.ts +10 -0
  713. package/src/media/types.ts +10 -0
  714. package/src/media/usage/activation.ts +162 -35
  715. package/src/media/usage/capture-triggers.ts +19 -3
  716. package/src/media/usage/collection-deletion-processor.ts +7 -3
  717. package/src/media/usage/collection-deletion.ts +10 -0
  718. package/src/media/usage/content-refresh.ts +258 -39
  719. package/src/media/usage/content-snapshots.ts +128 -31
  720. package/src/media/usage/extractor.ts +32 -13
  721. package/src/media/usage/maintenance-engine.ts +166 -0
  722. package/src/media/usage/reconciliation-processor.ts +106 -29
  723. package/src/media/usage/reconciliation.ts +90 -7
  724. package/src/media/usage/work-processor.ts +342 -101
  725. package/src/plugins/index.ts +1 -0
  726. package/src/plugins/save-rejection.ts +20 -0
  727. package/src/plugins/scheduler/node.ts +1 -13
  728. package/src/plugins/scheduler/types.ts +0 -2
  729. package/src/plugins/state.ts +13 -3
  730. package/src/registry/artifact-fetch.ts +530 -0
  731. package/src/registry/config.ts +24 -6
  732. package/src/registry/types.ts +12 -15
  733. package/src/schema/types.ts +1 -0
  734. package/src/schema/zod-generator.ts +12 -4
  735. package/src/security/ssrf.ts +21 -2
  736. package/src/seed/apply.ts +4 -4
  737. package/src/ui.ts +2 -0
  738. package/dist/api-BeBglVxU.mjs.map +0 -1
  739. package/dist/collection-deletion-DQPvMD8R.mjs.map +0 -1
  740. package/dist/content-ZHCc1ngN.mjs.map +0 -1
  741. package/dist/content-refresh-B_8SJzuU.mjs.map +0 -1
  742. package/dist/errors-CLUPm7aD.mjs.map +0 -1
  743. package/dist/index-BVBZj4-7.d.mts.map +0 -1
  744. package/dist/media-BWWWb8kg.mjs.map +0 -1
  745. package/dist/media-CzhAn2--.mjs.map +0 -1
  746. package/dist/media-allowlist-D6x-Us02.mjs.map +0 -1
  747. package/dist/media-usage-BZCydfvu.mjs.map +0 -1
  748. package/dist/media-usage-work-DrCbqBcl.mjs.map +0 -1
  749. package/dist/menus-DSo5QeW0.mjs.map +0 -1
  750. package/dist/mime-DfIVjNkr.mjs.map +0 -1
  751. package/dist/normalize-Bc-s_r-a.mjs.map +0 -1
  752. package/dist/placeholder-Dp0K3RkD.d.mts.map +0 -1
  753. package/dist/registry-Cqa2I749.mjs.map +0 -1
  754. package/dist/relations-BHo5A8Zf.d.mts.map +0 -1
  755. package/dist/runner-C1T11CfC.mjs.map +0 -1
  756. package/dist/state-DYwPue40.mjs.map +0 -1
  757. package/dist/types-C2pMvO62.d.mts.map +0 -1
  758. package/dist/types-DQIDRKF2.d.mts.map +0 -1
  759. package/dist/version-DB_fqCSI.mjs +0 -7
  760. package/dist/zod-generator-LC-on_Vb.mjs.map +0 -1
  761. /package/dist/{api-tokens-rIXC6LdE.mjs → api-tokens-B5NHLNUN.mjs} +0 -0
  762. /package/dist/{ssrf-DpXgKsna.mjs → ssrf-BPKzkm6O.mjs} +0 -0
  763. /package/dist/{types-CP1CflWf.mjs → types-D6OcIp2P.mjs} +0 -0
@@ -0,0 +1,2269 @@
1
+ import { t as validateIdentifier } from "./validate-BpwoDOgt.mjs";
2
+ import { c as isSqlite } from "./dialect-helpers-BSbfaO3F.mjs";
3
+ import { a as resolveConfiguredLocale, i as isI18nEnabled, r as getI18nConfig } from "./config-C3EBstHi.mjs";
4
+ import { t as after } from "./after-CBU2vkoO.mjs";
5
+ import { i as RevisionRepository, r as isSystemOrderField, t as ContentRepository } from "./content-CP-SeXNa.mjs";
6
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-bNiX6_DR.mjs";
7
+ import { n as isMissingTableError, t as isMissingColumnError } from "./db-errors-Cl-sKYTm.mjs";
8
+ import { i as encodeBase64, t as decodeBase64 } from "./base64-CvLhUKQv.mjs";
9
+ import { a as ScheduledNotDueError, i as InvalidCursorError, n as ContentMutationConflictError, r as EmDashValidationError, t as ContentCollectionNotFoundError } from "./types-CQPWhPxE.mjs";
10
+ import { t as isValidFocalPointUpdate } from "./focal-point-BeWFo6Xx.mjs";
11
+ import { t as MediaRepository } from "./media-D6SeAfBl.mjs";
12
+ import { t as UserRepository } from "./user-CtYY0tp2.mjs";
13
+ import { t as withTransaction } from "./transaction-0KOpOyMu.mjs";
14
+ import { t as TaxonomyRepository } from "./taxonomy-8X09dLN5.mjs";
15
+ import { t as CommentRepository } from "./comment-DhvLEdhQ.mjs";
16
+ import { t as OptionsRepository } from "./options-BHPeIbBd.mjs";
17
+ import { t as RedirectRepository } from "./redirect-CZkbozu9.mjs";
18
+ import { r as requestCached } from "./request-cache-DH9zdCOD.mjs";
19
+ import { t as BylineRepository } from "./byline-Cpnwrje1.mjs";
20
+ import { r as parseAllowedMimeTypes, t as matchesMimeAllowlist } from "./mime-CUIG6x84.mjs";
21
+ import { t as SeoRepository } from "./seo-Cm-dtvgk.mjs";
22
+ import { n as invalidateRedirectCache } from "./cache-5VirJ2Cn.mjs";
23
+ import { t as FTSManager } from "./fts-manager-BhOdW0bi.mjs";
24
+ import { u as invalidateTermCache } from "./taxonomies-kmyudNGP.mjs";
25
+ import { r as hashString } from "./hash-CTcngWvj.mjs";
26
+ import { t as ErrorCode } from "./errors-n2j49uNf.mjs";
27
+ import { t as PluginStateRepository } from "./state-BJfU9Uzw.mjs";
28
+ import { sql } from "kysely";
29
+
30
+ //#region src/api/rev.ts
31
+ /**
32
+ * Generate a _rev token from a content item's version and updatedAt.
33
+ */
34
+ function encodeRev(item) {
35
+ return encodeBase64(`${item.version}:${item.updatedAt}`);
36
+ }
37
+ /**
38
+ * Decode a _rev token into its components.
39
+ * Returns null if the token is malformed.
40
+ */
41
+ function decodeRev(rev) {
42
+ try {
43
+ const decoded = decodeBase64(rev);
44
+ const colonIdx = decoded.indexOf(":");
45
+ if (colonIdx === -1) return null;
46
+ const version = parseInt(decoded.slice(0, colonIdx), 10);
47
+ const updatedAt = decoded.slice(colonIdx + 1);
48
+ if (isNaN(version) || !updatedAt) return null;
49
+ return {
50
+ version,
51
+ updatedAt
52
+ };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Validate a _rev token against a content item.
59
+ * Returns null if valid (or if no _rev provided), or an error message if invalid.
60
+ */
61
+ function validateRev(rev, item) {
62
+ if (!rev) return { valid: true };
63
+ const decoded = decodeRev(rev);
64
+ if (!decoded) return {
65
+ valid: false,
66
+ message: "Malformed _rev token"
67
+ };
68
+ if (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) return {
69
+ valid: false,
70
+ message: "Content has been modified since last read (version conflict)"
71
+ };
72
+ return { valid: true };
73
+ }
74
+
75
+ //#endregion
76
+ //#region src/api/handlers/validate-media-fields.ts
77
+ function asMediaRef(value) {
78
+ if (value === null || value === void 0) return null;
79
+ if (typeof value !== "object" || Array.isArray(value)) return null;
80
+ return value;
81
+ }
82
+ function fail(message) {
83
+ return {
84
+ success: false,
85
+ error: {
86
+ code: "INVALID_MIME_FOR_FIELD",
87
+ message
88
+ }
89
+ };
90
+ }
91
+ async function loadMediaFieldsForCollection(db, collectionSlug) {
92
+ const rows = await db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
93
+ "_emdash_fields.slug",
94
+ "_emdash_fields.type",
95
+ "_emdash_fields.validation"
96
+ ]).where("_emdash_collections.slug", "=", collectionSlug).where("_emdash_fields.type", "in", ["file", "image"]).execute();
97
+ const out = [];
98
+ for (const row of rows) {
99
+ const list = parseAllowedMimeTypes(row.validation);
100
+ if (!list) continue;
101
+ out.push({
102
+ slug: row.slug,
103
+ type: row.type,
104
+ allowedMimeTypes: list
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+ async function validateMediaFields(db, collectionSlug, data) {
110
+ const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
111
+ if (fields.length === 0) return {
112
+ success: true,
113
+ data: true
114
+ };
115
+ const localIds = /* @__PURE__ */ new Set();
116
+ for (const field of fields) {
117
+ const ref = asMediaRef(data[field.slug]);
118
+ if (!ref) continue;
119
+ if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
120
+ }
121
+ const idList = [...localIds];
122
+ const mimeById = /* @__PURE__ */ new Map();
123
+ if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
124
+ const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
125
+ for (const r of rows) mimeById.set(r.id, r.mime_type);
126
+ }
127
+ for (const field of fields) {
128
+ const value = data[field.slug];
129
+ if (value === null || value === void 0) continue;
130
+ const ref = asMediaRef(value);
131
+ if (!ref) continue;
132
+ const provider = typeof ref.provider === "string" ? ref.provider : "local";
133
+ let mime;
134
+ if (provider === "local") {
135
+ if (typeof ref.id !== "string") return fail(`Field '${field.slug}' references media with an invalid id`);
136
+ mime = mimeById.get(ref.id);
137
+ if (!mime) return fail(`Field '${field.slug}' references media with unknown MIME type`);
138
+ } else {
139
+ if (typeof ref.mimeType !== "string") return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
140
+ mime = ref.mimeType;
141
+ }
142
+ if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) return fail(`Field '${field.slug}' does not accept ${mime}`);
143
+ }
144
+ return {
145
+ success: true,
146
+ data: true
147
+ };
148
+ }
149
+
150
+ //#endregion
151
+ //#region src/api/handlers/content.ts
152
+ /**
153
+ * Narrow a caught error to one carrying a structured `apiError` discriminant.
154
+ * Used by transaction callbacks that want to surface a specific error code
155
+ * through the standard Error throwing path.
156
+ */
157
+ function hasApiError(error) {
158
+ if (!(error instanceof Error) || !("apiError" in error)) return false;
159
+ const { apiError } = error;
160
+ return typeof apiError === "object" && apiError !== null && "code" in apiError && typeof apiError.code === "string";
161
+ }
162
+ /**
163
+ * Extract a slug source (title or name) from content data.
164
+ * Returns null if no suitable string field is found.
165
+ */
166
+ function getSlugSource(data) {
167
+ if (typeof data.title === "string" && data.title.length > 0) return data.title;
168
+ if (typeof data.name === "string" && data.name.length > 0) return data.name;
169
+ return null;
170
+ }
171
+ /** Default SEO values for content without an explicit SEO row */
172
+ const SEO_DEFAULTS = {
173
+ title: null,
174
+ description: null,
175
+ image: null,
176
+ canonical: null,
177
+ noIndex: false
178
+ };
179
+ /**
180
+ * Check if a collection has SEO enabled.
181
+ */
182
+ async function collectionHasSeo(db, collection) {
183
+ return (await db.selectFrom("_emdash_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
184
+ }
185
+ async function getCollectionPublishConfig(db, collection) {
186
+ const row = await db.selectFrom("_emdash_collections").select(["supports", "routable"]).where("slug", "=", collection).executeTakeFirst();
187
+ const supports = row?.supports ? JSON.parse(row.supports) : [];
188
+ return {
189
+ supportsRevisions: Array.isArray(supports) && supports.includes("revisions"),
190
+ routable: row?.routable !== 0
191
+ };
192
+ }
193
+ function requireRoutablePublishSlug(routable, slug) {
194
+ if (routable && !slug?.trim()) throw new EmDashValidationError("Cannot publish routable content without a slug");
195
+ }
196
+ /**
197
+ * Hydrate SEO data on a single content item if the collection has SEO enabled.
198
+ */
199
+ async function hydrateSeo(db, collection, item, hasSeo) {
200
+ if (!hasSeo) return;
201
+ item.seo = await new SeoRepository(db).get(collection, item.id);
202
+ }
203
+ /**
204
+ * Hydrate SEO data on multiple content items using a single batch query.
205
+ */
206
+ async function hydrateSeoMany(db, collection, items, hasSeo) {
207
+ if (!hasSeo || items.length === 0) return;
208
+ const seoMap = await new SeoRepository(db).getMany(collection, items.map((i) => i.id));
209
+ for (const item of items) item.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };
210
+ }
211
+ async function hydrateBylines(db, collection, item) {
212
+ const bylineRepo = new BylineRepository(db);
213
+ const localeOpt = item.locale ? { locale: item.locale } : void 0;
214
+ const bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);
215
+ if (bylines.length > 0) {
216
+ item.bylines = bylines.map((c) => ({
217
+ ...c,
218
+ source: "explicit"
219
+ }));
220
+ item.byline = bylines[0]?.byline ?? null;
221
+ return;
222
+ }
223
+ if (item.primaryBylineId) {
224
+ item.bylines = [];
225
+ item.byline = null;
226
+ return;
227
+ }
228
+ if (item.authorId) {
229
+ const fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);
230
+ if (fallback) {
231
+ item.bylines = [{
232
+ byline: fallback,
233
+ sortOrder: 0,
234
+ roleLabel: null,
235
+ source: "inferred"
236
+ }];
237
+ item.byline = fallback;
238
+ return;
239
+ }
240
+ }
241
+ item.bylines = [];
242
+ item.byline = null;
243
+ }
244
+ /**
245
+ * Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.
246
+ *
247
+ * Items may live at different locales (e.g. a list endpoint returning the
248
+ * translations of an entry). Group by `item.locale` and call the strict
249
+ * per-locale repo method once per group so each item resolves against its
250
+ * own locale's byline rows.
251
+ */
252
+ async function hydrateBylinesMany(db, collection, items) {
253
+ if (items.length === 0) return;
254
+ const bylineRepo = new BylineRepository(db);
255
+ const localeBuckets = /* @__PURE__ */ new Map();
256
+ for (const item of items) {
257
+ const key = item.locale ?? null;
258
+ const bucket = localeBuckets.get(key);
259
+ if (bucket) bucket.push(item);
260
+ else localeBuckets.set(key, [item]);
261
+ }
262
+ const bylinesByItem = /* @__PURE__ */ new Map();
263
+ const itemsNeedingAuthorCheck = [];
264
+ for (const [locale, bucket] of localeBuckets) {
265
+ const localeOpt = locale ? { locale } : void 0;
266
+ const ids = bucket.map((i) => i.id);
267
+ const credits = await bylineRepo.getContentBylinesMany(collection, ids, localeOpt);
268
+ for (const [id, list] of credits) bylinesByItem.set(id, list);
269
+ for (const item of bucket) {
270
+ if (credits.has(item.id) && credits.get(item.id).length > 0) continue;
271
+ if (item.authorId) itemsNeedingAuthorCheck.push(item);
272
+ }
273
+ }
274
+ const fallbackByItem = /* @__PURE__ */ new Map();
275
+ if (itemsNeedingAuthorCheck.length > 0) {
276
+ const authorBuckets = /* @__PURE__ */ new Map();
277
+ for (const item of itemsNeedingAuthorCheck) {
278
+ if (item.primaryBylineId) continue;
279
+ const key = item.locale ?? null;
280
+ const bucket = authorBuckets.get(key);
281
+ if (bucket) bucket.push(item);
282
+ else authorBuckets.set(key, [item]);
283
+ }
284
+ for (const [locale, bucket] of authorBuckets) {
285
+ const localeOpt = locale ? { locale } : void 0;
286
+ const authorIds = bucket.map((i) => i.authorId).filter((id) => id !== null);
287
+ const uniqueAuthorIds = [...new Set(authorIds)];
288
+ if (uniqueAuthorIds.length === 0) continue;
289
+ const authorMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);
290
+ for (const item of bucket) {
291
+ if (!item.authorId) continue;
292
+ const f = authorMap.get(item.authorId);
293
+ if (f) fallbackByItem.set(item.id, f);
294
+ }
295
+ }
296
+ }
297
+ for (const item of items) {
298
+ const explicit = bylinesByItem.get(item.id);
299
+ if (explicit && explicit.length > 0) {
300
+ item.bylines = explicit.map((c) => ({
301
+ ...c,
302
+ source: "explicit"
303
+ }));
304
+ item.byline = explicit[0]?.byline ?? null;
305
+ continue;
306
+ }
307
+ const fallback = fallbackByItem.get(item.id);
308
+ if (fallback) {
309
+ item.bylines = [{
310
+ byline: fallback,
311
+ sortOrder: 0,
312
+ roleLabel: null,
313
+ source: "inferred"
314
+ }];
315
+ item.byline = fallback;
316
+ continue;
317
+ }
318
+ item.bylines = [];
319
+ item.byline = null;
320
+ }
321
+ }
322
+ /**
323
+ * Resolve an identifier (ID or slug) to a real content ID.
324
+ * Returns the ID if found, null if not found.
325
+ * When locale is provided, slug lookups are scoped to that locale.
326
+ */
327
+ async function resolveId(repo, collection, identifier, locale) {
328
+ return (await repo.findByIdOrSlug(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
329
+ }
330
+ /**
331
+ * Resolve an identifier (ID or slug) to a real content ID,
332
+ * including trashed (soft-deleted) items.
333
+ */
334
+ async function resolveIdIncludingTrashed(repo, collection, identifier, locale) {
335
+ return (await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
336
+ }
337
+ /**
338
+ * Resolve the columns a content-list search should match against. Always
339
+ * includes `slug` (a standard column), adds the configured `titleField` plus
340
+ * the `title`/`name` display fields when the collection actually defines them,
341
+ * mirroring the admin's item-title resolution (titleField -> title -> name ->
342
+ * slug), and includes every field explicitly marked searchable. Returning only
343
+ * schema-backed columns avoids "no such column" errors.
344
+ */
345
+ async function resolveSearchColumns(db, collection) {
346
+ const row = await db.selectFrom("_emdash_collections").select(["id", "title_field"]).where("slug", "=", collection).executeTakeFirst();
347
+ if (!row) return ["slug"];
348
+ const fields = await db.selectFrom("_emdash_fields").select(["slug", "searchable"]).where("collection_id", "=", row.id).orderBy("sort_order", "asc").execute();
349
+ const columns = new Set(["slug"]);
350
+ const fieldSlugs = new Set(fields.map((f) => f.slug));
351
+ if (row.title_field && fieldSlugs.has(row.title_field)) columns.add(row.title_field);
352
+ for (const candidate of ["title", "name"]) if (fieldSlugs.has(candidate)) columns.add(candidate);
353
+ for (const field of fields) if (field.searchable === 1) columns.add(field.slug);
354
+ return [...columns];
355
+ }
356
+ /**
357
+ * Decide whether the content-list `q` filter can be served from the
358
+ * collection's FTS5 index instead of a full-scan substring LIKE (#1517).
359
+ *
360
+ * Requires SQLite (FTS5 is SQLite-only), search enabled on the collection,
361
+ * every non-slug display column present in the searchable-field set (or the
362
+ * index would miss matches the LIKE finds), and the index table actually
363
+ * existing.
364
+ */
365
+ async function canUseFtsForListFilter(db, collection, searchColumns) {
366
+ if (!isSqlite(db)) return false;
367
+ const ftsManager = new FTSManager(db);
368
+ if (!(await ftsManager.getSearchConfig(collection))?.enabled) return false;
369
+ const searchable = new Set(await ftsManager.getSearchableFields(collection));
370
+ if (!searchColumns.every((col) => col === "slug" || searchable.has(col))) return false;
371
+ return ftsManager.ftsTableExists(collection);
372
+ }
373
+ /**
374
+ * Create a 301 auto-redirect from an entry's old URL to its new one after a
375
+ * slug change, using the collection's URL pattern. Shared by
376
+ * handleContentUpdate (direct slug edits) and handleContentPublish (slug edits
377
+ * staged as `_slug` in a draft revision, which only land on publish).
378
+ */
379
+ async function createSlugChangeRedirect(db, collection, oldSlug, newSlug, contentId) {
380
+ if (await slugStillTaken(db, collection, oldSlug, contentId)) return;
381
+ const collectionRow = await db.selectFrom("_emdash_collections").select("url_pattern").where("slug", "=", collection).executeTakeFirst();
382
+ await new RedirectRepository(db).createAutoRedirect(collection, oldSlug, newSlug, contentId, collectionRow?.url_pattern ?? null);
383
+ invalidateRedirectCache();
384
+ }
385
+ /** Whether a row other than `contentId` still holds `slug` in this collection. */
386
+ async function slugStillTaken(db, collection, slug, contentId) {
387
+ validateIdentifier(collection, "collection slug");
388
+ return (await sql`
389
+ SELECT id FROM ${sql.ref(`ec_${collection}`)}
390
+ WHERE slug = ${slug}
391
+ AND id != ${contentId}
392
+ AND deleted_at IS NULL
393
+ LIMIT 1
394
+ `.execute(db)).rows.length > 0;
395
+ }
396
+ /** Matches a date-only `YYYY-MM-DD` bound (no time component). */
397
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
398
+ /**
399
+ * Normalize a date-range bound to an ISO datetime for lexicographic comparison
400
+ * against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the
401
+ * appropriate UTC day boundary so the range stays inclusive: a `start` bound
402
+ * becomes the start of the day and an `end` bound the end of the day.
403
+ * Otherwise a date-only upper bound would exclude every same-day row (since
404
+ * `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass
405
+ * through unchanged.
406
+ */
407
+ function normalizeDateBound(value, edge) {
408
+ if (!value) return void 0;
409
+ if (!DATE_ONLY_RE.test(value)) return value;
410
+ return edge === "start" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
411
+ }
412
+ /**
413
+ * Build the repository's byline filter from the wire params.
414
+ *
415
+ * `locale` is the locale the list is scoped to, which is the locale an
416
+ * inferred credit has to resolve at — the admin list is always scoped to the
417
+ * locale picked in its switcher.
418
+ */
419
+ function resolveBylineFilter(params, locale) {
420
+ const includeInferred = params.includeInferredBylines === true;
421
+ if (params.bylinesNone) return {
422
+ mode: "none",
423
+ includeInferred,
424
+ locale
425
+ };
426
+ const bylineIds = params.bylines ?? [];
427
+ if (bylineIds.length === 0) return void 0;
428
+ return {
429
+ mode: "any",
430
+ bylineIds,
431
+ includeInferred,
432
+ locale
433
+ };
434
+ }
435
+ /**
436
+ * Create content list handler
437
+ */
438
+ async function handleContentList(db, collection, params) {
439
+ try {
440
+ const repo = new ContentRepository(db);
441
+ const where = {};
442
+ if (params.status) where.status = params.status;
443
+ const locale = params.locale ? resolveConfiguredLocale(params.locale) : void 0;
444
+ if (locale) where.locale = locale;
445
+ if (params.authorId) where.authorId = params.authorId;
446
+ if (params.fieldFilters && Object.keys(params.fieldFilters).length > 0) where.fieldFilters = params.fieldFilters;
447
+ const bylineFilter = resolveBylineFilter(params, locale);
448
+ if (bylineFilter) where.bylineFilter = bylineFilter;
449
+ if (params.dateField && (params.dateFrom || params.dateTo)) where.dateFilter = {
450
+ field: params.dateField,
451
+ from: normalizeDateBound(params.dateFrom, "start"),
452
+ to: normalizeDateBound(params.dateTo, "end")
453
+ };
454
+ const q = params.q?.trim();
455
+ if (q) {
456
+ where.q = q;
457
+ where.searchColumns = await resolveSearchColumns(db, collection);
458
+ where.useFts = await canUseFtsForListFilter(db, collection, where.searchColumns);
459
+ }
460
+ let sortableExtras;
461
+ if (params.orderBy && !isSystemOrderField(params.orderBy)) {
462
+ const coll = await db.selectFrom("_emdash_collections").select(["title_field", "date_field"]).where("slug", "=", collection).executeTakeFirst();
463
+ sortableExtras = [coll?.title_field, coll?.date_field].filter((slug) => !!slug);
464
+ }
465
+ const result = await repo.findMany(collection, {
466
+ cursor: params.cursor,
467
+ limit: params.limit || 50,
468
+ where: Object.keys(where).length > 0 ? where : void 0,
469
+ orderBy: params.orderBy ? {
470
+ field: params.orderBy,
471
+ direction: params.order || "desc"
472
+ } : void 0,
473
+ sortableExtras
474
+ });
475
+ const hasSeo = await collectionHasSeo(db, collection);
476
+ await hydrateSeoMany(db, collection, result.items, hasSeo);
477
+ await hydrateBylinesMany(db, collection, result.items);
478
+ return {
479
+ success: true,
480
+ data: {
481
+ items: result.items,
482
+ nextCursor: result.nextCursor,
483
+ total: result.total
484
+ }
485
+ };
486
+ } catch (error) {
487
+ if (error instanceof InvalidCursorError) return {
488
+ success: false,
489
+ error: {
490
+ code: "INVALID_CURSOR",
491
+ message: error.message
492
+ }
493
+ };
494
+ if (error instanceof ContentCollectionNotFoundError || isMissingTableError(error)) return {
495
+ success: false,
496
+ error: {
497
+ code: "COLLECTION_NOT_FOUND",
498
+ message: `Collection '${collection}' not found`
499
+ }
500
+ };
501
+ if (isMissingColumnError(error, "deleted_at")) return {
502
+ success: false,
503
+ error: {
504
+ code: "COLLECTION_SCHEMA_MISMATCH",
505
+ message: `Collection '${collection}' backing table is missing the 'deleted_at' column`
506
+ }
507
+ };
508
+ if (error instanceof EmDashValidationError) return {
509
+ success: false,
510
+ error: {
511
+ code: "VALIDATION_ERROR",
512
+ message: error.message
513
+ }
514
+ };
515
+ console.error("Content list error:", error);
516
+ return {
517
+ success: false,
518
+ error: {
519
+ code: "CONTENT_LIST_ERROR",
520
+ message: "Failed to list content"
521
+ }
522
+ };
523
+ }
524
+ }
525
+ /**
526
+ * List the distinct authors of a collection's live content.
527
+ *
528
+ * Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN
529
+ * only), this is gated on `content:read`, so any editor can filter by author.
530
+ * Returns only users who have authored at least one non-trashed entry, sorted
531
+ * by display name then email for a stable dropdown order.
532
+ */
533
+ async function handleContentAuthors(db, collection) {
534
+ try {
535
+ const authorIds = await new ContentRepository(db).findDistinctAuthorIds(collection);
536
+ if (authorIds.length === 0) return {
537
+ success: true,
538
+ data: { items: [] }
539
+ };
540
+ return {
541
+ success: true,
542
+ data: { items: (await new UserRepository(db).findByIds(authorIds)).map((u) => ({
543
+ id: u.id,
544
+ name: u.name,
545
+ email: u.email,
546
+ avatarUrl: u.avatarUrl
547
+ })).toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email)) }
548
+ };
549
+ } catch (error) {
550
+ if (isMissingTableError(error)) return {
551
+ success: false,
552
+ error: {
553
+ code: "COLLECTION_NOT_FOUND",
554
+ message: `Collection '${collection}' not found`
555
+ }
556
+ };
557
+ console.error("Content authors error:", error);
558
+ return {
559
+ success: false,
560
+ error: {
561
+ code: "CONTENT_AUTHORS_ERROR",
562
+ message: "Failed to list content authors"
563
+ }
564
+ };
565
+ }
566
+ }
567
+ /**
568
+ * Get single content item
569
+ */
570
+ async function handleContentGet(db, collection, id, locale) {
571
+ try {
572
+ const item = await new ContentRepository(db).findByIdOrSlug(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
573
+ if (!item) return {
574
+ success: false,
575
+ error: {
576
+ code: "NOT_FOUND",
577
+ message: `Content item not found: ${id}`
578
+ }
579
+ };
580
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
581
+ await hydrateBylines(db, collection, item);
582
+ return {
583
+ success: true,
584
+ data: {
585
+ item,
586
+ _rev: encodeRev(item)
587
+ }
588
+ };
589
+ } catch (error) {
590
+ console.error("Content get error:", error);
591
+ return {
592
+ success: false,
593
+ error: {
594
+ code: "CONTENT_GET_ERROR",
595
+ message: "Failed to get content"
596
+ }
597
+ };
598
+ }
599
+ }
600
+ /**
601
+ * Get a content item by id, including trashed items.
602
+ * Used by restore endpoint for ownership checks on soft-deleted items.
603
+ */
604
+ async function handleContentGetIncludingTrashed(db, collection, id, locale) {
605
+ try {
606
+ const item = await new ContentRepository(db).findByIdOrSlugIncludingTrashed(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
607
+ if (!item) return {
608
+ success: false,
609
+ error: {
610
+ code: "NOT_FOUND",
611
+ message: `Content item not found: ${id}`
612
+ }
613
+ };
614
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
615
+ await hydrateBylines(db, collection, item);
616
+ return {
617
+ success: true,
618
+ data: {
619
+ item,
620
+ _rev: encodeRev(item)
621
+ }
622
+ };
623
+ } catch (error) {
624
+ console.error("Content get error:", error);
625
+ return {
626
+ success: false,
627
+ error: {
628
+ code: "CONTENT_GET_ERROR",
629
+ message: "Failed to get content"
630
+ }
631
+ };
632
+ }
633
+ }
634
+ /**
635
+ * Create content item.
636
+ *
637
+ * Content + SEO writes are wrapped in a transaction so either both succeed
638
+ * or neither does. If `body.seo` is provided for a non-SEO collection, the
639
+ * API returns a validation error rather than silently dropping it.
640
+ */
641
+ async function handleContentCreate(db, collection, body) {
642
+ try {
643
+ const hasSeo = await collectionHasSeo(db, collection);
644
+ if (body.seo && !hasSeo) return {
645
+ success: false,
646
+ error: {
647
+ code: "VALIDATION_ERROR",
648
+ message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
649
+ }
650
+ };
651
+ const mimeCheck = await validateMediaFields(db, collection, body.data);
652
+ if (!mimeCheck.success) return mimeCheck;
653
+ const item = await withTransaction(db, async (trx) => {
654
+ const repo = new ContentRepository(trx);
655
+ const bylineRepo = new BylineRepository(trx);
656
+ const effectiveLocale = body.locale ? resolveConfiguredLocale(body.locale) : getI18nConfig()?.defaultLocale;
657
+ let slug = body.slug;
658
+ if (!slug) {
659
+ const slugSource = getSlugSource(body.data);
660
+ if (slugSource) slug = await repo.generateUniqueSlug(collection, slugSource, effectiveLocale);
661
+ }
662
+ if (body.status === "published") requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, slug);
663
+ const created = await repo.create({
664
+ type: collection,
665
+ slug,
666
+ data: body.data,
667
+ status: body.status || "draft",
668
+ authorId: body.authorId,
669
+ locale: effectiveLocale,
670
+ translationOf: body.translationOf,
671
+ createdAt: body.createdAt,
672
+ publishedAt: body.publishedAt
673
+ });
674
+ if (body.bylines !== void 0) created.primaryBylineId = (await bylineRepo.setContentBylines(collection, created.id, body.bylines))[0]?.byline.translationGroup ?? null;
675
+ if (body.translationOf) {
676
+ if (body.bylines === void 0) {
677
+ await bylineRepo.copyContentBylines(collection, body.translationOf, created.id);
678
+ const source = await repo.findById(collection, body.translationOf);
679
+ if (source) created.primaryBylineId = source.primaryBylineId;
680
+ }
681
+ }
682
+ await hydrateBylines(trx, collection, created);
683
+ if (body.seo && hasSeo) created.seo = await new SeoRepository(trx).upsert(collection, created.id, body.seo);
684
+ else if (hasSeo) created.seo = { ...SEO_DEFAULTS };
685
+ if (body.taxonomies) await assignTaxonomies(trx, collection, created.id, effectiveLocale, body.taxonomies);
686
+ return created;
687
+ });
688
+ return {
689
+ success: true,
690
+ data: {
691
+ item,
692
+ _rev: encodeRev(item)
693
+ }
694
+ };
695
+ } catch (error) {
696
+ if (isMissingTableError(error)) return {
697
+ success: false,
698
+ error: {
699
+ code: "COLLECTION_NOT_FOUND",
700
+ message: `Collection '${collection}' not found`
701
+ }
702
+ };
703
+ if (error instanceof EmDashValidationError) return {
704
+ success: false,
705
+ error: {
706
+ code: "VALIDATION_ERROR",
707
+ message: error.message
708
+ }
709
+ };
710
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
711
+ if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
712
+ if (message.includes("slug")) return {
713
+ success: false,
714
+ error: {
715
+ code: "SLUG_CONFLICT",
716
+ message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`
717
+ }
718
+ };
719
+ return {
720
+ success: false,
721
+ error: {
722
+ code: "CONFLICT",
723
+ message: "Unique constraint violation"
724
+ }
725
+ };
726
+ }
727
+ console.error("Content create error:", error);
728
+ return {
729
+ success: false,
730
+ error: {
731
+ code: "CONTENT_CREATE_ERROR",
732
+ message: "Failed to create content"
733
+ }
734
+ };
735
+ }
736
+ }
737
+ /**
738
+ * Update content item.
739
+ * If `_rev` is provided, validates it against the current version before writing.
740
+ * No `_rev` = blind write (backwards-compatible for admin UI).
741
+ *
742
+ * Content + SEO writes are wrapped in a transaction for atomicity.
743
+ */
744
+ async function handleContentUpdate(db, collection, id, body) {
745
+ try {
746
+ const hasSeo = await collectionHasSeo(db, collection);
747
+ if (body.seo && !hasSeo) return {
748
+ success: false,
749
+ error: {
750
+ code: "VALIDATION_ERROR",
751
+ message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
752
+ }
753
+ };
754
+ if (body.data) {
755
+ const mimeCheck = await validateMediaFields(db, collection, body.data);
756
+ if (!mimeCheck.success) return mimeCheck;
757
+ }
758
+ const resolvedId = await resolveId(new ContentRepository(db), collection, id, body.locale) ?? id;
759
+ const item = await withTransaction(db, async (trx) => {
760
+ const trxRepo = new ContentRepository(trx);
761
+ const bylineRepo = new BylineRepository(trx);
762
+ const existing = body._rev || body.slug !== void 0 || body.status === "published" ? await trxRepo.findById(collection, resolvedId) : null;
763
+ if (body._rev) {
764
+ if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
765
+ const revCheck = validateRev(body._rev, existing);
766
+ if (!revCheck.valid) throw Object.assign(new Error(revCheck.message), { apiError: { code: "CONFLICT" } });
767
+ }
768
+ let oldSlug;
769
+ if (body.slug && existing?.slug && existing.slug !== body.slug) oldSlug = existing.slug;
770
+ if ((body.status ?? existing?.status) === "published") {
771
+ if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
772
+ const publishConfig = await getCollectionPublishConfig(trx, collection);
773
+ const intendedSlug = body.slug !== void 0 ? body.slug : existing.slug;
774
+ requireRoutablePublishSlug(publishConfig.routable, intendedSlug);
775
+ }
776
+ const updated = await trxRepo.update(collection, resolvedId, {
777
+ data: body.data,
778
+ slug: body.slug,
779
+ status: body.status,
780
+ authorId: body.authorId,
781
+ publishedAt: body.publishedAt
782
+ });
783
+ if (body.bylines !== void 0) updated.primaryBylineId = (await bylineRepo.setContentBylines(collection, resolvedId, body.bylines))[0]?.byline.translationGroup ?? null;
784
+ if (oldSlug && body.slug) await createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId);
785
+ if (isI18nEnabled() && body.data && updated.translationGroup) await syncNonTranslatableFields(trx, collection, updated.id, updated.translationGroup, body.data);
786
+ if (body.seo && hasSeo) updated.seo = await new SeoRepository(trx).upsert(collection, resolvedId, body.seo);
787
+ else if (hasSeo) updated.seo = await new SeoRepository(trx).get(collection, resolvedId);
788
+ await hydrateBylines(trx, collection, updated);
789
+ if (body.taxonomies) await assignTaxonomies(trx, collection, resolvedId, updated.locale ?? body.locale, body.taxonomies);
790
+ return updated;
791
+ });
792
+ return {
793
+ success: true,
794
+ data: {
795
+ item,
796
+ _rev: encodeRev(item)
797
+ }
798
+ };
799
+ } catch (error) {
800
+ if (hasApiError(error)) return {
801
+ success: false,
802
+ error: {
803
+ code: error.apiError.code,
804
+ message: error.message
805
+ }
806
+ };
807
+ if (isMissingTableError(error)) return {
808
+ success: false,
809
+ error: {
810
+ code: "COLLECTION_NOT_FOUND",
811
+ message: `Collection '${collection}' not found`
812
+ }
813
+ };
814
+ if (error instanceof EmDashValidationError) return {
815
+ success: false,
816
+ error: {
817
+ code: "VALIDATION_ERROR",
818
+ message: error.message
819
+ }
820
+ };
821
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
822
+ if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
823
+ if (message.includes("slug")) return {
824
+ success: false,
825
+ error: {
826
+ code: "SLUG_CONFLICT",
827
+ message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`
828
+ }
829
+ };
830
+ return {
831
+ success: false,
832
+ error: {
833
+ code: "CONFLICT",
834
+ message: "Unique constraint violation"
835
+ }
836
+ };
837
+ }
838
+ console.error("Content update error:", error);
839
+ return {
840
+ success: false,
841
+ error: {
842
+ code: "CONTENT_UPDATE_ERROR",
843
+ message: "Failed to update content"
844
+ }
845
+ };
846
+ }
847
+ }
848
+ /**
849
+ * Duplicate content item.
850
+ *
851
+ * Only copies SEO data if the collection has SEO enabled.
852
+ * Always returns consistent `seo` shape for SEO-enabled collections.
853
+ */
854
+ async function handleContentDuplicate(db, collection, id, authorId) {
855
+ try {
856
+ const hasSeo = await collectionHasSeo(db, collection);
857
+ return {
858
+ success: true,
859
+ data: { item: await withTransaction(db, async (trx) => {
860
+ const repo = new ContentRepository(trx);
861
+ const bylineRepo = new BylineRepository(trx);
862
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
863
+ const dup = await repo.duplicate(collection, resolvedId, authorId);
864
+ const existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);
865
+ if (existingBylines.length > 0) await bylineRepo.setContentBylines(collection, dup.id, existingBylines.map((entry) => ({
866
+ bylineId: entry.byline.id,
867
+ roleLabel: entry.roleLabel
868
+ })));
869
+ if (hasSeo) {
870
+ const seoRepo = new SeoRepository(trx);
871
+ await seoRepo.copyForDuplicate(collection, resolvedId, dup.id);
872
+ dup.seo = await seoRepo.get(collection, dup.id);
873
+ }
874
+ await hydrateBylines(trx, collection, dup);
875
+ return dup;
876
+ }) }
877
+ };
878
+ } catch (err) {
879
+ if (err instanceof EmDashValidationError) return {
880
+ success: false,
881
+ error: {
882
+ code: "NOT_FOUND",
883
+ message: err.message
884
+ }
885
+ };
886
+ console.error("Content duplicate error:", err);
887
+ return {
888
+ success: false,
889
+ error: {
890
+ code: "CONTENT_DUPLICATE_ERROR",
891
+ message: "Failed to duplicate content"
892
+ }
893
+ };
894
+ }
895
+ }
896
+ /**
897
+ * Delete content item (soft delete - moves to trash)
898
+ */
899
+ async function handleContentDelete(db, collection, id) {
900
+ try {
901
+ const result = await withTransaction(db, async (trx) => {
902
+ const repo = new ContentRepository(trx);
903
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
904
+ return {
905
+ id: resolvedId,
906
+ deleted: await repo.delete(collection, resolvedId)
907
+ };
908
+ });
909
+ if (!result.deleted) return {
910
+ success: false,
911
+ error: {
912
+ code: "NOT_FOUND",
913
+ message: `Content item not found: ${id}`
914
+ }
915
+ };
916
+ return {
917
+ success: true,
918
+ data: {
919
+ deleted: true,
920
+ id: result.id
921
+ }
922
+ };
923
+ } catch (error) {
924
+ console.error("Content delete error:", error);
925
+ return {
926
+ success: false,
927
+ error: {
928
+ code: "CONTENT_DELETE_ERROR",
929
+ message: "Failed to delete content"
930
+ }
931
+ };
932
+ }
933
+ }
934
+ /**
935
+ * Restore content item from trash
936
+ */
937
+ async function handleContentRestore(db, collection, id) {
938
+ try {
939
+ const item = await withTransaction(db, async (trx) => {
940
+ const repo = new ContentRepository(trx);
941
+ const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
942
+ return repo.restore(collection, resolvedId);
943
+ });
944
+ if (!item) return {
945
+ success: false,
946
+ error: {
947
+ code: "NOT_FOUND",
948
+ message: `Trashed content item not found: ${id}`
949
+ }
950
+ };
951
+ return {
952
+ success: true,
953
+ data: {
954
+ restored: true,
955
+ item
956
+ }
957
+ };
958
+ } catch (error) {
959
+ console.error("Content restore error:", error);
960
+ return {
961
+ success: false,
962
+ error: {
963
+ code: "CONTENT_RESTORE_ERROR",
964
+ message: "Failed to restore content"
965
+ }
966
+ };
967
+ }
968
+ }
969
+ /**
970
+ * Permanently delete content item (cannot be undone).
971
+ * Also cleans up associated SEO data.
972
+ */
973
+ async function handleContentPermanentDelete(db, collection, id) {
974
+ try {
975
+ const resolvedId = await resolveIdIncludingTrashed(new ContentRepository(db), collection, id) ?? id;
976
+ if (!await withTransaction(db, async (trx) => {
977
+ const wasDeleted = await new ContentRepository(trx).permanentDelete(collection, resolvedId);
978
+ if (wasDeleted) {
979
+ await new SeoRepository(trx).delete(collection, resolvedId);
980
+ await new CommentRepository(trx).deleteByContent(collection, resolvedId);
981
+ await new RevisionRepository(trx).deleteByEntry(collection, resolvedId);
982
+ }
983
+ return wasDeleted;
984
+ })) return {
985
+ success: false,
986
+ error: {
987
+ code: "NOT_FOUND",
988
+ message: `Content item not found: ${id}`
989
+ }
990
+ };
991
+ return {
992
+ success: true,
993
+ data: {
994
+ deleted: true,
995
+ id: resolvedId
996
+ }
997
+ };
998
+ } catch (error) {
999
+ console.error("Content permanent delete error:", error);
1000
+ return {
1001
+ success: false,
1002
+ error: {
1003
+ code: "CONTENT_DELETE_ERROR",
1004
+ message: "Failed to permanently delete content"
1005
+ }
1006
+ };
1007
+ }
1008
+ }
1009
+ /**
1010
+ * List trashed content items
1011
+ */
1012
+ async function handleContentListTrashed(db, collection, options = {}) {
1013
+ try {
1014
+ const result = await new ContentRepository(db).findTrashed(collection, {
1015
+ limit: options.limit,
1016
+ cursor: options.cursor
1017
+ });
1018
+ return {
1019
+ success: true,
1020
+ data: {
1021
+ items: result.items.map((item) => ({
1022
+ id: item.id,
1023
+ type: item.type,
1024
+ slug: item.slug,
1025
+ status: item.status,
1026
+ data: item.data,
1027
+ authorId: item.authorId,
1028
+ createdAt: item.createdAt,
1029
+ updatedAt: item.updatedAt,
1030
+ publishedAt: item.publishedAt,
1031
+ deletedAt: item.deletedAt
1032
+ })),
1033
+ nextCursor: result.nextCursor
1034
+ }
1035
+ };
1036
+ } catch (error) {
1037
+ if (error instanceof InvalidCursorError) return {
1038
+ success: false,
1039
+ error: {
1040
+ code: "INVALID_CURSOR",
1041
+ message: error.message
1042
+ }
1043
+ };
1044
+ console.error("Content list trashed error:", error);
1045
+ return {
1046
+ success: false,
1047
+ error: {
1048
+ code: "CONTENT_LIST_ERROR",
1049
+ message: "Failed to list trashed content"
1050
+ }
1051
+ };
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Count trashed content items
1056
+ */
1057
+ async function handleContentCountTrashed(db, collection) {
1058
+ try {
1059
+ return {
1060
+ success: true,
1061
+ data: { count: await new ContentRepository(db).countTrashed(collection) }
1062
+ };
1063
+ } catch (error) {
1064
+ console.error("Content count trashed error:", error);
1065
+ return {
1066
+ success: false,
1067
+ error: {
1068
+ code: "CONTENT_COUNT_ERROR",
1069
+ message: "Failed to count trashed content"
1070
+ }
1071
+ };
1072
+ }
1073
+ }
1074
+ /**
1075
+ * Schedule content for future publishing
1076
+ */
1077
+ async function handleContentSchedule(db, collection, id, scheduledAt) {
1078
+ try {
1079
+ const item = await withTransaction(db, async (trx) => {
1080
+ const repo = new ContentRepository(trx);
1081
+ const existing = await repo.findByIdOrSlug(collection, id);
1082
+ const resolvedId = existing?.id ?? id;
1083
+ if (existing) requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, existing.slug);
1084
+ return repo.schedule(collection, resolvedId, scheduledAt);
1085
+ });
1086
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1087
+ return {
1088
+ success: true,
1089
+ data: { item }
1090
+ };
1091
+ } catch (error) {
1092
+ if (error instanceof EmDashValidationError) return {
1093
+ success: false,
1094
+ error: {
1095
+ code: "VALIDATION_ERROR",
1096
+ message: error.message
1097
+ }
1098
+ };
1099
+ console.error("Content schedule error:", error);
1100
+ return {
1101
+ success: false,
1102
+ error: {
1103
+ code: "CONTENT_SCHEDULE_ERROR",
1104
+ message: "Failed to schedule content"
1105
+ }
1106
+ };
1107
+ }
1108
+ }
1109
+ /**
1110
+ * Unschedule content (revert to draft)
1111
+ */
1112
+ async function handleContentUnschedule(db, collection, id) {
1113
+ try {
1114
+ const item = await withTransaction(db, async (trx) => {
1115
+ const repo = new ContentRepository(trx);
1116
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
1117
+ return repo.unschedule(collection, resolvedId);
1118
+ });
1119
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1120
+ return {
1121
+ success: true,
1122
+ data: { item }
1123
+ };
1124
+ } catch (error) {
1125
+ if (error instanceof EmDashValidationError) return {
1126
+ success: false,
1127
+ error: {
1128
+ code: "VALIDATION_ERROR",
1129
+ message: error.message
1130
+ }
1131
+ };
1132
+ console.error("Content unschedule error:", error);
1133
+ return {
1134
+ success: false,
1135
+ error: {
1136
+ code: "CONTENT_UNSCHEDULE_ERROR",
1137
+ message: "Failed to unschedule content"
1138
+ }
1139
+ };
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Publish content immediately.
1144
+ *
1145
+ * Publication is one atomic content-row statement. On databases that support
1146
+ * transactions, the existing slug-redirect side write remains grouped with it.
1147
+ */
1148
+ async function handleContentPublish(db, collection, id, options = {}) {
1149
+ try {
1150
+ const item = await withTransaction(db, async (trx) => {
1151
+ const repo = new ContentRepository(trx);
1152
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
1153
+ const publishConfig = await getCollectionPublishConfig(trx, collection);
1154
+ const existing = await repo.findById(collection, resolvedId);
1155
+ const published = await repo.publish(collection, resolvedId, options.publishedAt, options.requireScheduledDue, options.expectedScheduledAt, publishConfig.supportsRevisions, publishConfig.routable);
1156
+ if (existing?.status === "published" && existing.slug && published.slug && existing.slug !== published.slug) await createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId);
1157
+ return published;
1158
+ });
1159
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1160
+ return {
1161
+ success: true,
1162
+ data: { item }
1163
+ };
1164
+ } catch (error) {
1165
+ if (error instanceof ContentMutationConflictError) return {
1166
+ success: false,
1167
+ error: {
1168
+ code: "CONFLICT",
1169
+ message: error.message
1170
+ }
1171
+ };
1172
+ if (error instanceof ScheduledNotDueError) return {
1173
+ success: false,
1174
+ error: {
1175
+ code: "NOT_DUE",
1176
+ message: error.message
1177
+ }
1178
+ };
1179
+ if (error instanceof EmDashValidationError) {
1180
+ const details = error.details;
1181
+ return {
1182
+ success: false,
1183
+ error: {
1184
+ code: typeof details === "object" && details !== null && "code" in details && details.code === "SLUG_CONFLICT" ? "SLUG_CONFLICT" : "VALIDATION_ERROR",
1185
+ message: error.message
1186
+ }
1187
+ };
1188
+ }
1189
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
1190
+ if ((message.includes("unique constraint failed") || message.includes("duplicate key")) && message.includes("slug")) return {
1191
+ success: false,
1192
+ error: {
1193
+ code: "SLUG_CONFLICT",
1194
+ message: `The staged slug is already used by another entry in collection '${collection}'`
1195
+ }
1196
+ };
1197
+ console.error("Content publish error:", error);
1198
+ return {
1199
+ success: false,
1200
+ error: {
1201
+ code: "CONTENT_PUBLISH_ERROR",
1202
+ message: "Failed to publish content"
1203
+ }
1204
+ };
1205
+ }
1206
+ }
1207
+ /**
1208
+ * Unpublish content (revert to draft).
1209
+ *
1210
+ * Wrapped in a transaction — unpublish may create a draft revision
1211
+ * from the live version then update the status, which is multi-step.
1212
+ */
1213
+ async function handleContentUnpublish(db, collection, id) {
1214
+ try {
1215
+ const item = await withTransaction(db, async (trx) => {
1216
+ const repo = new ContentRepository(trx);
1217
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
1218
+ return repo.unpublish(collection, resolvedId);
1219
+ });
1220
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1221
+ return {
1222
+ success: true,
1223
+ data: { item }
1224
+ };
1225
+ } catch (error) {
1226
+ if (error instanceof EmDashValidationError) return {
1227
+ success: false,
1228
+ error: {
1229
+ code: "VALIDATION_ERROR",
1230
+ message: error.message
1231
+ }
1232
+ };
1233
+ console.error("Content unpublish error:", error);
1234
+ return {
1235
+ success: false,
1236
+ error: {
1237
+ code: "CONTENT_UNPUBLISH_ERROR",
1238
+ message: "Failed to unpublish content"
1239
+ }
1240
+ };
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Count scheduled content items
1245
+ */
1246
+ async function handleContentCountScheduled(db, collection) {
1247
+ try {
1248
+ return {
1249
+ success: true,
1250
+ data: { count: await new ContentRepository(db).countScheduled(collection) }
1251
+ };
1252
+ } catch (error) {
1253
+ console.error("Content count scheduled error:", error);
1254
+ return {
1255
+ success: false,
1256
+ error: {
1257
+ code: "CONTENT_COUNT_ERROR",
1258
+ message: "Failed to count scheduled content"
1259
+ }
1260
+ };
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Discard draft changes (revert to live version)
1265
+ */
1266
+ async function handleContentDiscardDraft(db, collection, id) {
1267
+ try {
1268
+ const item = await withTransaction(db, async (trx) => {
1269
+ const repo = new ContentRepository(trx);
1270
+ const resolvedId = await resolveId(repo, collection, id) ?? id;
1271
+ return repo.discardDraft(collection, resolvedId);
1272
+ });
1273
+ await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1274
+ return {
1275
+ success: true,
1276
+ data: { item }
1277
+ };
1278
+ } catch (error) {
1279
+ if (error instanceof EmDashValidationError) return {
1280
+ success: false,
1281
+ error: {
1282
+ code: "NOT_FOUND",
1283
+ message: error.message
1284
+ }
1285
+ };
1286
+ console.error("Content discard draft error:", error);
1287
+ return {
1288
+ success: false,
1289
+ error: {
1290
+ code: "CONTENT_DISCARD_DRAFT_ERROR",
1291
+ message: "Failed to discard draft"
1292
+ }
1293
+ };
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Compare live and draft revisions
1298
+ */
1299
+ async function handleContentCompare(db, collection, id) {
1300
+ try {
1301
+ const entry = await new ContentRepository(db).findByIdOrSlug(collection, id);
1302
+ if (!entry) return {
1303
+ success: false,
1304
+ error: {
1305
+ code: "NOT_FOUND",
1306
+ message: `Content item not found: ${id}`
1307
+ }
1308
+ };
1309
+ const revisionRepo = new RevisionRepository(db);
1310
+ const live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;
1311
+ const draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;
1312
+ return {
1313
+ success: true,
1314
+ data: {
1315
+ hasChanges: entry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,
1316
+ live: live?.data ?? null,
1317
+ draft: draft?.data ?? null
1318
+ }
1319
+ };
1320
+ } catch (error) {
1321
+ console.error("Content compare error:", error);
1322
+ return {
1323
+ success: false,
1324
+ error: {
1325
+ code: "CONTENT_COMPARE_ERROR",
1326
+ message: "Failed to compare revisions"
1327
+ }
1328
+ };
1329
+ }
1330
+ }
1331
+ /**
1332
+ * Get all translations for a content item.
1333
+ * Returns the item's translation group members with locale and status info.
1334
+ */
1335
+ async function handleContentTranslations(db, collection, id) {
1336
+ try {
1337
+ const repo = new ContentRepository(db);
1338
+ const item = await repo.findByIdOrSlug(collection, id);
1339
+ if (!item) return {
1340
+ success: false,
1341
+ error: {
1342
+ code: "NOT_FOUND",
1343
+ message: `Content item not found: ${id}`
1344
+ }
1345
+ };
1346
+ if (!item.translationGroup) return {
1347
+ success: true,
1348
+ data: {
1349
+ translationGroup: item.id,
1350
+ translations: [{
1351
+ id: item.id,
1352
+ locale: item.locale,
1353
+ slug: item.slug,
1354
+ status: item.status,
1355
+ updatedAt: item.updatedAt
1356
+ }]
1357
+ }
1358
+ };
1359
+ const translations = await repo.findTranslations(collection, item.translationGroup);
1360
+ return {
1361
+ success: true,
1362
+ data: {
1363
+ translationGroup: item.translationGroup,
1364
+ translations: translations.map((t) => ({
1365
+ id: t.id,
1366
+ locale: t.locale,
1367
+ slug: t.slug,
1368
+ status: t.status,
1369
+ updatedAt: t.updatedAt
1370
+ }))
1371
+ }
1372
+ };
1373
+ } catch (error) {
1374
+ if (error instanceof Error) console.error("Content translations error:", error);
1375
+ return {
1376
+ success: false,
1377
+ error: {
1378
+ code: "CONTENT_TRANSLATIONS_ERROR",
1379
+ message: "Failed to get translations"
1380
+ }
1381
+ };
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Sync non-translatable fields to sibling locales.
1386
+ *
1387
+ * When a content item is updated and it belongs to a translation group,
1388
+ * any non-translatable fields in the update data are written to all other
1389
+ * rows in the same translation group within the same transaction.
1390
+ *
1391
+ * Non-translatable fields are **copied, not linked** — each row owns its
1392
+ * own data. This keeps queries simple and avoids cross-row joins.
1393
+ */
1394
+ async function syncNonTranslatableFields(trx, collectionSlug, updatedItemId, translationGroup, data) {
1395
+ const collection = await trx.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug).executeTakeFirst();
1396
+ if (!collection) return;
1397
+ const nonTranslatableSlugs = (await trx.selectFrom("_emdash_fields").select("slug").where("collection_id", "=", collection.id).where("translatable", "=", 0).execute()).map((f) => f.slug);
1398
+ if (nonTranslatableSlugs.length === 0) return;
1399
+ const syncData = {};
1400
+ for (const slug of nonTranslatableSlugs) if (slug in data) syncData[slug] = data[slug];
1401
+ if (Object.keys(syncData).length === 0) return;
1402
+ validateIdentifier(collectionSlug, "collection slug");
1403
+ const tableName = `ec_${collectionSlug}`;
1404
+ const setClauses = Object.entries(syncData).map(([key, value]) => {
1405
+ validateIdentifier(key, "field slug");
1406
+ const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
1407
+ return sql`${sql.ref(key)} = ${serialized}`;
1408
+ });
1409
+ await sql`
1410
+ UPDATE ${sql.ref(tableName)}
1411
+ SET ${sql.join(setClauses, sql`, `)}
1412
+ WHERE translation_group = ${translationGroup}
1413
+ AND id != ${updatedItemId}
1414
+ `.execute(trx);
1415
+ }
1416
+ /**
1417
+ * Resolve a `{ taxonomyName: [slug, ...] }` map to term IDs and replace the
1418
+ * entry's assignments for each named taxonomy.
1419
+ *
1420
+ * Shared by handleContentCreate and handleContentUpdate so both MCP entry
1421
+ * points behave identically. Slug resolution is scoped to `locale`; passing
1422
+ * `undefined` lets `findBySlug` fall back to its default (lowest locale code)
1423
+ * so callers on single-locale sites don't need to know the site's default.
1424
+ *
1425
+ * Throws EmDashValidationError on unknown slug or wrong shape; the calling
1426
+ * handler translates that into a VALIDATION_ERROR response.
1427
+ */
1428
+ async function assignTaxonomies(trx, collection, entryId, locale, taxonomies) {
1429
+ const taxRepo = new TaxonomyRepository(trx);
1430
+ let anyChange = false;
1431
+ for (const [taxonomyName, slugs] of Object.entries(taxonomies)) {
1432
+ if (!Array.isArray(slugs)) throw new EmDashValidationError(`taxonomies.${taxonomyName} must be an array of term slugs`);
1433
+ const termIds = [];
1434
+ for (const slug of slugs) {
1435
+ if (typeof slug !== "string" || slug.length === 0) throw new EmDashValidationError(`taxonomies.${taxonomyName} contains a non-string or empty slug`);
1436
+ const term = await taxRepo.findBySlug(taxonomyName, slug, locale);
1437
+ if (!term) throw new EmDashValidationError(`Unknown taxonomy term: ${taxonomyName}='${slug}'${locale ? ` (locale '${locale}')` : ""}`);
1438
+ termIds.push(term.id);
1439
+ }
1440
+ await taxRepo.setTermsForEntry(collection, entryId, taxonomyName, termIds);
1441
+ anyChange = true;
1442
+ }
1443
+ if (anyChange) invalidateTermCache();
1444
+ }
1445
+
1446
+ //#endregion
1447
+ //#region src/api/handlers/manifest.ts
1448
+ /**
1449
+ * Manifest generation handlers
1450
+ */
1451
+ /** Pattern to add spaces before capital letters */
1452
+ const CAMEL_CASE_PATTERN = /([A-Z])/g;
1453
+ const FIRST_CHAR_PATTERN = /^./;
1454
+ /**
1455
+ * Generate admin manifest from collections
1456
+ */
1457
+ async function generateManifest(collections, plugins = {}) {
1458
+ const manifestCollections = {};
1459
+ for (const [name, definition] of Object.entries(collections)) {
1460
+ const fields = extractFieldDescriptors(definition.schema);
1461
+ manifestCollections[name] = {
1462
+ label: definition.admin.label,
1463
+ labelSingular: definition.admin.labelSingular || definition.admin.label,
1464
+ supports: definition.admin.supports || [],
1465
+ routable: definition.admin.routable ?? true,
1466
+ fields
1467
+ };
1468
+ }
1469
+ return {
1470
+ version: "0.1.0",
1471
+ hash: await hashString(JSON.stringify(manifestCollections)),
1472
+ collections: manifestCollections,
1473
+ plugins
1474
+ };
1475
+ }
1476
+ /**
1477
+ * Extract field descriptors from Zod schema
1478
+ * Note: This is a simplified implementation that handles common types
1479
+ */
1480
+ function extractFieldDescriptors(schema) {
1481
+ const fields = {};
1482
+ const shape = typeof schema._def?.shape === "function" ? schema._def.shape() : schema.shape || {};
1483
+ for (const [name, fieldSchema] of Object.entries(shape)) fields[name] = extractFieldType(name, fieldSchema);
1484
+ return fields;
1485
+ }
1486
+ /**
1487
+ * Extract field type from Zod schema
1488
+ */
1489
+ /** Type guard: check if a value is a non-null object */
1490
+ function isObject(value) {
1491
+ return typeof value === "object" && value !== null;
1492
+ }
1493
+ function extractFieldType(name, schema) {
1494
+ if (!isObject(schema)) return {
1495
+ kind: "string",
1496
+ label: formatLabel(name)
1497
+ };
1498
+ if (schema.isPortableText) return {
1499
+ kind: "portableText",
1500
+ label: formatLabel(name)
1501
+ };
1502
+ if (schema.isImage) return {
1503
+ kind: "image",
1504
+ label: formatLabel(name)
1505
+ };
1506
+ if (schema.isReference) return {
1507
+ kind: "reference",
1508
+ label: formatLabel(name)
1509
+ };
1510
+ const def = isObject(schema._def) ? schema._def : void 0;
1511
+ switch (typeof def?.typeName === "string" ? def.typeName : void 0) {
1512
+ case "ZodString": return {
1513
+ kind: "string",
1514
+ label: formatLabel(name)
1515
+ };
1516
+ case "ZodNumber": return {
1517
+ kind: "number",
1518
+ label: formatLabel(name)
1519
+ };
1520
+ case "ZodBoolean": return {
1521
+ kind: "boolean",
1522
+ label: formatLabel(name)
1523
+ };
1524
+ case "ZodDate": return {
1525
+ kind: "datetime",
1526
+ label: formatLabel(name)
1527
+ };
1528
+ case "ZodEnum": {
1529
+ const values = Array.isArray(def?.values) ? def.values : [];
1530
+ return {
1531
+ kind: "select",
1532
+ label: formatLabel(name),
1533
+ options: values.filter((v) => typeof v === "string").map((v) => ({
1534
+ value: v,
1535
+ label: v.charAt(0).toUpperCase() + v.slice(1)
1536
+ }))
1537
+ };
1538
+ }
1539
+ case "ZodArray": return {
1540
+ kind: "array",
1541
+ label: formatLabel(name)
1542
+ };
1543
+ case "ZodObject": return {
1544
+ kind: "object",
1545
+ label: formatLabel(name)
1546
+ };
1547
+ case "ZodOptional":
1548
+ case "ZodDefault":
1549
+ if (def?.innerType) return extractFieldType(name, def.innerType);
1550
+ return {
1551
+ kind: "string",
1552
+ label: formatLabel(name)
1553
+ };
1554
+ default: return {
1555
+ kind: "string",
1556
+ label: formatLabel(name)
1557
+ };
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Format field name as label
1562
+ */
1563
+ function formatLabel(name) {
1564
+ return name.replace(CAMEL_CASE_PATTERN, " $1").replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase()).trim();
1565
+ }
1566
+
1567
+ //#endregion
1568
+ //#region src/api/handlers/revision.ts
1569
+ /**
1570
+ * List revisions for a content entry
1571
+ */
1572
+ async function handleRevisionList(db, collection, entryId, params = {}) {
1573
+ try {
1574
+ const repo = new RevisionRepository(db);
1575
+ const [items, total] = await Promise.all([repo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }), repo.countByEntry(collection, entryId)]);
1576
+ return {
1577
+ success: true,
1578
+ data: {
1579
+ items,
1580
+ total
1581
+ }
1582
+ };
1583
+ } catch {
1584
+ return {
1585
+ success: false,
1586
+ error: {
1587
+ code: "REVISION_LIST_ERROR",
1588
+ message: "Failed to list revisions"
1589
+ }
1590
+ };
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Get a specific revision
1595
+ */
1596
+ async function handleRevisionGet(db, revisionId) {
1597
+ try {
1598
+ const item = await new RevisionRepository(db).findById(revisionId);
1599
+ if (!item) return {
1600
+ success: false,
1601
+ error: {
1602
+ code: "NOT_FOUND",
1603
+ message: `Revision not found: ${revisionId}`
1604
+ }
1605
+ };
1606
+ return {
1607
+ success: true,
1608
+ data: { item }
1609
+ };
1610
+ } catch {
1611
+ return {
1612
+ success: false,
1613
+ error: {
1614
+ code: "REVISION_GET_ERROR",
1615
+ message: "Failed to get revision"
1616
+ }
1617
+ };
1618
+ }
1619
+ }
1620
+ /**
1621
+ * Restore a revision (updates content to this revision's data and creates new revision)
1622
+ */
1623
+ async function handleRevisionRestore(db, revisionId, callerUserId) {
1624
+ try {
1625
+ const revision = await new RevisionRepository(db).findById(revisionId);
1626
+ if (!revision) return {
1627
+ success: false,
1628
+ error: {
1629
+ code: "NOT_FOUND",
1630
+ message: `Revision not found: ${revisionId}`
1631
+ }
1632
+ };
1633
+ const { _slug, ...fieldData } = revision.data;
1634
+ const { item, queuedRevisionId } = await withTransaction(db, async (trx) => {
1635
+ const trxContentRepo = new ContentRepository(trx);
1636
+ const trxRevisionRepo = new RevisionRepository(trx);
1637
+ return {
1638
+ item: await trxContentRepo.update(revision.collection, revision.entryId, {
1639
+ data: fieldData,
1640
+ slug: typeof _slug === "string" ? _slug : void 0
1641
+ }),
1642
+ queuedRevisionId: (await trxRevisionRepo.create({
1643
+ collection: revision.collection,
1644
+ entryId: revision.entryId,
1645
+ data: revision.data,
1646
+ authorId: callerUserId
1647
+ })).id
1648
+ };
1649
+ });
1650
+ const pruneRepo = new RevisionRepository(db);
1651
+ after(async () => {
1652
+ try {
1653
+ await pruneRepo.pruneQueuedEntry(revision.collection, revision.entryId, queuedRevisionId, 50);
1654
+ } catch (error) {
1655
+ console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
1656
+ }
1657
+ });
1658
+ return {
1659
+ success: true,
1660
+ data: { item }
1661
+ };
1662
+ } catch {
1663
+ return {
1664
+ success: false,
1665
+ error: {
1666
+ code: "REVISION_RESTORE_ERROR",
1667
+ message: "Failed to restore revision"
1668
+ }
1669
+ };
1670
+ }
1671
+ }
1672
+
1673
+ //#endregion
1674
+ //#region src/api/handlers/media.ts
1675
+ const FOREIGN_KEY_VIOLATION_RE = /foreign key constraint failed/i;
1676
+ /**
1677
+ * List media items
1678
+ */
1679
+ async function handleMediaList(db, params) {
1680
+ try {
1681
+ if (params.page !== void 0) {
1682
+ const limit = Math.min(params.limit || 50, 100);
1683
+ const offset = (params.page - 1) * limit;
1684
+ if (params.cursor !== void 0 || !Number.isSafeInteger(params.page) || params.page < 1 || !Number.isSafeInteger(offset)) return {
1685
+ success: false,
1686
+ error: {
1687
+ code: "VALIDATION_ERROR",
1688
+ message: "Invalid media page"
1689
+ }
1690
+ };
1691
+ return {
1692
+ success: true,
1693
+ data: await new MediaRepository(db).findPage({
1694
+ page: params.page,
1695
+ limit,
1696
+ mimeType: params.mimeType,
1697
+ q: params.q,
1698
+ folderId: params.folderId
1699
+ })
1700
+ };
1701
+ }
1702
+ const result = await new MediaRepository(db).findMany({
1703
+ cursor: params.cursor,
1704
+ limit: Math.min(params.limit || 50, 100),
1705
+ mimeType: params.mimeType,
1706
+ q: params.q,
1707
+ folderId: params.folderId
1708
+ });
1709
+ return {
1710
+ success: true,
1711
+ data: {
1712
+ items: result.items,
1713
+ nextCursor: result.nextCursor
1714
+ }
1715
+ };
1716
+ } catch (error) {
1717
+ if (error instanceof InvalidCursorError) return {
1718
+ success: false,
1719
+ error: {
1720
+ code: "INVALID_CURSOR",
1721
+ message: error.message
1722
+ }
1723
+ };
1724
+ return {
1725
+ success: false,
1726
+ error: {
1727
+ code: "MEDIA_LIST_ERROR",
1728
+ message: "Failed to list media"
1729
+ }
1730
+ };
1731
+ }
1732
+ }
1733
+ /**
1734
+ * Get single media item
1735
+ */
1736
+ async function handleMediaGet(db, id) {
1737
+ try {
1738
+ const item = await new MediaRepository(db).findById(id);
1739
+ if (!item) return {
1740
+ success: false,
1741
+ error: {
1742
+ code: "NOT_FOUND",
1743
+ message: `Media item not found: ${id}`
1744
+ }
1745
+ };
1746
+ return {
1747
+ success: true,
1748
+ data: { item }
1749
+ };
1750
+ } catch {
1751
+ return {
1752
+ success: false,
1753
+ error: {
1754
+ code: "MEDIA_GET_ERROR",
1755
+ message: "Failed to get media"
1756
+ }
1757
+ };
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Create media item (after file upload)
1762
+ */
1763
+ async function handleMediaCreate(db, input) {
1764
+ try {
1765
+ return {
1766
+ success: true,
1767
+ data: { item: await new MediaRepository(db).create(input) }
1768
+ };
1769
+ } catch {
1770
+ return {
1771
+ success: false,
1772
+ error: {
1773
+ code: "MEDIA_CREATE_ERROR",
1774
+ message: "Failed to create media"
1775
+ }
1776
+ };
1777
+ }
1778
+ }
1779
+ /**
1780
+ * Update media metadata
1781
+ */
1782
+ async function handleMediaUpdate(db, id, input) {
1783
+ if (!isValidFocalPointUpdate(input)) return {
1784
+ success: false,
1785
+ error: {
1786
+ code: "VALIDATION_ERROR",
1787
+ message: "focalX and focalY must both be valid numbers or both be null"
1788
+ }
1789
+ };
1790
+ try {
1791
+ const item = await new MediaRepository(db).update(id, input);
1792
+ if (!item) return {
1793
+ success: false,
1794
+ error: {
1795
+ code: "NOT_FOUND",
1796
+ message: `Media item not found: ${id}`
1797
+ }
1798
+ };
1799
+ return {
1800
+ success: true,
1801
+ data: { item }
1802
+ };
1803
+ } catch (error) {
1804
+ if (isForeignKeyViolation(error)) return {
1805
+ success: false,
1806
+ error: {
1807
+ code: "NOT_FOUND",
1808
+ message: "Media folder not found"
1809
+ }
1810
+ };
1811
+ return {
1812
+ success: false,
1813
+ error: {
1814
+ code: "MEDIA_UPDATE_ERROR",
1815
+ message: "Failed to update media"
1816
+ }
1817
+ };
1818
+ }
1819
+ }
1820
+ function isForeignKeyViolation(error) {
1821
+ if (error && typeof error === "object") {
1822
+ if ("code" in error && error.code === "23503") return true;
1823
+ }
1824
+ const message = error instanceof Error ? error.message : "";
1825
+ if (FOREIGN_KEY_VIOLATION_RE.test(message)) return true;
1826
+ return Boolean(error && typeof error === "object" && "cause" in error && isForeignKeyViolation(error.cause));
1827
+ }
1828
+ /**
1829
+ * Delete media item
1830
+ */
1831
+ async function handleMediaDelete(db, id) {
1832
+ try {
1833
+ const storageKey = await new MediaRepository(db).deleteWithStorageKey(id);
1834
+ if (!storageKey) return {
1835
+ success: false,
1836
+ error: {
1837
+ code: "NOT_FOUND",
1838
+ message: `Media item not found: ${id}`
1839
+ }
1840
+ };
1841
+ return {
1842
+ success: true,
1843
+ data: {
1844
+ deleted: true,
1845
+ storageKey
1846
+ }
1847
+ };
1848
+ } catch {
1849
+ return {
1850
+ success: false,
1851
+ error: {
1852
+ code: "MEDIA_DELETE_ERROR",
1853
+ message: "Failed to delete media"
1854
+ }
1855
+ };
1856
+ }
1857
+ }
1858
+
1859
+ //#endregion
1860
+ //#region src/api/handlers/plugins.ts
1861
+ function marketplaceIconUrl(marketplaceUrl, pluginId) {
1862
+ return `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;
1863
+ }
1864
+ /**
1865
+ * Get plugin info from configured plugin and database state
1866
+ */
1867
+ function buildPluginInfo(plugin, state, marketplaceUrl) {
1868
+ const status = state?.status ?? "active";
1869
+ const enabled = status === "active";
1870
+ const isMarketplace = (state?.source ?? "config") === "marketplace";
1871
+ return {
1872
+ id: plugin.id,
1873
+ name: state?.displayName || plugin.id,
1874
+ version: plugin.version,
1875
+ package: void 0,
1876
+ enabled,
1877
+ status,
1878
+ source: state?.source ?? "config",
1879
+ marketplaceVersion: state?.marketplaceVersion ?? void 0,
1880
+ registryPublisherDid: state?.registryPublisherDid ?? void 0,
1881
+ registrySlug: state?.registrySlug ?? void 0,
1882
+ capabilities: plugin.capabilities,
1883
+ hasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,
1884
+ hasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,
1885
+ hasHooks: Object.keys(plugin.hooks ?? {}).length > 0,
1886
+ hasSettings: Object.keys(plugin.admin.settingsSchema ?? {}).length > 0,
1887
+ installedAt: state?.installedAt?.toISOString(),
1888
+ activatedAt: state?.activatedAt?.toISOString() ?? void 0,
1889
+ deactivatedAt: state?.deactivatedAt?.toISOString() ?? void 0,
1890
+ description: state?.description ?? void 0,
1891
+ iconUrl: isMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : void 0,
1892
+ mcpToolsEnabled: state?.mcpToolsEnabled ?? false,
1893
+ mcpTools: Object.entries(plugin.mcp?.tools ?? {}).flatMap(([name, tool]) => {
1894
+ const permission = plugin.routes[tool.route]?.permission;
1895
+ return permission ? [{
1896
+ name,
1897
+ description: tool.description,
1898
+ route: tool.route,
1899
+ permission,
1900
+ destructive: tool.destructive ?? false
1901
+ }] : [];
1902
+ })
1903
+ };
1904
+ }
1905
+ /**
1906
+ * Build plugin info for a statically-sandboxed plugin entry
1907
+ */
1908
+ function buildSandboxedPluginInfo(entry, state) {
1909
+ const status = state?.status ?? "active";
1910
+ const enabled = status === "active";
1911
+ return {
1912
+ id: entry.id,
1913
+ name: state?.displayName || entry.id,
1914
+ version: entry.version,
1915
+ package: void 0,
1916
+ enabled,
1917
+ status,
1918
+ source: "config",
1919
+ sandboxed: true,
1920
+ capabilities: entry.capabilities,
1921
+ hasAdminPages: (entry.adminPages?.length ?? 0) > 0,
1922
+ hasDashboardWidgets: (entry.adminWidgets?.length ?? 0) > 0,
1923
+ hasHooks: false,
1924
+ hasSettings: Object.keys(entry.settingsSchema ?? {}).length > 0,
1925
+ installedAt: state?.installedAt?.toISOString(),
1926
+ activatedAt: state?.activatedAt?.toISOString() ?? void 0,
1927
+ deactivatedAt: state?.deactivatedAt?.toISOString() ?? void 0,
1928
+ description: state?.description ?? void 0,
1929
+ mcpToolsEnabled: state?.mcpToolsEnabled ?? false,
1930
+ mcpTools: entry.mcp?.tools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool) ?? []
1931
+ };
1932
+ }
1933
+ /**
1934
+ * List all configured plugins with their state
1935
+ */
1936
+ async function handlePluginList(db, configuredPlugins, sandboxedPluginEntries, marketplaceUrl, runtimeSettingsSchemaLookup) {
1937
+ try {
1938
+ const allStates = await new PluginStateRepository(db).getAll();
1939
+ const stateMap = new Map(allStates.map((s) => [s.pluginId, s]));
1940
+ const configuredIds = new Set(configuredPlugins.map((p) => p.id));
1941
+ const items = configuredPlugins.map((plugin) => {
1942
+ return buildPluginInfo(plugin, stateMap.get(plugin.id) ?? null, marketplaceUrl);
1943
+ });
1944
+ for (const entry of sandboxedPluginEntries) {
1945
+ if (configuredIds.has(entry.id)) continue;
1946
+ configuredIds.add(entry.id);
1947
+ items.push(buildSandboxedPluginInfo(entry, stateMap.get(entry.id) ?? null));
1948
+ }
1949
+ for (const state of allStates) {
1950
+ if (state.source !== "marketplace" && state.source !== "registry") continue;
1951
+ if (configuredIds.has(state.pluginId)) continue;
1952
+ items.push({
1953
+ id: state.pluginId,
1954
+ name: state.displayName || state.pluginId,
1955
+ version: state.marketplaceVersion ?? state.version,
1956
+ enabled: state.status === "active",
1957
+ status: state.status,
1958
+ source: state.source,
1959
+ marketplaceVersion: state.marketplaceVersion ?? void 0,
1960
+ registryPublisherDid: state.registryPublisherDid ?? void 0,
1961
+ registrySlug: state.registrySlug ?? void 0,
1962
+ capabilities: [],
1963
+ hasAdminPages: false,
1964
+ hasDashboardWidgets: false,
1965
+ hasHooks: false,
1966
+ hasSettings: Object.keys(runtimeSettingsSchemaLookup?.(state.pluginId) ?? {}).length > 0,
1967
+ installedAt: state.installedAt?.toISOString(),
1968
+ activatedAt: state.activatedAt?.toISOString() ?? void 0,
1969
+ deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
1970
+ description: state.description ?? void 0,
1971
+ iconUrl: state.source === "marketplace" && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, state.pluginId) : void 0,
1972
+ mcpToolsEnabled: state.mcpToolsEnabled,
1973
+ mcpTools: []
1974
+ });
1975
+ }
1976
+ return {
1977
+ success: true,
1978
+ data: { items }
1979
+ };
1980
+ } catch {
1981
+ return {
1982
+ success: false,
1983
+ error: {
1984
+ code: "PLUGIN_LIST_ERROR",
1985
+ message: "Failed to list plugins"
1986
+ }
1987
+ };
1988
+ }
1989
+ }
1990
+ /**
1991
+ * Get a single plugin's info
1992
+ */
1993
+ async function handlePluginGet(db, configuredPlugins, sandboxedPluginEntries, pluginId, marketplaceUrl) {
1994
+ try {
1995
+ const stateRepo = new PluginStateRepository(db);
1996
+ const plugin = configuredPlugins.find((p) => p.id === pluginId);
1997
+ if (plugin) return {
1998
+ success: true,
1999
+ data: { item: buildPluginInfo(plugin, await stateRepo.get(pluginId), marketplaceUrl) }
2000
+ };
2001
+ const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2002
+ if (sandboxed) return {
2003
+ success: true,
2004
+ data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.get(pluginId)) }
2005
+ };
2006
+ return {
2007
+ success: false,
2008
+ error: {
2009
+ code: "NOT_FOUND",
2010
+ message: `Plugin not found: ${pluginId}`
2011
+ }
2012
+ };
2013
+ } catch {
2014
+ return {
2015
+ success: false,
2016
+ error: {
2017
+ code: "PLUGIN_GET_ERROR",
2018
+ message: "Failed to get plugin"
2019
+ }
2020
+ };
2021
+ }
2022
+ }
2023
+ /**
2024
+ * Build a minimal `PluginInfo` for a plugin that exists only as a
2025
+ * `_plugin_state` row (marketplace or registry install), with no
2026
+ * matching `configuredPlugins` entry. Runtime-installed plugins don't
2027
+ * have ResolvedPlugin metadata until they're loaded into the sandbox,
2028
+ * so the enable/disable response surfaces the state-row view as a
2029
+ * stable shape the admin UI already understands.
2030
+ */
2031
+ function buildStateOnlyPluginInfo(state) {
2032
+ return {
2033
+ id: state.pluginId,
2034
+ name: state.displayName || state.pluginId,
2035
+ version: state.marketplaceVersion ?? state.version,
2036
+ enabled: state.status === "active",
2037
+ status: state.status,
2038
+ source: state.source,
2039
+ marketplaceVersion: state.marketplaceVersion ?? void 0,
2040
+ registryPublisherDid: state.registryPublisherDid ?? void 0,
2041
+ registrySlug: state.registrySlug ?? void 0,
2042
+ capabilities: [],
2043
+ hasAdminPages: false,
2044
+ hasDashboardWidgets: false,
2045
+ hasHooks: false,
2046
+ hasSettings: false,
2047
+ installedAt: state.installedAt?.toISOString(),
2048
+ activatedAt: state.activatedAt?.toISOString() ?? void 0,
2049
+ deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
2050
+ description: state.description ?? void 0,
2051
+ mcpToolsEnabled: state.mcpToolsEnabled,
2052
+ mcpTools: []
2053
+ };
2054
+ }
2055
+ /**
2056
+ * Enable a plugin
2057
+ */
2058
+ async function handlePluginEnable(db, configuredPlugins, sandboxedPluginEntries, pluginId) {
2059
+ try {
2060
+ const stateRepo = new PluginStateRepository(db);
2061
+ const plugin = configuredPlugins.find((p) => p.id === pluginId);
2062
+ if (plugin) return {
2063
+ success: true,
2064
+ data: { item: buildPluginInfo(plugin, await stateRepo.enable(pluginId, plugin.version)) }
2065
+ };
2066
+ const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2067
+ if (sandboxed) return {
2068
+ success: true,
2069
+ data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.enable(pluginId, sandboxed.version)) }
2070
+ };
2071
+ const existing = await stateRepo.get(pluginId);
2072
+ if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
2073
+ success: false,
2074
+ error: {
2075
+ code: "NOT_FOUND",
2076
+ message: `Plugin not found: ${pluginId}`
2077
+ }
2078
+ };
2079
+ return {
2080
+ success: true,
2081
+ data: { item: buildStateOnlyPluginInfo(await stateRepo.enable(pluginId, existing.version)) }
2082
+ };
2083
+ } catch {
2084
+ return {
2085
+ success: false,
2086
+ error: {
2087
+ code: "PLUGIN_ENABLE_ERROR",
2088
+ message: "Failed to enable plugin"
2089
+ }
2090
+ };
2091
+ }
2092
+ }
2093
+ /**
2094
+ * Disable a plugin
2095
+ */
2096
+ async function handlePluginDisable(db, configuredPlugins, sandboxedPluginEntries, pluginId) {
2097
+ try {
2098
+ const stateRepo = new PluginStateRepository(db);
2099
+ const plugin = configuredPlugins.find((p) => p.id === pluginId);
2100
+ if (plugin) return {
2101
+ success: true,
2102
+ data: { item: buildPluginInfo(plugin, await stateRepo.disable(pluginId, plugin.version)) }
2103
+ };
2104
+ const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2105
+ if (sandboxed) return {
2106
+ success: true,
2107
+ data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.disable(pluginId, sandboxed.version)) }
2108
+ };
2109
+ const existing = await stateRepo.get(pluginId);
2110
+ if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
2111
+ success: false,
2112
+ error: {
2113
+ code: "NOT_FOUND",
2114
+ message: `Plugin not found: ${pluginId}`
2115
+ }
2116
+ };
2117
+ return {
2118
+ success: true,
2119
+ data: { item: buildStateOnlyPluginInfo(await stateRepo.disable(pluginId, existing.version)) }
2120
+ };
2121
+ } catch {
2122
+ return {
2123
+ success: false,
2124
+ error: {
2125
+ code: "PLUGIN_DISABLE_ERROR",
2126
+ message: "Failed to disable plugin"
2127
+ }
2128
+ };
2129
+ }
2130
+ }
2131
+
2132
+ //#endregion
2133
+ //#region src/api/handlers/plugin-settings.ts
2134
+ function settingsKey(pluginId, key) {
2135
+ return `plugin:${pluginId}:settings:${key}`;
2136
+ }
2137
+ /**
2138
+ * Resolve a plugin's settings schema from either the configured
2139
+ * (in-process) plugin list or the statically-sandboxed entries.
2140
+ * Returns null when the plugin doesn't exist, undefined-equivalent
2141
+ * empty object when it declares no schema.
2142
+ */
2143
+ function getPluginSettingsSchema(configuredPlugins, sandboxedPluginEntries, pluginId) {
2144
+ const plugin = configuredPlugins.find((p) => p.id === pluginId);
2145
+ if (plugin) return plugin.admin.settingsSchema ?? {};
2146
+ const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2147
+ if (sandboxed) return sandboxed.settingsSchema ?? {};
2148
+ return null;
2149
+ }
2150
+ /**
2151
+ * Validate a single value against its schema field.
2152
+ * Returns an error message, or null when valid.
2153
+ */
2154
+ function validateValue(key, field, value) {
2155
+ switch (field.type) {
2156
+ case "string":
2157
+ case "secret":
2158
+ case "url":
2159
+ case "email":
2160
+ if (typeof value !== "string") return `Setting "${key}" must be a string`;
2161
+ if (field.type === "url" && value !== "" && !URL.canParse(value)) return `Setting "${key}" must be a valid URL`;
2162
+ if (field.type === "email" && value !== "" && !value.includes("@")) return `Setting "${key}" must be a valid email address`;
2163
+ return null;
2164
+ case "number":
2165
+ if (typeof value !== "number" || Number.isNaN(value)) return `Setting "${key}" must be a number`;
2166
+ if (field.min !== void 0 && value < field.min) return `Setting "${key}" must be at least ${field.min}`;
2167
+ if (field.max !== void 0 && value > field.max) return `Setting "${key}" must be at most ${field.max}`;
2168
+ return null;
2169
+ case "boolean": return typeof value === "boolean" ? null : `Setting "${key}" must be a boolean`;
2170
+ case "select":
2171
+ if (typeof value !== "string" || !field.options.some((o) => o.value === value)) return `Setting "${key}" must be one of the defined options`;
2172
+ return null;
2173
+ default: return `Setting "${key}" has an unknown field type`;
2174
+ }
2175
+ }
2176
+ async function buildSettingsResponse(optionsRepo, pluginId, schema) {
2177
+ const keys = Object.keys(schema);
2178
+ const stored = await optionsRepo.getMany(keys.map((key) => settingsKey(pluginId, key)));
2179
+ const values = {};
2180
+ const secretsSet = {};
2181
+ for (const key of keys) {
2182
+ const field = schema[key];
2183
+ if (!field) continue;
2184
+ const storedValue = stored.get(settingsKey(pluginId, key));
2185
+ if (field.type === "secret") {
2186
+ secretsSet[key] = typeof storedValue === "string" && storedValue.length > 0;
2187
+ continue;
2188
+ }
2189
+ if (storedValue !== void 0 && storedValue !== null) values[key] = storedValue;
2190
+ else if ("default" in field && field.default !== void 0) values[key] = field.default;
2191
+ else values[key] = null;
2192
+ }
2193
+ return {
2194
+ schema,
2195
+ values,
2196
+ secretsSet
2197
+ };
2198
+ }
2199
+ /**
2200
+ * Get a plugin's settings (schema + current values, secrets masked)
2201
+ */
2202
+ async function handlePluginSettingsGet(db, pluginId, schema) {
2203
+ try {
2204
+ return {
2205
+ success: true,
2206
+ data: await buildSettingsResponse(new OptionsRepository(db), pluginId, schema)
2207
+ };
2208
+ } catch {
2209
+ return {
2210
+ success: false,
2211
+ error: {
2212
+ code: ErrorCode.PLUGIN_SETTINGS_READ_ERROR,
2213
+ message: "Failed to read plugin settings"
2214
+ }
2215
+ };
2216
+ }
2217
+ }
2218
+ /**
2219
+ * Update a plugin's settings.
2220
+ *
2221
+ * Only keys present in `updates` are written. A `null` value deletes the
2222
+ * stored value (reverting to the schema default). Secret fields are
2223
+ * write-only: they accept a new string value or `null` to clear, and the
2224
+ * response never echoes them back.
2225
+ */
2226
+ async function handlePluginSettingsUpdate(db, pluginId, schema, updates) {
2227
+ try {
2228
+ for (const [key, value] of Object.entries(updates)) {
2229
+ const field = schema[key];
2230
+ if (!field) return {
2231
+ success: false,
2232
+ error: {
2233
+ code: ErrorCode.VALIDATION_ERROR,
2234
+ message: `Unknown setting "${key}" for plugin "${pluginId}"`
2235
+ }
2236
+ };
2237
+ if (value === null) continue;
2238
+ const error = validateValue(key, field, value);
2239
+ if (error) return {
2240
+ success: false,
2241
+ error: {
2242
+ code: ErrorCode.VALIDATION_ERROR,
2243
+ message: error
2244
+ }
2245
+ };
2246
+ }
2247
+ return {
2248
+ success: true,
2249
+ data: await withTransaction(db, async (trx) => {
2250
+ const txRepo = new OptionsRepository(trx);
2251
+ for (const [key, value] of Object.entries(updates)) if (value === null) await txRepo.delete(settingsKey(pluginId, key));
2252
+ else await txRepo.set(settingsKey(pluginId, key), value);
2253
+ return buildSettingsResponse(txRepo, pluginId, schema);
2254
+ })
2255
+ };
2256
+ } catch {
2257
+ return {
2258
+ success: false,
2259
+ error: {
2260
+ code: ErrorCode.PLUGIN_SETTINGS_UPDATE_ERROR,
2261
+ message: "Failed to update plugin settings"
2262
+ }
2263
+ };
2264
+ }
2265
+ }
2266
+
2267
+ //#endregion
2268
+ export { handleContentPublish as A, handleContentDiscardDraft as C, handleContentList as D, handleContentGetIncludingTrashed as E, handleContentUnschedule as F, handleContentUpdate as I, validateRev as L, handleContentSchedule as M, handleContentTranslations as N, handleContentListTrashed as O, handleContentUnpublish as P, handleContentDelete as S, handleContentGet as T, handleContentAuthors as _, handlePluginEnable as a, handleContentCountTrashed as b, handleMediaCreate as c, handleMediaList as d, handleMediaUpdate as f, generateManifest as g, handleRevisionRestore as h, handlePluginDisable as i, handleContentRestore as j, handleContentPermanentDelete as k, handleMediaDelete as l, handleRevisionList as m, handlePluginSettingsGet as n, handlePluginGet as o, handleRevisionGet as p, handlePluginSettingsUpdate as r, handlePluginList as s, getPluginSettingsSchema as t, handleMediaGet as u, handleContentCompare as v, handleContentDuplicate as w, handleContentCreate as x, handleContentCountScheduled as y };
2269
+ //# sourceMappingURL=api-B0w3uBiG.mjs.map