emdash 0.14.0 → 0.15.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 (605) hide show
  1. package/dist/{adapters-9DybjTO6.d.mts → adapters-C4yd_UJR.d.mts} +1 -1
  2. package/dist/{adapters-9DybjTO6.d.mts.map → adapters-C4yd_UJR.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-CDdG-4Gd.mjs → allowed-origins-D0fFk9a6.mjs} +2 -2
  4. package/dist/{allowed-origins-CDdG-4Gd.mjs.map → allowed-origins-D0fFk9a6.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +15 -15
  7. package/dist/api/schemas/index.d.mts +2 -2
  8. package/dist/api/schemas/index.mjs +3 -3
  9. package/dist/{api-BMLZuwM4.mjs → api-CLwG_3dh.mjs} +519 -55
  10. package/dist/api-CLwG_3dh.mjs.map +1 -0
  11. package/dist/{api-tokens-eYymBhIT.mjs → api-tokens-ucpcNXDt.mjs} +2 -2
  12. package/dist/{api-tokens-eYymBhIT.mjs.map → api-tokens-ucpcNXDt.mjs.map} +1 -1
  13. package/dist/{apply-v4DBgjPw.mjs → apply-wJhM_bwU.mjs} +17 -17
  14. package/dist/{apply-v4DBgjPw.mjs.map → apply-wJhM_bwU.mjs.map} +1 -1
  15. package/dist/astro/index.d.mts +10 -10
  16. package/dist/astro/index.mjs +21 -5
  17. package/dist/astro/index.mjs.map +1 -1
  18. package/dist/astro/middleware/auth.d.mts +9 -9
  19. package/dist/astro/middleware/auth.mjs +6 -6
  20. package/dist/astro/middleware/auth.mjs.map +1 -1
  21. package/dist/astro/middleware/redirect.mjs +4 -4
  22. package/dist/astro/middleware/request-context.mjs +2 -2
  23. package/dist/astro/middleware/request-context.mjs.map +1 -1
  24. package/dist/astro/middleware/setup.mjs +1 -1
  25. package/dist/astro/middleware.d.mts.map +1 -1
  26. package/dist/astro/middleware.mjs +353 -71
  27. package/dist/astro/middleware.mjs.map +1 -1
  28. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  29. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  30. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
  31. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  32. package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts.map +1 -1
  33. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -17
  34. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  35. package/dist/astro/routes/api/admin/bylines/_id_/translations.d.mts +9 -0
  36. package/dist/astro/routes/api/admin/bylines/_id_/translations.d.mts.map +1 -0
  37. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +70 -0
  38. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -0
  39. package/dist/astro/routes/api/admin/bylines/index.d.mts.map +1 -1
  40. package/dist/astro/routes/api/admin/bylines/index.mjs +25 -16
  41. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  42. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
  43. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  44. package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
  45. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  46. package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
  47. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  48. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  49. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  50. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  51. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +32 -31
  52. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +32 -31
  54. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +31 -30
  56. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +31 -30
  58. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +33 -31
  60. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  61. package/dist/astro/routes/api/admin/plugins/index.mjs +31 -30
  62. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  63. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  64. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +31 -30
  65. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  66. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +33 -31
  67. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  68. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +31 -30
  69. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts +8 -0
  71. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -0
  72. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +59 -0
  73. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -0
  74. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts +8 -0
  75. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -0
  76. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +72 -0
  77. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -0
  78. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +31 -30
  79. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/updates.d.mts.map +1 -1
  81. package/dist/astro/routes/api/admin/plugins/updates.mjs +44 -31
  82. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +31 -30
  84. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  85. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  86. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +31 -30
  87. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  89. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  90. package/dist/astro/routes/api/admin/users/_id_/index.mjs +5 -5
  91. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  92. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  93. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
  94. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  95. package/dist/astro/routes/api/auth/invite/complete.mjs +9 -9
  96. package/dist/astro/routes/api/auth/invite/index.mjs +6 -6
  97. package/dist/astro/routes/api/auth/invite/register-options.mjs +8 -8
  98. package/dist/astro/routes/api/auth/logout.mjs +3 -3
  99. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  100. package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
  101. package/dist/astro/routes/api/auth/me.mjs +5 -5
  102. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  103. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  104. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs.map +1 -1
  105. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  106. package/dist/astro/routes/api/auth/oauth/_provider_.mjs.map +1 -1
  107. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  108. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  109. package/dist/astro/routes/api/auth/passkey/options.mjs +10 -10
  110. package/dist/astro/routes/api/auth/passkey/register/options.mjs +8 -8
  111. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +9 -9
  112. package/dist/astro/routes/api/auth/passkey/verify.mjs +9 -9
  113. package/dist/astro/routes/api/auth/signup/complete.mjs +9 -9
  114. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  115. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  116. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  117. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  118. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  119. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  120. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  121. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs.map +1 -1
  122. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  123. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +9 -9
  124. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +6 -6
  125. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  126. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  127. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs.map +1 -1
  128. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  129. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +6 -6
  130. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  131. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +10 -9
  132. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  133. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  134. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs.map +1 -1
  135. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  136. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  137. package/dist/astro/routes/api/content/_collection_/_id_.mjs +6 -6
  138. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  139. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  140. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  141. package/dist/astro/routes/api/dashboard.mjs +7 -7
  142. package/dist/astro/routes/api/dev/emails.mjs +3 -3
  143. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  144. package/dist/astro/routes/api/import/probe.mjs +10 -10
  145. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  146. package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
  147. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -8
  148. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  149. package/dist/astro/routes/api/import/wordpress/media.mjs +8 -8
  150. package/dist/astro/routes/api/import/wordpress/prepare.mjs +8 -8
  151. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  152. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
  153. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  154. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  155. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +10 -10
  156. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  157. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +11 -11
  158. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  159. package/dist/astro/routes/api/manifest.mjs +4 -4
  160. package/dist/astro/routes/api/mcp.mjs +29 -29
  161. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  162. package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
  163. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  164. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  165. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  166. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  167. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  168. package/dist/astro/routes/api/media/upload-url.mjs +7 -7
  169. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  170. package/dist/astro/routes/api/media.mjs +8 -8
  171. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
  172. package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
  173. package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
  174. package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
  175. package/dist/astro/routes/api/menus/_name_.mjs +7 -7
  176. package/dist/astro/routes/api/menus/index.mjs +7 -7
  177. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  178. package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
  179. package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
  180. package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
  181. package/dist/astro/routes/api/oauth/register.mjs +3 -3
  182. package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
  183. package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
  184. package/dist/astro/routes/api/oauth/token.mjs +6 -6
  185. package/dist/astro/routes/api/openapi.json.mjs +3 -3
  186. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  187. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  188. package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
  189. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  190. package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
  191. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  192. package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
  193. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  194. package/dist/astro/routes/api/redirects/index.mjs +9 -9
  195. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  196. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  197. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  198. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +31 -30
  199. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  200. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +31 -30
  201. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  202. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +31 -30
  203. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  204. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +31 -30
  205. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  206. package/dist/astro/routes/api/schema/collections/index.mjs +31 -30
  207. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  208. package/dist/astro/routes/api/schema/index.mjs +6 -6
  209. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  210. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +31 -30
  211. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  212. package/dist/astro/routes/api/schema/orphans/index.mjs +31 -30
  213. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  214. package/dist/astro/routes/api/search/enable.mjs +9 -9
  215. package/dist/astro/routes/api/search/index.mjs +8 -8
  216. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  217. package/dist/astro/routes/api/search/stats.mjs +6 -6
  218. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  219. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  220. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  221. package/dist/astro/routes/api/sections/index.mjs +8 -8
  222. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  223. package/dist/astro/routes/api/settings/email.mjs +4 -4
  224. package/dist/astro/routes/api/settings.mjs +10 -10
  225. package/dist/astro/routes/api/setup/admin-verify.mjs +10 -10
  226. package/dist/astro/routes/api/setup/admin.mjs +9 -9
  227. package/dist/astro/routes/api/setup/dev-bypass.mjs +22 -22
  228. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  229. package/dist/astro/routes/api/setup/index.mjs +22 -22
  230. package/dist/astro/routes/api/setup/status.mjs +4 -4
  231. package/dist/astro/routes/api/snapshot.mjs +5 -5
  232. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -10
  233. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  234. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -10
  235. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  236. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -10
  237. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  238. package/dist/astro/routes/api/taxonomies/index.mjs +11 -10
  239. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  240. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  241. package/dist/astro/routes/api/typegen.mjs +5 -5
  242. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  243. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  244. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  245. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  246. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  247. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  248. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  249. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  250. package/dist/astro/routes/api/widget-components.mjs +3 -3
  251. package/dist/astro/routes/robots.txt.mjs +5 -5
  252. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  253. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  254. package/dist/astro/types.d.mts +13 -12
  255. package/dist/astro/types.d.mts.map +1 -1
  256. package/dist/auth/providers/github.d.mts +1 -1
  257. package/dist/auth/providers/google.d.mts +1 -1
  258. package/dist/{authorize-BlyCH-96.mjs → authorize-Bkwe8kuL.mjs} +2 -2
  259. package/dist/{authorize-BlyCH-96.mjs.map → authorize-Bkwe8kuL.mjs.map} +1 -1
  260. package/dist/byline-CTaWkMh5.mjs +404 -0
  261. package/dist/byline-CTaWkMh5.mjs.map +1 -0
  262. package/dist/bylines-BYHWU3T7.mjs +174 -0
  263. package/dist/bylines-BYHWU3T7.mjs.map +1 -0
  264. package/dist/{bylines-BdUP8NuI.d.mts → bylines-DtDRNF1n.d.mts} +59 -14
  265. package/dist/bylines-DtDRNF1n.d.mts.map +1 -0
  266. package/dist/bylines-H0Xh5TMy.mjs +118 -0
  267. package/dist/bylines-H0Xh5TMy.mjs.map +1 -0
  268. package/dist/{cache-CXCpjWiL.mjs → cache-CNk1jIxp.mjs} +2 -2
  269. package/dist/{cache-CXCpjWiL.mjs.map → cache-CNk1jIxp.mjs.map} +1 -1
  270. package/dist/{challenge-store-CJ0OOHOr.mjs → challenge-store-Dng1SxKT.mjs} +1 -1
  271. package/dist/{challenge-store-CJ0OOHOr.mjs.map → challenge-store-Dng1SxKT.mjs.map} +1 -1
  272. package/dist/{chunks-DyGtu1Bv.mjs → chunks-BkfVdD-3.mjs} +2 -2
  273. package/dist/{chunks-DyGtu1Bv.mjs.map → chunks-BkfVdD-3.mjs.map} +1 -1
  274. package/dist/cli/index.mjs +21 -29
  275. package/dist/cli/index.mjs.map +1 -1
  276. package/dist/client/cf-access.d.mts +1 -1
  277. package/dist/client/index.d.mts +1 -1
  278. package/dist/client/index.mjs +1 -1
  279. package/dist/client/index.mjs.map +1 -1
  280. package/dist/{comment-Dd9MI82-.mjs → comment-_yzlBYPx.mjs} +2 -2
  281. package/dist/{comment-Dd9MI82-.mjs.map → comment-_yzlBYPx.mjs.map} +1 -1
  282. package/dist/{comments-koGI0FrK.mjs → comments-DxID-rsd.mjs} +3 -3
  283. package/dist/{comments-koGI0FrK.mjs.map → comments-DxID-rsd.mjs.map} +1 -1
  284. package/dist/{components-mZem7pbe.mjs → components-Dx3DM0gg.mjs} +1 -1
  285. package/dist/{components-mZem7pbe.mjs.map → components-Dx3DM0gg.mjs.map} +1 -1
  286. package/dist/config-CVssduLe.mjs.map +1 -1
  287. package/dist/{content-D6YG26WG.mjs → content-C0ooIs-f.mjs} +3 -3
  288. package/dist/{content-D6YG26WG.mjs.map → content-C0ooIs-f.mjs.map} +1 -1
  289. package/dist/{context-qF8d3IPR.mjs → context-sAnCaUIR.mjs} +10 -10
  290. package/dist/context-sAnCaUIR.mjs.map +1 -0
  291. package/dist/{cron-H8eJ46dv.mjs → cron-Bd3b3iuj.mjs} +1 -1
  292. package/dist/{cron-H8eJ46dv.mjs.map → cron-Bd3b3iuj.mjs.map} +1 -1
  293. package/dist/{dashboard-BmWSIUwY.mjs → dashboard-Cqw3ay2X.mjs} +4 -4
  294. package/dist/{dashboard-BmWSIUwY.mjs.map → dashboard-Cqw3ay2X.mjs.map} +1 -1
  295. package/dist/db/index.d.mts +3 -3
  296. package/dist/db/index.mjs +1 -1
  297. package/dist/db/libsql.d.mts +1 -1
  298. package/dist/db/postgres.d.mts +1 -1
  299. package/dist/db/sqlite.d.mts +1 -1
  300. package/dist/{default-Dbs22Gg4.mjs → default-BvTAYCzx.mjs} +1 -1
  301. package/dist/{default-Dbs22Gg4.mjs.map → default-BvTAYCzx.mjs.map} +1 -1
  302. package/dist/{device-flow-BqJRxa0Q.mjs → device-flow-B9oG8PwP.mjs} +4 -4
  303. package/dist/{device-flow-BqJRxa0Q.mjs.map → device-flow-B9oG8PwP.mjs.map} +1 -1
  304. package/dist/{email-console-Dmp5Q-P2.mjs → email-console-CubRll9q.mjs} +1 -1
  305. package/dist/email-console-CubRll9q.mjs.map +1 -0
  306. package/dist/{error-tSQWIl5U.mjs → error-CPh_8eLq.mjs} +16 -8
  307. package/dist/error-CPh_8eLq.mjs.map +1 -0
  308. package/dist/{escape-B8bdIryO.mjs → escape-Cg6kMELH.mjs} +1 -1
  309. package/dist/{escape-B8bdIryO.mjs.map → escape-Cg6kMELH.mjs.map} +1 -1
  310. package/dist/{fts-manager-B633C-kQ.mjs → fts-manager-Mnrtn-r2.mjs} +2 -2
  311. package/dist/{fts-manager-B633C-kQ.mjs.map → fts-manager-Mnrtn-r2.mjs.map} +1 -1
  312. package/dist/{import-CNfLOgDE.mjs → import-DG80rC_I.mjs} +3 -3
  313. package/dist/{import-CNfLOgDE.mjs.map → import-DG80rC_I.mjs.map} +1 -1
  314. package/dist/{index-BV8iJ-6s.d.mts → index-Bv1Wf1zB.d.mts} +235 -18
  315. package/dist/index-Bv1Wf1zB.d.mts.map +1 -0
  316. package/dist/{index-D2gvztOP.d.mts → index-CC42STEm.d.mts} +3 -3
  317. package/dist/{index-D2gvztOP.d.mts.map → index-CC42STEm.d.mts.map} +1 -1
  318. package/dist/index.d.mts +17 -17
  319. package/dist/index.mjs +50 -49
  320. package/dist/{load-QzYRpVN3.mjs → load-DmXNVhst.mjs} +2 -2
  321. package/dist/{load-QzYRpVN3.mjs.map → load-DmXNVhst.mjs.map} +1 -1
  322. package/dist/{loader-Cs6-Bqe6.mjs → loader-Chm5h7Gr.mjs} +3 -3
  323. package/dist/loader-Chm5h7Gr.mjs.map +1 -0
  324. package/dist/{manifest-schema-HCtSh4Jq.mjs → manifest-schema-Czqf0TLu.mjs} +1 -1
  325. package/dist/{manifest-schema-HCtSh4Jq.mjs.map → manifest-schema-Czqf0TLu.mjs.map} +1 -1
  326. package/dist/media/index.d.mts +1 -1
  327. package/dist/media/local-runtime.d.mts +11 -11
  328. package/dist/media/local-runtime.mjs +4 -4
  329. package/dist/{media-allowlist-B8EX01DH.mjs → media-allowlist-BNloC69x.mjs} +1 -1
  330. package/dist/{media-allowlist-B8EX01DH.mjs.map → media-allowlist-BNloC69x.mjs.map} +1 -1
  331. package/dist/{media-Dg7he9uK.mjs → media-oqRcNiQf.mjs} +2 -2
  332. package/dist/media-oqRcNiQf.mjs.map +1 -0
  333. package/dist/{menus-DOzIecHi.mjs → menus-Bjf5R1Qq.mjs} +2 -2
  334. package/dist/menus-Bjf5R1Qq.mjs.map +1 -0
  335. package/dist/{menus-X4Z-eBA1.mjs → menus-C75SSmRy.mjs} +30 -11
  336. package/dist/menus-C75SSmRy.mjs.map +1 -0
  337. package/dist/mime-KV5TqkMN.mjs.map +1 -1
  338. package/dist/{mode-DPRPvJYm.mjs → mode-CaaiebZI.mjs} +1 -1
  339. package/dist/{mode-DPRPvJYm.mjs.map → mode-CaaiebZI.mjs.map} +1 -1
  340. package/dist/{oauth-authorization-62GmpGIH.mjs → oauth-authorization-CTMeVfvj.mjs} +4 -4
  341. package/dist/{oauth-authorization-62GmpGIH.mjs.map → oauth-authorization-CTMeVfvj.mjs.map} +1 -1
  342. package/dist/{oauth-clients-D_B0_-Bz.mjs → oauth-clients-eJCbkVSG.mjs} +1 -1
  343. package/dist/oauth-clients-eJCbkVSG.mjs.map +1 -0
  344. package/dist/{oauth-state-store-DpsZViTu.mjs → oauth-state-store-vOSdOeGe.mjs} +1 -1
  345. package/dist/{oauth-state-store-DpsZViTu.mjs.map → oauth-state-store-vOSdOeGe.mjs.map} +1 -1
  346. package/dist/{oauth-user-lookup-meyS2oB1.mjs → oauth-user-lookup-3JwsVw6N.mjs} +1 -1
  347. package/dist/{oauth-user-lookup-meyS2oB1.mjs.map → oauth-user-lookup-3JwsVw6N.mjs.map} +1 -1
  348. package/dist/options-BL4X94qY.mjs.map +1 -1
  349. package/dist/{options-Cq64Wx0O.d.mts → options-DhV-gwJb.d.mts} +4 -4
  350. package/dist/options-DhV-gwJb.d.mts.map +1 -0
  351. package/dist/page/index.d.mts +2 -2
  352. package/dist/{parse-BFTPon-J.mjs → parse-3-caTKgt.mjs} +2 -2
  353. package/dist/{parse-BFTPon-J.mjs.map → parse-3-caTKgt.mjs.map} +1 -1
  354. package/dist/{passkey-config-Cg86_ISa.mjs → passkey-config-BloQOT3y.mjs} +1 -1
  355. package/dist/{passkey-config-Cg86_ISa.mjs.map → passkey-config-BloQOT3y.mjs.map} +1 -1
  356. package/dist/{placeholder-D3cFCU9y.d.mts → placeholder-KCkkCtgQ.d.mts} +1 -1
  357. package/dist/{placeholder-D3cFCU9y.d.mts.map → placeholder-KCkkCtgQ.d.mts.map} +1 -1
  358. package/dist/plugin-types.d.mts +1 -1
  359. package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
  360. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  361. package/dist/plugins/adapt-sandbox-entry.mjs +26 -15
  362. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  363. package/dist/{preview-C1LOEbWZ.mjs → preview-D4z0WONU.mjs} +2 -2
  364. package/dist/{preview-C1LOEbWZ.mjs.map → preview-D4z0WONU.mjs.map} +1 -1
  365. package/dist/{public-url-CseXl9Fv.mjs → public-url-CUWWFME2.mjs} +1 -1
  366. package/dist/{public-url-CseXl9Fv.mjs.map → public-url-CUWWFME2.mjs.map} +1 -1
  367. package/dist/{query-axZmO6Tn.mjs → query-BJn8TOPk.mjs} +16 -13
  368. package/dist/{query-axZmO6Tn.mjs.map → query-BJn8TOPk.mjs.map} +1 -1
  369. package/dist/{rate-limit-t5CVjCO6.mjs → rate-limit-D_-gAeJ0.mjs} +2 -2
  370. package/dist/{rate-limit-t5CVjCO6.mjs.map → rate-limit-D_-gAeJ0.mjs.map} +1 -1
  371. package/dist/{redirect-DGRsLO2I.mjs → redirect-BINiRYq4.mjs} +1 -1
  372. package/dist/{redirect-DGRsLO2I.mjs.map → redirect-BINiRYq4.mjs.map} +1 -1
  373. package/dist/{redirect-DkaDxq8e.mjs → redirect-CNv4mHX2.mjs} +2 -2
  374. package/dist/{redirect-DkaDxq8e.mjs.map → redirect-CNv4mHX2.mjs.map} +1 -1
  375. package/dist/{redirects-D1fdd68T.mjs → redirects-B-CUZ1Xh.mjs} +3 -3
  376. package/dist/{redirects-D1fdd68T.mjs.map → redirects-B-CUZ1Xh.mjs.map} +1 -1
  377. package/dist/{redirects-Dmj6KRU3.mjs → redirects-COMLwsV5.mjs} +19 -5
  378. package/dist/redirects-COMLwsV5.mjs.map +1 -0
  379. package/dist/{registry-BnCeHYsf.mjs → registry-DqrAQDXH.mjs} +4 -4
  380. package/dist/{registry-BnCeHYsf.mjs.map → registry-DqrAQDXH.mjs.map} +1 -1
  381. package/dist/request-cache-dzCt8TZB.mjs.map +1 -1
  382. package/dist/request-context.mjs.map +1 -1
  383. package/dist/{request-meta-CLCwSQOS.mjs → request-meta-C_Cjii-T.mjs} +2 -2
  384. package/dist/{request-meta-CLCwSQOS.mjs.map → request-meta-C_Cjii-T.mjs.map} +1 -1
  385. package/dist/resolve-Cj98DuqN.mjs +39 -0
  386. package/dist/resolve-Cj98DuqN.mjs.map +1 -0
  387. package/dist/{runner-DdnQIwz_.mjs → runner-CGlojznK.mjs} +472 -165
  388. package/dist/runner-CGlojznK.mjs.map +1 -0
  389. package/dist/{runner-DcfZewkO.d.mts → runner-CNHRo1mT.d.mts} +2 -2
  390. package/dist/{runner-DcfZewkO.d.mts.map → runner-CNHRo1mT.d.mts.map} +1 -1
  391. package/dist/runtime.d.mts +10 -10
  392. package/dist/runtime.mjs +2 -2
  393. package/dist/{schema-BmqagCwG.mjs → schema-Djdlfi5G.mjs} +4 -4
  394. package/dist/{schema-BmqagCwG.mjs.map → schema-Djdlfi5G.mjs.map} +1 -1
  395. package/dist/{search-CPrvO5u8.mjs → search-By-NN3da.mjs} +4 -4
  396. package/dist/{search-CPrvO5u8.mjs.map → search-By-NN3da.mjs.map} +1 -1
  397. package/dist/{secrets-6pgZyq0K.mjs → secrets-rPdhEBkD.mjs} +1 -1
  398. package/dist/{secrets-6pgZyq0K.mjs.map → secrets-rPdhEBkD.mjs.map} +1 -1
  399. package/dist/{sections-Cm-zb-gZ.mjs → sections-DcBIlOq1.mjs} +3 -3
  400. package/dist/{sections-Cm-zb-gZ.mjs.map → sections-DcBIlOq1.mjs.map} +1 -1
  401. package/dist/seed/index.d.mts +2 -2
  402. package/dist/seed/index.mjs +16 -16
  403. package/dist/seo/index.d.mts +1 -1
  404. package/dist/{seo-DRq9-EPP.mjs → seo-bjDoq9Eg.mjs} +2 -2
  405. package/dist/{seo-DRq9-EPP.mjs.map → seo-bjDoq9Eg.mjs.map} +1 -1
  406. package/dist/{service-vByySp-2.mjs → service-BuuTdGAT.mjs} +3 -3
  407. package/dist/{service-vByySp-2.mjs.map → service-BuuTdGAT.mjs.map} +1 -1
  408. package/dist/{settings-CBBj7HUd.mjs → settings-CJnKiWuR.mjs} +3 -3
  409. package/dist/{settings-CBBj7HUd.mjs.map → settings-CJnKiWuR.mjs.map} +1 -1
  410. package/dist/{settings-xQKsWnzQ.mjs → settings-hcubRfkr.mjs} +3 -3
  411. package/dist/settings-hcubRfkr.mjs.map +1 -0
  412. package/dist/{setup-BGAJ2uXs.mjs → setup-Cf_TyOv5.mjs} +2 -2
  413. package/dist/{setup-BGAJ2uXs.mjs.map → setup-Cf_TyOv5.mjs.map} +1 -1
  414. package/dist/{setup-complete-C6ZCLhKo.mjs → setup-complete-MzzN9u0b.mjs} +1 -1
  415. package/dist/{setup-complete-C6ZCLhKo.mjs.map → setup-complete-MzzN9u0b.mjs.map} +1 -1
  416. package/dist/{setup-nonce-CY1gQiAU.mjs → setup-nonce-DXuriHsg.mjs} +1 -1
  417. package/dist/{setup-nonce-CY1gQiAU.mjs.map → setup-nonce-DXuriHsg.mjs.map} +1 -1
  418. package/dist/{site-url-D-M4Fd8O.mjs → site-url-xkhw1tcz.mjs} +1 -1
  419. package/dist/{site-url-D-M4Fd8O.mjs.map → site-url-xkhw1tcz.mjs.map} +1 -1
  420. package/dist/{ssrf-DzFN_qV-.mjs → ssrf-MZ-zrG6-.mjs} +1 -1
  421. package/dist/{ssrf-DzFN_qV-.mjs.map → ssrf-MZ-zrG6-.mjs.map} +1 -1
  422. package/dist/storage/local.d.mts +1 -1
  423. package/dist/storage/local.mjs +1 -1
  424. package/dist/storage/local.mjs.map +1 -1
  425. package/dist/storage/s3.d.mts +1 -1
  426. package/dist/storage/s3.mjs +1 -1
  427. package/dist/storage/s3.mjs.map +1 -1
  428. package/dist/{taxonomies-Dc0mzlms.mjs → taxonomies-CLs9HPE2.mjs} +4 -4
  429. package/dist/{taxonomies-Dc0mzlms.mjs.map → taxonomies-CLs9HPE2.mjs.map} +1 -1
  430. package/dist/{taxonomies-Cn9UpaR2.mjs → taxonomies-WamPVA2x.mjs} +7 -42
  431. package/dist/taxonomies-WamPVA2x.mjs.map +1 -0
  432. package/dist/{taxonomy-wPfusMK9.mjs → taxonomy-D4Uc2LsZ.mjs} +3 -3
  433. package/dist/{taxonomy-wPfusMK9.mjs.map → taxonomy-D4Uc2LsZ.mjs.map} +1 -1
  434. package/dist/{tokens-DILYNZMi.mjs → tokens-N8otWMmj.mjs} +1 -1
  435. package/dist/{tokens-DILYNZMi.mjs.map → tokens-N8otWMmj.mjs.map} +1 -1
  436. package/dist/{transport-fw-mKJzT.mjs → transport-B6CHddbu.mjs} +1 -1
  437. package/dist/{transport-fw-mKJzT.mjs.map → transport-B6CHddbu.mjs.map} +1 -1
  438. package/dist/{transport-GeXlLscf.d.mts → transport-DOxLfUir.d.mts} +1 -1
  439. package/dist/{transport-GeXlLscf.d.mts.map → transport-DOxLfUir.d.mts.map} +1 -1
  440. package/dist/{trusted-proxy-CJhQIk65.mjs → trusted-proxy-97pajC2f.mjs} +1 -1
  441. package/dist/{trusted-proxy-CJhQIk65.mjs.map → trusted-proxy-97pajC2f.mjs.map} +1 -1
  442. package/dist/{types-CwXMEPRr.mjs → types-ByV5sgsv.mjs} +2 -2
  443. package/dist/types-ByV5sgsv.mjs.map +1 -0
  444. package/dist/{types-Dz9CGX_d.mjs → types-Cd9UCu3t.mjs} +1 -1
  445. package/dist/{types-Dz9CGX_d.mjs.map → types-Cd9UCu3t.mjs.map} +1 -1
  446. package/dist/{types-DmxPPXGf.d.mts → types-CkDSF81F.d.mts} +1 -1
  447. package/dist/{types-DmxPPXGf.d.mts.map → types-CkDSF81F.d.mts.map} +1 -1
  448. package/dist/{types-BWhaSS7U.d.mts → types-CpUuGcd5.d.mts} +1 -1
  449. package/dist/{types-BWhaSS7U.d.mts.map → types-CpUuGcd5.d.mts.map} +1 -1
  450. package/dist/{types-DFowNO60.d.mts → types-D599-ruj.d.mts} +1 -1
  451. package/dist/{types-DFowNO60.d.mts.map → types-D599-ruj.d.mts.map} +1 -1
  452. package/dist/{types-B05e2naf.d.mts → types-DGHWRQgr.d.mts} +3 -3
  453. package/dist/{types-B05e2naf.d.mts.map → types-DGHWRQgr.d.mts.map} +1 -1
  454. package/dist/{types-CzvJd1ND.d.mts → types-DaYDYW6g.d.mts} +14 -1
  455. package/dist/types-DaYDYW6g.d.mts.map +1 -0
  456. package/dist/{types-C1KKK4VP.d.mts → types-DaqNzqVt.d.mts} +16 -1
  457. package/dist/{types-C1KKK4VP.d.mts.map → types-DaqNzqVt.d.mts.map} +1 -1
  458. package/dist/{types-DW1l0gCv.d.mts → types-Dgo6y-Ut.d.mts} +1 -1
  459. package/dist/{types-DW1l0gCv.d.mts.map → types-Dgo6y-Ut.d.mts.map} +1 -1
  460. package/dist/{types-Cb2UCDJg.d.mts → types-bYmRn_Uy.d.mts} +1 -1
  461. package/dist/{types-Cb2UCDJg.d.mts.map → types-bYmRn_Uy.d.mts.map} +1 -1
  462. package/dist/{user-Dr1bOCqS.mjs → user-D3BD5zdT.mjs} +2 -2
  463. package/dist/{user-Dr1bOCqS.mjs.map → user-D3BD5zdT.mjs.map} +1 -1
  464. package/dist/{utils-_F-rWBTN.mjs → utils-C3wTAP-P.mjs} +1 -1
  465. package/dist/{utils-_F-rWBTN.mjs.map → utils-C3wTAP-P.mjs.map} +1 -1
  466. package/dist/{validate-BpQGsmd7.d.mts → validate-DQtHw9NT.d.mts} +5 -5
  467. package/dist/{validate-BpQGsmd7.d.mts.map → validate-DQtHw9NT.d.mts.map} +1 -1
  468. package/dist/{validate-DlFxcVVK.mjs → validate-mz87i8_1.mjs} +2 -2
  469. package/dist/{validate-DlFxcVVK.mjs.map → validate-mz87i8_1.mjs.map} +1 -1
  470. package/dist/{validation-BiFJqUp5.mjs → validation-DKHhXjPr.mjs} +5 -5
  471. package/dist/{validation-BiFJqUp5.mjs.map → validation-DKHhXjPr.mjs.map} +1 -1
  472. package/dist/version-Ct7C6RSo.mjs +7 -0
  473. package/dist/{version-DNmQakZO.mjs.map → version-Ct7C6RSo.mjs.map} +1 -1
  474. package/dist/{widgets-B9j_yzlk.mjs → widgets-lShIQXU5.mjs} +3 -3
  475. package/dist/widgets-lShIQXU5.mjs.map +1 -0
  476. package/dist/{zod-generator-DSyz01KE.mjs → zod-generator-dvxgmd1M.mjs} +2 -2
  477. package/dist/{zod-generator-DSyz01KE.mjs.map → zod-generator-dvxgmd1M.mjs.map} +1 -1
  478. package/package.json +11 -9
  479. package/src/api/error.ts +18 -3
  480. package/src/api/errors.ts +6 -0
  481. package/src/api/handlers/bylines.ts +161 -0
  482. package/src/api/handlers/content.ts +125 -43
  483. package/src/api/handlers/index.ts +6 -0
  484. package/src/api/handlers/marketplace.ts +27 -5
  485. package/src/api/handlers/oauth-clients.ts +1 -1
  486. package/src/api/handlers/registry.ts +553 -4
  487. package/src/api/openapi/document.ts +1 -1
  488. package/src/api/schemas/bylines.ts +46 -0
  489. package/src/astro/integration/index.ts +1 -1
  490. package/src/astro/integration/routes.ts +5 -0
  491. package/src/astro/integration/runtime.ts +12 -1
  492. package/src/astro/integration/virtual-modules.ts +19 -2
  493. package/src/astro/integration/vite-config.ts +2 -2
  494. package/src/astro/middleware/auth.ts +7 -7
  495. package/src/astro/middleware/request-context.ts +1 -1
  496. package/src/astro/middleware.ts +31 -20
  497. package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -12
  498. package/src/astro/routes/api/admin/bylines/[id]/translations.ts +99 -0
  499. package/src/astro/routes/api/admin/bylines/index.ts +22 -11
  500. package/src/astro/routes/api/admin/plugins/[id]/update.ts +1 -0
  501. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +6 -1
  502. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +51 -0
  503. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +79 -0
  504. package/src/astro/routes/api/admin/plugins/updates.ts +43 -6
  505. package/src/astro/routes/api/admin/themes/marketplace/index.ts +1 -1
  506. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +2 -2
  507. package/src/astro/routes/api/auth/oauth/[provider].ts +2 -2
  508. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +2 -2
  509. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +2 -2
  510. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +2 -2
  511. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +2 -2
  512. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +2 -2
  513. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +6 -6
  514. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  515. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +2 -2
  516. package/src/astro/routes/api/content/[collection]/[id].ts +6 -6
  517. package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
  518. package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
  519. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +3 -3
  520. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +2 -2
  521. package/src/astro/routes/api/media/upload-url.ts +1 -1
  522. package/src/astro/routes/api/redirects/404s/index.ts +3 -3
  523. package/src/astro/routes/api/redirects/404s/summary.ts +1 -1
  524. package/src/astro/routes/api/redirects/[id].ts +3 -3
  525. package/src/astro/routes/api/redirects/index.ts +2 -2
  526. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +4 -4
  527. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +2 -6
  528. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -1
  529. package/src/astro/routes/api/schema/collections/[slug]/index.ts +6 -6
  530. package/src/astro/routes/api/schema/collections/index.ts +4 -4
  531. package/src/astro/routes/api/schema/index.ts +1 -1
  532. package/src/astro/routes/api/schema/orphans/[slug].ts +1 -1
  533. package/src/astro/routes/api/schema/orphans/index.ts +1 -1
  534. package/src/astro/routes/api/sections/[slug].ts +3 -3
  535. package/src/astro/routes/api/sections/index.ts +2 -2
  536. package/src/astro/types.ts +4 -0
  537. package/src/auth/rate-limit.ts +1 -1
  538. package/src/auth/trusted-proxy.ts +1 -1
  539. package/src/bylines/index.ts +154 -55
  540. package/src/cli/commands/init.ts +4 -8
  541. package/src/client/index.ts +1 -1
  542. package/src/components/InlinePortableTextEditor.tsx +5 -1
  543. package/src/components/inline-code-block.tsx +343 -0
  544. package/src/config/secrets.ts +3 -3
  545. package/src/database/migrations/006_taxonomy_defs.ts +1 -1
  546. package/src/database/migrations/014_draft_revisions.ts +6 -6
  547. package/src/database/migrations/040_byline_i18n.ts +497 -0
  548. package/src/database/migrations/runner.ts +4 -1
  549. package/src/database/repositories/audit.ts +2 -2
  550. package/src/database/repositories/byline.ts +320 -50
  551. package/src/database/repositories/media.ts +2 -2
  552. package/src/database/repositories/menu.ts +1 -1
  553. package/src/database/repositories/options.ts +3 -3
  554. package/src/database/repositories/plugin-storage.ts +3 -3
  555. package/src/database/repositories/types.ts +13 -0
  556. package/src/database/types.ts +15 -0
  557. package/src/emdash-runtime.ts +492 -20
  558. package/src/i18n/config.ts +1 -1
  559. package/src/index.ts +7 -0
  560. package/src/loader.ts +1 -1
  561. package/src/mcp/server.ts +3 -3
  562. package/src/media/mime.ts +1 -1
  563. package/src/page/absolute-url.ts +1 -1
  564. package/src/plugins/adapt-sandbox-entry.ts +45 -40
  565. package/src/plugins/email-console.ts +1 -1
  566. package/src/plugins/index.ts +1 -0
  567. package/src/plugins/marketplace.ts +1 -1
  568. package/src/plugins/sandbox/index.ts +1 -0
  569. package/src/plugins/sandbox/noop.ts +11 -3
  570. package/src/plugins/sandbox/types.ts +28 -0
  571. package/src/query.ts +17 -2
  572. package/src/registry/config.ts +1 -1
  573. package/src/request-cache.ts +3 -3
  574. package/src/request-context.ts +1 -1
  575. package/src/settings/index.ts +4 -4
  576. package/src/storage/local.ts +1 -1
  577. package/src/storage/s3.ts +3 -3
  578. package/src/widgets/index.ts +1 -1
  579. package/dist/api-BMLZuwM4.mjs.map +0 -1
  580. package/dist/byline-D09BaS4j.mjs +0 -220
  581. package/dist/byline-D09BaS4j.mjs.map +0 -1
  582. package/dist/bylines-BTM2xtP8.mjs +0 -113
  583. package/dist/bylines-BTM2xtP8.mjs.map +0 -1
  584. package/dist/bylines-BdUP8NuI.d.mts.map +0 -1
  585. package/dist/context-qF8d3IPR.mjs.map +0 -1
  586. package/dist/email-console-Dmp5Q-P2.mjs.map +0 -1
  587. package/dist/error-tSQWIl5U.mjs.map +0 -1
  588. package/dist/index-BV8iJ-6s.d.mts.map +0 -1
  589. package/dist/loader-Cs6-Bqe6.mjs.map +0 -1
  590. package/dist/media-Dg7he9uK.mjs.map +0 -1
  591. package/dist/menus-DOzIecHi.mjs.map +0 -1
  592. package/dist/menus-X4Z-eBA1.mjs.map +0 -1
  593. package/dist/oauth-clients-D_B0_-Bz.mjs.map +0 -1
  594. package/dist/options-Cq64Wx0O.d.mts.map +0 -1
  595. package/dist/redirects-Dmj6KRU3.mjs.map +0 -1
  596. package/dist/runner-DdnQIwz_.mjs.map +0 -1
  597. package/dist/settings-xQKsWnzQ.mjs.map +0 -1
  598. package/dist/taxonomies-Cn9UpaR2.mjs.map +0 -1
  599. package/dist/types-CwXMEPRr.mjs.map +0 -1
  600. package/dist/types-CzvJd1ND.d.mts.map +0 -1
  601. package/dist/version-DNmQakZO.mjs +0 -7
  602. package/dist/widgets-B9j_yzlk.mjs.map +0 -1
  603. /package/dist/{api-tokens-D3C9v02m.mjs → api-tokens-iPIHAY8N.mjs} +0 -0
  604. /package/dist/{ssrf-CTul4uQi.mjs → ssrf-BIcd-aXW.mjs} +0 -0
  605. /package/dist/{types-Db67HHlU.mjs → types-1NNkmTIn.mjs} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"api-BMLZuwM4.mjs","names":["TRAILING_SLASHES","TRAILING_DOT"],"sources":["../src/api/rev.ts","../src/api/handlers/validate-media-fields.ts","../src/api/handlers/content.ts","../src/api/handlers/manifest.ts","../src/api/handlers/revision.ts","../src/api/handlers/media.ts","../src/api/handlers/schema.ts","../src/plugins/state.ts","../src/api/handlers/plugins.ts","../src/plugins/marketplace.ts","../src/api/handlers/marketplace.ts","../src/registry/config.ts","../src/registry/plugin-id.ts","../src/api/handlers/registry.ts"],"sourcesContent":["/**\n * Opaque _rev token generation and validation.\n *\n * Format: base64(\"version:updated_at\")\n * Stateless — server decodes and checks both components.\n *\n * Rules:\n * - No _rev sent → blind write (backwards-compatible)\n * - _rev matches → write proceeds, new _rev returned\n * - _rev mismatch → 409 Conflict\n */\n\nimport type { ContentItem } from \"../database/repositories/types.js\";\nimport { encodeBase64, decodeBase64 } from \"../utils/base64.js\";\n\n/**\n * Generate a _rev token from a content item's version and updatedAt.\n */\nexport function encodeRev(item: ContentItem): string {\n\treturn encodeBase64(`${item.version}:${item.updatedAt}`);\n}\n\n/**\n * Decode a _rev token into its components.\n * Returns null if the token is malformed.\n */\nexport function decodeRev(rev: string): { version: number; updatedAt: string } | null {\n\ttry {\n\t\tconst decoded = decodeBase64(rev);\n\t\tconst colonIdx = decoded.indexOf(\":\");\n\t\tif (colonIdx === -1) return null;\n\n\t\tconst version = parseInt(decoded.slice(0, colonIdx), 10);\n\t\tconst updatedAt = decoded.slice(colonIdx + 1);\n\n\t\tif (isNaN(version) || !updatedAt) return null;\n\t\treturn { version, updatedAt };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Validate a _rev token against a content item.\n * Returns null if valid (or if no _rev provided), or an error message if invalid.\n */\nexport function validateRev(\n\trev: string | undefined,\n\titem: ContentItem,\n): { valid: true } | { valid: false; message: string } {\n\t// No _rev = blind write (backwards-compatible)\n\tif (!rev) return { valid: true };\n\n\tconst decoded = decodeRev(rev);\n\tif (!decoded) {\n\t\treturn { valid: false, message: \"Malformed _rev token\" };\n\t}\n\n\tif (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tmessage: \"Content has been modified since last read (version conflict)\",\n\t\t};\n\t}\n\n\treturn { valid: true };\n}\n","import type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { matchesMimeAllowlist, parseAllowedMimeTypes } from \"../../media/mime.js\";\nimport { requestCached } from \"../../request-cache.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { ApiResult } from \"../types.js\";\n\ninterface FieldRow {\n\tslug: string;\n\ttype: string;\n\tallowedMimeTypes: string[];\n}\n\ninterface MediaRefValue {\n\tid?: unknown;\n\tprovider?: unknown;\n\tmimeType?: unknown;\n}\n\nfunction asMediaRef(value: unknown): MediaRefValue | null {\n\tif (value === null || value === undefined) return null;\n\tif (typeof value !== \"object\" || Array.isArray(value)) return null;\n\treturn value as MediaRefValue;\n}\n\nfunction fail(message: string): ApiResult<never> {\n\treturn { success: false, error: { code: \"INVALID_MIME_FOR_FIELD\", message } };\n}\n\nasync function loadMediaFieldsForCollection(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n): Promise<FieldRow[]> {\n\tconst rows = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.innerJoin(\"_emdash_collections\", \"_emdash_collections.id\", \"_emdash_fields.collection_id\")\n\t\t.select([\"_emdash_fields.slug\", \"_emdash_fields.type\", \"_emdash_fields.validation\"])\n\t\t.where(\"_emdash_collections.slug\", \"=\", collectionSlug)\n\t\t.where(\"_emdash_fields.type\", \"in\", [\"file\", \"image\"])\n\t\t.execute();\n\n\tconst out: FieldRow[] = [];\n\tfor (const row of rows) {\n\t\tconst list = parseAllowedMimeTypes(row.validation);\n\t\tif (!list) continue;\n\t\tout.push({ slug: row.slug, type: row.type, allowedMimeTypes: list });\n\t}\n\treturn out;\n}\n\nexport async function validateMediaFields(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tdata: Record<string, unknown>,\n): Promise<ApiResult<true>> {\n\t// Cache is keyed on slug only. If a handler creates/modifies a field and\n\t// then writes content in the same request (e.g. bulk import), the cached\n\t// list will be stale for that request. This is an edge case in normal use.\n\tconst fields = await requestCached(`mediaFields:${collectionSlug}`, () =>\n\t\tloadMediaFieldsForCollection(db, collectionSlug),\n\t);\n\tif (fields.length === 0) return { success: true, data: true };\n\n\t// Collect local media ids that need a MIME lookup\n\tconst localIds = new Set<string>();\n\tfor (const field of fields) {\n\t\tconst ref = asMediaRef(data[field.slug]);\n\t\tif (!ref) continue;\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\t\tif (provider === \"local\" && typeof ref.id === \"string\") {\n\t\t\tlocalIds.add(ref.id);\n\t\t}\n\t}\n\n\t// Batch-load local media MIMEs\n\tconst idList = [...localIds];\n\tconst mimeById = new Map<string, string>();\n\tif (idList.length > 0) {\n\t\tfor (const batch of chunks(idList, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await db\n\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t.select([\"id\", \"mime_type\"])\n\t\t\t\t.where(\"id\", \"in\", batch)\n\t\t\t\t.execute();\n\t\t\tfor (const r of rows) mimeById.set(r.id, r.mime_type);\n\t\t}\n\t}\n\n\tfor (const field of fields) {\n\t\tconst value = data[field.slug];\n\t\tif (value === null || value === undefined) continue;\n\t\tconst ref = asMediaRef(value);\n\t\tif (!ref) continue;\n\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\n\t\t// External providers carry mimeType in the ref; trust it as-is.\n\t\t// Local media: look up the stored mimeType by id.\n\t\tlet mime: string | undefined;\n\t\tif (provider === \"local\") {\n\t\t\tif (typeof ref.id !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with an invalid id`);\n\t\t\t}\n\t\t\tmime = mimeById.get(ref.id);\n\t\t\tif (!mime) {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with unknown MIME type`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (typeof ref.mimeType !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);\n\t\t\t}\n\t\t\t// TODO: long-term, consider a server-side HEAD probe or provider-vouched\n\t\t\t// MIMEs for non-local refs; for now the constraint is only as strong as\n\t\t\t// the client that constructed the ref.\n\t\t\tmime = ref.mimeType;\n\t\t}\n\n\t\tif (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) {\n\t\t\treturn fail(`Field '${field.slug}' does not accept ${mime}`);\n\t\t}\n\t}\n\n\treturn { success: true, data: true };\n}\n","/**\n * Content CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../../database/repositories/byline.js\";\nimport type { ContentBylineInput } from \"../../database/repositories/byline.js\";\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RedirectRepository } from \"../../database/repositories/redirect.js\";\nimport { RevisionRepository } from \"../../database/repositories/revision.js\";\nimport { SeoRepository } from \"../../database/repositories/seo.js\";\nimport {\n\tEmDashValidationError,\n\tInvalidCursorError,\n\ttype ContentItem,\n\ttype ContentSeo,\n\ttype ContentSeoInput,\n} from \"../../database/repositories/types.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { isI18nEnabled } from \"../../i18n/config.js\";\nimport { invalidateRedirectCache } from \"../../redirects/cache.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\nimport { encodeRev, validateRev } from \"../rev.js\";\nimport type { ApiResult, ContentListResponse, ContentResponse } from \"../types.js\";\nimport { validateMediaFields } from \"./validate-media-fields.js\";\n\n/**\n * Narrow a caught error to one carrying a structured `apiError` discriminant.\n * Used by transaction callbacks that want to surface a specific error code\n * through the standard Error throwing path.\n */\nfunction hasApiError(error: unknown): error is Error & { apiError: { code: string } } {\n\tif (!(error instanceof Error) || !(\"apiError\" in error)) return false;\n\tconst { apiError } = error;\n\treturn (\n\t\ttypeof apiError === \"object\" &&\n\t\tapiError !== null &&\n\t\t\"code\" in apiError &&\n\t\ttypeof apiError.code === \"string\"\n\t);\n}\n\n/**\n * Extract a slug source (title or name) from content data.\n * Returns null if no suitable string field is found.\n */\nfunction getSlugSource(data: Record<string, unknown>): string | null {\n\tif (typeof data.title === \"string\" && data.title.length > 0) return data.title;\n\tif (typeof data.name === \"string\" && data.name.length > 0) return data.name;\n\treturn null;\n}\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Check if a collection has SEO enabled.\n */\nasync function collectionHasSeo(db: Kysely<Database>, collection: string): Promise<boolean> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"has_seo\")\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\treturn row?.has_seo === 1;\n}\n\n/**\n * Hydrate SEO data on a single content item if the collection has SEO enabled.\n */\nasync function hydrateSeo(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo) return;\n\tconst seoRepo = new SeoRepository(db);\n\titem.seo = await seoRepo.get(collection, item.id);\n}\n\n/**\n * Hydrate SEO data on multiple content items using a single batch query.\n */\nasync function hydrateSeoMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo || items.length === 0) return;\n\tconst seoRepo = new SeoRepository(db);\n\tconst seoMap = await seoRepo.getMany(\n\t\tcollection,\n\t\titems.map((i) => i.id),\n\t);\n\tfor (const item of items) {\n\t\titem.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };\n\t}\n}\n\nasync function hydrateBylines(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n): Promise<void> {\n\tconst bylineRepo = new BylineRepository(db);\n\tconst bylines = await bylineRepo.getContentBylines(collection, item.id);\n\n\tif (bylines.length > 0) {\n\t\titem.bylines = bylines.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\titem.byline = bylines[0]?.byline ?? null;\n\t\treturn;\n\t}\n\n\t// Defensive: if primaryBylineId is set but no junction rows exist, it's orphaned\n\tif (item.primaryBylineId) {\n\t\titem.primaryBylineId = null;\n\t}\n\n\tif (item.authorId) {\n\t\tconst fallback = await bylineRepo.findByUserId(item.authorId);\n\t\tif (fallback) {\n\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\titem.byline = fallback;\n\t\t\treturn;\n\t\t}\n\t}\n\n\titem.bylines = [];\n\titem.byline = null;\n}\n\n/**\n * Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.\n */\nasync function hydrateBylinesMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n): Promise<void> {\n\tif (items.length === 0) return;\n\n\tconst bylineRepo = new BylineRepository(db);\n\n\t// 1. Batch fetch all explicit byline credits\n\tconst contentIds = items.map((i) => i.id);\n\tconst bylinesMap = await bylineRepo.getContentBylinesMany(collection, contentIds);\n\n\t// 2. Collect authorIds that need fallback lookup\n\tconst fallbackAuthorIds: string[] = [];\n\tfor (const item of items) {\n\t\tif (!bylinesMap.has(item.id) && item.authorId) {\n\t\t\tfallbackAuthorIds.push(item.authorId);\n\t\t}\n\t}\n\n\t// 3. Batch fetch user-linked bylines for fallback\n\tconst uniqueAuthorIds = [...new Set(fallbackAuthorIds)];\n\tconst authorBylineMap = await bylineRepo.findByUserIds(uniqueAuthorIds);\n\n\t// 4. Assign to each item\n\tfor (const item of items) {\n\t\tconst explicit = bylinesMap.get(item.id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\titem.bylines = explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\t\titem.byline = explicit[0]?.byline ?? null;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Defensive: if primaryBylineId is set but no junction rows exist, it's orphaned\n\t\tif (item.primaryBylineId) {\n\t\t\titem.primaryBylineId = null;\n\t\t}\n\n\t\tif (item.authorId) {\n\t\t\tconst fallback = authorBylineMap.get(item.authorId);\n\t\t\tif (fallback) {\n\t\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\t\titem.byline = fallback;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\titem.bylines = [];\n\t\titem.byline = null;\n\t}\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID.\n * Returns the ID if found, null if not found.\n * When locale is provided, slug lookups are scoped to that locale.\n */\nasync function resolveId(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlug(collection, identifier, locale);\n\treturn item?.id ?? null;\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID,\n * including trashed (soft-deleted) items.\n */\nasync function resolveIdIncludingTrashed(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale);\n\treturn item?.id ?? null;\n}\n\n/**\n * Trashed content item with deletion timestamp\n */\nexport interface TrashedContentItem {\n\tid: string;\n\ttype: string;\n\tslug: string | null;\n\tstatus: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tpublishedAt: string | null;\n\tdeletedAt: string;\n}\n\n/**\n * Create content list handler\n */\nexport async function handleContentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tparams: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tstatus?: string;\n\t\torderBy?: string;\n\t\torder?: \"asc\" | \"desc\";\n\t\tlocale?: string;\n\t},\n): Promise<ApiResult<ContentListResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst where: { status?: string; locale?: string } = {};\n\t\tif (params.status) where.status = params.status;\n\t\tif (params.locale) where.locale = params.locale;\n\n\t\tconst result = await repo.findMany(collection, {\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: params.limit || 50,\n\t\t\twhere: Object.keys(where).length > 0 ? where : undefined,\n\t\t\torderBy: params.orderBy\n\t\t\t\t? { field: params.orderBy, direction: params.order || \"desc\" }\n\t\t\t\t: undefined,\n\t\t});\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeoMany(db, collection, result.items, hasSeo);\n\t\tawait hydrateBylinesMany(db, collection, result.items);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t\ttotal: result.total,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\t// e.g. invalid orderBy field\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single content item\n */\nexport async function handleContentGet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(collection, id, locale);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a content item by id, including trashed items.\n * Used by restore endpoint for ownership checks on soft-deleted items.\n */\nexport async function handleContentGetIncludingTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlugIncludingTrashed(collection, id, locale);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create content item.\n *\n * Content + SEO writes are wrapped in a transaction so either both succeed\n * or neither does. If `body.seo` is provided for a non-SEO collection, the\n * API returns a validation error rather than silently dropping it.\n */\nexport async function handleContentCreate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tbody: {\n\t\tdata: Record<string, unknown>;\n\t\tslug?: string;\n\t\tstatus?: string;\n\t\tauthorId?: string;\n\t\tbylines?: ContentBylineInput[];\n\t\tlocale?: string;\n\t\ttranslationOf?: string;\n\t\tseo?: ContentSeoInput;\n\t\tcreatedAt?: string | null;\n\t\tpublishedAt?: string | null;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\tif (!mimeCheck.success) return mimeCheck;\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Auto-generate slug from title/name if not explicitly provided\n\t\t\tlet slug: string | null | undefined = body.slug;\n\t\t\tif (!slug) {\n\t\t\t\tconst slugSource = getSlugSource(body.data);\n\t\t\t\tif (slugSource) {\n\t\t\t\t\tslug = await repo.generateUniqueSlug(collection, slugSource, body.locale);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst created = await repo.create({\n\t\t\t\ttype: collection,\n\t\t\t\tslug,\n\t\t\t\tdata: body.data,\n\t\t\t\tstatus: body.status || \"draft\",\n\t\t\t\tauthorId: body.authorId,\n\t\t\t\tlocale: body.locale,\n\t\t\t\ttranslationOf: body.translationOf,\n\t\t\t\tcreatedAt: body.createdAt,\n\t\t\t\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tawait bylineRepo.setContentBylines(collection, created.id, body.bylines);\n\t\t\t\tcreated.primaryBylineId = body.bylines[0]?.bylineId ?? null;\n\t\t\t}\n\t\t\tawait hydrateBylines(trx, collection, created);\n\n\t\t\t// When this row is a translation of an existing item, inherit the\n\t\t\t// source's taxonomy assignments. The pivot stores translation_groups\n\t\t\t// so the copied rows apply to every locale of the translation group\n\t\t\t// (existing per-locale assignments still resolve correctly in\n\t\t\t// `getEntryTerms` because the join picks the locale-specific row).\n\t\t\tif (body.translationOf) {\n\t\t\t\tconst { TaxonomyRepository } = await import(\"../../database/repositories/taxonomy.js\");\n\t\t\t\tconst taxRepo = new TaxonomyRepository(trx);\n\t\t\t\tawait taxRepo.copyEntryTerms(collection, body.translationOf, created.id);\n\t\t\t}\n\n\t\t\t// Side-write SEO data if provided\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tcreated.seo = await seoRepo.upsert(collection, created.id, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\t// Assign defaults in-memory — no DB round-trip needed\n\t\t\t\tcreated.seo = { ...SEO_DEFAULTS };\n\t\t\t}\n\n\t\t\treturn created;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\t// SQLite UNIQUE constraint OR Postgres unique_violation — slug\n\t\t// collisions and any other unique violations land here. Match\n\t\t// specifically on \"unique constraint failed\" / \"duplicate key\" so we\n\t\t// don't false-positive on NOT NULL or CHECK violations whose\n\t\t// messages also contain \"constraint failed\".\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\t// Detect slug-specific collisions by message fingerprint\n\t\t\tif (message.includes(\"slug\")) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? \"(auto-generated)\"}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content create error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update content item.\n * If `_rev` is provided, validates it against the current version before writing.\n * No `_rev` = blind write (backwards-compatible for admin UI).\n *\n * Content + SEO writes are wrapped in a transaction for atomicity.\n */\nexport async function handleContentUpdate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tbody: {\n\t\tdata?: Record<string, unknown>;\n\t\tslug?: string;\n\t\tstatus?: string;\n\t\tauthorId?: string | null;\n\t\tbylines?: ContentBylineInput[];\n\t\t_rev?: string;\n\t\tseo?: ContentSeoInput;\n\t\tpublishedAt?: string | null;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (body.data) {\n\t\t\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\t\tif (!mimeCheck.success) return mimeCheck;\n\t\t}\n\n\t\tconst repo = new ContentRepository(db);\n\n\t\t// Resolve slug → ID if needed\n\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity.\n\t\t// The _rev check is inside the transaction so the read-then-write\n\t\t// is atomic -- no concurrent write can slip between the check and update.\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Read existing item once for both _rev check and old slug capture\n\t\t\tconst existing =\n\t\t\t\tbody._rev || body.slug ? await trxRepo.findById(collection, resolvedId) : null;\n\n\t\t\t// Validate _rev if provided (optimistic concurrency)\n\t\t\tif (body._rev) {\n\t\t\t\tif (!existing) {\n\t\t\t\t\tthrow Object.assign(new Error(`Content item not found: ${id}`), {\n\t\t\t\t\t\tapiError: { code: \"NOT_FOUND\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst revCheck = validateRev(body._rev, existing);\n\t\t\t\tif (!revCheck.valid) {\n\t\t\t\t\tthrow Object.assign(new Error(revCheck.message), {\n\t\t\t\t\t\tapiError: { code: \"CONFLICT\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Capture old slug before update for auto-redirect\n\t\t\tlet oldSlug: string | undefined;\n\t\t\tif (body.slug && existing?.slug && existing.slug !== body.slug) {\n\t\t\t\toldSlug = existing.slug;\n\t\t\t}\n\n\t\t\tconst updated = await trxRepo.update(collection, resolvedId, {\n\t\t\t\tdata: body.data,\n\t\t\t\tslug: body.slug,\n\t\t\t\tstatus: body.status,\n\t\t\t\tauthorId: body.authorId,\n\t\t\t\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tawait bylineRepo.setContentBylines(collection, resolvedId, body.bylines);\n\t\t\t\tupdated.primaryBylineId = body.bylines[0]?.bylineId ?? null;\n\t\t\t}\n\n\t\t\t// Create auto-redirect when slug changes\n\t\t\tif (oldSlug && body.slug) {\n\t\t\t\tconst collectionRow = await trx\n\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t.select(\"url_pattern\")\n\t\t\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t\t\t.executeTakeFirst();\n\n\t\t\t\tconst redirectRepo = new RedirectRepository(trx);\n\t\t\t\tawait redirectRepo.createAutoRedirect(\n\t\t\t\t\tcollection,\n\t\t\t\t\toldSlug,\n\t\t\t\t\tbody.slug,\n\t\t\t\t\tresolvedId,\n\t\t\t\t\tcollectionRow?.url_pattern ?? null,\n\t\t\t\t);\n\t\t\t\tinvalidateRedirectCache();\n\t\t\t}\n\n\t\t\t// Sync non-translatable fields to sibling locales in the same\n\t\t\t// translation group. Only runs when i18n is enabled, data was updated,\n\t\t\t// and the item belongs to a translation group with siblings.\n\t\t\tif (isI18nEnabled() && body.data && updated.translationGroup) {\n\t\t\t\tawait syncNonTranslatableFields(\n\t\t\t\t\ttrx,\n\t\t\t\t\tcollection,\n\t\t\t\t\tupdated.id,\n\t\t\t\t\tupdated.translationGroup,\n\t\t\t\t\tbody.data,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Side-write SEO data if provided, always hydrate for SEO-enabled collections\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.upsert(collection, resolvedId, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.get(collection, resolvedId);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, updated);\n\n\t\t\treturn updated;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\t// Handle structured errors thrown from inside the transaction\n\t\t// (rev check failures, not-found)\n\t\tif (hasApiError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: error.apiError.code, message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\tif (message.includes(\"slug\")) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content update error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Duplicate content item.\n *\n * Only copies SEO data if the collection has SEO enabled.\n * Always returns consistent `seo` shape for SEO-enabled collections.\n */\nexport async function handleContentDuplicate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tauthorId?: string,\n): Promise<ApiResult<{ item: ContentItem }>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Wrap duplicate + SEO copy in a transaction for atomicity\n\t\tconst duplicate = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\tconst dup = await repo.duplicate(collection, resolvedId, authorId);\n\n\t\t\tconst existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);\n\t\t\tif (existingBylines.length > 0) {\n\t\t\t\tawait bylineRepo.setContentBylines(\n\t\t\t\t\tcollection,\n\t\t\t\t\tdup.id,\n\t\t\t\t\texistingBylines.map((entry) => ({\n\t\t\t\t\t\tbylineId: entry.byline.id,\n\t\t\t\t\t\troleLabel: entry.roleLabel,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (hasSeo) {\n\t\t\t\t// Copy SEO data from the original (clears canonical)\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.copyForDuplicate(collection, resolvedId, dup.id);\n\t\t\t\t// Always hydrate SEO for consistent response shape\n\t\t\t\tdup.seo = await seoRepo.get(collection, dup.id);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, dup);\n\n\t\t\treturn dup;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item: duplicate },\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: err.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content duplicate error:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DUPLICATE_ERROR\",\n\t\t\t\tmessage: \"Failed to duplicate content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete content item (soft delete - moves to trash)\n */\nexport async function handleContentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst deleted = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.delete(collection, resolvedId);\n\t\t});\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore content item from trash\n */\nexport async function handleContentRestore(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ restored: true }>> {\n\ttry {\n\t\tconst restored = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\t\t\treturn repo.restore(collection, resolvedId);\n\t\t});\n\n\t\tif (!restored) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Trashed content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { restored: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content restore error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Permanently delete content item (cannot be undone).\n * Also cleans up associated SEO data.\n */\nexport async function handleContentPermanentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\n\t\t// Wrap content delete + SEO/comment cleanup in a transaction\n\t\tconst deleted = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst wasDeleted = await trxRepo.permanentDelete(collection, resolvedId);\n\n\t\t\tif (wasDeleted) {\n\t\t\t\t// Clean up SEO data for permanently deleted content\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.delete(collection, resolvedId);\n\t\t\t\t// Clean up comments for permanently deleted content\n\t\t\t\tconst commentRepo = new CommentRepository(trx);\n\t\t\t\tawait commentRepo.deleteByContent(collection, resolvedId);\n\t\t\t\t// Clean up revisions for permanently deleted content\n\t\t\t\tconst revisionRepo = new RevisionRepository(trx);\n\t\t\t\tawait revisionRepo.deleteByEntry(collection, resolvedId);\n\t\t\t}\n\n\t\t\treturn wasDeleted;\n\t\t});\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content permanent delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to permanently delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List trashed content items\n */\nexport async function handleContentListTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\toptions: { limit?: number; cursor?: string } = {},\n): Promise<ApiResult<{ items: TrashedContentItem[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst result = await repo.findTrashed(collection, {\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items.map((item) => ({\n\t\t\t\t\tid: item.id,\n\t\t\t\t\ttype: item.type,\n\t\t\t\t\tslug: item.slug,\n\t\t\t\t\tstatus: item.status,\n\t\t\t\t\tdata: item.data,\n\t\t\t\t\tauthorId: item.authorId,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t\tdeletedAt: item.deletedAt,\n\t\t\t\t})),\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content list trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count trashed content items\n */\nexport async function handleContentCountTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countTrashed(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Schedule content for future publishing\n */\nexport async function handleContentSchedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tscheduledAt: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.schedule(collection, resolvedId, scheduledAt);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content schedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_SCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to schedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unschedule content (revert to draft)\n */\nexport async function handleContentUnschedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unschedule(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content unschedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNSCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to unschedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Publish content immediately.\n *\n * Wrapped in a transaction because publish performs multiple writes\n * (syncDataColumns, slug sync, status/revision update) that must\n * be atomic to prevent FTS shadow table corruption on crash.\n */\nexport async function handleContentPublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\toptions: { publishedAt?: string } = {},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.publish(collection, resolvedId, options.publishedAt);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content publish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_PUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to publish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unpublish content (revert to draft).\n *\n * Wrapped in a transaction — unpublish may create a draft revision\n * from the live version then update the status, which is multi-step.\n */\nexport async function handleContentUnpublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unpublish(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content unpublish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNPUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to unpublish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count scheduled content items\n */\nexport async function handleContentCountScheduled(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countScheduled(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count scheduled error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count scheduled content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Discard draft changes (revert to live version)\n */\nexport async function handleContentDiscardDraft(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.discardDraft(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content discard draft error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DISCARD_DRAFT_ERROR\",\n\t\t\t\tmessage: \"Failed to discard draft\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Compare live and draft revisions\n */\nexport async function handleContentCompare(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\thasChanges: boolean;\n\t\tlive: Record<string, unknown> | null;\n\t\tdraft: Record<string, unknown> | null;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst entry = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!entry) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(db);\n\n\t\tconst live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;\n\t\tconst draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\thasChanges:\n\t\t\t\t\tentry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,\n\t\t\t\tlive: live?.data ?? null,\n\t\t\t\tdraft: draft?.data ?? null,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content compare error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COMPARE_ERROR\",\n\t\t\t\tmessage: \"Failed to compare revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get all translations for a content item.\n * Returns the item's translation group members with locale and status info.\n */\nexport async function handleContentTranslations(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\ttranslationGroup: string;\n\t\ttranslations: Array<{\n\t\t\tid: string;\n\t\t\tlocale: string | null;\n\t\t\tslug: string | null;\n\t\t\tstatus: string;\n\t\t\tupdatedAt: string;\n\t\t}>;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (!item.translationGroup) {\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\ttranslationGroup: item.id,\n\t\t\t\t\ttranslations: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\t\t\tslug: item.slug,\n\t\t\t\t\t\t\tstatus: item.status,\n\t\t\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst translations = await repo.findTranslations(collection, item.translationGroup);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: item.translationGroup,\n\t\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\t\tid: t.id,\n\t\t\t\t\tlocale: t.locale,\n\t\t\t\t\tslug: t.slug,\n\t\t\t\t\tstatus: t.status,\n\t\t\t\t\tupdatedAt: t.updatedAt,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tconsole.error(\"Content translations error:\", error);\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_TRANSLATIONS_ERROR\",\n\t\t\t\tmessage: \"Failed to get translations\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Non-translatable field sync\n// ---------------------------------------------------------------------------\n\n/**\n * Sync non-translatable fields to sibling locales.\n *\n * When a content item is updated and it belongs to a translation group,\n * any non-translatable fields in the update data are written to all other\n * rows in the same translation group within the same transaction.\n *\n * Non-translatable fields are **copied, not linked** — each row owns its\n * own data. This keeps queries simple and avoids cross-row joins.\n */\nasync function syncNonTranslatableFields(\n\ttrx: Kysely<Database>,\n\tcollectionSlug: string,\n\tupdatedItemId: string,\n\ttranslationGroup: string,\n\tdata: Record<string, unknown>,\n): Promise<void> {\n\t// Get the collection to find its fields\n\tconst collection = await trx\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"id\")\n\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t.executeTakeFirst();\n\n\tif (!collection) return;\n\n\t// Find non-translatable fields that are present in the update data\n\tconst fields = await trx\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select(\"slug\")\n\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t.where(\"translatable\", \"=\", 0)\n\t\t.execute();\n\n\tconst nonTranslatableSlugs = fields.map((f) => f.slug);\n\tif (nonTranslatableSlugs.length === 0) return;\n\n\t// Filter to only the non-translatable fields present in this update\n\tconst syncData: Record<string, unknown> = {};\n\tfor (const slug of nonTranslatableSlugs) {\n\t\tif (slug in data) {\n\t\t\tsyncData[slug] = data[slug];\n\t\t}\n\t}\n\tif (Object.keys(syncData).length === 0) return;\n\n\t// Build the SET clause for sibling rows\n\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\tconst tableName = `ec_${collectionSlug}`;\n\n\t// Update all sibling rows (same translation_group, different id)\n\tconst setClauses = Object.entries(syncData).map(([key, value]) => {\n\t\tvalidateIdentifier(key, \"field slug\");\n\t\tconst serialized = typeof value === \"object\" && value !== null ? JSON.stringify(value) : value;\n\t\treturn sql`${sql.ref(key)} = ${serialized}`;\n\t});\n\n\tawait sql`\n\t\tUPDATE ${sql.ref(tableName)}\n\t\tSET ${sql.join(setClauses, sql`, `)}\n\t\tWHERE translation_group = ${translationGroup}\n\t\tAND id != ${updatedItemId}\n\t`.execute(trx);\n}\n","/**\n * Manifest generation handlers\n */\n\nimport { hashString } from \"../../utils/hash.js\";\nimport type { ManifestResponse, FieldDescriptor } from \"../types.js\";\n\n/** Pattern to add spaces before capital letters */\nconst CAMEL_CASE_PATTERN = /([A-Z])/g;\nconst FIRST_CHAR_PATTERN = /^./;\n\n// Collection definition shape for manifest generation\ninterface CollectionDefinition {\n\tschema: {\n\t\t_def?: { shape?: () => Record<string, unknown> };\n\t\tshape?: Record<string, unknown>;\n\t};\n\tadmin: {\n\t\tlabel: string;\n\t\tlabelSingular?: string;\n\t\tsupports?: string[];\n\t};\n}\ntype CollectionMap = Record<string, CollectionDefinition>;\n\n/**\n * Generate admin manifest from collections\n */\nexport async function generateManifest(\n\tcollections: CollectionMap,\n\tplugins: Record<\n\t\tstring,\n\t\t{\n\t\t\tadminPages?: Array<{ path: string; component: string }>;\n\t\t\twidgets?: string[];\n\t\t}\n\t> = {},\n): Promise<ManifestResponse> {\n\tconst manifestCollections: ManifestResponse[\"collections\"] = {};\n\n\tfor (const [name, definition] of Object.entries(collections)) {\n\t\t// Extract field descriptors from Zod schema\n\t\tconst fields = extractFieldDescriptors(definition.schema);\n\n\t\tmanifestCollections[name] = {\n\t\t\tlabel: definition.admin.label,\n\t\t\tlabelSingular: definition.admin.labelSingular || definition.admin.label,\n\t\t\tsupports: definition.admin.supports || [],\n\t\t\tfields,\n\t\t};\n\t}\n\n\t// Generate hash from collections (for cache invalidation)\n\tconst hash = await hashString(JSON.stringify(manifestCollections));\n\n\treturn {\n\t\tversion: \"0.1.0\",\n\t\thash,\n\t\tcollections: manifestCollections,\n\t\tplugins,\n\t};\n}\n\n/**\n * Extract field descriptors from Zod schema\n * Note: This is a simplified implementation that handles common types\n */\nfunction extractFieldDescriptors(schema: {\n\t_def?: { shape?: () => Record<string, unknown> };\n\tshape?: Record<string, unknown>;\n}): Record<string, FieldDescriptor> {\n\tconst fields: Record<string, FieldDescriptor> = {};\n\n\t// Handle Zod object schema\n\tconst shape = typeof schema._def?.shape === \"function\" ? schema._def.shape() : schema.shape || {};\n\n\tfor (const [name, fieldSchema] of Object.entries(shape)) {\n\t\tfields[name] = extractFieldType(name, fieldSchema);\n\t}\n\n\treturn fields;\n}\n\n/**\n * Extract field type from Zod schema\n */\n/** Type guard: check if a value is a non-null object */\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction extractFieldType(name: string, schema: unknown): FieldDescriptor {\n\tif (!isObject(schema)) {\n\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n\n\t// Check for custom field markers\n\tif (schema.isPortableText) {\n\t\treturn { kind: \"portableText\", label: formatLabel(name) };\n\t}\n\tif (schema.isImage) {\n\t\treturn { kind: \"image\", label: formatLabel(name) };\n\t}\n\tif (schema.isReference) {\n\t\treturn { kind: \"reference\", label: formatLabel(name) };\n\t}\n\n\t// Handle standard Zod types\n\tconst def = isObject(schema._def) ? schema._def : undefined;\n\tconst typeName = typeof def?.typeName === \"string\" ? def.typeName : undefined;\n\n\tswitch (typeName) {\n\t\tcase \"ZodString\":\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tcase \"ZodNumber\":\n\t\t\treturn { kind: \"number\", label: formatLabel(name) };\n\t\tcase \"ZodBoolean\":\n\t\t\treturn { kind: \"boolean\", label: formatLabel(name) };\n\t\tcase \"ZodDate\":\n\t\t\treturn { kind: \"datetime\", label: formatLabel(name) };\n\t\tcase \"ZodEnum\": {\n\t\t\tconst values = Array.isArray(def?.values) ? def.values : [];\n\t\t\treturn {\n\t\t\t\tkind: \"select\",\n\t\t\t\tlabel: formatLabel(name),\n\t\t\t\toptions: values\n\t\t\t\t\t.filter((v): v is string => typeof v === \"string\")\n\t\t\t\t\t.map((v) => ({\n\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t})),\n\t\t\t};\n\t\t}\n\t\tcase \"ZodArray\":\n\t\t\treturn { kind: \"array\", label: formatLabel(name) };\n\t\tcase \"ZodObject\":\n\t\t\treturn { kind: \"object\", label: formatLabel(name) };\n\t\tcase \"ZodOptional\":\n\t\tcase \"ZodDefault\":\n\t\t\t// Unwrap optional/default types\n\t\t\tif (def?.innerType) {\n\t\t\t\treturn extractFieldType(name, def.innerType);\n\t\t\t}\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tdefault:\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n}\n\n/**\n * Format field name as label\n */\nfunction formatLabel(name: string): string {\n\treturn name\n\t\t.replace(CAMEL_CASE_PATTERN, \" $1\")\n\t\t.replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase())\n\t\t.trim();\n}\n","/**\n * Revision history handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RevisionRepository, type Revision } from \"../../database/repositories/revision.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult, ContentResponse } from \"../types.js\";\n\nexport interface RevisionListResponse {\n\titems: Revision[];\n\ttotal: number;\n}\n\nexport interface RevisionResponse {\n\titem: Revision;\n}\n\n/**\n * List revisions for a content entry\n */\nexport async function handleRevisionList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tparams: { limit?: number } = {},\n): Promise<ApiResult<RevisionListResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst [items, total] = await Promise.all([\n\t\t\trepo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }),\n\t\t\trepo.countByEntry(collection, entryId),\n\t\t]);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items, total },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a specific revision\n */\nexport async function handleRevisionGet(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n): Promise<ApiResult<RevisionResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst item = await repo.findById(revisionId);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore a revision (updates content to this revision's data and creates new revision)\n */\nexport async function handleRevisionRestore(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n\tcallerUserId: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst revisionRepo = new RevisionRepository(db);\n\n\t\t// Get the revision\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Extract _slug from revision data (stored as metadata, not a real column)\n\t\tconst { _slug, ...fieldData } = revision.data;\n\n\t\t// Atomically update content and create a new revision to record the restore.\n\t\t// If either operation fails, neither is committed (on engines that support\n\t\t// transactions; on D1, withTransaction falls back to sequential execution).\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\tconst trxRevisionRepo = new RevisionRepository(trx);\n\n\t\t\tconst updated = await trxContentRepo.update(revision.collection, revision.entryId, {\n\t\t\t\tdata: fieldData,\n\t\t\t\tslug: typeof _slug === \"string\" ? _slug : undefined,\n\t\t\t});\n\n\t\t\tawait trxRevisionRepo.create({\n\t\t\t\tcollection: revision.collection,\n\t\t\t\tentryId: revision.entryId,\n\t\t\t\tdata: revision.data,\n\t\t\t\tauthorId: callerUserId,\n\t\t\t});\n\n\t\t\treturn updated;\n\t\t});\n\n\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\tconst pruneRepo = new RevisionRepository(db);\n\t\tvoid pruneRepo.pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Media CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface MediaListResponse {\n\titems: MediaItem[];\n\tnextCursor?: string;\n}\n\nexport interface MediaResponse {\n\titem: MediaItem;\n}\n\n/**\n * List media items\n */\nexport async function handleMediaList(\n\tdb: Kysely<Database>,\n\tparams: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tmimeType?: string | readonly string[];\n\t},\n): Promise<ApiResult<MediaListResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst result = await repo.findMany({\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: Math.min(params.limit || 50, 100),\n\t\t\tmimeType: params.mimeType,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single media item\n */\nexport async function handleMediaGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.findById(id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create media item (after file upload)\n */\nexport async function handleMediaCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\talt?: string;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t\tauthorId?: string;\n\t},\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.create(input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update media metadata\n */\nexport async function handleMediaUpdate(\n\tdb: Kysely<Database>,\n\tid: string,\n\tinput: {\n\t\talt?: string;\n\t\tcaption?: string;\n\t\twidth?: number;\n\t\theight?: number;\n\t},\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.update(id, input);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete media item\n */\nexport async function handleMediaDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete media\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Schema/collection management handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport {\n\tSchemaRegistry,\n\tSchemaError,\n\ttype Collection,\n\ttype Field,\n\ttype CreateCollectionInput,\n\ttype UpdateCollectionInput,\n\ttype CreateFieldInput,\n\ttype UpdateFieldInput,\n\ttype CollectionWithFields,\n} from \"../../schema/index.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface CollectionListResponse {\n\titems: Collection[];\n}\n\nexport interface CollectionResponse {\n\titem: Collection;\n}\n\nexport interface CollectionWithFieldsResponse {\n\titem: CollectionWithFields;\n}\n\nexport interface FieldListResponse {\n\titems: Field[];\n}\n\nexport interface FieldResponse {\n\titem: Field;\n}\n\n/**\n * List all collections\n */\nexport async function handleSchemaCollectionList(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<CollectionListResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst items = await registry.listCollections();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list collections\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a collection by slug\n */\nexport async function handleSchemaCollectionGet(\n\tdb: Kysely<Database>,\n\tslug: string,\n\toptions?: { includeFields?: boolean },\n): Promise<ApiResult<CollectionResponse | CollectionWithFieldsResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\n\t\tif (options?.includeFields) {\n\t\t\tconst item = await registry.getCollectionWithFields(slug);\n\t\t\tif (!item) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\t\tmessage: `Collection not found: ${slug}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { item },\n\t\t\t};\n\t\t}\n\n\t\tconst item = await registry.getCollection(slug);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection not found: ${slug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get collection\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create a collection\n */\nexport async function handleSchemaCollectionCreate(\n\tdb: Kysely<Database>,\n\tinput: CreateCollectionInput,\n): Promise<ApiResult<CollectionResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.createCollection(input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"[emdash] Failed to create collection:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create collection\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update a collection\n */\nexport async function handleSchemaCollectionUpdate(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tinput: UpdateCollectionInput,\n): Promise<ApiResult<CollectionResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.updateCollection(slug, input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update collection\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete a collection\n */\nexport async function handleSchemaCollectionDelete(\n\tdb: Kysely<Database>,\n\tslug: string,\n\toptions?: { force?: boolean },\n): Promise<ApiResult<{ success: boolean }>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tawait registry.deleteCollection(slug, options);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { success: true },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete collection\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List fields for a collection\n */\nexport async function handleSchemaFieldList(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n): Promise<ApiResult<FieldListResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collection = await registry.getCollection(collectionSlug);\n\n\t\tif (!collection) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection not found: ${collectionSlug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst items = await registry.listFields(collection.id);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list fields\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a field\n */\nexport async function handleSchemaFieldGet(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tfieldSlug: string,\n): Promise<ApiResult<FieldResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.getField(collectionSlug, fieldSlug);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Field not found: ${fieldSlug} in collection ${collectionSlug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get field\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create a field\n */\nexport async function handleSchemaFieldCreate(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tinput: CreateFieldInput,\n): Promise<ApiResult<FieldResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.createField(collectionSlug, input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create field\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update a field\n */\nexport async function handleSchemaFieldUpdate(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tfieldSlug: string,\n\tinput: UpdateFieldInput,\n): Promise<ApiResult<FieldResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.updateField(collectionSlug, fieldSlug, input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update field\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete a field\n */\nexport async function handleSchemaFieldDelete(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tfieldSlug: string,\n): Promise<ApiResult<{ success: boolean }>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tawait registry.deleteField(collectionSlug, fieldSlug);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { success: true },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete field\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Reorder fields\n */\nexport async function handleSchemaFieldReorder(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tfieldSlugs: string[],\n): Promise<ApiResult<{ success: boolean }>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tawait registry.reorderFields(collectionSlug, fieldSlugs);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { success: true },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SCHEMA_FIELD_REORDER_ERROR\",\n\t\t\t\tmessage: \"Failed to reorder fields\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ============================================\n// Orphaned Table Discovery\n// ============================================\n\nexport interface OrphanedTable {\n\tslug: string;\n\ttableName: string;\n\trowCount: number;\n}\n\nexport interface OrphanedTableListResponse {\n\titems: OrphanedTable[];\n}\n\n/**\n * List orphaned content tables\n */\nexport async function handleOrphanedTableList(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<OrphanedTableListResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst items = await registry.discoverOrphanedTables();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"[emdash] Failed to list orphaned tables:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"ORPHAN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list orphaned tables\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Register an orphaned table as a collection\n */\nexport async function handleOrphanedTableRegister(\n\tdb: Kysely<Database>,\n\tslug: string,\n\toptions?: {\n\t\tlabel?: string;\n\t\tlabelSingular?: string;\n\t\tdescription?: string;\n\t},\n): Promise<ApiResult<CollectionResponse>> {\n\ttry {\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst item = await registry.registerOrphanedTable(slug, options);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SchemaError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: error.code,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: error.details,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"ORPHAN_REGISTER_ERROR\",\n\t\t\t\tmessage: \"Failed to register orphaned table\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Plugin State Repository\n *\n * Database-backed storage for plugin activation state.\n * Used by the admin API to persist plugin enable/disable across restarts.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nexport type PluginStatus = \"active\" | \"inactive\";\nexport type PluginSource = \"config\" | \"marketplace\" | \"registry\";\n\nfunction toPluginStatus(value: string): PluginStatus {\n\tif (value === \"active\") return \"active\";\n\treturn \"inactive\";\n}\n\nfunction toPluginSource(value: string | undefined | null): PluginSource {\n\tif (value === \"marketplace\") return \"marketplace\";\n\tif (value === \"registry\") return \"registry\";\n\treturn \"config\";\n}\n\nexport interface PluginState {\n\tpluginId: string;\n\tstatus: PluginStatus;\n\tversion: string;\n\tinstalledAt: Date;\n\tactivatedAt: Date | null;\n\tdeactivatedAt: Date | null;\n\tsource: PluginSource;\n\tmarketplaceVersion: string | null;\n\tdisplayName: string | null;\n\tdescription: string | null;\n\t/**\n\t * Publisher DID this plugin was published under. Populated only when\n\t * `source === \"registry\"`; null otherwise.\n\t */\n\tregistryPublisherDid: string | null;\n\t/**\n\t * Slug under which the plugin was published in the publisher's repo\n\t * (the rkey of the `pm.fair.package.profile` record). Populated only\n\t * when `source === \"registry\"`; null otherwise.\n\t *\n\t * The opaque `pluginId` for registry installs is derived from\n\t * `(registryPublisherDid, registrySlug)` -- see\n\t * `packages/core/src/registry/plugin-id.ts`.\n\t */\n\tregistrySlug: string | null;\n}\n\n/**\n * Repository for plugin state in the database\n */\nexport class PluginStateRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Get state for a specific plugin\n\t */\n\tasync get(pluginId: string): Promise<PluginState | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t.selectAll()\n\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) return null;\n\n\t\treturn rowToPluginState(row);\n\t}\n\n\t/**\n\t * Get all plugin states\n\t */\n\tasync getAll(): Promise<PluginState[]> {\n\t\tconst rows = await this.db.selectFrom(\"_plugin_state\").selectAll().execute();\n\t\treturn rows.map(rowToPluginState);\n\t}\n\n\t/**\n\t * Get all marketplace-installed plugin states\n\t */\n\tasync getMarketplacePlugins(): Promise<PluginState[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", \"marketplace\")\n\t\t\t.execute();\n\t\treturn rows.map(rowToPluginState);\n\t}\n\n\t/**\n\t * Get all registry-installed plugin states.\n\t *\n\t * The runtime's registry sync path uses this to discover which\n\t * registry plugins should be loaded into the sandbox on this worker.\n\t */\n\tasync getRegistryPlugins(): Promise<PluginState[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", \"registry\")\n\t\t\t.execute();\n\t\treturn rows.map(rowToPluginState);\n\t}\n\n\t/**\n\t * Create or update plugin state\n\t */\n\tasync upsert(\n\t\tpluginId: string,\n\t\tversion: string,\n\t\tstatus: PluginStatus,\n\t\topts?: {\n\t\t\tsource?: PluginSource;\n\t\t\tmarketplaceVersion?: string;\n\t\t\tdisplayName?: string;\n\t\t\tdescription?: string;\n\t\t\tregistryPublisherDid?: string;\n\t\t\tregistrySlug?: string;\n\t\t},\n\t): Promise<PluginState> {\n\t\tconst now = new Date().toISOString();\n\t\tconst existing = await this.get(pluginId);\n\n\t\tif (existing) {\n\t\t\t// Update existing state\n\t\t\tconst updates: Record<string, string | null> = {\n\t\t\t\tstatus,\n\t\t\t\tversion,\n\t\t\t};\n\n\t\t\tif (status === \"active\" && existing.status !== \"active\") {\n\t\t\t\tupdates.activated_at = now;\n\t\t\t} else if (status === \"inactive\" && existing.status !== \"inactive\") {\n\t\t\t\tupdates.deactivated_at = now;\n\t\t\t}\n\n\t\t\tif (opts?.source) updates.source = opts.source;\n\t\t\tif (opts?.marketplaceVersion !== undefined) {\n\t\t\t\tupdates.marketplace_version = opts.marketplaceVersion;\n\t\t\t}\n\t\t\tif (opts?.displayName !== undefined) {\n\t\t\t\tupdates.display_name = opts.displayName;\n\t\t\t}\n\t\t\tif (opts?.description !== undefined) {\n\t\t\t\tupdates.description = opts.description;\n\t\t\t}\n\t\t\tif (opts?.registryPublisherDid !== undefined) {\n\t\t\t\tupdates.registry_publisher_did = opts.registryPublisherDid;\n\t\t\t}\n\t\t\tif (opts?.registrySlug !== undefined) {\n\t\t\t\tupdates.registry_slug = opts.registrySlug;\n\t\t\t}\n\n\t\t\tawait this.db\n\t\t\t\t.updateTable(\"_plugin_state\")\n\t\t\t\t.set(updates)\n\t\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t\t.execute();\n\t\t} else {\n\t\t\t// Create new state\n\t\t\tawait this.db\n\t\t\t\t.insertInto(\"_plugin_state\")\n\t\t\t\t.values({\n\t\t\t\t\tplugin_id: pluginId,\n\t\t\t\t\tstatus,\n\t\t\t\t\tversion,\n\t\t\t\t\tinstalled_at: now,\n\t\t\t\t\tactivated_at: status === \"active\" ? now : null,\n\t\t\t\t\tdeactivated_at: null,\n\t\t\t\t\tdata: null,\n\t\t\t\t\tsource: opts?.source ?? \"config\",\n\t\t\t\t\tmarketplace_version: opts?.marketplaceVersion ?? null,\n\t\t\t\t\tdisplay_name: opts?.displayName ?? null,\n\t\t\t\t\tdescription: opts?.description ?? null,\n\t\t\t\t\tregistry_publisher_did: opts?.registryPublisherDid ?? null,\n\t\t\t\t\tregistry_slug: opts?.registrySlug ?? null,\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t}\n\n\t\treturn (await this.get(pluginId))!;\n\t}\n\n\t/**\n\t * Enable a plugin\n\t */\n\tasync enable(pluginId: string, version: string): Promise<PluginState> {\n\t\treturn this.upsert(pluginId, version, \"active\");\n\t}\n\n\t/**\n\t * Disable a plugin\n\t */\n\tasync disable(pluginId: string, version: string): Promise<PluginState> {\n\t\treturn this.upsert(pluginId, version, \"inactive\");\n\t}\n\n\t/**\n\t * Delete plugin state\n\t */\n\tasync delete(pluginId: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_state\")\n\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n}\n\n/**\n * Internal: map a `_plugin_state` row to the public `PluginState` shape.\n *\n * Kept at module scope so the three select paths (`get`, `getAll`,\n * `getMarketplacePlugins`, `getRegistryPlugins`) stay byte-identical in\n * their handling of nullable columns -- adding a new column to the table\n * means changing this function and nothing else.\n */\ninterface PluginStateRow {\n\tplugin_id: string;\n\tstatus: string;\n\tversion: string;\n\tinstalled_at: string;\n\tactivated_at: string | null;\n\tdeactivated_at: string | null;\n\tsource: string;\n\tmarketplace_version: string | null;\n\tdisplay_name: string | null;\n\tdescription: string | null;\n\tregistry_publisher_did: string | null;\n\tregistry_slug: string | null;\n}\n\nfunction rowToPluginState(row: PluginStateRow): PluginState {\n\treturn {\n\t\tpluginId: row.plugin_id,\n\t\tstatus: toPluginStatus(row.status),\n\t\tversion: row.version,\n\t\tinstalledAt: new Date(row.installed_at),\n\t\tactivatedAt: row.activated_at ? new Date(row.activated_at) : null,\n\t\tdeactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,\n\t\tsource: toPluginSource(row.source),\n\t\tmarketplaceVersion: row.marketplace_version ?? null,\n\t\tdisplayName: row.display_name ?? null,\n\t\tdescription: row.description ?? null,\n\t\tregistryPublisherDid: row.registry_publisher_did ?? null,\n\t\tregistrySlug: row.registry_slug ?? null,\n\t};\n}\n","/**\n * Plugin management handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { PluginStateRepository, type PluginState, type PluginStatus } from \"../../plugins/state.js\";\nimport type { ResolvedPlugin } from \"../../plugins/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface PluginInfo {\n\tid: string;\n\tname: string;\n\tversion: string;\n\tpackage?: string;\n\tenabled: boolean;\n\tstatus: PluginStatus;\n\tsource?: \"config\" | \"marketplace\" | \"registry\";\n\tmarketplaceVersion?: string;\n\t/** Publisher DID, for registry-source plugins */\n\tregistryPublisherDid?: string;\n\t/** Publisher slug, for registry-source plugins */\n\tregistrySlug?: string;\n\tcapabilities: string[];\n\thasAdminPages: boolean;\n\thasDashboardWidgets: boolean;\n\thasHooks: boolean;\n\tinstalledAt?: string;\n\tactivatedAt?: string;\n\tdeactivatedAt?: string;\n\t/** Description of what the plugin does */\n\tdescription?: string;\n\t/** URL to the plugin icon on the marketplace */\n\ticonUrl?: string;\n}\n\nexport interface PluginListResponse {\n\titems: PluginInfo[];\n}\n\nexport interface PluginResponse {\n\titem: PluginInfo;\n}\n\nfunction marketplaceIconUrl(marketplaceUrl: string, pluginId: string): string {\n\treturn `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;\n}\n\n/**\n * Get plugin info from configured plugin and database state\n */\nfunction buildPluginInfo(\n\tplugin: ResolvedPlugin,\n\tstate: PluginState | null,\n\tmarketplaceUrl?: string,\n): PluginInfo {\n\t// If no state exists, plugin is considered active (default on first run)\n\tconst status = state?.status ?? \"active\";\n\tconst enabled = status === \"active\";\n\tconst isMarketplace = (state?.source ?? \"config\") === \"marketplace\";\n\n\treturn {\n\t\tid: plugin.id,\n\t\tname: state?.displayName || plugin.id,\n\t\tversion: plugin.version,\n\t\tpackage: undefined, // v2 doesn't have package field\n\t\tenabled,\n\t\tstatus,\n\t\tsource: state?.source ?? \"config\",\n\t\tmarketplaceVersion: state?.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state?.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state?.registrySlug ?? undefined,\n\t\tcapabilities: plugin.capabilities,\n\t\thasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,\n\t\thasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,\n\t\thasHooks: Object.keys(plugin.hooks ?? {}).length > 0,\n\t\tinstalledAt: state?.installedAt?.toISOString(),\n\t\tactivatedAt: state?.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state?.description ?? undefined,\n\t\ticonUrl:\n\t\t\tisMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : undefined,\n\t};\n}\n\n/**\n * List all configured plugins with their state\n */\nexport async function handlePluginList(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tmarketplaceUrl?: string,\n): Promise<ApiResult<PluginListResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst allStates = await stateRepo.getAll();\n\t\tconst stateMap = new Map(allStates.map((s) => [s.pluginId, s]));\n\n\t\tconst configuredIds = new Set(configuredPlugins.map((p) => p.id));\n\n\t\tconst items = configuredPlugins.map((plugin) => {\n\t\t\tconst state = stateMap.get(plugin.id) ?? null;\n\t\t\treturn buildPluginInfo(plugin, state, marketplaceUrl);\n\t\t});\n\n\t\t// Include runtime-installed plugins (marketplace or registry) that\n\t\t// aren't in the configured plugins list.\n\t\tfor (const state of allStates) {\n\t\t\tif (state.source !== \"marketplace\" && state.source !== \"registry\") continue;\n\t\t\tif (configuredIds.has(state.pluginId)) continue;\n\n\t\t\titems.push({\n\t\t\t\tid: state.pluginId,\n\t\t\t\tname: state.displayName || state.pluginId,\n\t\t\t\tversion: state.marketplaceVersion ?? state.version,\n\t\t\t\tenabled: state.status === \"active\",\n\t\t\t\tstatus: state.status,\n\t\t\t\tsource: state.source,\n\t\t\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\t\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\t\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\t\t\tcapabilities: [],\n\t\t\t\thasAdminPages: false,\n\t\t\t\thasDashboardWidgets: false,\n\t\t\t\thasHooks: false,\n\t\t\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\t\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\t\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\t\t\tdescription: state.description ?? undefined,\n\t\t\t\ticonUrl:\n\t\t\t\t\tstate.source === \"marketplace\" && marketplaceUrl\n\t\t\t\t\t\t? marketplaceIconUrl(marketplaceUrl, state.pluginId)\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list plugins\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a single plugin's info\n */\nexport async function handlePluginGet(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tpluginId: string,\n\tmarketplaceUrl?: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (!plugin) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Plugin not found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst state = await stateRepo.get(pluginId);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item: buildPluginInfo(plugin, state, marketplaceUrl) },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Build a minimal `PluginInfo` for a plugin that exists only as a\n * `_plugin_state` row (marketplace or registry install), with no\n * matching `configuredPlugins` entry. Runtime-installed plugins don't\n * have ResolvedPlugin metadata until they're loaded into the sandbox,\n * so the enable/disable response surfaces the state-row view as a\n * stable shape the admin UI already understands.\n */\nfunction buildStateOnlyPluginInfo(\n\tstate: NonNullable<Awaited<ReturnType<PluginStateRepository[\"get\"]>>>,\n): PluginInfo {\n\treturn {\n\t\tid: state.pluginId,\n\t\tname: state.displayName || state.pluginId,\n\t\tversion: state.marketplaceVersion ?? state.version,\n\t\tenabled: state.status === \"active\",\n\t\tstatus: state.status,\n\t\tsource: state.source,\n\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\tcapabilities: [],\n\t\thasAdminPages: false,\n\t\thasDashboardWidgets: false,\n\t\thasHooks: false,\n\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state.description ?? undefined,\n\t};\n}\n\n/**\n * Enable a plugin\n */\nexport async function handlePluginEnable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\t// Configured plugin: use its version as the source of truth.\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.enable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\t// Runtime-installed plugin (marketplace or registry): only\n\t\t// addressable through the state row. Fall back to the existing\n\t\t// version recorded there.\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tconst enabled = await stateRepo.enable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(enabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_ENABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to enable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Disable a plugin\n */\nexport async function handlePluginDisable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.disable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tconst disabled = await stateRepo.disable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(disabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_DISABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to disable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * MarketplaceClient — HTTP client for the EmDash Plugin Marketplace\n *\n * Used by the install/update/proxy endpoints in EmDash core to communicate\n * with the marketplace Worker. The marketplace is a distribution channel,\n * not a runtime dependency — bundles are copied to site-local R2 at install time.\n */\n\nimport { createGzipDecoder, unpackTar } from \"modern-tar\";\n\nimport { pluginManifestSchema } from \"./manifest-schema.js\";\nimport type { PluginManifest } from \"./types.js\";\n\n// ── Module-level regex patterns ───────────────────────────────────\n\nconst TRAILING_SLASHES = /\\/+$/;\nconst LEADING_DOT_SLASH = /^\\.\\//;\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface MarketplacePluginSummary {\n\tid: string;\n\tname: string;\n\tdescription: string | null;\n\tauthor: {\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\tcapabilities: string[];\n\tkeywords: string[];\n\tinstallCount: number;\n\thasIcon: boolean;\n\ticonUrl: string;\n\tlatestVersion?: {\n\t\tversion: string;\n\t\taudit?: {\n\t\t\tverdict: string;\n\t\t\triskScore: number;\n\t\t};\n\t\timageAudit?: {\n\t\t\tverdict: string;\n\t\t};\n\t};\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface MarketplaceVersionSummary {\n\tversion: string;\n\tminEmDashVersion: string | null;\n\tbundleSize: number;\n\tchecksum: string;\n\tchangelog: string | null;\n\tcapabilities: string[];\n\tstatus: string;\n\tauditVerdict: string | null;\n\timageAuditVerdict: string | null;\n\tpublishedAt: string;\n}\n\nexport interface MarketplacePluginDetail extends MarketplacePluginSummary {\n\trepositoryUrl: string | null;\n\thomepageUrl: string | null;\n\tlicense: string | null;\n\tlatestVersion?: {\n\t\tversion: string;\n\t\tminEmDashVersion: string | null;\n\t\tbundleSize: number;\n\t\tchecksum: string;\n\t\tchangelog: string | null;\n\t\treadme: string | null;\n\t\thasIcon: boolean;\n\t\tscreenshotCount: number;\n\t\tscreenshotUrls: string[];\n\t\tcapabilities: string[];\n\t\tstatus: string;\n\t\taudit?: {\n\t\t\tverdict: string;\n\t\t\triskScore: number;\n\t\t};\n\t\timageAudit?: {\n\t\t\tverdict: string;\n\t\t};\n\t\tpublishedAt: string;\n\t};\n}\n\nexport interface MarketplaceSearchOpts {\n\tcategory?: string;\n\tcapability?: string;\n\tsort?: \"installs\" | \"updated\" | \"created\" | \"name\";\n\tcursor?: string;\n\tlimit?: number;\n}\n\nexport interface MarketplaceSearchResult {\n\titems: MarketplacePluginSummary[];\n\tnextCursor?: string;\n}\n\n// ── Theme types ───────────────────────────────────────────────────\n\nexport interface MarketplaceThemeSummary {\n\tid: string;\n\tname: string;\n\tdescription: string | null;\n\tauthor: {\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\tkeywords: string[];\n\tpreviewUrl: string;\n\tdemoUrl: string | null;\n\thasThumbnail: boolean;\n\tthumbnailUrl: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface MarketplaceThemeDetail extends MarketplaceThemeSummary {\n\tauthor: {\n\t\tid: string;\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\trepositoryUrl: string | null;\n\thomepageUrl: string | null;\n\tlicense: string | null;\n\tscreenshotCount: number;\n\tscreenshotUrls: string[];\n}\n\nexport interface MarketplaceThemeSearchOpts {\n\tkeyword?: string;\n\tsort?: \"name\" | \"created\" | \"updated\";\n\tcursor?: string;\n\tlimit?: number;\n}\n\nexport interface MarketplaceThemeSearchResult {\n\titems: MarketplaceThemeSummary[];\n\tnextCursor?: string;\n}\n\nexport interface PluginBundle {\n\tmanifest: PluginManifest;\n\tbackendCode: string;\n\tadminCode?: string;\n\tchecksum: string;\n}\n\n// ── Interface ──────────────────────────────────────────────────────\n\nexport interface MarketplaceClient {\n\t/** Search the marketplace catalog */\n\tsearch(query?: string, opts?: MarketplaceSearchOpts): Promise<MarketplaceSearchResult>;\n\n\t/** Get full plugin detail */\n\tgetPlugin(id: string): Promise<MarketplacePluginDetail>;\n\n\t/** Get version history for a plugin */\n\tgetVersions(id: string): Promise<MarketplaceVersionSummary[]>;\n\n\t/** Download and extract a plugin bundle */\n\tdownloadBundle(id: string, version: string): Promise<PluginBundle>;\n\n\t/** Fire-and-forget install stat (never throws) */\n\treportInstall(id: string, version: string): Promise<void>;\n\n\t/** Search theme listings */\n\tsearchThemes(\n\t\tquery?: string,\n\t\topts?: MarketplaceThemeSearchOpts,\n\t): Promise<MarketplaceThemeSearchResult>;\n\n\t/** Get full theme detail */\n\tgetTheme(id: string): Promise<MarketplaceThemeDetail>;\n}\n\n// ── Errors ─────────────────────────────────────────────────────────\n\nexport class MarketplaceError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly status?: number,\n\t\tpublic readonly code?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MarketplaceError\";\n\t}\n}\n\nexport class MarketplaceUnavailableError extends MarketplaceError {\n\tconstructor(cause?: unknown) {\n\t\tsuper(\"Plugin marketplace is unavailable\", undefined, \"MARKETPLACE_UNAVAILABLE\");\n\t\tif (cause) this.cause = cause;\n\t}\n}\n\n// ── Implementation ─────────────────────────────────────────────────\n\nclass MarketplaceClientImpl implements MarketplaceClient {\n\tprivate readonly baseUrl: string;\n\tprivate readonly siteOrigin: string | undefined;\n\n\tconstructor(baseUrl: string, siteOrigin?: string) {\n\t\t// Strip trailing slash\n\t\tthis.baseUrl = baseUrl.replace(TRAILING_SLASHES, \"\");\n\t\tthis.siteOrigin = siteOrigin;\n\t}\n\n\tasync search(query?: string, opts?: MarketplaceSearchOpts): Promise<MarketplaceSearchResult> {\n\t\tconst params = new URLSearchParams();\n\t\tif (query) params.set(\"q\", query);\n\t\tif (opts?.category) params.set(\"category\", opts.category);\n\t\tif (opts?.capability) params.set(\"capability\", opts.capability);\n\t\tif (opts?.sort) params.set(\"sort\", opts.sort);\n\t\tif (opts?.cursor) params.set(\"cursor\", opts.cursor);\n\t\tif (opts?.limit) params.set(\"limit\", String(opts.limit));\n\n\t\tconst qs = params.toString();\n\t\tconst url = `${this.baseUrl}/api/v1/plugins${qs ? `?${qs}` : \"\"}`;\n\t\tconst data = await this.fetchJson<MarketplaceSearchResult>(url);\n\t\treturn data;\n\t}\n\n\tasync getPlugin(id: string): Promise<MarketplacePluginDetail> {\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}`;\n\t\treturn this.fetchJson<MarketplacePluginDetail>(url);\n\t}\n\n\tasync getVersions(id: string): Promise<MarketplaceVersionSummary[]> {\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions`;\n\t\tconst data = await this.fetchJson<{ items: MarketplaceVersionSummary[] }>(url);\n\t\treturn data.items;\n\t}\n\n\tasync downloadBundle(id: string, version: string): Promise<PluginBundle> {\n\t\tconst bundleUrl = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/bundle`;\n\n\t\tconst marketplaceOrigin = new URL(this.baseUrl).origin;\n\t\tconst MAX_REDIRECTS = 5;\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tlet currentUrl = bundleUrl;\n\t\t\tresponse = await fetch(currentUrl, { redirect: \"manual\" });\n\n\t\t\t// Follow redirects manually, validating each target stays on the marketplace host\n\t\t\tfor (let i = 0; i < MAX_REDIRECTS; i++) {\n\t\t\t\tif (response.status < 300 || response.status >= 400) break;\n\n\t\t\t\tconst location = response.headers.get(\"location\");\n\t\t\t\tif (!location) break;\n\n\t\t\t\tconst target = new URL(location, currentUrl);\n\t\t\t\tif (target.origin !== marketplaceOrigin) {\n\t\t\t\t\tthrow new MarketplaceError(\n\t\t\t\t\t\t`Bundle download redirected to untrusted host: ${target.origin}`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t\"BUNDLE_REDIRECT_UNTRUSTED\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcurrentUrl = target.href;\n\t\t\t\tresponse = await fetch(currentUrl, { redirect: \"manual\" });\n\t\t\t}\n\n\t\t\t// If still a redirect after MAX_REDIRECTS, fail explicitly\n\t\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\t\tthrow new MarketplaceError(\n\t\t\t\t\t`Bundle download exceeded maximum redirects (${MAX_REDIRECTS})`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t\t\"BUNDLE_TOO_MANY_REDIRECTS\",\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (err instanceof MarketplaceError) throw err;\n\t\t\tthrow new MarketplaceUnavailableError(err);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t`Failed to download bundle: ${response.status} ${response.statusText}`,\n\t\t\t\tresponse.status,\n\t\t\t\t\"BUNDLE_DOWNLOAD_FAILED\",\n\t\t\t);\n\t\t}\n\n\t\tconst tarballBytes = new Uint8Array(await response.arrayBuffer());\n\t\ttry {\n\t\t\treturn await extractBundle(tarballBytes);\n\t\t} catch (err) {\n\t\t\tif (err instanceof MarketplaceError) throw err;\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t\"Failed to extract plugin bundle\",\n\t\t\t\tundefined,\n\t\t\t\t\"BUNDLE_EXTRACT_FAILED\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync reportInstall(id: string, version: string): Promise<void> {\n\t\t// Generate a stable site hash from the site origin (best-effort, non-identifying)\n\t\tconst siteHash = await generateSiteHash(this.siteOrigin);\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/installs`;\n\n\t\ttry {\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ siteHash, version }),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Fire-and-forget — never throw\n\t\t}\n\t}\n\n\tasync searchThemes(\n\t\tquery?: string,\n\t\topts?: MarketplaceThemeSearchOpts,\n\t): Promise<MarketplaceThemeSearchResult> {\n\t\tconst params = new URLSearchParams();\n\t\tif (query) params.set(\"q\", query);\n\t\tif (opts?.keyword) params.set(\"keyword\", opts.keyword);\n\t\tif (opts?.sort) params.set(\"sort\", opts.sort);\n\t\tif (opts?.cursor) params.set(\"cursor\", opts.cursor);\n\t\tif (opts?.limit) params.set(\"limit\", String(opts.limit));\n\n\t\tconst qs = params.toString();\n\t\tconst url = `${this.baseUrl}/api/v1/themes${qs ? `?${qs}` : \"\"}`;\n\t\treturn this.fetchJson<MarketplaceThemeSearchResult>(url);\n\t}\n\n\tasync getTheme(id: string): Promise<MarketplaceThemeDetail> {\n\t\tconst url = `${this.baseUrl}/api/v1/themes/${encodeURIComponent(id)}`;\n\t\treturn this.fetchJson<MarketplaceThemeDetail>(url);\n\t}\n\n\tprivate async fetchJson<T>(url: string): Promise<T> {\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetch(url, {\n\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tthrow new MarketplaceUnavailableError(err);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Marketplace request failed: ${response.status}`;\n\t\t\ttry {\n\t\t\t\tconst body: { error?: string } = await response.json();\n\t\t\t\tif (body.error) errorMessage = body.error;\n\t\t\t} catch {\n\t\t\t\t// use default message\n\t\t\t}\n\t\t\tthrow new MarketplaceError(errorMessage, response.status);\n\t\t}\n\n\t\tconst data: T = await response.json();\n\t\treturn data;\n\t}\n}\n\n// ── Bundle extraction ──────────────────────────────────────────────\n\n/**\n * Extract manifest + code files from a tarball.\n *\n * The tarball is a gzipped tar archive containing:\n * - manifest.json\n * - backend.js\n * - admin.js (optional)\n *\n * We use a minimal tar parser since we only need to read a few small files.\n */\n/**\n * Exported so the experimental registry install handler can reuse the\n * same parse / validate / hash primitive. Despite the file name, this\n * function predates the marketplace-vs-registry split and is generic\n * over plugin bundle tarballs regardless of distribution channel.\n */\n// Aligns with RFC 0001 §\"Bundle size limits\" (256 KiB decompressed,\n// 20 files). Matches `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts\n// (the publish-side cap). We don't import that constant to keep this\n// runtime module independent of the CLI; the two values are\n// load-bearing identical and must stay in sync.\n//\n// Tar adds per-file headers (~512 bytes each) plus directory entries,\n// so the entry count cap is set comfortably above RFC's 20-file limit.\n// Going over either is a strong signal the bundle isn't a legitimate\n// sandboxed plugin.\nconst MAX_DECOMPRESSED_BUNDLE_BYTES = 256 * 1024;\nconst MAX_BUNDLE_TAR_ENTRIES = 32;\n\nexport async function extractBundle(tarballBytes: Uint8Array): Promise<PluginBundle> {\n\t// Decompress fully into memory first, then parse the tar.\n\t// Passing a pipeThrough() stream directly to unpackTar causes a backpressure\n\t// deadlock in workerd: the tar decoder's body-stream pull() needs more\n\t// decompressed data, but the upstream pipe is stalled waiting for the\n\t// decoder's writable side to drain — a circular dependency.\n\tconst decompressedStream = new ReadableStream<Uint8Array>({\n\t\tstart(controller) {\n\t\t\tcontroller.enqueue(tarballBytes);\n\t\t\tcontroller.close();\n\t\t},\n\t}).pipeThrough(createGzipDecoder());\n\n\t// Collect decompressed bytes with a hard cap. A gzip-bomb -- a small\n\t// tarball that decompresses to gigabytes -- otherwise exhausts\n\t// worker / Node memory before we know to reject it. The cap matches\n\t// RFC 0001's publish-time bundle size limit (MAX_DECOMPRESSED_BUNDLE_BYTES);\n\t// anything past that isn't a legitimate sandboxed plugin.\n\tconst reader = decompressedStream.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\tif (!value) continue;\n\t\ttotal += value.byteLength;\n\t\tif (total > MAX_DECOMPRESSED_BUNDLE_BYTES) {\n\t\t\ttry {\n\t\t\t\tawait reader.cancel();\n\t\t\t} catch {\n\t\t\t\t// nothing to do\n\t\t\t}\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t`Bundle decompressed size exceeds limit (${MAX_DECOMPRESSED_BUNDLE_BYTES} bytes)`,\n\t\t\t\tundefined,\n\t\t\t\t\"INVALID_BUNDLE\",\n\t\t\t);\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst decompressedBytes = new Uint8Array(total);\n\t{\n\t\tlet offset = 0;\n\t\tfor (const chunk of chunks) {\n\t\t\tdecompressedBytes.set(chunk, offset);\n\t\t\toffset += chunk.byteLength;\n\t\t}\n\t}\n\n\tconst decompressed = new ReadableStream<Uint8Array>({\n\t\tstart(controller) {\n\t\t\tcontroller.enqueue(decompressedBytes);\n\t\t\tcontroller.close();\n\t\t},\n\t});\n\n\tconst entries = await unpackTar(decompressed);\n\tif (entries.length > MAX_BUNDLE_TAR_ENTRIES) {\n\t\tthrow new MarketplaceError(\n\t\t\t`Bundle has too many tar entries (${entries.length} > ${MAX_BUNDLE_TAR_ENTRIES})`,\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\n\tconst decoder = new TextDecoder();\n\tconst files = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tif (entry.data && entry.header.type === \"file\") {\n\t\t\t// Strip leading ./ prefix that tar tools commonly add\n\t\t\tconst name = entry.header.name.replace(LEADING_DOT_SLASH, \"\");\n\t\t\tfiles.set(name, decoder.decode(entry.data));\n\t\t}\n\t}\n\n\tconst manifestJson = files.get(\"manifest.json\");\n\tconst backendCode = files.get(\"backend.js\");\n\n\tif (!manifestJson) {\n\t\tthrow new MarketplaceError(\n\t\t\t\"Invalid bundle: missing manifest.json\",\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\tif (!backendCode) {\n\t\tthrow new MarketplaceError(\"Invalid bundle: missing backend.js\", undefined, \"INVALID_BUNDLE\");\n\t}\n\n\tlet manifest: PluginManifest;\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(manifestJson);\n\t\tconst result = pluginManifestSchema.safeParse(parsed);\n\t\tif (!result.success) {\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t\"Invalid bundle: manifest.json failed validation\",\n\t\t\t\tundefined,\n\t\t\t\t\"INVALID_BUNDLE\",\n\t\t\t);\n\t\t}\n\t\t// Elements are validated as unknown[] by Zod; cast to PluginManifest\n\t\t// for the Element[] type (Block Kit validation happens at render time).\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Zod types elements as unknown[]; Element type validated at render time\n\t\tmanifest = result.data as unknown as PluginManifest;\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError) throw err;\n\t\tthrow new MarketplaceError(\n\t\t\t\"Invalid bundle: malformed manifest.json\",\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\n\t// Compute SHA-256 checksum of the tarball for verification\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Uint8Array is a valid BufferSource at runtime; TS lib mismatch\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", tarballBytes as unknown as BufferSource);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\tconst checksum = Array.from(hashArray, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n\treturn {\n\t\tmanifest,\n\t\tbackendCode,\n\t\tadminCode: files.get(\"admin.js\"),\n\t\tchecksum,\n\t};\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/**\n * Generate a stable non-identifying site hash from the site origin.\n * The same origin always produces the same hash, so the marketplace\n * installs table deduplicates correctly per (plugin_id, site_hash).\n */\nasync function generateSiteHash(siteOrigin?: string): Promise<string> {\n\tconst seed = siteOrigin ? `emdash-site:${siteOrigin}` : `emdash-anonymous`;\n\ttry {\n\t\tconst hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(seed));\n\t\tconst arr = new Uint8Array(hash);\n\t\treturn Array.from(arr.slice(0, 8), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\t} catch {\n\t\t// Fallback for environments without crypto.subtle: FNV-1a hash encoded as hex.\n\t\t// Deterministic, uniform distribution, no origin leakage.\n\t\tlet h = 0x811c9dc5;\n\t\tfor (let i = 0; i < seed.length; i++) {\n\t\t\th ^= seed.charCodeAt(i);\n\t\t\th = Math.imul(h, 0x01000193);\n\t\t}\n\t\tconst h2 = h ^ (h >>> 16);\n\t\treturn (h >>> 0).toString(16).padStart(8, \"0\") + (h2 >>> 0).toString(16).padStart(8, \"0\");\n\t}\n}\n\n// ── Factory ────────────────────────────────────────────────────────\n\n/**\n * Create a MarketplaceClient for the given marketplace URL.\n *\n * @param baseUrl - The marketplace API base URL (e.g. \"https://marketplace.emdashcms.com\")\n * @param siteOrigin - The origin of the EmDash site (e.g. \"https://myblog.example.com\").\n * Used to generate a stable, non-identifying site hash for install deduplication.\n */\nexport function createMarketplaceClient(baseUrl: string, siteOrigin?: string): MarketplaceClient {\n\treturn new MarketplaceClientImpl(baseUrl, siteOrigin);\n}\n","/**\n * Marketplace plugin handlers\n *\n * Business logic for installing, updating, uninstalling, and checking\n * updates for marketplace plugins. Routes are thin wrappers around these.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validatePluginIdentifier } from \"../../database/validate.js\";\nimport { pluginManifestSchema } from \"../../plugins/manifest-schema.js\";\nimport { normalizeManifestRoute } from \"../../plugins/manifest-schema.js\";\nimport {\n\tcreateMarketplaceClient,\n\tMarketplaceError,\n\tMarketplaceUnavailableError,\n\ttype MarketplaceClient,\n\ttype MarketplacePluginDetail,\n\ttype MarketplaceSearchOpts,\n\ttype MarketplaceThemeSearchOpts,\n\ttype MarketplaceVersionSummary,\n\ttype PluginBundle,\n} from \"../../plugins/marketplace.js\";\nimport type { SandboxRunner } from \"../../plugins/sandbox/types.js\";\nimport { PluginStateRepository } from \"../../plugins/state.js\";\nimport { normalizeCapabilities } from \"../../plugins/types.js\";\nimport type { PluginManifest } from \"../../plugins/types.js\";\nimport { EmDashStorageError } from \"../../storage/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface MarketplaceInstallResult {\n\tpluginId: string;\n\tversion: string;\n\tcapabilities: string[];\n}\n\nexport interface MarketplaceUpdateResult {\n\tpluginId: string;\n\toldVersion: string;\n\tnewVersion: string;\n\tcapabilityChanges: {\n\t\tadded: string[];\n\t\tremoved: string[];\n\t};\n\trouteVisibilityChanges?: {\n\t\tnewlyPublic: string[];\n\t};\n}\n\nexport interface MarketplaceUpdateCheck {\n\tpluginId: string;\n\tinstalled: string;\n\tlatest: string;\n\thasUpdate: boolean;\n\thasCapabilityChanges: boolean;\n\tcapabilityChanges?: {\n\t\tadded: string[];\n\t\tremoved: string[];\n\t};\n\thasRouteVisibilityChanges: boolean;\n\trouteVisibilityChanges?: {\n\t\tnewlyPublic: string[];\n\t};\n}\n\nexport interface MarketplaceUninstallResult {\n\tpluginId: string;\n\tdataDeleted: boolean;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/** Semver-like pattern: digits, dots, hyphens, plus signs (e.g. 1.0.0, 1.0.0-beta.1) */\nconst VERSION_PATTERN = /^[a-z0-9][a-z0-9._+-]*$/i;\n\nfunction validateVersion(version: string): void {\n\tif (version.includes(\"..\")) throw new Error(\"Invalid version format\");\n\tif (!VERSION_PATTERN.test(version)) {\n\t\tthrow new Error(\"Invalid version format\");\n\t}\n}\n\nfunction getClient(\n\tmarketplaceUrl: string | undefined,\n\tsiteOrigin?: string,\n): MarketplaceClient | null {\n\tif (!marketplaceUrl) return null;\n\treturn createMarketplaceClient(marketplaceUrl, siteOrigin);\n}\n\nfunction diffCapabilities(\n\toldCaps: string[],\n\tnewCaps: string[],\n): { added: string[]; removed: string[] } {\n\t// Normalize both sides before diffing so that an installed v1 manifest\n\t// declaring `read:content` and an upgrade v2 manifest declaring\n\t// `content:read` produces an empty diff — users should not see a\n\t// spurious \"capability changed\" prompt for a pure rename.\n\tconst oldNorm = normalizeCapabilities(oldCaps);\n\tconst newNorm = normalizeCapabilities(newCaps);\n\tconst oldSet = new Set(oldNorm);\n\tconst newSet = new Set(newNorm);\n\treturn {\n\t\tadded: newNorm.filter((c) => !oldSet.has(c)),\n\t\tremoved: oldNorm.filter((c) => !newSet.has(c)),\n\t};\n}\n\n/**\n * Diff route visibility between two manifests.\n * Returns routes that changed from private to public (newly exposed).\n */\nfunction diffRouteVisibility(\n\toldManifest: PluginManifest | undefined,\n\tnewManifest: PluginManifest,\n): { newlyPublic: string[] } {\n\tconst oldPublicRoutes = new Set<string>();\n\tif (oldManifest) {\n\t\tfor (const entry of oldManifest.routes) {\n\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\tif (normalized.public === true) {\n\t\t\t\toldPublicRoutes.add(normalized.name);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst newlyPublic: string[] = [];\n\tfor (const entry of newManifest.routes) {\n\t\tconst normalized = normalizeManifestRoute(entry);\n\t\tif (normalized.public === true && !oldPublicRoutes.has(normalized.name)) {\n\t\t\tnewlyPublic.push(normalized.name);\n\t\t}\n\t}\n\n\treturn { newlyPublic };\n}\n\nasync function resolveVersionMetadata(\n\tclient: MarketplaceClient,\n\tpluginId: string,\n\tpluginDetail: MarketplacePluginDetail,\n\tversion: string,\n): Promise<MarketplaceVersionSummary | null> {\n\tif (pluginDetail.latestVersion?.version === version) {\n\t\treturn {\n\t\t\tversion: pluginDetail.latestVersion.version,\n\t\t\tminEmDashVersion: pluginDetail.latestVersion.minEmDashVersion,\n\t\t\tbundleSize: pluginDetail.latestVersion.bundleSize,\n\t\t\tchecksum: pluginDetail.latestVersion.checksum,\n\t\t\tchangelog: pluginDetail.latestVersion.changelog,\n\t\t\tcapabilities: pluginDetail.latestVersion.capabilities,\n\t\t\tstatus: pluginDetail.latestVersion.status,\n\t\t\tauditVerdict: pluginDetail.latestVersion.audit?.verdict ?? null,\n\t\t\timageAuditVerdict: pluginDetail.latestVersion.imageAudit?.verdict ?? null,\n\t\t\tpublishedAt: pluginDetail.latestVersion.publishedAt,\n\t\t};\n\t}\n\n\tconst versions = await client.getVersions(pluginId);\n\treturn versions.find((v) => v.version === version) ?? null;\n}\n\nfunction validateBundleIdentity(\n\tbundle: PluginBundle,\n\tpluginId: string,\n\tversion: string,\n): ApiResult<never> | null {\n\tif (bundle.manifest.id !== pluginId) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MANIFEST_MISMATCH\",\n\t\t\t\tmessage: `Bundle manifest ID (${bundle.manifest.id}) does not match requested plugin (${pluginId})`,\n\t\t\t},\n\t\t};\n\t}\n\n\tif (bundle.manifest.version !== version) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MANIFEST_VERSION_MISMATCH\",\n\t\t\t\tmessage: `Bundle manifest version (${bundle.manifest.version}) does not match requested version (${version})`,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/** Store a plugin bundle's files in site-local R2 storage */\n/**\n * Storage source for an installed plugin bundle. Determines the R2\n * key prefix and is used to keep marketplace and registry installs\n * cleanly separated in object listings.\n */\nexport type PluginBundleSource = \"marketplace\" | \"registry\";\n\nfunction bundlePrefix(source: PluginBundleSource, pluginId: string, version: string): string {\n\treturn `${source}/${pluginId}/${version}`;\n}\n\nexport async function storeBundleInR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tbundle: PluginBundle,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, pluginId, version);\n\n\t// Store manifest\n\tawait storage.upload({\n\t\tkey: `${prefix}/manifest.json`,\n\t\tbody: new TextEncoder().encode(JSON.stringify(bundle.manifest)),\n\t\tcontentType: \"application/json\",\n\t});\n\n\t// Store backend code\n\tawait storage.upload({\n\t\tkey: `${prefix}/backend.js`,\n\t\tbody: new TextEncoder().encode(bundle.backendCode),\n\t\tcontentType: \"application/javascript\",\n\t});\n\n\t// Store admin code if present\n\tif (bundle.adminCode) {\n\t\tawait storage.upload({\n\t\t\tkey: `${prefix}/admin.js`,\n\t\t\tbody: new TextEncoder().encode(bundle.adminCode),\n\t\t\tcontentType: \"application/javascript\",\n\t\t});\n\t}\n}\n\n/** Read a ReadableStream to string */\nasync function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {\n\treturn new Response(stream).text();\n}\n\n/**\n * Load a plugin bundle from site-local R2 storage.\n *\n * `source` selects the R2 key prefix: marketplace plugins are stored\n * under `marketplace/<id>/<version>/`, registry plugins under\n * `registry/<id>/<version>/`. Defaults to `\"marketplace\"` for\n * backwards compatibility with pre-registry call sites.\n */\nexport async function loadBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<{ manifest: PluginManifest; backendCode: string; adminCode?: string } | null> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, pluginId, version);\n\n\ttry {\n\t\tconst manifestResult = await storage.download(`${prefix}/manifest.json`);\n\t\tconst backendResult = await storage.download(`${prefix}/backend.js`);\n\n\t\tconst manifestText = await streamToText(manifestResult.body);\n\t\tconst backendCode = await streamToText(backendResult.body);\n\t\tconst parsed: unknown = JSON.parse(manifestText);\n\t\tconst result = pluginManifestSchema.safeParse(parsed);\n\t\tif (!result.success) return null;\n\t\t// Elements are validated as unknown[] by Zod; cast to PluginManifest\n\t\t// for the Element[] type (Block Kit validation happens at render time).\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Zod types elements as unknown[]; Element type validated at render time\n\t\tconst manifest = result.data as unknown as PluginManifest;\n\n\t\t// Try to load admin code (optional)\n\t\tlet adminCode: string | undefined;\n\t\ttry {\n\t\t\tconst adminResult = await storage.download(`${prefix}/admin.js`);\n\t\t\tadminCode = await streamToText(adminResult.body);\n\t\t} catch {\n\t\t\t// admin.js is optional\n\t\t}\n\n\t\treturn { manifest, backendCode, adminCode };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** Delete a plugin bundle from site-local R2 storage */\nexport async function deleteBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, pluginId, version);\n\tconst files = [\"manifest.json\", \"backend.js\", \"admin.js\"];\n\n\tfor (const file of files) {\n\t\ttry {\n\t\t\tawait storage.delete(`${prefix}/${file}`);\n\t\t} catch {\n\t\t\t// Ignore missing files\n\t\t}\n\t}\n}\n\n// ── Install ────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceInstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n\topts?: { version?: string; configuredPluginIds?: Set<string>; siteOrigin?: string },\n): Promise<ApiResult<MarketplaceInstallResult>> {\n\tconst client = getClient(marketplaceUrl, opts?.siteOrigin);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MARKETPLACE_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Marketplace is not configured\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!storage) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"STORAGE_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Storage is required for marketplace plugin installation\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SANDBOX_NOT_AVAILABLE\",\n\t\t\t\tmessage: \"Sandbox runner is required for marketplace plugins\",\n\t\t\t},\n\t\t};\n\t}\n\n\ttry {\n\t\t// Check if already installed\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (existing && existing.source === \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ALREADY_INSTALLED\",\n\t\t\t\t\tmessage: `Plugin ${pluginId} is already installed`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Block installation if a configured (trusted) plugin with the same ID exists.\n\t\t// Without this check, the sandboxed plugin could shadow the trusted plugin's\n\t\t// route handlers while auth decisions are made against the trusted plugin's metadata.\n\t\tif (opts?.configuredPluginIds?.has(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_ID_CONFLICT\",\n\t\t\t\t\tmessage: `Cannot install marketplace plugin \"${pluginId}\" — a configured plugin with the same ID already exists`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Fetch plugin detail from marketplace\n\t\tconst pluginDetail = await client.getPlugin(pluginId);\n\t\tconst version = opts?.version ?? pluginDetail.latestVersion?.version;\n\t\tif (!version) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `No published versions found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, version);\n\t\tif (!versionMetadata) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `Version ${version} was not found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Block installation of plugins that haven't passed audit.\n\t\t// Both \"fail\" (explicitly malicious) and \"warn\" (audit error or\n\t\t// inconclusive) are non-installable — only \"pass\" or null (no audit\n\t\t// ran) are allowed through.\n\t\tif (versionMetadata.auditVerdict === \"fail\" || versionMetadata.auditVerdict === \"warn\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AUDIT_FAILED\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\tversionMetadata.auditVerdict === \"fail\"\n\t\t\t\t\t\t\t? \"Plugin failed security audit and cannot be installed\"\n\t\t\t\t\t\t\t: \"Plugin audit was inconclusive and cannot be installed until reviewed\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Download and extract bundle\n\t\tconst bundle = await client.downloadBundle(pluginId, version);\n\n\t\t// Verify checksum matches marketplace-published checksum\n\t\tif (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CHECKSUM_MISMATCH\",\n\t\t\t\t\tmessage: \"Bundle checksum does not match marketplace record. Download may be corrupted.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst bundleIdentityError = validateBundleIdentity(bundle, pluginId, version);\n\t\tif (bundleIdentityError) return bundleIdentityError;\n\n\t\t// Store bundle in site-local R2\n\t\tawait storeBundleInR2(storage, pluginId, version, bundle);\n\n\t\t// Write plugin state\n\t\tawait stateRepo.upsert(pluginId, version, \"active\", {\n\t\t\tsource: \"marketplace\",\n\t\t\tmarketplaceVersion: version,\n\t\t\tdisplayName: pluginDetail.name,\n\t\t\tdescription: pluginDetail.description ?? undefined,\n\t\t});\n\n\t\t// Fire-and-forget install stat\n\t\tclient.reportInstall(pluginId, version).catch(() => {\n\t\t\t// Intentional: never fails the install\n\t\t});\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\tversion,\n\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t},\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MARKETPLACE_UNAVAILABLE\",\n\t\t\t\t\tmessage: \"Plugin marketplace is currently unavailable\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.code ?? \"MARKETPLACE_ERROR\",\n\t\t\t\t\tmessage: err.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof EmDashStorageError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.code ?? \"STORAGE_ERROR\",\n\t\t\t\t\tmessage: \"Storage error while installing plugin\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err && typeof err === \"object\" && \"code\" in err) {\n\t\t\tconst code = (err as { code?: unknown }).code;\n\t\t\tif (typeof code === \"string\" && code.trim()) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode,\n\t\t\t\t\t\tmessage: \"Failed to install plugin from marketplace\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tconsole.error(\"Failed to install marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"INSTALL_FAILED\",\n\t\t\t\tmessage: \"Failed to install plugin from marketplace\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Update ─────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n\topts?: {\n\t\tversion?: string;\n\t\tconfirmCapabilityChanges?: boolean;\n\t\tconfirmRouteVisibilityChanges?: boolean;\n\t},\n): Promise<ApiResult<MarketplaceUpdateResult>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\tif (!storage) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"STORAGE_NOT_CONFIGURED\", message: \"Storage is required\" },\n\t\t};\n\t}\n\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SANDBOX_NOT_AVAILABLE\", message: \"Sandbox runner is required\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || existing.source !== \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `No marketplace plugin found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst oldVersion = existing.marketplaceVersion ?? existing.version;\n\n\t\t// Get target version\n\t\tconst pluginDetail = await client.getPlugin(pluginId);\n\t\tconst newVersion = opts?.version ?? pluginDetail.latestVersion?.version;\n\t\tif (!newVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NO_VERSION\", message: \"No newer version available\" },\n\t\t\t};\n\t\t}\n\n\t\tif (newVersion === oldVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"ALREADY_UP_TO_DATE\", message: \"Plugin is already up to date\" },\n\t\t\t};\n\t\t}\n\n\t\tconst versionMetadata = await resolveVersionMetadata(\n\t\t\tclient,\n\t\t\tpluginId,\n\t\t\tpluginDetail,\n\t\t\tnewVersion,\n\t\t);\n\t\tif (!versionMetadata) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `Version ${newVersion} was not found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Download new bundle\n\t\tconst bundle = await client.downloadBundle(pluginId, newVersion);\n\n\t\t// Verify checksum matches marketplace-published checksum for this version\n\t\tif (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CHECKSUM_MISMATCH\",\n\t\t\t\t\tmessage: \"Bundle checksum does not match marketplace record. Download may be corrupted.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst bundleIdentityError = validateBundleIdentity(bundle, pluginId, newVersion);\n\t\tif (bundleIdentityError) return bundleIdentityError;\n\n\t\t// Diff capabilities and route visibility against old version\n\t\tconst oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion);\n\t\tconst oldCaps = oldBundle?.manifest.capabilities ?? [];\n\t\tconst capabilityChanges = diffCapabilities(oldCaps, bundle.manifest.capabilities);\n\t\tconst hasEscalation = capabilityChanges.added.length > 0;\n\n\t\t// If capabilities escalated, require explicit confirmation\n\t\tif (hasEscalation && !opts?.confirmCapabilityChanges) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CAPABILITY_ESCALATION\",\n\t\t\t\t\tmessage: \"Plugin update requires new capabilities\",\n\t\t\t\t\tdetails: { capabilityChanges },\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Diff route visibility — routes going from private to public are a\n\t\t// security-sensitive change that exposes unauthenticated endpoints.\n\t\tconst routeVisibilityChanges = diffRouteVisibility(oldBundle?.manifest, bundle.manifest);\n\t\tconst hasNewPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;\n\n\t\tif (hasNewPublicRoutes && !opts?.confirmRouteVisibilityChanges) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_VISIBILITY_ESCALATION\",\n\t\t\t\t\tmessage: \"Plugin update exposes new public (unauthenticated) routes\",\n\t\t\t\t\tdetails: { routeVisibilityChanges, capabilityChanges },\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Store new bundle\n\t\tawait storeBundleInR2(storage, pluginId, newVersion, bundle);\n\n\t\t// Update state\n\t\tawait stateRepo.upsert(pluginId, newVersion, \"active\", {\n\t\t\tsource: \"marketplace\",\n\t\t\tmarketplaceVersion: newVersion,\n\t\t\tdisplayName: pluginDetail.name,\n\t\t\tdescription: pluginDetail.description ?? undefined,\n\t\t});\n\n\t\t// Clean up old bundle from R2 (best-effort)\n\t\tdeleteBundleFromR2(storage, pluginId, oldVersion).catch(() => {});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\toldVersion,\n\t\t\t\tnewVersion,\n\t\t\t\tcapabilityChanges,\n\t\t\t\trouteVisibilityChanges: hasNewPublicRoutes ? routeVisibilityChanges : undefined,\n\t\t\t},\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: err.code ?? \"MARKETPLACE_ERROR\", message: err.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to update marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"UPDATE_FAILED\", message: \"Failed to update plugin\" },\n\t\t};\n\t}\n}\n\n// ── Uninstall ──────────────────────────────────────────────────────\n\nexport async function handleMarketplaceUninstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tpluginId: string,\n\topts?: { deleteData?: boolean },\n): Promise<ApiResult<MarketplaceUninstallResult>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || existing.source !== \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `No marketplace plugin found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst version = existing.marketplaceVersion ?? existing.version;\n\n\t\t// Delete bundle from site R2\n\t\tif (storage) {\n\t\t\tawait deleteBundleFromR2(storage, pluginId, version);\n\t\t}\n\n\t\t// Optionally delete plugin storage data\n\t\tlet dataDeleted = false;\n\t\tif (opts?.deleteData) {\n\t\t\ttry {\n\t\t\t\tawait db.deleteFrom(\"_plugin_storage\").where(\"plugin_id\", \"=\", pluginId).execute();\n\t\t\t\tdataDeleted = true;\n\t\t\t} catch {\n\t\t\t\t// Plugin storage table may not have data for this plugin\n\t\t\t}\n\t\t}\n\n\t\t// Delete state row\n\t\tawait stateRepo.delete(pluginId);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { pluginId, dataDeleted },\n\t\t};\n\t} catch (err) {\n\t\tconsole.error(\"Failed to uninstall marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"UNINSTALL_FAILED\",\n\t\t\t\tmessage: \"Failed to uninstall plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Update check ───────────────────────────────────────────────────\n\nexport async function handleMarketplaceUpdateCheck(\n\tdb: Kysely<Database>,\n\tmarketplaceUrl: string | undefined,\n): Promise<ApiResult<{ items: MarketplaceUpdateCheck[] }>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\n\t\tconst items: MarketplaceUpdateCheck[] = [];\n\n\t\tfor (const plugin of marketplacePlugins) {\n\t\t\ttry {\n\t\t\t\tconst detail = await client.getPlugin(plugin.pluginId);\n\t\t\t\tconst latest = detail.latestVersion?.version;\n\t\t\t\tconst installed = plugin.marketplaceVersion ?? plugin.version;\n\n\t\t\t\tif (!latest) continue;\n\n\t\t\t\tconst hasUpdate = latest !== installed;\n\t\t\t\tlet capabilityChanges: { added: string[]; removed: string[] } | undefined;\n\t\t\t\tlet hasCapabilityChanges = false;\n\n\t\t\t\tif (hasUpdate && detail.latestVersion) {\n\t\t\t\t\tconst oldCaps = detail.capabilities ?? [];\n\t\t\t\t\tconst newCaps = detail.latestVersion.capabilities ?? [];\n\t\t\t\t\tcapabilityChanges = diffCapabilities(oldCaps, newCaps);\n\t\t\t\t\thasCapabilityChanges =\n\t\t\t\t\t\tcapabilityChanges.added.length > 0 || capabilityChanges.removed.length > 0;\n\t\t\t\t}\n\n\t\t\t\titems.push({\n\t\t\t\t\tpluginId: plugin.pluginId,\n\t\t\t\t\tinstalled,\n\t\t\t\t\tlatest: latest ?? installed,\n\t\t\t\t\thasUpdate,\n\t\t\t\t\thasCapabilityChanges,\n\t\t\t\t\tcapabilityChanges: hasCapabilityChanges ? capabilityChanges : undefined,\n\t\t\t\t\t// Route visibility changes require downloading both bundles to compare\n\t\t\t\t\t// manifests, which is too expensive for a preview check. The actual\n\t\t\t\t\t// enforcement happens at update time in handleMarketplaceUpdate.\n\t\t\t\t\thasRouteVisibilityChanges: false,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\t// Skip plugins that can't be checked (marketplace down, plugin delisted)\n\t\t\t\tconsole.warn(`Failed to check updates for ${plugin.pluginId}:`, err);\n\t\t\t}\n\t\t}\n\n\t\treturn { success: true, data: { items } };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to check marketplace updates:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"UPDATE_CHECK_FAILED\", message: \"Failed to check for updates\" },\n\t\t};\n\t}\n}\n\n// ── Proxy ──────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceSearch(\n\tmarketplaceUrl: string | undefined,\n\tquery?: string,\n\topts?: MarketplaceSearchOpts,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.search(query, opts);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to search marketplace:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SEARCH_FAILED\", message: \"Failed to search marketplace\" },\n\t\t};\n\t}\n}\n\nexport async function handleMarketplaceGetPlugin(\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.getPlugin(pluginId);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError && err.status === 404) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to get marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"GET_PLUGIN_FAILED\", message: \"Failed to get plugin details\" },\n\t\t};\n\t}\n}\n\n// ── Theme proxy handlers ──────────────────────────────────────────\n\nexport async function handleThemeSearch(\n\tmarketplaceUrl: string | undefined,\n\tquery?: string,\n\topts?: MarketplaceThemeSearchOpts,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.searchThemes(query, opts);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to search themes:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"THEME_SEARCH_FAILED\", message: \"Failed to search themes\" },\n\t\t};\n\t}\n}\n\nexport async function handleThemeGetDetail(\n\tmarketplaceUrl: string | undefined,\n\tthemeId: string,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.getTheme(themeId);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError && err.status === 404) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Theme not found: ${themeId}` },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to get marketplace theme:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"GET_THEME_FAILED\", message: \"Failed to get theme details\" },\n\t\t};\n\t}\n}\n","/**\n * Helpers for normalizing the experimental registry integration option\n * (`config.experimental.registry` in `astro.config.mjs`) into the shape\n * exposed on the admin manifest.\n *\n * The integration option accepts a human-friendly duration string for\n * `policy.minimumReleaseAge` (`\"48h\"`, `\"7d\"`); the manifest exposes\n * seconds so the browser doesn't need a duration parser.\n */\n\nimport type { RegistryConfig, RegistryConfigInput } from \"./types.js\";\n\n/**\n * Shape returned in the admin manifest's `registry` field. The browser\n * consumes this directly -- all duration normalization and aggregator URL\n * validation has already happened by the time it gets here.\n */\nexport interface ManifestRegistryConfig {\n\taggregatorUrl: string;\n\tacceptLabelers?: string;\n\tpolicy?: {\n\t\tminimumReleaseAgeSeconds?: number;\n\t\t/**\n\t\t * Allowlist of publishers / packages exempt from the\n\t\t * {@link minimumReleaseAgeSeconds} holdback. Each entry is either:\n\t\t *\n\t\t * - A bare publisher identifier: `\"did:plc:abc123\"` or a handle\n\t\t * like `\"example.dev\"`. Every package from that publisher is\n\t\t * exempt.\n\t\t * - A `publisher/slug` pair: only that specific package is exempt.\n\t\t *\n\t\t * Normalized to lowercase strings at config load time so the\n\t\t * browser does case-insensitive comparison. See\n\t\t * {@link releaseExemptFromMinimumAge}.\n\t\t */\n\t\tminimumReleaseAgeExclude?: string[];\n\t};\n}\n\n/**\n * Canonicalize a capabilities list for set-style comparison.\n *\n * Capabilities (the legacy declared-access shape used by the current\n * sandbox enforcer) are conceptually a *set*: order, duplicates, and\n * non-string entries don't carry meaning. The install handler's drift\n * check compares the admin's acknowledged set against the bundle\n * manifest's set; both sides pass through this canonicalizer first so\n * an aggregator-supplied array with unstable order or junk entries\n * can't cause a spurious drift rejection.\n *\n * Filters non-strings, deduplicates, and sorts lexically. Named to\n * avoid shadowing `@emdash-cms/plugin-types`'s existing\n * `normalizeCapabilities` (which dedupes + applies the deprecated →\n * current alias map but does not filter junk or sort).\n *\n * Exported so the same shape is produced by the browser before sending\n * the `acknowledgedDeclaredAccess` payload and by the server before\n * comparing against the bundle.\n */\nexport function canonicalCapabilitiesForDriftCheck(value: unknown): string[] {\n\tif (!Array.isArray(value)) return [];\n\tconst seen = new Set<string>();\n\tfor (const entry of value) {\n\t\tif (typeof entry === \"string\" && entry.length > 0) {\n\t\t\tseen.add(entry);\n\t\t}\n\t}\n\treturn [...seen].toSorted();\n}\n\n/**\n * Returns whether a `(publisher_did, slug)` pair is on the\n * minimum-release-age exemption list. Exported so the same matcher is\n * used by the browser policy filter and the server-side install\n * enforcement.\n *\n * Matching is DID-only. Handles are aggregator-supplied envelope data\n * (mutable, controlled by an attacker who compromises the aggregator)\n * and cannot be used as a trust input -- a compromised aggregator\n * could claim any handle for any package and bypass the holdback. DIDs\n * are part of the AT URI of the package record and are independently\n * resolvable, so even a compromised aggregator can't lie about the\n * publisher DID without also breaking checksum verification downstream.\n *\n * Entries from config are already lowercased at manifest-build time.\n * Runtime values are lowercased here at compare time.\n */\nexport function releaseExemptFromMinimumAge(\n\texclude: readonly string[] | undefined,\n\tpublisherDid: string,\n\tslug: string,\n): boolean {\n\tif (!exclude || exclude.length === 0) return false;\n\tconst didLower = publisherDid.toLowerCase();\n\tconst slugLower = slug.toLowerCase();\n\tconst fullDid = `${didLower}/${slugLower}`;\n\n\tfor (const entry of exclude) {\n\t\tif (entry === didLower) return true;\n\t\tif (entry === fullDid) return true;\n\t}\n\treturn false;\n}\n\nconst DURATION_PATTERN = /^(\\d+)(s|m|h|d|w)$/;\n\n/** Trailing slashes on the aggregator URL, stripped during normalization. */\nconst TRAILING_SLASHES = /\\/+$/;\n\n/** Trailing dot on a hostname, stripped before URL host comparisons. */\nconst TRAILING_DOT = /\\.$/;\n\n/**\n * Parse a duration string or raw second count into a non-negative\n * integer count of seconds. Throws on unrecognised input so config\n * mistakes fail at startup rather than silently disabling the policy.\n */\nexport function parseDurationSeconds(duration: string | number): number {\n\tif (typeof duration === \"number\") {\n\t\tif (!Number.isFinite(duration) || duration < 0) {\n\t\t\tthrow new Error(`Invalid duration: ${duration} (must be a non-negative finite number)`);\n\t\t}\n\t\treturn Math.floor(duration);\n\t}\n\n\tconst match = duration.match(DURATION_PATTERN);\n\tif (!match) {\n\t\tthrow new Error(\n\t\t\t`Invalid duration format: \"${duration}\". Use a duration string like \"48h\", \"7d\", \"30m\", or a number of seconds.`,\n\t\t);\n\t}\n\n\tconst value = parseInt(match[1]!, 10);\n\tconst unit = match[2];\n\n\tswitch (unit) {\n\t\tcase \"s\":\n\t\t\treturn value;\n\t\tcase \"m\":\n\t\t\treturn value * 60;\n\t\tcase \"h\":\n\t\t\treturn value * 60 * 60;\n\t\tcase \"d\":\n\t\t\treturn value * 24 * 60 * 60;\n\t\tcase \"w\":\n\t\t\treturn value * 7 * 24 * 60 * 60;\n\t\tdefault:\n\t\t\t// Unreachable given the regex, but keep the exhaustive arm for\n\t\t\t// future maintainers who add a unit to the pattern.\n\t\t\tthrow new Error(`Unknown duration unit: ${unit}`);\n\t}\n}\n\n/**\n * Validate that `aggregatorUrl` is a safe outbound target for the\n * registry's XRPC calls. Same posture as artifact downloads: HTTPS\n * required in production; `http://localhost` allowed only in dev.\n *\n * The aggregator's responses are the trust source for release records,\n * checksums, labels, mirrors, and `indexedAt` (until full MST\n * verification lands). Allowing plain HTTP here would let a network\n * attacker swap a release record and point the artifact URL at their\n * own HTTPS bundle, defeating the checksum trust chain because the\n * attacker controls the unsigned transport that supplied the checksum.\n */\nexport function validateAggregatorUrl(aggregatorUrl: string): URL {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(aggregatorUrl);\n\t} catch {\n\t\tthrow new Error(`registry.aggregatorUrl is not a valid URL: ${aggregatorUrl}`);\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\tthrow new Error(`registry.aggregatorUrl must use http or https: ${aggregatorUrl}`);\n\t}\n\t// Reject embedded credentials. The normalized aggregator URL ends\n\t// up in the admin manifest and is shipped to every admin browser;\n\t// browser `fetch()` also outright rejects URLs with `user:pass@`,\n\t// so leaving them in would both leak the credentials and break the\n\t// registry UI at runtime.\n\tif (parsed.username || parsed.password) {\n\t\tthrow new Error(\"registry.aggregatorUrl must not contain embedded credentials (user:pass@)\");\n\t}\n\n\t// WHATWG URL preserves the brackets on IPv6 hostnames -- strip them\n\t// before any comparison so `https://[::1]/` is recognised as localhost\n\t// and not treated as a generic domain string.\n\tconst rawHostname = parsed.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\tconst hostname =\n\t\trawHostname.startsWith(\"[\") && rawHostname.endsWith(\"]\")\n\t\t\t? rawHostname.slice(1, -1)\n\t\t\t: rawHostname;\n\tconst isLocalhost =\n\t\thostname === \"localhost\" ||\n\t\thostname.endsWith(\".localhost\") ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"::1\" ||\n\t\t// IPv4-mapped IPv6 forms of loopback, e.g. `::ffff:127.0.0.1` and `::ffff:7f00:1`.\n\t\thostname.startsWith(\"::ffff:127.\") ||\n\t\thostname.startsWith(\"::ffff:7f00:\");\n\n\tif (!import.meta.env.DEV) {\n\t\tif (parsed.protocol === \"http:\") {\n\t\t\tthrow new Error(`registry.aggregatorUrl must use https in production: ${aggregatorUrl}`);\n\t\t}\n\t\tif (isLocalhost) {\n\t\t\tthrow new Error(\n\t\t\t\t`registry.aggregatorUrl points at localhost; allowed only in dev: ${aggregatorUrl}`,\n\t\t\t);\n\t\t}\n\t} else if (parsed.protocol === \"http:\" && !isLocalhost) {\n\t\tthrow new Error(\n\t\t\t`registry.aggregatorUrl must use https (http allowed only for localhost in dev): ${aggregatorUrl}`,\n\t\t);\n\t}\n\n\treturn parsed;\n}\n\n/**\n * Expand the `RegistryConfigInput` shorthand into the full\n * `RegistryConfig` object shape.\n *\n * Users can pass a bare aggregator URL string for the common case\n * (`experimental.registry: \"https://registry.emdashcms.com\"`); the\n * normalizer handles either form transparently.\n *\n * Returns `undefined` for `undefined` input so callers can chain with\n * optional chaining.\n */\nexport function coerceRegistryConfig(\n\tinput: RegistryConfigInput | undefined,\n): RegistryConfig | undefined {\n\tif (input === undefined) return undefined;\n\tif (typeof input === \"string\") return { aggregatorUrl: input };\n\treturn input;\n}\n\n/**\n * Normalize the user-supplied `RegistryConfigInput` into the shape that\n * ships to the admin browser via the manifest endpoint.\n *\n * Accepts either the shorthand string form\n * (`\"https://registry.emdashcms.com\"`) or the full `RegistryConfig`\n * object. Returns `null` when `input` is undefined so callers can\n * spread the result directly into the manifest object.\n *\n * Throws if the aggregator URL is malformed, points at a forbidden host,\n * or `policy.minimumReleaseAge` is unparseable. These surface at\n * runtime startup as 500s from the manifest endpoint -- intended,\n * because the alternative is silently disabling the registry on\n * misconfigured sites.\n *\n * TODO: switch to a Zod schema for richer per-field error messages and\n * to surface misconfigurations to the admin UI as a banner instead of\n * a manifest 500.\n */\nexport function normalizeRegistryConfig(\n\tinput: RegistryConfigInput | undefined,\n): ManifestRegistryConfig | null {\n\tconst config = coerceRegistryConfig(input);\n\tif (!config) return null;\n\n\tconst aggregatorUrl = config.aggregatorUrl?.trim();\n\tif (!aggregatorUrl) {\n\t\tthrow new Error(\"registry.aggregatorUrl is required when registry is configured\");\n\t}\n\n\tvalidateAggregatorUrl(aggregatorUrl);\n\n\tconst out: ManifestRegistryConfig = {\n\t\t// Strip any trailing slash so `${aggregatorUrl}/xrpc/...` works\n\t\t// regardless of how the user wrote it.\n\t\taggregatorUrl: aggregatorUrl.replace(TRAILING_SLASHES, \"\"),\n\t};\n\n\tif (config.acceptLabelers) {\n\t\tout.acceptLabelers = config.acceptLabelers;\n\t}\n\n\tconst policy: ManifestRegistryConfig[\"policy\"] = {};\n\tlet hasPolicy = false;\n\n\tif (config.policy?.minimumReleaseAge !== undefined) {\n\t\tpolicy.minimumReleaseAgeSeconds = parseDurationSeconds(config.policy.minimumReleaseAge);\n\t\thasPolicy = true;\n\t}\n\n\tif (config.policy?.minimumReleaseAgeExclude !== undefined) {\n\t\t// Normalize at load time so callers (browser and server) can do\n\t\t// plain string compares without each one re-implementing the\n\t\t// case-folding rule.\n\t\tconst list = config.policy.minimumReleaseAgeExclude.map((entry) => {\n\t\t\tconst trimmed = entry.trim();\n\t\t\tif (!trimmed) {\n\t\t\t\tthrow new Error(\"registry.policy.minimumReleaseAgeExclude entries cannot be empty\");\n\t\t\t}\n\t\t\treturn trimmed.toLowerCase();\n\t\t});\n\t\tif (list.length > 0) {\n\t\t\tpolicy.minimumReleaseAgeExclude = list;\n\t\t\thasPolicy = true;\n\t\t}\n\t}\n\n\tif (hasPolicy) {\n\t\tout.policy = policy;\n\t}\n\n\treturn out;\n}\n","/**\n * Plugin identifier helpers for the experimental decentralized plugin\n * registry.\n *\n * Registry plugins are addressed by `(publisher_did, slug)`, but the\n * EmDash runtime threads a single `pluginId: string` through every\n * install primitive (R2 storage keys, `PluginStateRepository`,\n * `syncMarketplacePlugins`, sandbox cache keys). Rather than refactor\n * everything to carry a composite identifier, we normalize the registry\n * tuple to an opaque content-addressed id that satisfies the existing\n * `validatePluginIdentifier` shape (`/^[a-z][a-z0-9_-]*$/`).\n *\n * The normalized id is:\n *\n * `r_` + base32-encoded SHA-256(publisher_did + \"\\n\" + slug), truncated.\n *\n * Properties:\n *\n * - Deterministic. The same `(publisher, slug)` always produces the\n * same id, so re-resolving an installed plugin's metadata against\n * the aggregator is a straightforward lookup keyed by the columns\n * stored alongside `plugin_id` in `plugin_states`.\n * - Collision-resistant. 80 bits of truncated hash; a 50% birthday\n * collision happens around 2^40 distinct plugins, well beyond what\n * this registry will ever index.\n * - R2-safe. Lowercase alphanumerics + underscore (no hyphens), no\n * `:` or `/`. Existing sandbox cache keys (`${pluginId}:${version}`)\n * keep working because the id contains no `:`.\n * - Syntactically distinct from typical marketplace plugin ids: the\n * `r_` prefix plus exactly 16 base32 characters is unlikely to be\n * chosen as a marketplace id. Not formally guaranteed by the\n * validator -- marketplace ids may begin with `r_` and contain\n * hyphens -- so the install handler also performs an explicit\n * pre-existing-row check at the derived id and rejects any cross-\n * source collision (`PLUGIN_ID_COLLISION`).\n *\n * Reverse lookup (id → publisher + slug) requires the `plugin_states`\n * row -- the hash is one-way. That's intentional: any code path that\n * needs the human-meaningful pair already has the state row in hand.\n */\n\n/** Length (in base32 characters) of the truncated hash portion of the id. */\nconst HASH_LENGTH = 16;\n\n/** Total expected length of a registry plugin id. */\nexport const REGISTRY_PLUGIN_ID_LENGTH = 2 /* \"r_\" */ + HASH_LENGTH;\n\n/**\n * Regex matching a well-formed registry plugin id. Used by call sites\n * that need to distinguish registry installs from marketplace installs\n * without consulting the `source` column on `plugin_states`.\n *\n * The base32 alphabet here uses RFC 4648 lowercase without padding,\n * matching {@link base32Encode}'s output.\n */\nexport const REGISTRY_PLUGIN_ID_PATTERN = /^r_[a-z2-7]{16}$/;\n\nconst BASE32_ALPHABET = \"abcdefghijklmnopqrstuvwxyz234567\";\n\n/**\n * RFC 4648 base32 encoding without padding, lowercase. Implemented inline\n * rather than depending on a multibase library because (a) we only need\n * lowercase base32 here, (b) we need it to run identically in workerd,\n * Node, and the browser, and (c) the implementation is fewer lines than\n * the import statement would be.\n */\nfunction base32Encode(bytes: Uint8Array): string {\n\tlet bits = 0;\n\tlet value = 0;\n\tlet out = \"\";\n\tfor (const byte of bytes) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\tbits -= 5;\n\t\t\tout += BASE32_ALPHABET[(value >>> bits) & 0x1f];\n\t\t}\n\t}\n\tif (bits > 0) {\n\t\tout += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];\n\t}\n\treturn out;\n}\n\n/**\n * Derive the normalized plugin id for a registry-published plugin.\n *\n * Throws if either input is empty or whitespace-only -- a missing DID\n * or slug is always a programming error in the install path, not a\n * recoverable runtime condition.\n */\nexport async function makeRegistryPluginId(publisherDid: string, slug: string): Promise<string> {\n\tconst did = publisherDid.trim();\n\tconst s = slug.trim();\n\tif (!did) throw new Error(\"makeRegistryPluginId: publisherDid is required\");\n\tif (!s) throw new Error(\"makeRegistryPluginId: slug is required\");\n\n\t// `\\n` separator avoids ambiguity: no canonical did:plc / did:web form\n\t// contains a literal newline, so `(\"a\", \"b\\nc\")` cannot hash to the\n\t// same bytes as `(\"a\\nb\", \"c\")`.\n\tconst input = `${did}\\n${s}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(input));\n\tconst encoded = base32Encode(new Uint8Array(hashBuffer));\n\treturn `r_${encoded.slice(0, HASH_LENGTH)}`;\n}\n\n/**\n * Return whether `pluginId` is a well-formed registry plugin id.\n *\n * This is a syntactic check, not a database lookup -- it answers\n * \"could this id have come from `makeRegistryPluginId`?\", not \"is this\n * plugin installed?\".\n */\nexport function isRegistryPluginId(pluginId: string): boolean {\n\treturn REGISTRY_PLUGIN_ID_PATTERN.test(pluginId);\n}\n","/**\n * Registry plugin install handler.\n *\n * Installs a plugin published to the experimental decentralized plugin\n * registry described in RFC 0001. The install flow:\n *\n * 1. Resolve `(handle, slug)` to a publisher DID via the configured\n * aggregator's `resolvePackage` XRPC.\n * 2. Look up the requested release (or the policy-filtered latest one)\n * via `getLatestRelease` / `listReleases`.\n * 3. Reject the install if the aggregator surfaces a `security:yanked`\n * hard-enforcement label or the release is below the configured\n * minimum release age.\n * 4. Fetch the bundle artifact, walking aggregator mirrors first and\n * falling back to the publisher-declared URL.\n * 5. Verify the artifact's multibase checksum against the signed\n * release record's `artifacts.package.checksum`.\n * 6. Extract `manifest.json` + `backend.js` + optional `admin.js` from\n * the gzipped tar bundle.\n * 7. Store the extracted files in site-local R2 under the\n * `registry/<plugin-id>/<version>/` prefix.\n * 8. Write a `plugin_states` row with `source = \"registry\"` and the\n * `(publisher_did, slug)` pair so updates can be resolved later.\n * 9. Sync the runtime so the plugin becomes active immediately.\n *\n * Known gaps (tracked separately):\n *\n * - The aggregator-supplied records are not yet cryptographically\n * verified against the publisher's MST signature. The signed bytes\n * and CIDs are passed through verbatim per the lexicon, but full\n * PDS-direct verification with proof traversal is follow-up work.\n * The artifact checksum is verified end-to-end against the value\n * in the (aggregator-relayed) release record, which is the actual\n * trust boundary for the bytes that end up in the sandbox.\n * - `acceptLabelers` is forwarded as-is to the aggregator; this\n * handler does not independently re-fetch and verify labels from\n * each labeller's DID. Aggregator label envelope tampering is\n * mitigated by the artifact checksum but not detected.\n */\n\nimport type { Did } from \"@atcute/lexicons\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { extractBundle } from \"../../plugins/marketplace.js\";\nimport type { PluginBundle } from \"../../plugins/marketplace.js\";\nimport type { SandboxRunner } from \"../../plugins/sandbox/types.js\";\nimport { PluginStateRepository } from \"../../plugins/state.js\";\nimport {\n\tcanonicalCapabilitiesForDriftCheck,\n\tcoerceRegistryConfig,\n\tparseDurationSeconds,\n\treleaseExemptFromMinimumAge,\n\tvalidateAggregatorUrl,\n} from \"../../registry/config.js\";\nimport { makeRegistryPluginId } from \"../../registry/plugin-id.js\";\nimport type { RegistryConfigInput } from \"../../registry/types.js\";\nimport { resolveAndValidateExternalUrl, SsrfError } from \"../../security/ssrf.js\";\nimport { EmDashStorageError } from \"../../storage/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { deleteBundleFromR2, storeBundleInR2 } from \"./marketplace.js\";\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface RegistryInstallInput {\n\t/**\n\t * Publisher DID. Required. The browser is expected to resolve\n\t * `(handle, slug) → (did, slug)` via the aggregator's\n\t * `resolvePackage` XRPC before posting -- the server then skips that\n\t * round-trip and looks up the package directly.\n\t *\n\t * Passing DID rather than handle here means installs work for\n\t * publishers whose handle the aggregator couldn't resolve at view\n\t * time (handle is \"best-effort\" per the lexicon -- absent for any\n\t * publisher whose DID document didn't resolve cleanly at ingest).\n\t */\n\tdid: string;\n\t/** Package slug (rkey of the publisher's profile record). */\n\tslug: string;\n\t/** Optional explicit version. When omitted, the aggregator's latest. */\n\tversion?: string;\n\t/**\n\t * Capabilities the admin acknowledged in the consent dialog, lifted\n\t * from the release record's `declaredAccess` block. Compared against\n\t * the bundle's `manifest.declaredAccess` to detect drift between\n\t * what the admin agreed to and what the bundle actually requests.\n\t *\n\t * When omitted, drift detection is skipped -- callers that don't\n\t * surface a consent UI before posting (e.g. CI scripts) opt out.\n\t */\n\tacknowledgedDeclaredAccess?: unknown;\n}\n\nexport interface RegistryInstallResult {\n\t/** Hashed, opaque plugin id used everywhere in the runtime. */\n\tpluginId: string;\n\t/** Publisher DID resolved from the handle. */\n\tpublisherDid: string;\n\t/** Publisher slug (== the registry slug). */\n\tslug: string;\n\t/** Installed version. */\n\tversion: string;\n\t/** Capabilities surfaced from the bundle's manifest. */\n\tcapabilities: string[];\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/** Matches a bare 64-character lowercase/uppercase hex SHA-256 digest. */\nconst SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/i;\n\n/** Compute the SHA-256 of `bytes` as a lowercase hex string. */\nasync function sha256Hex(bytes: Uint8Array): Promise<string> {\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Uint8Array is a valid BufferSource at runtime\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", bytes as unknown as BufferSource);\n\tconst arr = new Uint8Array(buf);\n\treturn Array.from(arr, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** multihash code for sha2-256 (single-byte varint). */\nconst MULTIHASH_SHA256_CODE = 0x12;\n/** sha2-256 digest length in bytes (single-byte varint). */\nconst MULTIHASH_SHA256_LENGTH = 0x20;\n\n/**\n * Compute the multibase-multihash sha2-256 checksum of `bytes`, in the\n * same `b<base32>` shape the registry CLI publishes\n * (`packages/plugin-cli/src/multihash.ts`). Returns a 56-character\n * string starting with `b`.\n *\n * The trust contract is: if both sides produce the same string for\n * the same bytes, the bytes are unchanged. We don't decode the\n * publisher-supplied checksum -- we just re-encode our own and compare,\n * which is equivalent and avoids needing a base32 decoder.\n */\nasync function sha256MultibaseMultihash(bytes: Uint8Array): Promise<string> {\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Uint8Array is a valid BufferSource at runtime\n\tconst digestBuf = await crypto.subtle.digest(\"SHA-256\", bytes as unknown as BufferSource);\n\tconst digest = new Uint8Array(digestBuf);\n\tconst multihash = new Uint8Array(2 + digest.length);\n\tmultihash[0] = MULTIHASH_SHA256_CODE;\n\tmultihash[1] = MULTIHASH_SHA256_LENGTH;\n\tmultihash.set(digest, 2);\n\tconst { toBase32 } = await import(\"@atcute/multibase\");\n\treturn `b${toBase32(multihash)}`;\n}\n\n/**\n * Verify that a checksum string from a release record's\n * `artifact.checksum` field corresponds to the SHA-256 of the given\n * bytes.\n *\n * Accepts two formats:\n *\n * - Bare lowercase/uppercase hex SHA-256 (64 chars). Convenience for\n * publishers / tools that emit hex rather than multibase.\n * - Multibase-multihash with the `b` (base32) prefix and sha2-256.\n * This is the format RFC 0001 mandates and the registry CLI emits\n * (see `packages/plugin-cli/src/multihash.ts`).\n *\n * Hash functions other than sha2-256 are out of scope for this\n * initial release; the install fails closed.\n */\nasync function verifyChecksum(bytes: Uint8Array, checksum: string): Promise<boolean> {\n\tif (SHA256_HEX_PATTERN.test(checksum)) {\n\t\tconst actual = await sha256Hex(bytes);\n\t\treturn checksum.toLowerCase() === actual;\n\t}\n\n\t// Multibase-base32 multihash with sha2-256. We re-encode our own\n\t// digest in the same shape and compare strings -- equivalent to\n\t// decoding and comparing bytes, but doesn't need a base32 decoder.\n\t// 56 chars = 'b' + base32(34 bytes) = 'b' + 55 chars.\n\tif (checksum.length === 56 && checksum.startsWith(\"b\")) {\n\t\tconst actual = await sha256MultibaseMultihash(bytes);\n\t\t// Case-insensitive: multibase 'b' is lowercase by convention but\n\t\t// some emitters use uppercase. RFC 4648 base32 alphabets are\n\t\t// case-insensitive.\n\t\treturn actual.toLowerCase() === checksum.toLowerCase();\n\t}\n\n\treturn false;\n}\n\n/**\n * Bytes-per-artifact cap on the gzipped tarball we'll download before\n * decompression. RFC 0001 caps a sandboxed plugin bundle at 256 KiB\n * decompressed (see `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts);\n * gzip on a mix of JSON manifest + JS code typically gives 0.3-0.6\n * ratio, so compressed bundles are well under 200 KiB in practice.\n * 512 KiB leaves margin for unusual file mixes that compress poorly\n * while still rejecting anything that's obviously not a legitimate\n * plugin bundle.\n */\nconst MAX_ARTIFACT_BYTES = 512 * 1024;\n\n/**\n * Maximum number of HTTP redirects followed during artifact download.\n * Each hop is independently URL-validated, so a malicious server cannot\n * redirect through a series of allowed-looking origins to reach a\n * forbidden one.\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * Wall-clock cap on any single artifact fetch attempt (per URL).\n * Defends against slow-loris mirrors that accept the connection but\n * never finish sending headers or body.\n */\nconst ARTIFACT_FETCH_TIMEOUT_MS = 15_000;\n\n/**\n * Total wall-clock budget for the artifact-download phase across all\n * mirrors and the declared URL. Even with the per-URL timeout, a\n * malicious mirror list could otherwise tie up the install request for\n * minutes; this caps total time at a budget interactive admins can\n * tolerate. Tuned so a fast happy path takes <1s of budget per\n * attempt and a worst case still completes in under a minute.\n */\nconst ARTIFACT_TOTAL_BUDGET_MS = 45_000;\n\n/**\n * Cap on the number of mirror URLs we try before falling back to the\n * publisher-declared URL. Matches the aggregator lexicon's\n * `mirrors` array length cap (16) but enforced here independently so\n * a misbehaving aggregator can't slow-loris us through hundreds of\n * URLs.\n */\nconst MAX_MIRRORS = 16;\n\n/**\n * Per-request timeout applied to every aggregator XRPC call\n * (`resolvePackage`, `getLatestRelease`, `listReleases`). Matches the\n * per-URL artifact-fetch cap. Without this, a slow-loris aggregator\n * can stall the install before the artifact phase even starts.\n */\nconst AGGREGATOR_REQUEST_TIMEOUT_MS = 15_000;\n\n/**\n * Total wall-clock budget for the aggregator-discovery phase\n * (resolve + selected-release lookup). Mirrors the artifact-download\n * budget. Worst case with the pinned-version path's 20-page cap is\n * 20 + 1 calls; capping the total ensures any one stalled call\n * still bounds the whole phase.\n */\nconst AGGREGATOR_TOTAL_BUDGET_MS = 30_000;\n\n/** Build a fetch function that enforces a per-request and per-budget timeout. */\nfunction timedFetch(totalDeadline: number): typeof fetch {\n\treturn (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {\n\t\tconst now = Date.now();\n\t\tconst remaining = Math.max(0, totalDeadline - now);\n\t\tif (remaining === 0) {\n\t\t\treturn Promise.reject(new Error(\"Aggregator request budget exhausted\"));\n\t\t}\n\t\tconst timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeout);\n\t\tconst callerSignal = init?.signal;\n\t\tif (callerSignal) {\n\t\t\tif (callerSignal.aborted) controller.abort(callerSignal.reason);\n\t\t\telse callerSignal.addEventListener(\"abort\", () => controller.abort(callerSignal.reason));\n\t\t}\n\t\treturn fetch(input, { ...init, signal: controller.signal }).finally(() => {\n\t\t\tclearTimeout(timer);\n\t\t});\n\t};\n}\n\n/**\n * Localhost-equivalent hostnames the artifact fetcher rejects in\n * production. The full literal-IP / DNS-rebinding blocklist lives in\n * `#security/ssrf.js` and is invoked via `resolveAndValidateExternalUrl`\n * below; this small set exists only because the artifact handler has\n * a dev-mode escape hatch that lets `http://localhost` through.\n */\nconst FORBIDDEN_HOSTNAMES = new Set([\n\t\"localhost\",\n\t\"localhost.localdomain\",\n\t\"ip6-localhost\",\n\t\"ip6-loopback\",\n]);\n\n/** Trailing dot on a hostname, stripped before URL host comparisons. */\nconst TRAILING_DOT = /\\.$/;\n\n/** Hostnames that resolve to the local machine; rejected outright in production. */\nfunction isLocalhostHostname(hostname: string): boolean {\n\t// WHATWG URL preserves brackets on IPv6 hostnames; strip them before\n\t// comparison so `[::1]` is recognised as localhost.\n\tconst stripped = hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\tconst h = stripped.startsWith(\"[\") && stripped.endsWith(\"]\") ? stripped.slice(1, -1) : stripped;\n\tif (FORBIDDEN_HOSTNAMES.has(h)) return true;\n\tif (h === \"localhost\") return true;\n\tif (h.endsWith(\".localhost\")) return true;\n\tif (h === \"127.0.0.1\" || h === \"::1\") return true;\n\tif (h.startsWith(\"::ffff:127.\") || h.startsWith(\"::ffff:7f00:\")) return true;\n\treturn false;\n}\n\n/**\n * Validate that `urlString` is a safe outbound target for artifact\n * downloads. Rejects non-HTTPS (except localhost in dev), embedded\n * credentials, any host that's a loopback / private / link-local\n * literal address, and any hostname whose resolved A or AAAA records\n * point at one of those addresses (closes the DNS-rebinding gap).\n *\n * Wraps `resolveAndValidateExternalUrl` from the import-pipeline SSRF\n * module so both code paths share one DoH cache, one resolver, one\n * blocklist, and one set of regression tests. Layers an\n * artifact-specific protocol/dev-localhost policy on top.\n *\n * `import.meta.env.DEV` is a Vite/Astro compile-time constant, so\n * production bundles cannot enable the dev escape hatch at runtime.\n */\nasync function assertSafeArtifactUrl(urlString: string): Promise<URL> {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(urlString);\n\t} catch {\n\t\tthrow new Error(`Invalid artifact URL: ${urlString}`);\n\t}\n\tif (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n\t\tthrow new Error(`Artifact URL protocol not allowed: ${url.protocol}`);\n\t}\n\tif (url.username || url.password) {\n\t\tthrow new Error(\"Artifact URL must not contain embedded credentials\");\n\t}\n\n\tconst rawHostname = url.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\t// Strip brackets so the IPv4/IPv6 checks see the canonical form.\n\tconst hostname =\n\t\trawHostname.startsWith(\"[\") && rawHostname.endsWith(\"]\")\n\t\t\t? rawHostname.slice(1, -1)\n\t\t\t: rawHostname;\n\tconst localhost = isLocalhostHostname(hostname);\n\n\t// In production: reject HTTP entirely and reject localhost over any\n\t// protocol -- a publisher pointing at `https://localhost` is still\n\t// trying to bounce the server through its own loopback interface.\n\tif (!import.meta.env.DEV) {\n\t\tif (url.protocol === \"http:\") {\n\t\t\tthrow new Error(\"Artifact URL must use https\");\n\t\t}\n\t\tif (localhost) {\n\t\t\tthrow new Error(`Artifact URL points to localhost: ${hostname}`);\n\t\t}\n\t} else if (url.protocol === \"http:\" && !localhost) {\n\t\t// Dev mode: http allowed only for localhost.\n\t\tthrow new Error(\"Artifact URL must use https (http allowed only for localhost in dev)\");\n\t}\n\n\tif (localhost) {\n\t\t// Dev-only path; nothing to resolve.\n\t\treturn url;\n\t}\n\n\t// Delegate IP-literal + DNS-rebinding validation to the import\n\t// pipeline's SSRF helper. Adapts the SsrfError to the existing\n\t// artifact-URL error vocabulary so callers keep their current\n\t// catch shape.\n\ttry {\n\t\treturn await resolveAndValidateExternalUrl(url.href);\n\t} catch (err) {\n\t\tif (err instanceof SsrfError) {\n\t\t\tthrow new Error(`Artifact URL rejected: ${err.message}`);\n\t\t}\n\t\tthrow err;\n\t}\n}\n\n/**\n * Fetch one URL with manual redirect handling so every hop is\n * URL-validated, a hard byte cap so a malicious response body cannot\n * exhaust memory before the checksum check rejects it, and a wall-clock\n * timeout that covers connect, headers, and body together. The timeout\n * is the minimum of the per-URL cap and the remaining total budget so\n * a late-arriving mirror still respects the install's global budget.\n */\nasync function fetchWithLimits(initialUrl: string, totalDeadline: number): Promise<Uint8Array> {\n\tconst now = Date.now();\n\tconst remaining = Math.max(0, totalDeadline - now);\n\tif (remaining === 0) {\n\t\tthrow new Error(\"Artifact download budget exhausted\");\n\t}\n\tconst perUrlTimeout = Math.min(ARTIFACT_FETCH_TIMEOUT_MS, remaining);\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(() => controller.abort(), perUrlTimeout);\n\ttry {\n\t\tlet current = await assertSafeArtifactUrl(initialUrl);\n\t\tlet response: Response;\n\t\tfor (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n\t\t\tresponse = await fetch(current.href, { redirect: \"manual\", signal: controller.signal });\n\t\t\tif (response.status < 300 || response.status >= 400) break;\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) break;\n\t\t\tif (hop === MAX_REDIRECTS) {\n\t\t\t\tthrow new Error(`Too many redirects fetching artifact (>${MAX_REDIRECTS})`);\n\t\t\t}\n\t\t\tconst next = new URL(location, current);\n\t\t\tcurrent = await assertSafeArtifactUrl(next.href);\n\t\t}\n\t\t// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- response is assigned in the first loop iteration\n\t\tconst finalResponse = response!;\n\t\tif (!finalResponse.ok) {\n\t\t\tthrow new Error(`HTTP ${finalResponse.status}`);\n\t\t}\n\n\t\t// Check Content-Length up front when present. Untrusted servers can\n\t\t// lie or omit it; the streaming cap below is the real defense.\n\t\tconst lengthHeader = finalResponse.headers.get(\"content-length\");\n\t\tif (lengthHeader) {\n\t\t\tconst declared = Number(lengthHeader);\n\t\t\tif (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Artifact too large (declared ${declared} bytes, limit ${MAX_ARTIFACT_BYTES})`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst body = finalResponse.body;\n\t\tif (!body) {\n\t\t\t// Workers can't return a null body for a normal GET; defensive fallback.\n\t\t\tconst buf = new Uint8Array(await finalResponse.arrayBuffer());\n\t\t\tif (buf.byteLength > MAX_ARTIFACT_BYTES) {\n\t\t\t\tthrow new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);\n\t\t\t}\n\t\t\treturn buf;\n\t\t}\n\n\t\tconst reader = body.getReader();\n\t\tconst chunks: Uint8Array[] = [];\n\t\tlet total = 0;\n\t\twhile (true) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tif (!value) continue;\n\t\t\ttotal += value.byteLength;\n\t\t\tif (total > MAX_ARTIFACT_BYTES) {\n\t\t\t\ttry {\n\t\t\t\t\tawait reader.cancel();\n\t\t\t\t} catch {\n\t\t\t\t\t// nothing to do\n\t\t\t\t}\n\t\t\t\tthrow new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);\n\t\t\t}\n\t\t\tchunks.push(value);\n\t\t}\n\n\t\tconst out = new Uint8Array(total);\n\t\tlet offset = 0;\n\t\tfor (const chunk of chunks) {\n\t\t\tout.set(chunk, offset);\n\t\t\toffset += chunk.byteLength;\n\t\t}\n\t\treturn out;\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/**\n * Strip query string and fragment from a URL for use in\n * client-visible error messages. Registry artifacts are often hosted\n * on storage backends that include presigned tokens in the query\n * string; surfacing the raw URL on a failed install leaks those\n * tokens into the admin's HTTP response and any log drain that\n * captures the error chain. Origin + pathname is enough to identify\n * the host and resource without exposing credentials.\n *\n * Falls back to a generic placeholder when the URL is malformed.\n */\nfunction redactUrlForError(raw: string): string {\n\ttry {\n\t\tconst u = new URL(raw);\n\t\treturn `${u.origin}${u.pathname}`;\n\t} catch {\n\t\treturn \"<malformed url>\";\n\t}\n}\n\n/** Walk artifact source URLs in priority order and return the first that fetches successfully. */\nasync function fetchArtifact(mirrors: string[], declaredUrl: string): Promise<Uint8Array> {\n\t// Clamp mirrors regardless of what the lexicon type says -- a buggy\n\t// or malicious aggregator could return more than the spec'd limit\n\t// and slow-loris each one. The declared URL is always tried last.\n\tconst clampedMirrors = mirrors.slice(0, MAX_MIRRORS);\n\tconst urls = [...clampedMirrors, declaredUrl];\n\t// Client-visible errors carry redacted URLs (origin + path only).\n\t// The full URL with any query-string token is logged server-side\n\t// so operators can still debug delivery failures.\n\tconst clientErrors: string[] = [];\n\n\tconst totalDeadline = Date.now() + ARTIFACT_TOTAL_BUDGET_MS;\n\n\tfor (const url of urls) {\n\t\tif (Date.now() >= totalDeadline) {\n\t\t\tclientErrors.push(\"(total artifact download budget exhausted)\");\n\t\t\tbreak;\n\t\t}\n\t\ttry {\n\t\t\treturn await fetchWithLimits(url, totalDeadline);\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(`[registry-install] Artifact fetch failed from ${url}:`, message);\n\t\t\tclientErrors.push(`${redactUrlForError(url)}: ${message}`);\n\t\t}\n\t}\n\n\tthrow new Error(\n\t\t`Failed to download artifact from any source. Tried:\\n ${clientErrors.join(\"\\n \")}`,\n\t);\n}\n\n// ── Install ────────────────────────────────────────────────────────\n\nexport async function handleRegistryInstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tregistryConfigInput: RegistryConfigInput | undefined,\n\tinput: RegistryInstallInput,\n\topts?: { configuredPluginIds?: Set<string> },\n): Promise<ApiResult<RegistryInstallResult>> {\n\t// Accept either the bare-string shorthand or the full\n\t// `RegistryConfig` object (see `RegistryConfigInput`).\n\tconst registryConfig = coerceRegistryConfig(registryConfigInput);\n\tif (!registryConfig) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REGISTRY_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Registry is not configured\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!storage) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"STORAGE_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Storage is required for registry plugin installation\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SANDBOX_NOT_AVAILABLE\",\n\t\t\t\tmessage: \"Sandbox runner is required for registry plugins\",\n\t\t\t},\n\t\t};\n\t}\n\n\t// Defense in depth: validate the aggregator URL even though the same\n\t// check runs at config-normalize time. Keeps every entrypoint into\n\t// `handleRegistryInstall` safe regardless of how the caller obtained\n\t// the config.\n\ttry {\n\t\tvalidateAggregatorUrl(registryConfig.aggregatorUrl);\n\t} catch (err) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REGISTRY_NOT_CONFIGURED\",\n\t\t\t\tmessage: err instanceof Error ? err.message : \"Invalid aggregator URL\",\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { did, slug, version: requestedVersion } = input;\n\n\t// Lazy-load the discovery client. Avoids pulling @atcute/client into\n\t// every code path that imports core/api/handlers.\n\tconst { DiscoveryClient } = await import(\"@emdash-cms/registry-client/discovery\");\n\n\t// Every aggregator XRPC call passes through `timedFetch`, which\n\t// enforces a per-request timeout and shares a single total-budget\n\t// deadline. Defends against a slow-loris aggregator stalling the\n\t// install before the artifact phase begins.\n\tconst aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;\n\tconst discovery = new DiscoveryClient({\n\t\taggregatorUrl: registryConfig.aggregatorUrl,\n\t\tacceptLabelers: registryConfig.acceptLabelers,\n\t\tfetch: timedFetch(aggregatorDeadline),\n\t});\n\n\t// Basic shape check on the DID. The browser is expected to send a\n\t// DID resolved via the aggregator's `resolvePackage`; reject obvious\n\t// malformations here rather than letting the XRPC call fail\n\t// opaquely. The lexicon's `did:${string}:${string}` template is the\n\t// authoritative check.\n\tif (!did.startsWith(\"did:\") || did.split(\":\").length < 3) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"INVALID_DID\",\n\t\t\t\tmessage: \"DID must be a valid atproto DID (e.g. did:plc:abc123)\",\n\t\t\t},\n\t\t};\n\t}\n\n\ttry {\n\t\t// Step 1: look up the package by DID + slug. The browser already\n\t\t// resolved any handle to a DID via `resolvePackage`; we skip that\n\t\t// round-trip and go straight to `getPackage`.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above\n\t\tconst publisherDid = did as Did;\n\t\tconst packageView = await discovery.getPackage({\n\t\t\tdid: publisherDid,\n\t\t\tslug,\n\t\t});\n\n\t\t// Step 2: select the target release.\n\t\t// For an explicit version, page through listReleases until we find\n\t\t// the matching record; the aggregator returns releases ordered by\n\t\t// semver descending. For \"latest\", use the dedicated convenience\n\t\t// endpoint which applies the aggregator's policy filter (yanked\n\t\t// exclusion etc.) server-side.\n\t\t//\n\t\t// Pagination is bounded both by total pages and by repeated-cursor\n\t\t// detection: a buggy or compromised aggregator could otherwise\n\t\t// return endless distinct cursors that never include the\n\t\t// requested version, hanging the install for the platform's\n\t\t// request-time budget.\n\t\tconst MAX_LIST_PAGES = 20; // 20 * 50 limit = 1000 releases worth\n\t\tconst latestRelease = await (async () => {\n\t\t\tif (!requestedVersion) {\n\t\t\t\treturn discovery.getLatestRelease({\n\t\t\t\t\tdid: publisherDid,\n\t\t\t\t\tpackage: slug,\n\t\t\t\t});\n\t\t\t}\n\t\t\tlet cursor: string | undefined;\n\t\t\tconst seenCursors = new Set<string>();\n\t\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\t\tif (cursor !== undefined) {\n\t\t\t\t\tif (seenCursors.has(cursor)) break;\n\t\t\t\t\tseenCursors.add(cursor);\n\t\t\t\t}\n\t\t\t\tconst result = await discovery.listReleases({\n\t\t\t\t\tdid: publisherDid,\n\t\t\t\t\tpackage: slug,\n\t\t\t\t\tcursor,\n\t\t\t\t\tlimit: 50,\n\t\t\t\t});\n\t\t\t\tfor (const r of result.releases) {\n\t\t\t\t\tif (r.version === requestedVersion) return r;\n\t\t\t\t}\n\t\t\t\tif (!result.cursor) break;\n\t\t\t\tcursor = result.cursor;\n\t\t\t}\n\t\t\treturn undefined;\n\t\t})();\n\t\tconst releaseView = latestRelease;\n\n\t\tif (!releaseView) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_RELEASE\",\n\t\t\t\t\tmessage: requestedVersion\n\t\t\t\t\t\t? `Version ${requestedVersion} not found for ${publisherDid}/${slug}`\n\t\t\t\t\t\t: `No installable release found for ${publisherDid}/${slug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Identity cross-check on every field the aggregator denormalises\n\t\t// onto the package and release views. A buggy or compromised\n\t\t// aggregator could otherwise return a release view for a\n\t\t// different `(did, slug, version)` than we asked for; the\n\t\t// handler would then fetch + checksum-verify + install bytes\n\t\t// under the requested package's pluginId but for a different\n\t\t// publisher's record. Checksum verification only proves the bytes\n\t\t// match the *returned* record, not that the record belongs to\n\t\t// the package we requested.\n\t\t// `releaseView.release` is validated against the release lexicon by\n\t\t// DiscoveryClient (or `null` if it didn't conform). A `null` here makes\n\t\t// the identity checks below fail closed, which is the desired outcome.\n\t\tconst signedRelease = releaseView.release;\n\t\tif (packageView.did !== publisherDid || packageView.slug !== slug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage: \"Aggregator returned a package view for a different publisher or slug.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (\n\t\t\treleaseView.did !== publisherDid ||\n\t\t\treleaseView.package !== slug ||\n\t\t\tsignedRelease?.package !== slug ||\n\t\t\t(requestedVersion !== undefined && releaseView.version !== requestedVersion) ||\n\t\t\tsignedRelease?.version !== releaseView.version\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Aggregator returned a release view that does not match the requested package or version.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst version = releaseView.version;\n\n\t\t// Step 3: takedown label check (hard-enforced via aggregator's\n\t\t// `atproto-accept-labelers` filtering, but we belt-and-suspenders\n\t\t// the package-level labels too).\n\t\tconst yanked = (packageView.labels ?? []).some(\n\t\t\t(l: { val?: string }) => l.val === \"security:yanked\",\n\t\t);\n\t\tconst releaseYanked = (releaseView.labels ?? []).some(\n\t\t\t(l: { val?: string }) => l.val === \"security:yanked\",\n\t\t);\n\t\tif (yanked || releaseYanked) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"RELEASE_YANKED\",\n\t\t\t\t\tmessage: \"This release has been withdrawn (security:yanked label).\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Step 3a: enforce the configured minimum release age. The browser\n\t\t// applies the same check up front for UX, but the gate lives here\n\t\t// -- a stale browser tab, a deep link, or a non-admin-UI caller\n\t\t// must still hit the holdback. The `minimumReleaseAgeExclude`\n\t\t// allowlist short-circuits the check for trusted publisher DIDs.\n\t\t//\n\t\t// Caveat: `releaseView.indexedAt` is aggregator-supplied envelope\n\t\t// data, not a signed timestamp. A compromised aggregator can\n\t\t// claim an arbitrary indexed-at date and bypass the holdback;\n\t\t// closing this gap requires fetching the release record's\n\t\t// signed createdAt from the publisher's PDS (deferred to the\n\t\t// follow-up that adds full MST verification). If the timestamp\n\t\t// is missing or malformed, we fail closed and reject the install.\n\t\t// `registryConfig` is the user-supplied integration option, not\n\t\t// the normalized manifest shape, so the duration parse runs once\n\t\t// per install. Catch a malformed value here -- normally caught at\n\t\t// `normalizeRegistryConfig` time, but a future config-mutation\n\t\t// path could re-enter with a bad value -- and surface it as a\n\t\t// structured error rather than letting it bubble out as a generic\n\t\t// 500.\n\t\tconst minimumReleaseAge = registryConfig.policy?.minimumReleaseAge;\n\t\tlet minimumReleaseAgeSeconds = 0;\n\t\tif (minimumReleaseAge !== undefined) {\n\t\t\ttry {\n\t\t\t\tminimumReleaseAgeSeconds = parseDurationSeconds(minimumReleaseAge);\n\t\t\t} catch (err) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"REGISTRY_POLICY_INVALID\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\terr instanceof Error\n\t\t\t\t\t\t\t\t? err.message\n\t\t\t\t\t\t\t\t: \"Invalid minimumReleaseAge value in registry config\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (minimumReleaseAgeSeconds > 0) {\n\t\t\tconst exclude = registryConfig.policy?.minimumReleaseAgeExclude?.map((e) =>\n\t\t\t\te.trim().toLowerCase(),\n\t\t\t);\n\t\t\tconst exempt = releaseExemptFromMinimumAge(exclude, publisherDid, slug);\n\t\t\tif (!exempt) {\n\t\t\t\tconst indexedAt = Date.parse(releaseView.indexedAt);\n\t\t\t\tif (!Number.isFinite(indexedAt)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: \"RELEASE_TIMESTAMP_INVALID\",\n\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\"Release record is missing a valid indexed-at timestamp; cannot evaluate minimum release age policy.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst ageSeconds = (Date.now() - indexedAt) / 1000;\n\t\t\t\tif (ageSeconds < minimumReleaseAgeSeconds) {\n\t\t\t\t\tconst remaining = Math.ceil(minimumReleaseAgeSeconds - ageSeconds);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: \"RELEASE_TOO_NEW\",\n\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t`This release does not meet the configured minimum release age of ` +\n\t\t\t\t\t\t\t\t`${minimumReleaseAgeSeconds}s. It will be installable in ~${remaining}s.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Derive the normalized opaque plugin id we'll use as the\n\t\t// runtime-wide identifier from here on. The publisher_did + slug\n\t\t// stay in the state row for update resolution and admin display.\n\t\tconst pluginId = await makeRegistryPluginId(publisherDid, slug);\n\n\t\t// Block installation if a configured (trusted) plugin shares this\n\t\t// id. Mirrors the marketplace install's PLUGIN_ID_CONFLICT check.\n\t\tif (opts?.configuredPluginIds?.has(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_ID_CONFLICT\",\n\t\t\t\t\tmessage: \"A configured plugin with the same derived id already exists\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check for an existing install (any source) under the derived id.\n\t\t// We reject all pre-existing rows -- if the row is from a registry\n\t\t// install of this same package, the caller should go through the\n\t\t// (future) update flow; if it's from any other source, the\n\t\t// pluginId collision means installing would silently mutate an\n\t\t// unrelated plugin's lifecycle row.\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (existing) {\n\t\t\tif (existing.source === \"registry\") {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"ALREADY_INSTALLED\",\n\t\t\t\t\t\tmessage: `Plugin ${publisherDid}/${slug} is already installed`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_ID_COLLISION\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t`A non-registry plugin already exists at the derived id ${pluginId}. ` +\n\t\t\t\t\t\t\"Uninstall it before installing this registry plugin.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Step 4: fetch the artifact bytes.\n\t\t// `releaseView.release` is lexicon-validated by DiscoveryClient (or\n\t\t// `null`); a missing url/checksum (incl. the `null` case) fails closed\n\t\t// below. Mirrors come from the envelope (aggregator operational data,\n\t\t// not part of the signed record).\n\t\tconst release = releaseView.release;\n\t\tconst declaredUrl = release?.artifacts?.package?.url;\n\t\tconst declaredChecksum = release?.artifacts?.package?.checksum;\n\n\t\tif (!declaredUrl || !declaredChecksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_RELEASE\",\n\t\t\t\t\tmessage: \"Release record is missing artifact url or checksum\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst mirrors = releaseView.mirrors ?? [];\n\t\tconst artifactBytes = await fetchArtifact(mirrors, declaredUrl);\n\n\t\t// Step 5: verify the bytes against the signed record's checksum.\n\t\tconst checksumOk = await verifyChecksum(artifactBytes, declaredChecksum);\n\t\tif (!checksumOk) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CHECKSUM_MISMATCH\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Artifact bytes do not match the release record's checksum, or the checksum encoding is unsupported.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Step 6: extract the bundle.\n\t\tlet bundle: PluginBundle;\n\t\ttry {\n\t\t\tbundle = await extractBundle(artifactBytes);\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_BUNDLE\",\n\t\t\t\t\tmessage: err instanceof Error ? err.message : \"Failed to extract plugin bundle\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Manifest sanity: declared version must match the release's version.\n\t\tif (bundle.manifest.version !== version) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MANIFEST_VERSION_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest version (${bundle.manifest.version}) does not match release version (${version})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Manifest identity: the bundle's `manifest.id` is the publisher's\n\t\t// natural plugin id (their slug). It MUST equal the slug the\n\t\t// install was requested for; otherwise a malicious registry bundle\n\t\t// could declare `manifest.id: \"audit-log\"` and confuse the sandbox\n\t\t// bridge, which uses `manifest.id` as the trust key for\n\t\t// per-plugin storage, cron schedules, and bridge-scoped\n\t\t// operations.\n\t\tif (bundle.manifest.id !== slug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MANIFEST_ID_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest id (${bundle.manifest.id}) does not match registry slug (${slug})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Rewrite the manifest's id to the derived opaque pluginId before\n\t\t// it reaches R2 storage or the sandbox loader. The sandbox uses\n\t\t// `manifest.id` as its identity for per-plugin storage and bridge\n\t\t// calls; addressing it by the same pluginId we use in the runtime\n\t\t// cache, R2 prefix, and `_plugin_state` row keeps every layer\n\t\t// in sync and prevents registry installs from colliding with\n\t\t// marketplace plugins that happen to share the publisher's slug.\n\t\tbundle.manifest = { ...bundle.manifest, id: pluginId };\n\n\t\t// Capability consent gate: the admin MUST acknowledge the\n\t\t// capabilities the bundle's manifest actually declares before we\n\t\t// install it. The bundle manifest is the only source of truth\n\t\t// the runtime sandbox enforces -- the release record's\n\t\t// `declaredAccess` extension is an aggregator-supplied\n\t\t// assertion that the publisher may or may not have included,\n\t\t// and trusting it would let a malicious publisher (or a\n\t\t// compromised aggregator) ship a bundle whose manifest\n\t\t// requests `content:*` etc. behind an empty consent dialog.\n\t\t//\n\t\t// Two outcomes after normalization (filter to strings, dedupe,\n\t\t// sort):\n\t\t//\n\t\t// 1. The bundle declares no capabilities: install is allowed\n\t\t// without any acknowledgement (nothing to consent to).\n\t\t// 2. The bundle declares capabilities: install requires the\n\t\t// caller to send `acknowledgedDeclaredAccess`, and the\n\t\t// sorted lists must match exactly.\n\t\t//\n\t\t// We compare against the bundle's *capabilities* (the legacy\n\t\t// shape) for v1 because EmDash's existing sandbox enforces\n\t\t// capabilities, not the RFC's structured `declaredAccess`. Once\n\t\t// the runtime starts enforcing `declaredAccess` natively, this\n\t\t// comparison switches to that shape.\n\t\tconst actualCapabilities = canonicalCapabilitiesForDriftCheck(bundle.manifest.capabilities);\n\t\tif (actualCapabilities.length > 0) {\n\t\t\tif (input.acknowledgedDeclaredAccess === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"DECLARED_ACCESS_REQUIRED\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"This plugin declares capabilities that require consent. Re-open the install dialog to review and acknowledge them.\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst acknowledged = canonicalCapabilitiesForDriftCheck(input.acknowledgedDeclaredAccess);\n\t\t\tif (\n\t\t\t\tacknowledged.length !== actualCapabilities.length ||\n\t\t\t\tacknowledged.some((cap, i) => cap !== actualCapabilities[i])\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"DECLARED_ACCESS_DRIFT\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"Plugin manifest has changed since you consented. Re-open the install dialog to review the new permissions.\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Step 7: store in R2 under the registry prefix.\n\t\tawait storeBundleInR2(storage, pluginId, version, bundle, \"registry\");\n\n\t\t// Step 8: write plugin state.\n\t\t// Display name and description come from the *package profile*\n\t\t// (the signed record from the publisher's repo), not from the\n\t\t// bundle manifest -- the manifest carries the trust contract,\n\t\t// the profile carries the marketing copy.\n\t\t//\n\t\t// On failure, we may need to clean up the R2 bundle we just\n\t\t// wrote. But two parallel installs of the same (did, slug,\n\t\t// version) both pass the earlier `existing` check at line 822\n\t\t// (the read is not transactional with the insert), both upload\n\t\t// to the same deterministic R2 prefix (overwrites are\n\t\t// content-identical because R2 keys include the version and\n\t\t// the bundle is checksum-verified upstream), and then one wins\n\t\t// the insert while the other fails with a PK constraint\n\t\t// violation.\n\t\t//\n\t\t// If we blindly clean up R2 on every state-write failure, the\n\t\t// loser of that race would delete the winner's bundle and the\n\t\t// runtime would fail to load the plugin on the next sync.\n\t\t//\n\t\t// Instead: on state-write failure, re-query the state row. If\n\t\t// a row now exists for this pluginId, we lost the race -- the\n\t\t// winner owns the R2 bundle and we must not touch it. If the\n\t\t// row doesn't exist, the failure was a real DB error and the\n\t\t// R2 bytes are orphans; clean them up.\n\t\t//\n\t\t// Cleanup is best-effort; if it also fails, the row failure\n\t\t// still surfaces to the caller and the orphan R2 bundle costs\n\t\t// only the storage of a single checksum-verified zip.\n\t\t// `packageView.profile` is lexicon-validated by DiscoveryClient (or null).\n\t\tconst profile = packageView.profile;\n\t\ttry {\n\t\t\tawait stateRepo.upsert(pluginId, version, \"active\", {\n\t\t\t\tsource: \"registry\",\n\t\t\t\tdisplayName: profile?.name ?? slug,\n\t\t\t\tdescription: profile?.description ?? undefined,\n\t\t\t\tregistryPublisherDid: publisherDid,\n\t\t\t\tregistrySlug: slug,\n\t\t\t});\n\t\t} catch (stateErr) {\n\t\t\tlet lostRace = false;\n\t\t\ttry {\n\t\t\t\tconst winner = await stateRepo.get(pluginId);\n\t\t\t\tlostRace = winner !== undefined && winner !== null;\n\t\t\t} catch (probeErr) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[registry-install] Failed to probe state row for ${pluginId} after state-write failure; treating as orphan:`,\n\t\t\t\t\tprobeErr,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!lostRace) {\n\t\t\t\ttry {\n\t\t\t\t\tawait deleteBundleFromR2(storage, pluginId, version, \"registry\");\n\t\t\t\t} catch (cleanupErr) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[registry-install] Failed to clean up R2 bundle for ${pluginId}@${version} after state-row write failure:`,\n\t\t\t\t\t\tcleanupErr,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow stateErr;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\tpublisherDid,\n\t\t\t\tslug,\n\t\t\t\tversion,\n\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t},\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof EmDashStorageError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.code ?? \"STORAGE_ERROR\",\n\t\t\t\t\tmessage: \"Storage error while installing plugin\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"[registry-install] Failed:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"INSTALL_FAILED\",\n\t\t\t\tmessage: err instanceof Error ? err.message : \"Failed to install plugin from registry\",\n\t\t\t},\n\t\t};\n\t}\n}\n"],"mappings":"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"}