emdash 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (763) hide show
  1. package/dist/activation-K0kGBWuJ.mjs +382 -0
  2. package/dist/activation-K0kGBWuJ.mjs.map +1 -0
  3. package/dist/{adapters-Bb1vG41D.d.mts → adapters-CzL3H752.d.mts} +3 -3
  4. package/dist/{adapters-Bb1vG41D.d.mts.map → adapters-CzL3H752.d.mts.map} +1 -1
  5. package/dist/{allowed-origins-DW7sYyqg.mjs → allowed-origins-maylGaHv.mjs} +2 -2
  6. package/dist/{allowed-origins-DW7sYyqg.mjs.map → allowed-origins-maylGaHv.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -3
  8. package/dist/api/route-utils.mjs +22 -20
  9. package/dist/api/route-utils.mjs.map +1 -1
  10. package/dist/api/schemas/index.d.mts +2 -2
  11. package/dist/api/schemas/index.mjs +5 -5
  12. package/dist/api-B0w3uBiG.mjs +2269 -0
  13. package/dist/api-B0w3uBiG.mjs.map +1 -0
  14. package/dist/{api-tokens-KO24G1Fd.mjs → api-tokens-9YrrCoE6.mjs} +2 -2
  15. package/dist/{api-tokens-KO24G1Fd.mjs.map → api-tokens-9YrrCoE6.mjs.map} +1 -1
  16. package/dist/{apply-kUDt80-G.mjs → apply-B_VMpJ2P.mjs} +34 -35
  17. package/dist/{apply-kUDt80-G.mjs.map → apply-B_VMpJ2P.mjs.map} +1 -1
  18. package/dist/astro/index.d.mts +12 -12
  19. package/dist/astro/index.mjs +26 -15
  20. package/dist/astro/index.mjs.map +1 -1
  21. package/dist/astro/middleware/auth.d.mts +10 -10
  22. package/dist/astro/middleware/auth.d.mts.map +1 -1
  23. package/dist/astro/middleware/auth.mjs +10 -7
  24. package/dist/astro/middleware/auth.mjs.map +1 -1
  25. package/dist/astro/middleware/media-usage-write-fence.mjs +2 -2
  26. package/dist/astro/middleware/redirect.mjs +4 -4
  27. package/dist/astro/middleware/request-context.mjs +3 -3
  28. package/dist/astro/middleware/setup.mjs +1 -1
  29. package/dist/astro/middleware.d.mts +11 -12
  30. package/dist/astro/middleware.d.mts.map +1 -1
  31. package/dist/astro/middleware.mjs +168 -1305
  32. package/dist/astro/middleware.mjs.map +1 -1
  33. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  34. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  35. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
  36. package/dist/astro/routes/api/admin/api-tokens/index.mjs +4 -4
  37. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
  38. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +9 -9
  39. package/dist/astro/routes/api/admin/byline-fields/index.mjs +9 -9
  40. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +9 -9
  41. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +15 -15
  42. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +15 -15
  43. package/dist/astro/routes/api/admin/bylines/index.mjs +15 -15
  44. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
  45. package/dist/astro/routes/api/admin/comments/_id_.mjs +4 -4
  46. package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
  47. package/dist/astro/routes/api/admin/comments/counts.mjs +4 -4
  48. package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
  49. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  50. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  51. package/dist/astro/routes/api/admin/media-usage/activation.d.mts +9 -0
  52. package/dist/astro/routes/api/admin/media-usage/activation.d.mts.map +1 -0
  53. package/dist/astro/routes/api/admin/media-usage/activation.mjs +46 -0
  54. package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -0
  55. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +14 -13
  56. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +14 -13
  58. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/media-usage/progress.d.mts +9 -0
  60. package/dist/astro/routes/api/admin/media-usage/progress.d.mts.map +1 -0
  61. package/dist/astro/routes/api/admin/media-usage/progress.mjs +44 -0
  62. package/dist/astro/routes/api/admin/media-usage/progress.mjs.map +1 -0
  63. package/dist/astro/routes/api/admin/media-usage/repair.mjs +16 -11
  64. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  65. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +14 -13
  66. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +14 -13
  68. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
  69. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  70. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  71. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +54 -48
  72. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +54 -48
  74. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
  76. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
  78. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
  79. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +53 -47
  81. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +53 -47
  83. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
  85. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  87. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
  88. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +53 -47
  90. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
  92. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +53 -47
  94. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +54 -48
  96. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  97. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +172 -133
  99. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +54 -48
  101. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  102. package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
  103. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  104. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
  105. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  107. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
  108. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  109. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  110. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  111. package/dist/astro/routes/api/admin/users/_id_/index.mjs +7 -7
  112. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  113. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  114. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  115. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  116. package/dist/astro/routes/api/auth/invite/complete.mjs +11 -11
  117. package/dist/astro/routes/api/auth/invite/index.mjs +8 -8
  118. package/dist/astro/routes/api/auth/invite/register-options.mjs +10 -10
  119. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  120. package/dist/astro/routes/api/auth/magic-link/send.mjs +10 -10
  121. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  122. package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
  123. package/dist/astro/routes/api/auth/me.mjs +19 -9
  124. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  125. package/dist/astro/routes/api/auth/mode.mjs +2 -2
  126. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  127. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  128. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  129. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  130. package/dist/astro/routes/api/auth/passkey/options.mjs +12 -12
  131. package/dist/astro/routes/api/auth/passkey/register/options.mjs +10 -10
  132. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +11 -11
  133. package/dist/astro/routes/api/auth/passkey/verify.mjs +11 -11
  134. package/dist/astro/routes/api/auth/signup/complete.mjs +11 -11
  135. package/dist/astro/routes/api/auth/signup/request.mjs +10 -10
  136. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  137. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
  138. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
  139. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  140. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  141. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  142. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  143. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +11 -11
  144. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +8 -8
  145. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +16 -14
  146. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  147. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +16 -14
  148. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  149. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  150. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  151. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -8
  152. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
  153. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  154. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  155. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
  156. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  157. package/dist/astro/routes/api/content/_collection_/index.mjs +8 -8
  158. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  159. package/dist/astro/routes/api/dashboard.mjs +7 -7
  160. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  161. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  162. package/dist/astro/routes/api/import/probe.mjs +16 -16
  163. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  164. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  165. package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -12
  166. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  167. package/dist/astro/routes/api/import/wordpress/media.mjs +11 -11
  168. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  169. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  170. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +12 -10
  171. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  172. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  173. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +16 -16
  174. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  175. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +26 -26
  176. package/dist/astro/routes/api/manifest.mjs +4 -4
  177. package/dist/astro/routes/api/mcp.mjs +42 -42
  178. package/dist/astro/routes/api/media/_id_/confirm.mjs +11 -11
  179. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  180. package/dist/astro/routes/api/media/_id_/usage.mjs +18 -13
  181. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  182. package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
  183. package/dist/astro/routes/api/media/_id_.mjs +21 -13
  184. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  185. package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
  186. package/dist/astro/routes/api/media/folders/_id_.d.mts +10 -0
  187. package/dist/astro/routes/api/media/folders/_id_.d.mts.map +1 -0
  188. package/dist/astro/routes/api/media/folders/_id_.mjs +52 -0
  189. package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -0
  190. package/dist/astro/routes/api/media/folders/index.d.mts +9 -0
  191. package/dist/astro/routes/api/media/folders/index.d.mts.map +1 -0
  192. package/dist/astro/routes/api/media/folders/index.mjs +41 -0
  193. package/dist/astro/routes/api/media/folders/index.mjs.map +1 -0
  194. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  195. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  196. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  197. package/dist/astro/routes/api/media/upload-url.mjs +9 -9
  198. package/dist/astro/routes/api/media.d.mts.map +1 -1
  199. package/dist/astro/routes/api/media.mjs +28 -19
  200. package/dist/astro/routes/api/media.mjs.map +1 -1
  201. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
  202. package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
  203. package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
  204. package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
  205. package/dist/astro/routes/api/menus/_name_.mjs +8 -8
  206. package/dist/astro/routes/api/menus/index.mjs +8 -8
  207. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  208. package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
  209. package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
  210. package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
  211. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  212. package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
  213. package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
  214. package/dist/astro/routes/api/oauth/token.mjs +5 -5
  215. package/dist/astro/routes/api/openapi.json.mjs +153 -6
  216. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  217. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  218. package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
  219. package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
  220. package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
  221. package/dist/astro/routes/api/redirects/index.mjs +11 -11
  222. package/dist/astro/routes/api/relations/_id_/index.mjs +16 -14
  223. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  224. package/dist/astro/routes/api/relations/_id_/translations.mjs +11 -9
  225. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  226. package/dist/astro/routes/api/relations/index.mjs +16 -14
  227. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  229. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  230. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
  231. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  232. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
  233. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  234. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
  235. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  236. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
  237. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  238. package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
  239. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  240. package/dist/astro/routes/api/schema/collections/reorder.mjs +52 -46
  241. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  242. package/dist/astro/routes/api/schema/index.mjs +10 -8
  243. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  244. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
  245. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  246. package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
  247. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  248. package/dist/astro/routes/api/search/enable.mjs +10 -10
  249. package/dist/astro/routes/api/search/index.mjs +9 -9
  250. package/dist/astro/routes/api/search/rebuild.mjs +10 -10
  251. package/dist/astro/routes/api/search/stats.mjs +5 -5
  252. package/dist/astro/routes/api/search/suggest.mjs +9 -9
  253. package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
  254. package/dist/astro/routes/api/sections/index.mjs +9 -9
  255. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +6 -6
  256. package/dist/astro/routes/api/settings/backups/archives/index.mjs +5 -5
  257. package/dist/astro/routes/api/settings/backups/export.mjs +6 -6
  258. package/dist/astro/routes/api/settings/backups/index.mjs +7 -7
  259. package/dist/astro/routes/api/settings/email.mjs +4 -4
  260. package/dist/astro/routes/api/settings.mjs +13 -13
  261. package/dist/astro/routes/api/setup/admin-verify.mjs +12 -12
  262. package/dist/astro/routes/api/setup/admin.mjs +11 -11
  263. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  264. package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -27
  265. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  266. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  267. package/dist/astro/routes/api/setup/index.mjs +31 -29
  268. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/setup/status.mjs +3 -3
  270. package/dist/astro/routes/api/snapshot.mjs +7 -7
  271. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +13 -13
  272. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
  273. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
  274. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
  275. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +13 -13
  276. package/dist/astro/routes/api/taxonomies/_name_.mjs +13 -13
  277. package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
  278. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  279. package/dist/astro/routes/api/typegen.mjs +4 -4
  280. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  281. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  282. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  283. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
  284. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
  285. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
  286. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  287. package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
  288. package/dist/astro/routes/api/widget-components.mjs +2 -2
  289. package/dist/astro/routes/robots.txt.mjs +6 -6
  290. package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
  291. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  292. package/dist/astro/types.d.mts +19 -14
  293. package/dist/astro/types.d.mts.map +1 -1
  294. package/dist/auth/providers/github.d.mts +1 -1
  295. package/dist/auth/providers/google.d.mts +1 -1
  296. package/dist/{authorize-DUElxgOQ.mjs → authorize-ByYs-Uxs.mjs} +2 -2
  297. package/dist/{authorize-DUElxgOQ.mjs.map → authorize-ByYs-Uxs.mjs.map} +1 -1
  298. package/dist/{backup-DWYpJlLj.mjs → backup-ChyHfIoX.mjs} +5 -5
  299. package/dist/{backup-DWYpJlLj.mjs.map → backup-ChyHfIoX.mjs.map} +1 -1
  300. package/dist/{bundle-vn56sLos.mjs → bundle-DZFhFurR.mjs} +4 -4
  301. package/dist/{bundle-vn56sLos.mjs.map → bundle-DZFhFurR.mjs.map} +1 -1
  302. package/dist/{byline-yjvWq4ip.mjs → byline-Cpnwrje1.mjs} +6 -6
  303. package/dist/{byline-yjvWq4ip.mjs.map → byline-Cpnwrje1.mjs.map} +1 -1
  304. package/dist/{byline-fields-txaLGoME.mjs → byline-fields-CAyA_Umd.mjs} +2 -2
  305. package/dist/{byline-fields-txaLGoME.mjs.map → byline-fields-CAyA_Umd.mjs.map} +1 -1
  306. package/dist/{byline-registry-DDun8sxh.mjs → byline-registry-Bpu9-MVs.mjs} +2 -2
  307. package/dist/{byline-registry-DDun8sxh.mjs.map → byline-registry-Bpu9-MVs.mjs.map} +1 -1
  308. package/dist/{bylines-DjXS5p0P.mjs → bylines-BSo9B9RX.mjs} +7 -7
  309. package/dist/{bylines-DjXS5p0P.mjs.map → bylines-BSo9B9RX.mjs.map} +1 -1
  310. package/dist/{bylines-Bu7pchQ4.mjs → bylines-CHjvehVX.mjs} +3 -3
  311. package/dist/{bylines-Bu7pchQ4.mjs.map → bylines-CHjvehVX.mjs.map} +1 -1
  312. package/dist/{cache-C46-3Irl.mjs → cache-5VirJ2Cn.mjs} +4 -4
  313. package/dist/{cache-C46-3Irl.mjs.map → cache-5VirJ2Cn.mjs.map} +1 -1
  314. package/dist/capture-triggers-CNW1q3HK.mjs +362 -0
  315. package/dist/capture-triggers-CNW1q3HK.mjs.map +1 -0
  316. package/dist/{challenge-store-SIL5DsF2.mjs → challenge-store-D5pSY2S1.mjs} +1 -1
  317. package/dist/{challenge-store-SIL5DsF2.mjs.map → challenge-store-D5pSY2S1.mjs.map} +1 -1
  318. package/dist/{chunks-fiZ8G9RJ.mjs → chunks-bNiX6_DR.mjs} +2 -2
  319. package/dist/{chunks-fiZ8G9RJ.mjs.map → chunks-bNiX6_DR.mjs.map} +1 -1
  320. package/dist/cli/index.mjs +36 -52
  321. package/dist/cli/index.mjs.map +1 -1
  322. package/dist/client/cf-access.d.mts +1 -1
  323. package/dist/client/index.d.mts +74 -4
  324. package/dist/client/index.d.mts.map +1 -1
  325. package/dist/client/index.mjs +54 -2
  326. package/dist/client/index.mjs.map +1 -1
  327. package/dist/{collection-deletion-DQPvMD8R.mjs → collection-deletion-m_oi2c0N.mjs} +13 -356
  328. package/dist/collection-deletion-m_oi2c0N.mjs.map +1 -0
  329. package/dist/{comment-Dxlep6cE.mjs → comment-DhvLEdhQ.mjs} +1 -1
  330. package/dist/{comment-Dxlep6cE.mjs.map → comment-DhvLEdhQ.mjs.map} +1 -1
  331. package/dist/{comment-reaction-D-2J84UE.mjs → comment-reaction-CRuItEjt.mjs} +2 -2
  332. package/dist/{comment-reaction-D-2J84UE.mjs.map → comment-reaction-CRuItEjt.mjs.map} +1 -1
  333. package/dist/{comments-CTfZtLmM.mjs → comments-CLj4bi8I.mjs} +2 -2
  334. package/dist/{comments-CTfZtLmM.mjs.map → comments-CLj4bi8I.mjs.map} +1 -1
  335. package/dist/{components-CQy3lh9x.mjs → components-v4U4DT2y.mjs} +1 -1
  336. package/dist/{components-CQy3lh9x.mjs.map → components-v4U4DT2y.mjs.map} +1 -1
  337. package/dist/{config-COQzTe5Q.d.mts → config-DrfghTPe.d.mts} +1 -1
  338. package/dist/{config-COQzTe5Q.d.mts.map → config-DrfghTPe.d.mts.map} +1 -1
  339. package/dist/{content-ZHCc1ngN.mjs → content-CP-SeXNa.mjs} +5 -4
  340. package/dist/content-CP-SeXNa.mjs.map +1 -0
  341. package/dist/{content-list-query-wJ2WN8Vp.d.mts → content-list-query-BhejKVqb.d.mts} +1 -1
  342. package/dist/{content-list-query-wJ2WN8Vp.d.mts.map → content-list-query-BhejKVqb.d.mts.map} +1 -1
  343. package/dist/{content-refresh-B_8SJzuU.mjs → content-refresh-DLOfIhYc.mjs} +1094 -93
  344. package/dist/content-refresh-DLOfIhYc.mjs.map +1 -0
  345. package/dist/{media-usage-BZCydfvu.mjs → content-repair-BCxzy0r4.mjs} +5 -196
  346. package/dist/content-repair-BCxzy0r4.mjs.map +1 -0
  347. package/dist/{context-ZldbZcle.mjs → context-BR5hRWiK.mjs} +13 -13
  348. package/dist/{context-ZldbZcle.mjs.map → context-BR5hRWiK.mjs.map} +1 -1
  349. package/dist/{cron-Y9aLtgrA.mjs → cron-CWfWrWKG.mjs} +1 -1
  350. package/dist/{cron-Y9aLtgrA.mjs.map → cron-CWfWrWKG.mjs.map} +1 -1
  351. package/dist/{dashboard-DvHbk_cv.mjs → dashboard-CkIV2rnQ.mjs} +5 -5
  352. package/dist/{dashboard-DvHbk_cv.mjs.map → dashboard-CkIV2rnQ.mjs.map} +1 -1
  353. package/dist/db/index.d.mts +3 -3
  354. package/dist/db/index.mjs +3 -3
  355. package/dist/db/index.mjs.map +1 -1
  356. package/dist/db/libsql-migrations.d.mts +1 -1
  357. package/dist/db/libsql-migrations.mjs +4 -4
  358. package/dist/db/libsql.d.mts +1 -1
  359. package/dist/db/postgres-migrations.d.mts +1 -1
  360. package/dist/db/postgres-migrations.mjs +4 -4
  361. package/dist/db/postgres.d.mts +1 -1
  362. package/dist/db/sqlite-migrations.d.mts +1 -1
  363. package/dist/db/sqlite-migrations.mjs +4 -4
  364. package/dist/db/sqlite.d.mts +1 -1
  365. package/dist/db/sqlite.mjs +3 -3
  366. package/dist/db/sqlite.mjs.map +1 -1
  367. package/dist/{default-DdDl3fxv.mjs → default-YMEfcQUx.mjs} +1 -1
  368. package/dist/{default-DdDl3fxv.mjs.map → default-YMEfcQUx.mjs.map} +1 -1
  369. package/dist/{deferred-tasks-SDWxrgzJ.d.mts → deferred-tasks-DfqxH5Xf.d.mts} +1 -1
  370. package/dist/{deferred-tasks-SDWxrgzJ.d.mts.map → deferred-tasks-DfqxH5Xf.d.mts.map} +1 -1
  371. package/dist/{device-flow-C0GdD_z8.mjs → device-flow-CojjSBwb.mjs} +5 -5
  372. package/dist/{device-flow-C0GdD_z8.mjs.map → device-flow-CojjSBwb.mjs.map} +1 -1
  373. package/dist/{direct-executor-CDXeJCSs.mjs → direct-executor-BWeWiRYB.mjs} +3 -3
  374. package/dist/{direct-executor-CDXeJCSs.mjs.map → direct-executor-BWeWiRYB.mjs.map} +1 -1
  375. package/dist/{dist-D_6CKR8z.mjs → dist-89xCrGqi.mjs} +1 -1
  376. package/dist/{dist-D_6CKR8z.mjs.map → dist-89xCrGqi.mjs.map} +1 -1
  377. package/dist/{email-console-BHNjIwiB.mjs → email-console-CEJgR_LB.mjs} +1 -1
  378. package/dist/{email-console-BHNjIwiB.mjs.map → email-console-CEJgR_LB.mjs.map} +1 -1
  379. package/dist/{enrich-_ySMEvEE.mjs → enrich-BX0IDp4z.mjs} +3 -3
  380. package/dist/{enrich-_ySMEvEE.mjs.map → enrich-BX0IDp4z.mjs.map} +1 -1
  381. package/dist/{error-DupyJZE2.mjs → error-D9w94fjV.mjs} +2 -2
  382. package/dist/{error-DupyJZE2.mjs.map → error-D9w94fjV.mjs.map} +1 -1
  383. package/dist/{errors-CLUPm7aD.mjs → errors-n2j49uNf.mjs} +22 -2
  384. package/dist/errors-n2j49uNf.mjs.map +1 -0
  385. package/dist/{escape-DUY2r2Zs.mjs → escape-DN4Qmjsl.mjs} +1 -1
  386. package/dist/{escape-DUY2r2Zs.mjs.map → escape-DN4Qmjsl.mjs.map} +1 -1
  387. package/dist/{field-defs-cache-BzwBlJrR.mjs → field-defs-cache-Bh4q6oar.mjs} +5 -5
  388. package/dist/{field-defs-cache-BzwBlJrR.mjs.map → field-defs-cache-Bh4q6oar.mjs.map} +1 -1
  389. package/dist/focal-point-BeWFo6Xx.mjs +23 -0
  390. package/dist/focal-point-BeWFo6Xx.mjs.map +1 -0
  391. package/dist/{fts-manager-DyZEohZC.mjs → fts-manager-BhOdW0bi.mjs} +3 -3
  392. package/dist/{fts-manager-DyZEohZC.mjs.map → fts-manager-BhOdW0bi.mjs.map} +1 -1
  393. package/dist/{hash-Cg0TXJPV.mjs → hash-CTcngWvj.mjs} +1 -1
  394. package/dist/{hash-Cg0TXJPV.mjs.map → hash-CTcngWvj.mjs.map} +1 -1
  395. package/dist/{hreflang-DwT2E0Sa.d.mts → hreflang-7pTHsLZJ.d.mts} +2 -2
  396. package/dist/{hreflang-DwT2E0Sa.d.mts.map → hreflang-7pTHsLZJ.d.mts.map} +1 -1
  397. package/dist/{hreflang-BBe83kKa.mjs → hreflang-Huc6O5wa.mjs} +7 -7
  398. package/dist/{hreflang-BBe83kKa.mjs.map → hreflang-Huc6O5wa.mjs.map} +1 -1
  399. package/dist/{identity-Ys6Dlc9C.mjs → identity-DXmb4aVp.mjs} +3 -3
  400. package/dist/{identity-Ys6Dlc9C.mjs.map → identity-DXmb4aVp.mjs.map} +1 -1
  401. package/dist/{import-DPkLmeXO.mjs → import-C4_O3KXU.mjs} +5 -5
  402. package/dist/{import-DPkLmeXO.mjs.map → import-C4_O3KXU.mjs.map} +1 -1
  403. package/dist/{index-C9KuQyO-.d.mts → index-6bRee8zb.d.mts} +3 -3
  404. package/dist/{index-C9KuQyO-.d.mts.map → index-6bRee8zb.d.mts.map} +1 -1
  405. package/dist/{index-BVBZj4-7.d.mts → index-DWbJrsAg.d.mts} +128 -52
  406. package/dist/index-DWbJrsAg.d.mts.map +1 -0
  407. package/dist/index.d.mts +21 -21
  408. package/dist/index.mjs +78 -72
  409. package/dist/{init-lock-6b309ZrF.mjs → init-lock-DJkX6Hto.mjs} +1 -1
  410. package/dist/{init-lock-6b309ZrF.mjs.map → init-lock-DJkX6Hto.mjs.map} +1 -1
  411. package/dist/{load-YJ2xl3ra.mjs → load-Bo7fZAbg.mjs} +2 -2
  412. package/dist/{load-YJ2xl3ra.mjs.map → load-Bo7fZAbg.mjs.map} +1 -1
  413. package/dist/{loader-CdG_Ci3H.mjs → loader-ZsjE4paL.mjs} +2 -2
  414. package/dist/{loader-CdG_Ci3H.mjs.map → loader-ZsjE4paL.mjs.map} +1 -1
  415. package/dist/{manifest-builder-B252yQUf.mjs → manifest-builder-QXuqvVMP.mjs} +2 -2
  416. package/dist/{manifest-builder-B252yQUf.mjs.map → manifest-builder-QXuqvVMP.mjs.map} +1 -1
  417. package/dist/{manifest-schema-BXbUAHod.mjs → manifest-schema-CUYPfCEH.mjs} +1 -1
  418. package/dist/{manifest-schema-BXbUAHod.mjs.map → manifest-schema-CUYPfCEH.mjs.map} +1 -1
  419. package/dist/media/image-endpoint.mjs +1 -1
  420. package/dist/media/index.d.mts +1 -1
  421. package/dist/media/index.mjs +13 -3
  422. package/dist/media/index.mjs.map +1 -1
  423. package/dist/media/local-runtime.d.mts +14 -12
  424. package/dist/media/local-runtime.d.mts.map +1 -1
  425. package/dist/media/local-runtime.mjs +11 -5
  426. package/dist/media/local-runtime.mjs.map +1 -1
  427. package/dist/{media-BWWWb8kg.mjs → media-BjwNuLEu.mjs} +100 -6
  428. package/dist/media-BjwNuLEu.mjs.map +1 -0
  429. package/dist/{media-CzhAn2--.mjs → media-D6SeAfBl.mjs} +41 -12
  430. package/dist/media-D6SeAfBl.mjs.map +1 -0
  431. package/dist/{media-allowlist-D6x-Us02.mjs → media-allowlist-D35v7bmL.mjs} +3 -2
  432. package/dist/media-allowlist-D35v7bmL.mjs.map +1 -0
  433. package/dist/media-folders-D-0CmRZj.mjs +210 -0
  434. package/dist/media-folders-D-0CmRZj.mjs.map +1 -0
  435. package/dist/{media-upload-CHBVn6m-.mjs → media-upload-C7bLXGcI.mjs} +9 -9
  436. package/dist/{media-upload-CHBVn6m-.mjs.map → media-upload-C7bLXGcI.mjs.map} +1 -1
  437. package/dist/{media-url-BvoX88FA.mjs → media-url-DH7giMx_.mjs} +1 -1
  438. package/dist/{media-url-BvoX88FA.mjs.map → media-url-DH7giMx_.mjs.map} +1 -1
  439. package/dist/media-usage-BTRJvS6V.mjs +1833 -0
  440. package/dist/media-usage-BTRJvS6V.mjs.map +1 -0
  441. package/dist/media-usage-activation-Dx0kPV6b.mjs +78 -0
  442. package/dist/media-usage-activation-Dx0kPV6b.mjs.map +1 -0
  443. package/dist/{media-usage-work-DrCbqBcl.mjs → media-usage-work-C-fhM30c.mjs} +221 -59
  444. package/dist/media-usage-work-C-fhM30c.mjs.map +1 -0
  445. package/dist/{media-usage-work-CLo_6tct.mjs → media-usage-work-DWb2XPMI.mjs} +4 -4
  446. package/dist/{media-usage-work-CLo_6tct.mjs.map → media-usage-work-DWb2XPMI.mjs.map} +1 -1
  447. package/dist/{media-usage-write-fence-aHEgN1zB.mjs → media-usage-write-fence-BwoJVRDC.mjs} +2 -2
  448. package/dist/{media-usage-write-fence-aHEgN1zB.mjs.map → media-usage-write-fence-BwoJVRDC.mjs.map} +1 -1
  449. package/dist/{menus-TCNSd2vp.mjs → menus-CD7EFLwz.mjs} +3 -3
  450. package/dist/{menus-TCNSd2vp.mjs.map → menus-CD7EFLwz.mjs.map} +1 -1
  451. package/dist/{menus-DSo5QeW0.mjs → menus-CNg0QlDt.mjs} +42 -26
  452. package/dist/menus-CNg0QlDt.mjs.map +1 -0
  453. package/dist/migrations/index.d.mts +1 -1
  454. package/dist/migrations/index.mjs +3 -3
  455. package/dist/{mime-DfIVjNkr.mjs → mime-CUIG6x84.mjs} +1 -1
  456. package/dist/mime-CUIG6x84.mjs.map +1 -0
  457. package/dist/{mode-BDoH_WcF.mjs → mode-DaUM_MB2.mjs} +1 -1
  458. package/dist/{mode-BDoH_WcF.mjs.map → mode-DaUM_MB2.mjs.map} +1 -1
  459. package/dist/node-sqlite-compat-Udb4Y-rs.mjs +46 -0
  460. package/dist/node-sqlite-compat-Udb4Y-rs.mjs.map +1 -0
  461. package/dist/{normalize-Bc-s_r-a.mjs → normalize-CSxdKBVa.mjs} +22 -1
  462. package/dist/normalize-CSxdKBVa.mjs.map +1 -0
  463. package/dist/{oauth-authorization-iOikmrJd.mjs → oauth-authorization-Dpsb-fMJ.mjs} +5 -5
  464. package/dist/{oauth-authorization-iOikmrJd.mjs.map → oauth-authorization-Dpsb-fMJ.mjs.map} +1 -1
  465. package/dist/{oauth-clients-CvI0nHMJ.mjs → oauth-clients-Bxnoe_67.mjs} +1 -1
  466. package/dist/{oauth-clients-CvI0nHMJ.mjs.map → oauth-clients-Bxnoe_67.mjs.map} +1 -1
  467. package/dist/{oauth-state-store-Ba8KZlwW.mjs → oauth-state-store-0C4s0K6w.mjs} +1 -1
  468. package/dist/{oauth-state-store-Ba8KZlwW.mjs.map → oauth-state-store-0C4s0K6w.mjs.map} +1 -1
  469. package/dist/{oauth-user-lookup-C5SQwGYC.mjs → oauth-user-lookup-wlGYSeqX.mjs} +1 -1
  470. package/dist/{oauth-user-lookup-C5SQwGYC.mjs.map → oauth-user-lookup-wlGYSeqX.mjs.map} +1 -1
  471. package/dist/object-cache/memory.d.mts +1 -1
  472. package/dist/{options-CBXIHUoT.mjs → options-BHPeIbBd.mjs} +1 -1
  473. package/dist/{options-CBXIHUoT.mjs.map → options-BHPeIbBd.mjs.map} +1 -1
  474. package/dist/{options--hhz-jZ1.d.mts → options-BhZIeFY1.d.mts} +3 -3
  475. package/dist/{options--hhz-jZ1.d.mts.map → options-BhZIeFY1.d.mts.map} +1 -1
  476. package/dist/page/index.d.mts +2 -2
  477. package/dist/{parse-DFbuc-Fh.mjs → parse-BRJg1kz8.mjs} +2 -2
  478. package/dist/{parse-DFbuc-Fh.mjs.map → parse-BRJg1kz8.mjs.map} +1 -1
  479. package/dist/{passkey-config-DFAmUyuE.mjs → passkey-config-B3P0nHRR.mjs} +1 -1
  480. package/dist/{passkey-config-DFAmUyuE.mjs.map → passkey-config-B3P0nHRR.mjs.map} +1 -1
  481. package/dist/{patterns-CiyXeDgr.mjs → patterns-D9iDO3j8.mjs} +1 -1
  482. package/dist/{patterns-CiyXeDgr.mjs.map → patterns-D9iDO3j8.mjs.map} +1 -1
  483. package/dist/{placeholder-Dp0K3RkD.d.mts → placeholder-BR4ZhVJl.d.mts} +7 -1
  484. package/dist/placeholder-BR4ZhVJl.d.mts.map +1 -0
  485. package/dist/plugin-types.d.mts +1 -1
  486. package/dist/plugin-utils.d.mts +10 -10
  487. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  488. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  489. package/dist/{policy-TluUIxuu.mjs → policy-BVRganJQ.mjs} +2 -2
  490. package/dist/{policy-TluUIxuu.mjs.map → policy-BVRganJQ.mjs.map} +1 -1
  491. package/dist/{portable-text-imBW5cHK.mjs → portable-text-C8Tvp4_g.mjs} +1 -1
  492. package/dist/{portable-text-imBW5cHK.mjs.map → portable-text-C8Tvp4_g.mjs.map} +1 -1
  493. package/dist/{preview-PW-nOnq4.mjs → preview-dn2dUyM0.mjs} +2 -2
  494. package/dist/{preview-PW-nOnq4.mjs.map → preview-dn2dUyM0.mjs.map} +1 -1
  495. package/dist/{protocol-DjsJHTlK.d.mts → protocol-CuBEsqgE.d.mts} +2 -2
  496. package/dist/{protocol-DjsJHTlK.d.mts.map → protocol-CuBEsqgE.d.mts.map} +1 -1
  497. package/dist/{public-url-CWoFhQT8.mjs → public-url-DRz_AinV.mjs} +1 -1
  498. package/dist/{public-url-CWoFhQT8.mjs.map → public-url-DRz_AinV.mjs.map} +1 -1
  499. package/dist/{query-zTVAR1Sl.mjs → query-D18vTdha.mjs} +14 -14
  500. package/dist/{query-zTVAR1Sl.mjs.map → query-D18vTdha.mjs.map} +1 -1
  501. package/dist/{query-BWs7O5Il.mjs → query-tZDEdtCN.mjs} +4 -4
  502. package/dist/{query-BWs7O5Il.mjs.map → query-tZDEdtCN.mjs.map} +1 -1
  503. package/dist/{rate-limit-Bx4g9m3p.mjs → rate-limit-on7KPo-v.mjs} +2 -2
  504. package/dist/{rate-limit-Bx4g9m3p.mjs.map → rate-limit-on7KPo-v.mjs.map} +1 -1
  505. package/dist/{redirect-COsS6SWJ.mjs → redirect-CZkbozu9.mjs} +2 -2
  506. package/dist/{redirect-COsS6SWJ.mjs.map → redirect-CZkbozu9.mjs.map} +1 -1
  507. package/dist/{redirect-BgAOGHDq.mjs → redirect-DrbEmU9a.mjs} +1 -1
  508. package/dist/{redirect-BgAOGHDq.mjs.map → redirect-DrbEmU9a.mjs.map} +1 -1
  509. package/dist/{redirects-DyChwDkG.mjs → redirects-BahWiu8Z.mjs} +5 -5
  510. package/dist/{redirects-DyChwDkG.mjs.map → redirects-BahWiu8Z.mjs.map} +1 -1
  511. package/dist/{redirects-DTDHWH31.mjs → redirects-CRIM55KH.mjs} +6 -6
  512. package/dist/{redirects-DTDHWH31.mjs.map → redirects-CRIM55KH.mjs.map} +1 -1
  513. package/dist/{api-BeBglVxU.mjs → registry-BiO5WwPq.mjs} +483 -2462
  514. package/dist/registry-BiO5WwPq.mjs.map +1 -0
  515. package/dist/{registry-Cqa2I749.mjs → registry-Cx-jEJOS.mjs} +9 -123
  516. package/dist/registry-Cx-jEJOS.mjs.map +1 -0
  517. package/dist/{relation-BnHB0T4J.mjs → relation-A6wLF_DS.mjs} +2 -2
  518. package/dist/{relation-BnHB0T4J.mjs.map → relation-A6wLF_DS.mjs.map} +1 -1
  519. package/dist/{relations-BBApI4e2.mjs → relations-BTPM2saV.mjs} +2 -2
  520. package/dist/{relations-BBApI4e2.mjs.map → relations-BTPM2saV.mjs.map} +1 -1
  521. package/dist/{relations-BHo5A8Zf.d.mts → relations-C7B46bcU.d.mts} +192 -33
  522. package/dist/relations-C7B46bcU.d.mts.map +1 -0
  523. package/dist/{relations-D5sM5fW0.mjs → relations-Cj5hZ6Zg.mjs} +4 -4
  524. package/dist/{relations-D5sM5fW0.mjs.map → relations-Cj5hZ6Zg.mjs.map} +1 -1
  525. package/dist/{request-cache-BSUptuJR.mjs → request-cache-DH9zdCOD.mjs} +1 -1
  526. package/dist/{request-cache-BSUptuJR.mjs.map → request-cache-DH9zdCOD.mjs.map} +1 -1
  527. package/dist/request-context.d.mts +1 -1
  528. package/dist/{request-meta-DgZJh3zG.mjs → request-meta-DRp5ENd_.mjs} +2 -2
  529. package/dist/{request-meta-DgZJh3zG.mjs.map → request-meta-DRp5ENd_.mjs.map} +1 -1
  530. package/dist/{resolve-DOhVEvty.mjs → resolve-DxUpTMmE.mjs} +1 -1
  531. package/dist/{resolve-DOhVEvty.mjs.map → resolve-DxUpTMmE.mjs.map} +1 -1
  532. package/dist/{runner-C1T11CfC.mjs → runner-77GOZCnH.mjs} +410 -286
  533. package/dist/runner-77GOZCnH.mjs.map +1 -0
  534. package/dist/{runner-BYHZn3Jy.d.mts → runner-zgqA_CqP.d.mts} +2 -2
  535. package/dist/{runner-BYHZn3Jy.d.mts.map → runner-zgqA_CqP.d.mts.map} +1 -1
  536. package/dist/runtime.d.mts +11 -11
  537. package/dist/runtime.mjs +1 -1
  538. package/dist/{schema-DZ5YVVZj.mjs → schema-BY-oVntF.mjs} +3 -3
  539. package/dist/{schema-DZ5YVVZj.mjs.map → schema-BY-oVntF.mjs.map} +1 -1
  540. package/dist/{schema-2AOsSn2v.mjs → schema-Cw9kVK0I.mjs} +35 -5
  541. package/dist/{schema-2AOsSn2v.mjs.map → schema-Cw9kVK0I.mjs.map} +1 -1
  542. package/dist/{scopes-8ClPF2bC.mjs → scopes-vMR1uIbt.mjs} +3 -3
  543. package/dist/{scopes-8ClPF2bC.mjs.map → scopes-vMR1uIbt.mjs.map} +1 -1
  544. package/dist/{search-u1QYdOCG.mjs → search-CfT3KxO5.mjs} +4 -4
  545. package/dist/{search-u1QYdOCG.mjs.map → search-CfT3KxO5.mjs.map} +1 -1
  546. package/dist/{secrets-BEgsAQEU.mjs → secrets-BH2Qow1-.mjs} +3 -3
  547. package/dist/{secrets-BEgsAQEU.mjs.map → secrets-BH2Qow1-.mjs.map} +1 -1
  548. package/dist/{sections-DmHviGyM.mjs → sections-DTfjyf1z.mjs} +2 -2
  549. package/dist/{sections-DmHviGyM.mjs.map → sections-DTfjyf1z.mjs.map} +1 -1
  550. package/dist/seed/index.d.mts +2 -2
  551. package/dist/seed/index.mjs +24 -22
  552. package/dist/seo/index.d.mts +3 -3
  553. package/dist/seo/index.mjs +16 -14
  554. package/dist/seo/index.mjs.map +1 -1
  555. package/dist/{seo-AFpVvTqr.mjs → seo-BSiTIXzd.mjs} +1 -1
  556. package/dist/{seo-AFpVvTqr.mjs.map → seo-BSiTIXzd.mjs.map} +1 -1
  557. package/dist/{seo-Dq4G-RrO.mjs → seo-Cm-dtvgk.mjs} +2 -2
  558. package/dist/{seo-Dq4G-RrO.mjs.map → seo-Cm-dtvgk.mjs.map} +1 -1
  559. package/dist/{service-BpArayPr.mjs → service-DYsDHEKe.mjs} +3 -3
  560. package/dist/{service-BpArayPr.mjs.map → service-DYsDHEKe.mjs.map} +1 -1
  561. package/dist/{session-user-BEorKVT-.mjs → session-user-CNAfvsdw.mjs} +1 -1
  562. package/dist/{session-user-BEorKVT-.mjs.map → session-user-CNAfvsdw.mjs.map} +1 -1
  563. package/dist/{settings-xRdoUZr0.mjs → settings-BgN-0DT-.mjs} +6 -6
  564. package/dist/{settings-xRdoUZr0.mjs.map → settings-BgN-0DT-.mjs.map} +1 -1
  565. package/dist/{settings-DE9xtzqd.mjs → settings-DjtkUo__.mjs} +3 -3
  566. package/dist/{settings-DE9xtzqd.mjs.map → settings-DjtkUo__.mjs.map} +1 -1
  567. package/dist/{setup-complete-BnT8pq_C.mjs → setup-complete-CcpXaPU7.mjs} +2 -2
  568. package/dist/{setup-complete-BnT8pq_C.mjs.map → setup-complete-CcpXaPU7.mjs.map} +1 -1
  569. package/dist/{setup-nonce-CBitf_oQ.mjs → setup-nonce-Reyw30jg.mjs} +1 -1
  570. package/dist/{setup-nonce-CBitf_oQ.mjs.map → setup-nonce-Reyw30jg.mjs.map} +1 -1
  571. package/dist/{single-flight-cache-B0eLKS6K.mjs → single-flight-cache-BPuGPdcN.mjs} +2 -2
  572. package/dist/{single-flight-cache-B0eLKS6K.mjs.map → single-flight-cache-BPuGPdcN.mjs.map} +1 -1
  573. package/dist/{site-url-DjpIXMXJ.mjs → site-url-Cs_V9n3V.mjs} +3 -3
  574. package/dist/{site-url-DjpIXMXJ.mjs.map → site-url-Cs_V9n3V.mjs.map} +1 -1
  575. package/dist/{snapshot-BBlGkb2q.mjs → snapshot-DSB8P7Lm.mjs} +1 -1
  576. package/dist/{snapshot-BBlGkb2q.mjs.map → snapshot-DSB8P7Lm.mjs.map} +1 -1
  577. package/dist/{ssrf-Cpvg_n3A.mjs → ssrf-DqebVE34.mjs} +21 -5
  578. package/dist/{ssrf-Cpvg_n3A.mjs.map → ssrf-DqebVE34.mjs.map} +1 -1
  579. package/dist/{state-DYwPue40.mjs → state-BJfU9Uzw.mjs} +12 -4
  580. package/dist/state-BJfU9Uzw.mjs.map +1 -0
  581. package/dist/{status-CWVYpbD8.mjs → status-DDu8HIQl.mjs} +1 -1
  582. package/dist/{status-CWVYpbD8.mjs.map → status-DDu8HIQl.mjs.map} +1 -1
  583. package/dist/storage/local.d.mts +1 -1
  584. package/dist/storage/local.mjs +1 -1
  585. package/dist/storage/s3.d.mts +1 -1
  586. package/dist/storage/s3.mjs +1 -1
  587. package/dist/{target-DixqqV5i.mjs → target-D0o26Azo.mjs} +1 -1
  588. package/dist/{target-DixqqV5i.mjs.map → target-D0o26Azo.mjs.map} +1 -1
  589. package/dist/{taxonomies-OHUIiIpI.mjs → taxonomies-CPGxo-bn.mjs} +5 -5
  590. package/dist/{taxonomies-OHUIiIpI.mjs.map → taxonomies-CPGxo-bn.mjs.map} +1 -1
  591. package/dist/{taxonomies-BlrncHi9.mjs → taxonomies-kmyudNGP.mjs} +7 -7
  592. package/dist/{taxonomies-BlrncHi9.mjs.map → taxonomies-kmyudNGP.mjs.map} +1 -1
  593. package/dist/{taxonomy-BqCZ1TdU.mjs → taxonomy-8X09dLN5.mjs} +4 -4
  594. package/dist/{taxonomy-BqCZ1TdU.mjs.map → taxonomy-8X09dLN5.mjs.map} +1 -1
  595. package/dist/{tokens-C39klX8R.mjs → tokens-gTS-IVUp.mjs} +1 -1
  596. package/dist/{tokens-C39klX8R.mjs.map → tokens-gTS-IVUp.mjs.map} +1 -1
  597. package/dist/{transaction-NQj4VJ7Z.mjs → transaction-0KOpOyMu.mjs} +2 -1
  598. package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-0KOpOyMu.mjs.map} +1 -1
  599. package/dist/{transport-RQE36APG.d.mts → transport-_6R1ub7t.d.mts} +1 -1
  600. package/dist/{transport-RQE36APG.d.mts.map → transport-_6R1ub7t.d.mts.map} +1 -1
  601. package/dist/{transport-DBJwa-Si.mjs → transport-fX59xaJz.mjs} +4 -3
  602. package/dist/{transport-DBJwa-Si.mjs.map → transport-fX59xaJz.mjs.map} +1 -1
  603. package/dist/{trusted-proxy-CEVekpby.mjs → trusted-proxy-fe6nEgR4.mjs} +1 -1
  604. package/dist/{trusted-proxy-CEVekpby.mjs.map → trusted-proxy-fe6nEgR4.mjs.map} +1 -1
  605. package/dist/types-75KIqzL9.mjs.map +1 -1
  606. package/dist/{types-BW1syrlw.mjs → types-BXUmsINL.mjs} +1 -1
  607. package/dist/{types-BW1syrlw.mjs.map → types-BXUmsINL.mjs.map} +1 -1
  608. package/dist/{types-BntycPTF.d.mts → types-Blb-MRC3.d.mts} +1 -1
  609. package/dist/{types-BntycPTF.d.mts.map → types-Blb-MRC3.d.mts.map} +1 -1
  610. package/dist/{types-C2pMvO62.d.mts → types-Br-ERaxh.d.mts} +2 -1
  611. package/dist/types-Br-ERaxh.d.mts.map +1 -0
  612. package/dist/{types-DWRhk9xZ.d.mts → types-BzBd3PeX.d.mts} +1 -1
  613. package/dist/{types-DWRhk9xZ.d.mts.map → types-BzBd3PeX.d.mts.map} +1 -1
  614. package/dist/{types-CXnOzDe1.d.mts → types-CLqvnd05.d.mts} +1 -1
  615. package/dist/{types-CXnOzDe1.d.mts.map → types-CLqvnd05.d.mts.map} +1 -1
  616. package/dist/{types-Dfk8KI5R.d.mts → types-D5VVLr7J.d.mts} +1 -1
  617. package/dist/{types-Dfk8KI5R.d.mts.map → types-D5VVLr7J.d.mts.map} +1 -1
  618. package/dist/{types-DQIDRKF2.d.mts → types-DHxzBjbD.d.mts} +10 -1
  619. package/dist/types-DHxzBjbD.d.mts.map +1 -0
  620. package/dist/{types-DN64_zzZ.d.mts → types-DJQxln7q.d.mts} +3 -3
  621. package/dist/{types-DN64_zzZ.d.mts.map → types-DJQxln7q.d.mts.map} +1 -1
  622. package/dist/{types-CtzytVXn.mjs → types-DM3vioBY.mjs} +1 -1
  623. package/dist/{types-CtzytVXn.mjs.map → types-DM3vioBY.mjs.map} +1 -1
  624. package/dist/{types-BcqdcT-r.d.mts → types-D_DWe7k9.d.mts} +1 -1
  625. package/dist/{types-BcqdcT-r.d.mts.map → types-D_DWe7k9.d.mts.map} +1 -1
  626. package/dist/{types-DrJrVCT1.d.mts → types-Vg6f5i3s.d.mts} +3 -3
  627. package/dist/{types-DrJrVCT1.d.mts.map → types-Vg6f5i3s.d.mts.map} +1 -1
  628. package/dist/{upload-attempts-D4FS8LT6.mjs → upload-attempts-CNyGSepg.mjs} +1 -1
  629. package/dist/{upload-attempts-D4FS8LT6.mjs.map → upload-attempts-CNyGSepg.mjs.map} +1 -1
  630. package/dist/{url-pattern-BHS0V5Ng.mjs → url-pattern-K-KdXcSe.mjs} +1 -1
  631. package/dist/{url-pattern-BHS0V5Ng.mjs.map → url-pattern-K-KdXcSe.mjs.map} +1 -1
  632. package/dist/{user-DJZfUJVE.mjs → user-CtYY0tp2.mjs} +2 -2
  633. package/dist/{user-DJZfUJVE.mjs.map → user-CtYY0tp2.mjs.map} +1 -1
  634. package/dist/{utils-D4vD_Y98.mjs → utils-HztC73hq.mjs} +1 -1
  635. package/dist/{utils-D4vD_Y98.mjs.map → utils-HztC73hq.mjs.map} +1 -1
  636. package/dist/{validate-BMLBvxOC.mjs → validate-Dd-IUjnZ.mjs} +2 -2
  637. package/dist/{validate-BMLBvxOC.mjs.map → validate-Dd-IUjnZ.mjs.map} +1 -1
  638. package/dist/{validate-CTqt5nuE.d.mts → validate-DlqdOZXB.d.mts} +5 -5
  639. package/dist/{validate-CTqt5nuE.d.mts.map → validate-DlqdOZXB.d.mts.map} +1 -1
  640. package/dist/{validation-npvENUCB.mjs → validation-D83n7Npi.mjs} +10 -8
  641. package/dist/{validation-npvENUCB.mjs.map → validation-D83n7Npi.mjs.map} +1 -1
  642. package/dist/version-ri-sCyAp.mjs +7 -0
  643. package/dist/{version-DB_fqCSI.mjs.map → version-ri-sCyAp.mjs.map} +1 -1
  644. package/dist/{widgets-BD3ho7Rq.mjs → widgets-CpmZd9Q-.mjs} +4 -4
  645. package/dist/{widgets-BD3ho7Rq.mjs.map → widgets-CpmZd9Q-.mjs.map} +1 -1
  646. package/dist/{wxr-taxonomies-Ca68766D.d.mts → wxr-taxonomies-BFd0y72_.d.mts} +1 -1
  647. package/dist/{wxr-taxonomies-Ca68766D.d.mts.map → wxr-taxonomies-BFd0y72_.d.mts.map} +1 -1
  648. package/dist/{wxr-taxonomies-B1R3vBoD.mjs → wxr-taxonomies-Df4vxWcU.mjs} +4 -4
  649. package/dist/{wxr-taxonomies-B1R3vBoD.mjs.map → wxr-taxonomies-Df4vxWcU.mjs.map} +1 -1
  650. package/dist/{zod-generator-LC-on_Vb.mjs → zod-generator-DylzusHh.mjs} +24 -18
  651. package/dist/zod-generator-DylzusHh.mjs.map +1 -0
  652. package/package.json +16 -8
  653. package/src/api/errors.ts +20 -0
  654. package/src/api/handlers/index.ts +17 -0
  655. package/src/api/handlers/media-allowlist.ts +1 -0
  656. package/src/api/handlers/media-folders.ts +130 -0
  657. package/src/api/handlers/media-usage-activation.ts +102 -0
  658. package/src/api/handlers/media-usage.ts +108 -0
  659. package/src/api/handlers/media.ts +62 -2
  660. package/src/api/handlers/registry.ts +134 -316
  661. package/src/api/openapi/document.ts +192 -2
  662. package/src/api/schemas/media-usage.ts +77 -0
  663. package/src/api/schemas/media.ts +69 -0
  664. package/src/astro/integration/routes.ts +18 -0
  665. package/src/astro/integration/vite-config.ts +2 -7
  666. package/src/astro/middleware/auth.ts +4 -0
  667. package/src/astro/middleware.ts +0 -7
  668. package/src/astro/routes/api/admin/media-usage/activation.ts +42 -0
  669. package/src/astro/routes/api/admin/media-usage/progress.ts +37 -0
  670. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +246 -96
  671. package/src/astro/routes/api/auth/me.ts +14 -2
  672. package/src/astro/routes/api/media/[id].ts +3 -0
  673. package/src/astro/routes/api/media/folders/[id].ts +55 -0
  674. package/src/astro/routes/api/media/folders/index.ts +37 -0
  675. package/src/astro/routes/api/media.ts +12 -2
  676. package/src/astro/routes/api/setup/dev-bypass.ts +12 -0
  677. package/src/astro/types.ts +11 -1
  678. package/src/client/index.ts +140 -2
  679. package/src/client/transport.ts +6 -2
  680. package/src/components/EmDashImage.astro +154 -50
  681. package/src/components/Gallery.astro +7 -0
  682. package/src/components/InlinePortableTextEditor.tsx +130 -1
  683. package/src/components/LiveSearch.astro +23 -0
  684. package/src/components/inline-code-block.tsx +180 -49
  685. package/src/content/converters/gallery.ts +2 -0
  686. package/src/content/converters/types.ts +2 -0
  687. package/src/database/connection.ts +2 -31
  688. package/src/database/index.ts +1 -1
  689. package/src/database/json-recordset.ts +16 -0
  690. package/src/database/migrations/019_i18n.ts +1 -1
  691. package/src/database/migrations/071_restore_content_bylines_table.ts +43 -0
  692. package/src/database/migrations/072_media_folders.ts +72 -0
  693. package/src/database/migrations/073_media_focal_point.ts +57 -0
  694. package/src/database/migrations/runner.ts +8 -2
  695. package/src/database/repositories/content.ts +5 -4
  696. package/src/database/repositories/media-folders.ts +115 -0
  697. package/src/database/repositories/media-usage-work.ts +395 -111
  698. package/src/database/repositories/media-usage.ts +1038 -10
  699. package/src/database/repositories/media.ts +97 -29
  700. package/src/database/transaction.ts +1 -0
  701. package/src/database/types.ts +13 -0
  702. package/src/db/adapters.ts +2 -2
  703. package/src/db/node-sqlite-compat.ts +78 -0
  704. package/src/db/sqlite.ts +3 -3
  705. package/src/emdash-runtime.ts +110 -105
  706. package/src/fields/image.ts +7 -1
  707. package/src/fields/types.ts +6 -2
  708. package/src/index.ts +5 -0
  709. package/src/media/focal-point.ts +37 -0
  710. package/src/media/local-runtime.ts +8 -0
  711. package/src/media/mime.ts +1 -0
  712. package/src/media/normalize.ts +10 -0
  713. package/src/media/types.ts +10 -0
  714. package/src/media/usage/activation.ts +162 -35
  715. package/src/media/usage/capture-triggers.ts +19 -3
  716. package/src/media/usage/collection-deletion-processor.ts +7 -3
  717. package/src/media/usage/collection-deletion.ts +10 -0
  718. package/src/media/usage/content-refresh.ts +258 -39
  719. package/src/media/usage/content-snapshots.ts +128 -31
  720. package/src/media/usage/extractor.ts +32 -13
  721. package/src/media/usage/maintenance-engine.ts +166 -0
  722. package/src/media/usage/reconciliation-processor.ts +106 -29
  723. package/src/media/usage/reconciliation.ts +90 -7
  724. package/src/media/usage/work-processor.ts +342 -101
  725. package/src/plugins/index.ts +1 -0
  726. package/src/plugins/save-rejection.ts +20 -0
  727. package/src/plugins/scheduler/node.ts +1 -13
  728. package/src/plugins/scheduler/types.ts +0 -2
  729. package/src/plugins/state.ts +13 -3
  730. package/src/registry/artifact-fetch.ts +530 -0
  731. package/src/registry/config.ts +24 -6
  732. package/src/registry/types.ts +12 -15
  733. package/src/schema/types.ts +1 -0
  734. package/src/schema/zod-generator.ts +12 -4
  735. package/src/security/ssrf.ts +21 -2
  736. package/src/seed/apply.ts +4 -4
  737. package/src/ui.ts +2 -0
  738. package/dist/api-BeBglVxU.mjs.map +0 -1
  739. package/dist/collection-deletion-DQPvMD8R.mjs.map +0 -1
  740. package/dist/content-ZHCc1ngN.mjs.map +0 -1
  741. package/dist/content-refresh-B_8SJzuU.mjs.map +0 -1
  742. package/dist/errors-CLUPm7aD.mjs.map +0 -1
  743. package/dist/index-BVBZj4-7.d.mts.map +0 -1
  744. package/dist/media-BWWWb8kg.mjs.map +0 -1
  745. package/dist/media-CzhAn2--.mjs.map +0 -1
  746. package/dist/media-allowlist-D6x-Us02.mjs.map +0 -1
  747. package/dist/media-usage-BZCydfvu.mjs.map +0 -1
  748. package/dist/media-usage-work-DrCbqBcl.mjs.map +0 -1
  749. package/dist/menus-DSo5QeW0.mjs.map +0 -1
  750. package/dist/mime-DfIVjNkr.mjs.map +0 -1
  751. package/dist/normalize-Bc-s_r-a.mjs.map +0 -1
  752. package/dist/placeholder-Dp0K3RkD.d.mts.map +0 -1
  753. package/dist/registry-Cqa2I749.mjs.map +0 -1
  754. package/dist/relations-BHo5A8Zf.d.mts.map +0 -1
  755. package/dist/runner-C1T11CfC.mjs.map +0 -1
  756. package/dist/state-DYwPue40.mjs.map +0 -1
  757. package/dist/types-C2pMvO62.d.mts.map +0 -1
  758. package/dist/types-DQIDRKF2.d.mts.map +0 -1
  759. package/dist/version-DB_fqCSI.mjs +0 -7
  760. package/dist/zod-generator-LC-on_Vb.mjs.map +0 -1
  761. /package/dist/{api-tokens-rIXC6LdE.mjs → api-tokens-B5NHLNUN.mjs} +0 -0
  762. /package/dist/{ssrf-DpXgKsna.mjs → ssrf-BPKzkm6O.mjs} +0 -0
  763. /package/dist/{types-CP1CflWf.mjs → types-D6OcIp2P.mjs} +0 -0
@@ -1,2231 +1,19 @@
1
- import { i as validateStorageCollectionName, n as validateJsonFieldName, r as validatePluginIdentifier, t as validateIdentifier } from "./validate-BpwoDOgt.mjs";
2
- import { c as isSqlite, l as jsonExtractExpr, s as isPostgres } from "./dialect-helpers-BSbfaO3F.mjs";
3
- import { a as resolveConfiguredLocale, i as isI18nEnabled, r as getI18nConfig } from "./config-C3EBstHi.mjs";
4
- import { t as after } from "./after-CBU2vkoO.mjs";
5
- import { i as RevisionRepository, r as isSystemOrderField, t as ContentRepository } from "./content-ZHCc1ngN.mjs";
6
- import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-fiZ8G9RJ.mjs";
7
- import { n as isMissingTableError, t as isMissingColumnError } from "./db-errors-Cl-sKYTm.mjs";
8
- import { i as encodeBase64, t as decodeBase64 } from "./base64-CvLhUKQv.mjs";
9
- import { a as ScheduledNotDueError, i as InvalidCursorError, n as ContentMutationConflictError, r as EmDashValidationError, t as ContentCollectionNotFoundError } from "./types-CQPWhPxE.mjs";
10
- import { t as MediaRepository } from "./media-CzhAn2--.mjs";
11
- import { t as UserRepository } from "./user-DJZfUJVE.mjs";
12
- import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
13
- import { t as TaxonomyRepository } from "./taxonomy-BqCZ1TdU.mjs";
14
- import { t as CommentRepository } from "./comment-Dxlep6cE.mjs";
15
- import { t as OptionsRepository } from "./options-CBXIHUoT.mjs";
16
- import { t as RedirectRepository } from "./redirect-COsS6SWJ.mjs";
17
- import { r as requestCached } from "./request-cache-BSUptuJR.mjs";
18
- import { t as BylineRepository } from "./byline-yjvWq4ip.mjs";
19
- import { r as parseAllowedMimeTypes, t as matchesMimeAllowlist } from "./mime-DfIVjNkr.mjs";
20
- import { t as SeoRepository } from "./seo-Dq4G-RrO.mjs";
21
- import { n as invalidateRedirectCache } from "./cache-C46-3Irl.mjs";
22
- import { t as FTSManager } from "./fts-manager-DyZEohZC.mjs";
23
- import { u as invalidateTermCache } from "./taxonomies-BlrncHi9.mjs";
24
- import { r as hashString } from "./hash-Cg0TXJPV.mjs";
25
- import { t as ErrorCode } from "./errors-CLUPm7aD.mjs";
26
- import { t as PluginStateRepository } from "./state-DYwPue40.mjs";
27
- import { a as reconcileManifestAccess, i as pluginManifestSchema, r as normalizeManifestRoute } from "./manifest-schema-BXbUAHod.mjs";
28
- import { a as normalizeCapabilities, r as declaredAccessToCapabilities } from "./types-CP1CflWf.mjs";
29
- import { t as EmDashStorageError } from "./types-BW1syrlw.mjs";
30
- import { n as resolveAndValidateExternalUrl, t as SsrfError } from "./ssrf-Cpvg_n3A.mjs";
1
+ import { i as validateStorageCollectionName, n as validateJsonFieldName, r as validatePluginIdentifier } from "./validate-BpwoDOgt.mjs";
2
+ import { l as jsonExtractExpr, s as isPostgres } from "./dialect-helpers-BSbfaO3F.mjs";
3
+ import { t as PluginStateRepository } from "./state-BJfU9Uzw.mjs";
4
+ import { a as reconcileManifestAccess, i as pluginManifestSchema, r as normalizeManifestRoute } from "./manifest-schema-CUYPfCEH.mjs";
5
+ import { a as normalizeCapabilities, r as declaredAccessToCapabilities } from "./types-D6OcIp2P.mjs";
6
+ import { t as EmDashStorageError } from "./types-BXUmsINL.mjs";
7
+ import { r as resolveAndValidateExternalUrlTarget, t as SsrfError } from "./ssrf-DqebVE34.mjs";
31
8
  import { sql } from "kysely";
32
9
  import { createGzipDecoder, unpackTar } from "modern-tar";
33
10
  import { ClientResponseError, ClientValidationError } from "@atcute/client";
34
11
  import { checkEnvCompatibility, findSkippedEnvConstraints } from "@emdash-cms/registry-client/env";
12
+ import { evaluateRegistryReleaseWithdrawal } from "@emdash-cms/registry-client/withdrawal";
13
+ import { NSID } from "@emdash-cms/registry-lexicons";
14
+ import { fetchReleaseArtifact } from "@emdash-cms/registry-verification/artifact";
15
+ import { isDid } from "@atcute/lexicons/syntax";
35
16
 
36
- //#region src/api/rev.ts
37
- /**
38
- * Generate a _rev token from a content item's version and updatedAt.
39
- */
40
- function encodeRev(item) {
41
- return encodeBase64(`${item.version}:${item.updatedAt}`);
42
- }
43
- /**
44
- * Decode a _rev token into its components.
45
- * Returns null if the token is malformed.
46
- */
47
- function decodeRev(rev) {
48
- try {
49
- const decoded = decodeBase64(rev);
50
- const colonIdx = decoded.indexOf(":");
51
- if (colonIdx === -1) return null;
52
- const version = parseInt(decoded.slice(0, colonIdx), 10);
53
- const updatedAt = decoded.slice(colonIdx + 1);
54
- if (isNaN(version) || !updatedAt) return null;
55
- return {
56
- version,
57
- updatedAt
58
- };
59
- } catch {
60
- return null;
61
- }
62
- }
63
- /**
64
- * Validate a _rev token against a content item.
65
- * Returns null if valid (or if no _rev provided), or an error message if invalid.
66
- */
67
- function validateRev(rev, item) {
68
- if (!rev) return { valid: true };
69
- const decoded = decodeRev(rev);
70
- if (!decoded) return {
71
- valid: false,
72
- message: "Malformed _rev token"
73
- };
74
- if (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) return {
75
- valid: false,
76
- message: "Content has been modified since last read (version conflict)"
77
- };
78
- return { valid: true };
79
- }
80
-
81
- //#endregion
82
- //#region src/api/handlers/validate-media-fields.ts
83
- function asMediaRef(value) {
84
- if (value === null || value === void 0) return null;
85
- if (typeof value !== "object" || Array.isArray(value)) return null;
86
- return value;
87
- }
88
- function fail(message) {
89
- return {
90
- success: false,
91
- error: {
92
- code: "INVALID_MIME_FOR_FIELD",
93
- message
94
- }
95
- };
96
- }
97
- async function loadMediaFieldsForCollection(db, collectionSlug) {
98
- const rows = await db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
99
- "_emdash_fields.slug",
100
- "_emdash_fields.type",
101
- "_emdash_fields.validation"
102
- ]).where("_emdash_collections.slug", "=", collectionSlug).where("_emdash_fields.type", "in", ["file", "image"]).execute();
103
- const out = [];
104
- for (const row of rows) {
105
- const list = parseAllowedMimeTypes(row.validation);
106
- if (!list) continue;
107
- out.push({
108
- slug: row.slug,
109
- type: row.type,
110
- allowedMimeTypes: list
111
- });
112
- }
113
- return out;
114
- }
115
- async function validateMediaFields(db, collectionSlug, data) {
116
- const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
117
- if (fields.length === 0) return {
118
- success: true,
119
- data: true
120
- };
121
- const localIds = /* @__PURE__ */ new Set();
122
- for (const field of fields) {
123
- const ref = asMediaRef(data[field.slug]);
124
- if (!ref) continue;
125
- if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
126
- }
127
- const idList = [...localIds];
128
- const mimeById = /* @__PURE__ */ new Map();
129
- if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
130
- const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
131
- for (const r of rows) mimeById.set(r.id, r.mime_type);
132
- }
133
- for (const field of fields) {
134
- const value = data[field.slug];
135
- if (value === null || value === void 0) continue;
136
- const ref = asMediaRef(value);
137
- if (!ref) continue;
138
- const provider = typeof ref.provider === "string" ? ref.provider : "local";
139
- let mime;
140
- if (provider === "local") {
141
- if (typeof ref.id !== "string") return fail(`Field '${field.slug}' references media with an invalid id`);
142
- mime = mimeById.get(ref.id);
143
- if (!mime) return fail(`Field '${field.slug}' references media with unknown MIME type`);
144
- } else {
145
- if (typeof ref.mimeType !== "string") return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
146
- mime = ref.mimeType;
147
- }
148
- if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) return fail(`Field '${field.slug}' does not accept ${mime}`);
149
- }
150
- return {
151
- success: true,
152
- data: true
153
- };
154
- }
155
-
156
- //#endregion
157
- //#region src/api/handlers/content.ts
158
- /**
159
- * Narrow a caught error to one carrying a structured `apiError` discriminant.
160
- * Used by transaction callbacks that want to surface a specific error code
161
- * through the standard Error throwing path.
162
- */
163
- function hasApiError(error) {
164
- if (!(error instanceof Error) || !("apiError" in error)) return false;
165
- const { apiError } = error;
166
- return typeof apiError === "object" && apiError !== null && "code" in apiError && typeof apiError.code === "string";
167
- }
168
- /**
169
- * Extract a slug source (title or name) from content data.
170
- * Returns null if no suitable string field is found.
171
- */
172
- function getSlugSource(data) {
173
- if (typeof data.title === "string" && data.title.length > 0) return data.title;
174
- if (typeof data.name === "string" && data.name.length > 0) return data.name;
175
- return null;
176
- }
177
- /** Default SEO values for content without an explicit SEO row */
178
- const SEO_DEFAULTS = {
179
- title: null,
180
- description: null,
181
- image: null,
182
- canonical: null,
183
- noIndex: false
184
- };
185
- /**
186
- * Check if a collection has SEO enabled.
187
- */
188
- async function collectionHasSeo(db, collection) {
189
- return (await db.selectFrom("_emdash_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
190
- }
191
- async function getCollectionPublishConfig(db, collection) {
192
- const row = await db.selectFrom("_emdash_collections").select(["supports", "routable"]).where("slug", "=", collection).executeTakeFirst();
193
- const supports = row?.supports ? JSON.parse(row.supports) : [];
194
- return {
195
- supportsRevisions: Array.isArray(supports) && supports.includes("revisions"),
196
- routable: row?.routable !== 0
197
- };
198
- }
199
- function requireRoutablePublishSlug(routable, slug) {
200
- if (routable && !slug?.trim()) throw new EmDashValidationError("Cannot publish routable content without a slug");
201
- }
202
- /**
203
- * Hydrate SEO data on a single content item if the collection has SEO enabled.
204
- */
205
- async function hydrateSeo(db, collection, item, hasSeo) {
206
- if (!hasSeo) return;
207
- item.seo = await new SeoRepository(db).get(collection, item.id);
208
- }
209
- /**
210
- * Hydrate SEO data on multiple content items using a single batch query.
211
- */
212
- async function hydrateSeoMany(db, collection, items, hasSeo) {
213
- if (!hasSeo || items.length === 0) return;
214
- const seoMap = await new SeoRepository(db).getMany(collection, items.map((i) => i.id));
215
- for (const item of items) item.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };
216
- }
217
- async function hydrateBylines(db, collection, item) {
218
- const bylineRepo = new BylineRepository(db);
219
- const localeOpt = item.locale ? { locale: item.locale } : void 0;
220
- const bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);
221
- if (bylines.length > 0) {
222
- item.bylines = bylines.map((c) => ({
223
- ...c,
224
- source: "explicit"
225
- }));
226
- item.byline = bylines[0]?.byline ?? null;
227
- return;
228
- }
229
- if (item.primaryBylineId) {
230
- item.bylines = [];
231
- item.byline = null;
232
- return;
233
- }
234
- if (item.authorId) {
235
- const fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);
236
- if (fallback) {
237
- item.bylines = [{
238
- byline: fallback,
239
- sortOrder: 0,
240
- roleLabel: null,
241
- source: "inferred"
242
- }];
243
- item.byline = fallback;
244
- return;
245
- }
246
- }
247
- item.bylines = [];
248
- item.byline = null;
249
- }
250
- /**
251
- * Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.
252
- *
253
- * Items may live at different locales (e.g. a list endpoint returning the
254
- * translations of an entry). Group by `item.locale` and call the strict
255
- * per-locale repo method once per group so each item resolves against its
256
- * own locale's byline rows.
257
- */
258
- async function hydrateBylinesMany(db, collection, items) {
259
- if (items.length === 0) return;
260
- const bylineRepo = new BylineRepository(db);
261
- const localeBuckets = /* @__PURE__ */ new Map();
262
- for (const item of items) {
263
- const key = item.locale ?? null;
264
- const bucket = localeBuckets.get(key);
265
- if (bucket) bucket.push(item);
266
- else localeBuckets.set(key, [item]);
267
- }
268
- const bylinesByItem = /* @__PURE__ */ new Map();
269
- const itemsNeedingAuthorCheck = [];
270
- for (const [locale, bucket] of localeBuckets) {
271
- const localeOpt = locale ? { locale } : void 0;
272
- const ids = bucket.map((i) => i.id);
273
- const credits = await bylineRepo.getContentBylinesMany(collection, ids, localeOpt);
274
- for (const [id, list] of credits) bylinesByItem.set(id, list);
275
- for (const item of bucket) {
276
- if (credits.has(item.id) && credits.get(item.id).length > 0) continue;
277
- if (item.authorId) itemsNeedingAuthorCheck.push(item);
278
- }
279
- }
280
- const fallbackByItem = /* @__PURE__ */ new Map();
281
- if (itemsNeedingAuthorCheck.length > 0) {
282
- const authorBuckets = /* @__PURE__ */ new Map();
283
- for (const item of itemsNeedingAuthorCheck) {
284
- if (item.primaryBylineId) continue;
285
- const key = item.locale ?? null;
286
- const bucket = authorBuckets.get(key);
287
- if (bucket) bucket.push(item);
288
- else authorBuckets.set(key, [item]);
289
- }
290
- for (const [locale, bucket] of authorBuckets) {
291
- const localeOpt = locale ? { locale } : void 0;
292
- const authorIds = bucket.map((i) => i.authorId).filter((id) => id !== null);
293
- const uniqueAuthorIds = [...new Set(authorIds)];
294
- if (uniqueAuthorIds.length === 0) continue;
295
- const authorMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);
296
- for (const item of bucket) {
297
- if (!item.authorId) continue;
298
- const f = authorMap.get(item.authorId);
299
- if (f) fallbackByItem.set(item.id, f);
300
- }
301
- }
302
- }
303
- for (const item of items) {
304
- const explicit = bylinesByItem.get(item.id);
305
- if (explicit && explicit.length > 0) {
306
- item.bylines = explicit.map((c) => ({
307
- ...c,
308
- source: "explicit"
309
- }));
310
- item.byline = explicit[0]?.byline ?? null;
311
- continue;
312
- }
313
- const fallback = fallbackByItem.get(item.id);
314
- if (fallback) {
315
- item.bylines = [{
316
- byline: fallback,
317
- sortOrder: 0,
318
- roleLabel: null,
319
- source: "inferred"
320
- }];
321
- item.byline = fallback;
322
- continue;
323
- }
324
- item.bylines = [];
325
- item.byline = null;
326
- }
327
- }
328
- /**
329
- * Resolve an identifier (ID or slug) to a real content ID.
330
- * Returns the ID if found, null if not found.
331
- * When locale is provided, slug lookups are scoped to that locale.
332
- */
333
- async function resolveId(repo, collection, identifier, locale) {
334
- return (await repo.findByIdOrSlug(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
335
- }
336
- /**
337
- * Resolve an identifier (ID or slug) to a real content ID,
338
- * including trashed (soft-deleted) items.
339
- */
340
- async function resolveIdIncludingTrashed(repo, collection, identifier, locale) {
341
- return (await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale ? resolveConfiguredLocale(locale) : void 0))?.id ?? null;
342
- }
343
- /**
344
- * Resolve the columns a content-list search should match against. Always
345
- * includes `slug` (a standard column), adds the configured `titleField` plus
346
- * the `title`/`name` display fields when the collection actually defines them,
347
- * mirroring the admin's item-title resolution (titleField -> title -> name ->
348
- * slug), and includes every field explicitly marked searchable. Returning only
349
- * schema-backed columns avoids "no such column" errors.
350
- */
351
- async function resolveSearchColumns(db, collection) {
352
- const row = await db.selectFrom("_emdash_collections").select(["id", "title_field"]).where("slug", "=", collection).executeTakeFirst();
353
- if (!row) return ["slug"];
354
- const fields = await db.selectFrom("_emdash_fields").select(["slug", "searchable"]).where("collection_id", "=", row.id).orderBy("sort_order", "asc").execute();
355
- const columns = new Set(["slug"]);
356
- const fieldSlugs = new Set(fields.map((f) => f.slug));
357
- if (row.title_field && fieldSlugs.has(row.title_field)) columns.add(row.title_field);
358
- for (const candidate of ["title", "name"]) if (fieldSlugs.has(candidate)) columns.add(candidate);
359
- for (const field of fields) if (field.searchable === 1) columns.add(field.slug);
360
- return [...columns];
361
- }
362
- /**
363
- * Decide whether the content-list `q` filter can be served from the
364
- * collection's FTS5 index instead of a full-scan substring LIKE (#1517).
365
- *
366
- * Requires SQLite (FTS5 is SQLite-only), search enabled on the collection,
367
- * every non-slug display column present in the searchable-field set (or the
368
- * index would miss matches the LIKE finds), and the index table actually
369
- * existing.
370
- */
371
- async function canUseFtsForListFilter(db, collection, searchColumns) {
372
- if (!isSqlite(db)) return false;
373
- const ftsManager = new FTSManager(db);
374
- if (!(await ftsManager.getSearchConfig(collection))?.enabled) return false;
375
- const searchable = new Set(await ftsManager.getSearchableFields(collection));
376
- if (!searchColumns.every((col) => col === "slug" || searchable.has(col))) return false;
377
- return ftsManager.ftsTableExists(collection);
378
- }
379
- /**
380
- * Create a 301 auto-redirect from an entry's old URL to its new one after a
381
- * slug change, using the collection's URL pattern. Shared by
382
- * handleContentUpdate (direct slug edits) and handleContentPublish (slug edits
383
- * staged as `_slug` in a draft revision, which only land on publish).
384
- */
385
- async function createSlugChangeRedirect(db, collection, oldSlug, newSlug, contentId) {
386
- if (await slugStillTaken(db, collection, oldSlug, contentId)) return;
387
- const collectionRow = await db.selectFrom("_emdash_collections").select("url_pattern").where("slug", "=", collection).executeTakeFirst();
388
- await new RedirectRepository(db).createAutoRedirect(collection, oldSlug, newSlug, contentId, collectionRow?.url_pattern ?? null);
389
- invalidateRedirectCache();
390
- }
391
- /** Whether a row other than `contentId` still holds `slug` in this collection. */
392
- async function slugStillTaken(db, collection, slug, contentId) {
393
- validateIdentifier(collection, "collection slug");
394
- return (await sql`
395
- SELECT id FROM ${sql.ref(`ec_${collection}`)}
396
- WHERE slug = ${slug}
397
- AND id != ${contentId}
398
- AND deleted_at IS NULL
399
- LIMIT 1
400
- `.execute(db)).rows.length > 0;
401
- }
402
- /** Matches a date-only `YYYY-MM-DD` bound (no time component). */
403
- const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
404
- /**
405
- * Normalize a date-range bound to an ISO datetime for lexicographic comparison
406
- * against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the
407
- * appropriate UTC day boundary so the range stays inclusive: a `start` bound
408
- * becomes the start of the day and an `end` bound the end of the day.
409
- * Otherwise a date-only upper bound would exclude every same-day row (since
410
- * `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass
411
- * through unchanged.
412
- */
413
- function normalizeDateBound(value, edge) {
414
- if (!value) return void 0;
415
- if (!DATE_ONLY_RE.test(value)) return value;
416
- return edge === "start" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
417
- }
418
- /**
419
- * Build the repository's byline filter from the wire params.
420
- *
421
- * `locale` is the locale the list is scoped to, which is the locale an
422
- * inferred credit has to resolve at — the admin list is always scoped to the
423
- * locale picked in its switcher.
424
- */
425
- function resolveBylineFilter(params, locale) {
426
- const includeInferred = params.includeInferredBylines === true;
427
- if (params.bylinesNone) return {
428
- mode: "none",
429
- includeInferred,
430
- locale
431
- };
432
- const bylineIds = params.bylines ?? [];
433
- if (bylineIds.length === 0) return void 0;
434
- return {
435
- mode: "any",
436
- bylineIds,
437
- includeInferred,
438
- locale
439
- };
440
- }
441
- /**
442
- * Create content list handler
443
- */
444
- async function handleContentList(db, collection, params) {
445
- try {
446
- const repo = new ContentRepository(db);
447
- const where = {};
448
- if (params.status) where.status = params.status;
449
- const locale = params.locale ? resolveConfiguredLocale(params.locale) : void 0;
450
- if (locale) where.locale = locale;
451
- if (params.authorId) where.authorId = params.authorId;
452
- if (params.fieldFilters && Object.keys(params.fieldFilters).length > 0) where.fieldFilters = params.fieldFilters;
453
- const bylineFilter = resolveBylineFilter(params, locale);
454
- if (bylineFilter) where.bylineFilter = bylineFilter;
455
- if (params.dateField && (params.dateFrom || params.dateTo)) where.dateFilter = {
456
- field: params.dateField,
457
- from: normalizeDateBound(params.dateFrom, "start"),
458
- to: normalizeDateBound(params.dateTo, "end")
459
- };
460
- const q = params.q?.trim();
461
- if (q) {
462
- where.q = q;
463
- where.searchColumns = await resolveSearchColumns(db, collection);
464
- where.useFts = await canUseFtsForListFilter(db, collection, where.searchColumns);
465
- }
466
- let sortableExtras;
467
- if (params.orderBy && !isSystemOrderField(params.orderBy)) {
468
- const coll = await db.selectFrom("_emdash_collections").select(["title_field", "date_field"]).where("slug", "=", collection).executeTakeFirst();
469
- sortableExtras = [coll?.title_field, coll?.date_field].filter((slug) => !!slug);
470
- }
471
- const result = await repo.findMany(collection, {
472
- cursor: params.cursor,
473
- limit: params.limit || 50,
474
- where: Object.keys(where).length > 0 ? where : void 0,
475
- orderBy: params.orderBy ? {
476
- field: params.orderBy,
477
- direction: params.order || "desc"
478
- } : void 0,
479
- sortableExtras
480
- });
481
- const hasSeo = await collectionHasSeo(db, collection);
482
- await hydrateSeoMany(db, collection, result.items, hasSeo);
483
- await hydrateBylinesMany(db, collection, result.items);
484
- return {
485
- success: true,
486
- data: {
487
- items: result.items,
488
- nextCursor: result.nextCursor,
489
- total: result.total
490
- }
491
- };
492
- } catch (error) {
493
- if (error instanceof InvalidCursorError) return {
494
- success: false,
495
- error: {
496
- code: "INVALID_CURSOR",
497
- message: error.message
498
- }
499
- };
500
- if (error instanceof ContentCollectionNotFoundError || isMissingTableError(error)) return {
501
- success: false,
502
- error: {
503
- code: "COLLECTION_NOT_FOUND",
504
- message: `Collection '${collection}' not found`
505
- }
506
- };
507
- if (isMissingColumnError(error, "deleted_at")) return {
508
- success: false,
509
- error: {
510
- code: "COLLECTION_SCHEMA_MISMATCH",
511
- message: `Collection '${collection}' backing table is missing the 'deleted_at' column`
512
- }
513
- };
514
- if (error instanceof EmDashValidationError) return {
515
- success: false,
516
- error: {
517
- code: "VALIDATION_ERROR",
518
- message: error.message
519
- }
520
- };
521
- console.error("Content list error:", error);
522
- return {
523
- success: false,
524
- error: {
525
- code: "CONTENT_LIST_ERROR",
526
- message: "Failed to list content"
527
- }
528
- };
529
- }
530
- }
531
- /**
532
- * List the distinct authors of a collection's live content.
533
- *
534
- * Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN
535
- * only), this is gated on `content:read`, so any editor can filter by author.
536
- * Returns only users who have authored at least one non-trashed entry, sorted
537
- * by display name then email for a stable dropdown order.
538
- */
539
- async function handleContentAuthors(db, collection) {
540
- try {
541
- const authorIds = await new ContentRepository(db).findDistinctAuthorIds(collection);
542
- if (authorIds.length === 0) return {
543
- success: true,
544
- data: { items: [] }
545
- };
546
- return {
547
- success: true,
548
- data: { items: (await new UserRepository(db).findByIds(authorIds)).map((u) => ({
549
- id: u.id,
550
- name: u.name,
551
- email: u.email,
552
- avatarUrl: u.avatarUrl
553
- })).toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email)) }
554
- };
555
- } catch (error) {
556
- if (isMissingTableError(error)) return {
557
- success: false,
558
- error: {
559
- code: "COLLECTION_NOT_FOUND",
560
- message: `Collection '${collection}' not found`
561
- }
562
- };
563
- console.error("Content authors error:", error);
564
- return {
565
- success: false,
566
- error: {
567
- code: "CONTENT_AUTHORS_ERROR",
568
- message: "Failed to list content authors"
569
- }
570
- };
571
- }
572
- }
573
- /**
574
- * Get single content item
575
- */
576
- async function handleContentGet(db, collection, id, locale) {
577
- try {
578
- const item = await new ContentRepository(db).findByIdOrSlug(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
579
- if (!item) return {
580
- success: false,
581
- error: {
582
- code: "NOT_FOUND",
583
- message: `Content item not found: ${id}`
584
- }
585
- };
586
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
587
- await hydrateBylines(db, collection, item);
588
- return {
589
- success: true,
590
- data: {
591
- item,
592
- _rev: encodeRev(item)
593
- }
594
- };
595
- } catch (error) {
596
- console.error("Content get error:", error);
597
- return {
598
- success: false,
599
- error: {
600
- code: "CONTENT_GET_ERROR",
601
- message: "Failed to get content"
602
- }
603
- };
604
- }
605
- }
606
- /**
607
- * Get a content item by id, including trashed items.
608
- * Used by restore endpoint for ownership checks on soft-deleted items.
609
- */
610
- async function handleContentGetIncludingTrashed(db, collection, id, locale) {
611
- try {
612
- const item = await new ContentRepository(db).findByIdOrSlugIncludingTrashed(collection, id, locale ? resolveConfiguredLocale(locale) : void 0);
613
- if (!item) return {
614
- success: false,
615
- error: {
616
- code: "NOT_FOUND",
617
- message: `Content item not found: ${id}`
618
- }
619
- };
620
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
621
- await hydrateBylines(db, collection, item);
622
- return {
623
- success: true,
624
- data: {
625
- item,
626
- _rev: encodeRev(item)
627
- }
628
- };
629
- } catch (error) {
630
- console.error("Content get error:", error);
631
- return {
632
- success: false,
633
- error: {
634
- code: "CONTENT_GET_ERROR",
635
- message: "Failed to get content"
636
- }
637
- };
638
- }
639
- }
640
- /**
641
- * Create content item.
642
- *
643
- * Content + SEO writes are wrapped in a transaction so either both succeed
644
- * or neither does. If `body.seo` is provided for a non-SEO collection, the
645
- * API returns a validation error rather than silently dropping it.
646
- */
647
- async function handleContentCreate(db, collection, body) {
648
- try {
649
- const hasSeo = await collectionHasSeo(db, collection);
650
- if (body.seo && !hasSeo) return {
651
- success: false,
652
- error: {
653
- code: "VALIDATION_ERROR",
654
- message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
655
- }
656
- };
657
- const mimeCheck = await validateMediaFields(db, collection, body.data);
658
- if (!mimeCheck.success) return mimeCheck;
659
- const item = await withTransaction(db, async (trx) => {
660
- const repo = new ContentRepository(trx);
661
- const bylineRepo = new BylineRepository(trx);
662
- const effectiveLocale = body.locale ? resolveConfiguredLocale(body.locale) : getI18nConfig()?.defaultLocale;
663
- let slug = body.slug;
664
- if (!slug) {
665
- const slugSource = getSlugSource(body.data);
666
- if (slugSource) slug = await repo.generateUniqueSlug(collection, slugSource, effectiveLocale);
667
- }
668
- if (body.status === "published") requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, slug);
669
- const created = await repo.create({
670
- type: collection,
671
- slug,
672
- data: body.data,
673
- status: body.status || "draft",
674
- authorId: body.authorId,
675
- locale: effectiveLocale,
676
- translationOf: body.translationOf,
677
- createdAt: body.createdAt,
678
- publishedAt: body.publishedAt
679
- });
680
- if (body.bylines !== void 0) created.primaryBylineId = (await bylineRepo.setContentBylines(collection, created.id, body.bylines))[0]?.byline.translationGroup ?? null;
681
- if (body.translationOf) {
682
- if (body.bylines === void 0) {
683
- await bylineRepo.copyContentBylines(collection, body.translationOf, created.id);
684
- const source = await repo.findById(collection, body.translationOf);
685
- if (source) created.primaryBylineId = source.primaryBylineId;
686
- }
687
- }
688
- await hydrateBylines(trx, collection, created);
689
- if (body.seo && hasSeo) created.seo = await new SeoRepository(trx).upsert(collection, created.id, body.seo);
690
- else if (hasSeo) created.seo = { ...SEO_DEFAULTS };
691
- if (body.taxonomies) await assignTaxonomies(trx, collection, created.id, effectiveLocale, body.taxonomies);
692
- return created;
693
- });
694
- return {
695
- success: true,
696
- data: {
697
- item,
698
- _rev: encodeRev(item)
699
- }
700
- };
701
- } catch (error) {
702
- if (isMissingTableError(error)) return {
703
- success: false,
704
- error: {
705
- code: "COLLECTION_NOT_FOUND",
706
- message: `Collection '${collection}' not found`
707
- }
708
- };
709
- if (error instanceof EmDashValidationError) return {
710
- success: false,
711
- error: {
712
- code: "VALIDATION_ERROR",
713
- message: error.message
714
- }
715
- };
716
- const message = error instanceof Error ? error.message.toLowerCase() : "";
717
- if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
718
- if (message.includes("slug")) return {
719
- success: false,
720
- error: {
721
- code: "SLUG_CONFLICT",
722
- message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`
723
- }
724
- };
725
- return {
726
- success: false,
727
- error: {
728
- code: "CONFLICT",
729
- message: "Unique constraint violation"
730
- }
731
- };
732
- }
733
- console.error("Content create error:", error);
734
- return {
735
- success: false,
736
- error: {
737
- code: "CONTENT_CREATE_ERROR",
738
- message: "Failed to create content"
739
- }
740
- };
741
- }
742
- }
743
- /**
744
- * Update content item.
745
- * If `_rev` is provided, validates it against the current version before writing.
746
- * No `_rev` = blind write (backwards-compatible for admin UI).
747
- *
748
- * Content + SEO writes are wrapped in a transaction for atomicity.
749
- */
750
- async function handleContentUpdate(db, collection, id, body) {
751
- try {
752
- const hasSeo = await collectionHasSeo(db, collection);
753
- if (body.seo && !hasSeo) return {
754
- success: false,
755
- error: {
756
- code: "VALIDATION_ERROR",
757
- message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
758
- }
759
- };
760
- if (body.data) {
761
- const mimeCheck = await validateMediaFields(db, collection, body.data);
762
- if (!mimeCheck.success) return mimeCheck;
763
- }
764
- const resolvedId = await resolveId(new ContentRepository(db), collection, id, body.locale) ?? id;
765
- const item = await withTransaction(db, async (trx) => {
766
- const trxRepo = new ContentRepository(trx);
767
- const bylineRepo = new BylineRepository(trx);
768
- const existing = body._rev || body.slug !== void 0 || body.status === "published" ? await trxRepo.findById(collection, resolvedId) : null;
769
- if (body._rev) {
770
- if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
771
- const revCheck = validateRev(body._rev, existing);
772
- if (!revCheck.valid) throw Object.assign(new Error(revCheck.message), { apiError: { code: "CONFLICT" } });
773
- }
774
- let oldSlug;
775
- if (body.slug && existing?.slug && existing.slug !== body.slug) oldSlug = existing.slug;
776
- if ((body.status ?? existing?.status) === "published") {
777
- if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`Content item not found: ${id}`), { apiError: { code: "NOT_FOUND" } });
778
- const publishConfig = await getCollectionPublishConfig(trx, collection);
779
- const intendedSlug = body.slug !== void 0 ? body.slug : existing.slug;
780
- requireRoutablePublishSlug(publishConfig.routable, intendedSlug);
781
- }
782
- const updated = await trxRepo.update(collection, resolvedId, {
783
- data: body.data,
784
- slug: body.slug,
785
- status: body.status,
786
- authorId: body.authorId,
787
- publishedAt: body.publishedAt
788
- });
789
- if (body.bylines !== void 0) updated.primaryBylineId = (await bylineRepo.setContentBylines(collection, resolvedId, body.bylines))[0]?.byline.translationGroup ?? null;
790
- if (oldSlug && body.slug) await createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId);
791
- if (isI18nEnabled() && body.data && updated.translationGroup) await syncNonTranslatableFields(trx, collection, updated.id, updated.translationGroup, body.data);
792
- if (body.seo && hasSeo) updated.seo = await new SeoRepository(trx).upsert(collection, resolvedId, body.seo);
793
- else if (hasSeo) updated.seo = await new SeoRepository(trx).get(collection, resolvedId);
794
- await hydrateBylines(trx, collection, updated);
795
- if (body.taxonomies) await assignTaxonomies(trx, collection, resolvedId, updated.locale ?? body.locale, body.taxonomies);
796
- return updated;
797
- });
798
- return {
799
- success: true,
800
- data: {
801
- item,
802
- _rev: encodeRev(item)
803
- }
804
- };
805
- } catch (error) {
806
- if (hasApiError(error)) return {
807
- success: false,
808
- error: {
809
- code: error.apiError.code,
810
- message: error.message
811
- }
812
- };
813
- if (isMissingTableError(error)) return {
814
- success: false,
815
- error: {
816
- code: "COLLECTION_NOT_FOUND",
817
- message: `Collection '${collection}' not found`
818
- }
819
- };
820
- if (error instanceof EmDashValidationError) return {
821
- success: false,
822
- error: {
823
- code: "VALIDATION_ERROR",
824
- message: error.message
825
- }
826
- };
827
- const message = error instanceof Error ? error.message.toLowerCase() : "";
828
- if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
829
- if (message.includes("slug")) return {
830
- success: false,
831
- error: {
832
- code: "SLUG_CONFLICT",
833
- message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`
834
- }
835
- };
836
- return {
837
- success: false,
838
- error: {
839
- code: "CONFLICT",
840
- message: "Unique constraint violation"
841
- }
842
- };
843
- }
844
- console.error("Content update error:", error);
845
- return {
846
- success: false,
847
- error: {
848
- code: "CONTENT_UPDATE_ERROR",
849
- message: "Failed to update content"
850
- }
851
- };
852
- }
853
- }
854
- /**
855
- * Duplicate content item.
856
- *
857
- * Only copies SEO data if the collection has SEO enabled.
858
- * Always returns consistent `seo` shape for SEO-enabled collections.
859
- */
860
- async function handleContentDuplicate(db, collection, id, authorId) {
861
- try {
862
- const hasSeo = await collectionHasSeo(db, collection);
863
- return {
864
- success: true,
865
- data: { item: await withTransaction(db, async (trx) => {
866
- const repo = new ContentRepository(trx);
867
- const bylineRepo = new BylineRepository(trx);
868
- const resolvedId = await resolveId(repo, collection, id) ?? id;
869
- const dup = await repo.duplicate(collection, resolvedId, authorId);
870
- const existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);
871
- if (existingBylines.length > 0) await bylineRepo.setContentBylines(collection, dup.id, existingBylines.map((entry) => ({
872
- bylineId: entry.byline.id,
873
- roleLabel: entry.roleLabel
874
- })));
875
- if (hasSeo) {
876
- const seoRepo = new SeoRepository(trx);
877
- await seoRepo.copyForDuplicate(collection, resolvedId, dup.id);
878
- dup.seo = await seoRepo.get(collection, dup.id);
879
- }
880
- await hydrateBylines(trx, collection, dup);
881
- return dup;
882
- }) }
883
- };
884
- } catch (err) {
885
- if (err instanceof EmDashValidationError) return {
886
- success: false,
887
- error: {
888
- code: "NOT_FOUND",
889
- message: err.message
890
- }
891
- };
892
- console.error("Content duplicate error:", err);
893
- return {
894
- success: false,
895
- error: {
896
- code: "CONTENT_DUPLICATE_ERROR",
897
- message: "Failed to duplicate content"
898
- }
899
- };
900
- }
901
- }
902
- /**
903
- * Delete content item (soft delete - moves to trash)
904
- */
905
- async function handleContentDelete(db, collection, id) {
906
- try {
907
- const result = await withTransaction(db, async (trx) => {
908
- const repo = new ContentRepository(trx);
909
- const resolvedId = await resolveId(repo, collection, id) ?? id;
910
- return {
911
- id: resolvedId,
912
- deleted: await repo.delete(collection, resolvedId)
913
- };
914
- });
915
- if (!result.deleted) return {
916
- success: false,
917
- error: {
918
- code: "NOT_FOUND",
919
- message: `Content item not found: ${id}`
920
- }
921
- };
922
- return {
923
- success: true,
924
- data: {
925
- deleted: true,
926
- id: result.id
927
- }
928
- };
929
- } catch (error) {
930
- console.error("Content delete error:", error);
931
- return {
932
- success: false,
933
- error: {
934
- code: "CONTENT_DELETE_ERROR",
935
- message: "Failed to delete content"
936
- }
937
- };
938
- }
939
- }
940
- /**
941
- * Restore content item from trash
942
- */
943
- async function handleContentRestore(db, collection, id) {
944
- try {
945
- const item = await withTransaction(db, async (trx) => {
946
- const repo = new ContentRepository(trx);
947
- const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
948
- return repo.restore(collection, resolvedId);
949
- });
950
- if (!item) return {
951
- success: false,
952
- error: {
953
- code: "NOT_FOUND",
954
- message: `Trashed content item not found: ${id}`
955
- }
956
- };
957
- return {
958
- success: true,
959
- data: {
960
- restored: true,
961
- item
962
- }
963
- };
964
- } catch (error) {
965
- console.error("Content restore error:", error);
966
- return {
967
- success: false,
968
- error: {
969
- code: "CONTENT_RESTORE_ERROR",
970
- message: "Failed to restore content"
971
- }
972
- };
973
- }
974
- }
975
- /**
976
- * Permanently delete content item (cannot be undone).
977
- * Also cleans up associated SEO data.
978
- */
979
- async function handleContentPermanentDelete(db, collection, id) {
980
- try {
981
- const resolvedId = await resolveIdIncludingTrashed(new ContentRepository(db), collection, id) ?? id;
982
- if (!await withTransaction(db, async (trx) => {
983
- const wasDeleted = await new ContentRepository(trx).permanentDelete(collection, resolvedId);
984
- if (wasDeleted) {
985
- await new SeoRepository(trx).delete(collection, resolvedId);
986
- await new CommentRepository(trx).deleteByContent(collection, resolvedId);
987
- await new RevisionRepository(trx).deleteByEntry(collection, resolvedId);
988
- }
989
- return wasDeleted;
990
- })) return {
991
- success: false,
992
- error: {
993
- code: "NOT_FOUND",
994
- message: `Content item not found: ${id}`
995
- }
996
- };
997
- return {
998
- success: true,
999
- data: {
1000
- deleted: true,
1001
- id: resolvedId
1002
- }
1003
- };
1004
- } catch (error) {
1005
- console.error("Content permanent delete error:", error);
1006
- return {
1007
- success: false,
1008
- error: {
1009
- code: "CONTENT_DELETE_ERROR",
1010
- message: "Failed to permanently delete content"
1011
- }
1012
- };
1013
- }
1014
- }
1015
- /**
1016
- * List trashed content items
1017
- */
1018
- async function handleContentListTrashed(db, collection, options = {}) {
1019
- try {
1020
- const result = await new ContentRepository(db).findTrashed(collection, {
1021
- limit: options.limit,
1022
- cursor: options.cursor
1023
- });
1024
- return {
1025
- success: true,
1026
- data: {
1027
- items: result.items.map((item) => ({
1028
- id: item.id,
1029
- type: item.type,
1030
- slug: item.slug,
1031
- status: item.status,
1032
- data: item.data,
1033
- authorId: item.authorId,
1034
- createdAt: item.createdAt,
1035
- updatedAt: item.updatedAt,
1036
- publishedAt: item.publishedAt,
1037
- deletedAt: item.deletedAt
1038
- })),
1039
- nextCursor: result.nextCursor
1040
- }
1041
- };
1042
- } catch (error) {
1043
- if (error instanceof InvalidCursorError) return {
1044
- success: false,
1045
- error: {
1046
- code: "INVALID_CURSOR",
1047
- message: error.message
1048
- }
1049
- };
1050
- console.error("Content list trashed error:", error);
1051
- return {
1052
- success: false,
1053
- error: {
1054
- code: "CONTENT_LIST_ERROR",
1055
- message: "Failed to list trashed content"
1056
- }
1057
- };
1058
- }
1059
- }
1060
- /**
1061
- * Count trashed content items
1062
- */
1063
- async function handleContentCountTrashed(db, collection) {
1064
- try {
1065
- return {
1066
- success: true,
1067
- data: { count: await new ContentRepository(db).countTrashed(collection) }
1068
- };
1069
- } catch (error) {
1070
- console.error("Content count trashed error:", error);
1071
- return {
1072
- success: false,
1073
- error: {
1074
- code: "CONTENT_COUNT_ERROR",
1075
- message: "Failed to count trashed content"
1076
- }
1077
- };
1078
- }
1079
- }
1080
- /**
1081
- * Schedule content for future publishing
1082
- */
1083
- async function handleContentSchedule(db, collection, id, scheduledAt) {
1084
- try {
1085
- const item = await withTransaction(db, async (trx) => {
1086
- const repo = new ContentRepository(trx);
1087
- const existing = await repo.findByIdOrSlug(collection, id);
1088
- const resolvedId = existing?.id ?? id;
1089
- if (existing) requireRoutablePublishSlug((await getCollectionPublishConfig(trx, collection)).routable, existing.slug);
1090
- return repo.schedule(collection, resolvedId, scheduledAt);
1091
- });
1092
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1093
- return {
1094
- success: true,
1095
- data: { item }
1096
- };
1097
- } catch (error) {
1098
- if (error instanceof EmDashValidationError) return {
1099
- success: false,
1100
- error: {
1101
- code: "VALIDATION_ERROR",
1102
- message: error.message
1103
- }
1104
- };
1105
- console.error("Content schedule error:", error);
1106
- return {
1107
- success: false,
1108
- error: {
1109
- code: "CONTENT_SCHEDULE_ERROR",
1110
- message: "Failed to schedule content"
1111
- }
1112
- };
1113
- }
1114
- }
1115
- /**
1116
- * Unschedule content (revert to draft)
1117
- */
1118
- async function handleContentUnschedule(db, collection, id) {
1119
- try {
1120
- const item = await withTransaction(db, async (trx) => {
1121
- const repo = new ContentRepository(trx);
1122
- const resolvedId = await resolveId(repo, collection, id) ?? id;
1123
- return repo.unschedule(collection, resolvedId);
1124
- });
1125
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1126
- return {
1127
- success: true,
1128
- data: { item }
1129
- };
1130
- } catch (error) {
1131
- if (error instanceof EmDashValidationError) return {
1132
- success: false,
1133
- error: {
1134
- code: "VALIDATION_ERROR",
1135
- message: error.message
1136
- }
1137
- };
1138
- console.error("Content unschedule error:", error);
1139
- return {
1140
- success: false,
1141
- error: {
1142
- code: "CONTENT_UNSCHEDULE_ERROR",
1143
- message: "Failed to unschedule content"
1144
- }
1145
- };
1146
- }
1147
- }
1148
- /**
1149
- * Publish content immediately.
1150
- *
1151
- * Publication is one atomic content-row statement. On databases that support
1152
- * transactions, the existing slug-redirect side write remains grouped with it.
1153
- */
1154
- async function handleContentPublish(db, collection, id, options = {}) {
1155
- try {
1156
- const item = await withTransaction(db, async (trx) => {
1157
- const repo = new ContentRepository(trx);
1158
- const resolvedId = await resolveId(repo, collection, id) ?? id;
1159
- const publishConfig = await getCollectionPublishConfig(trx, collection);
1160
- const existing = await repo.findById(collection, resolvedId);
1161
- const published = await repo.publish(collection, resolvedId, options.publishedAt, options.requireScheduledDue, options.expectedScheduledAt, publishConfig.supportsRevisions, publishConfig.routable);
1162
- if (existing?.status === "published" && existing.slug && published.slug && existing.slug !== published.slug) await createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId);
1163
- return published;
1164
- });
1165
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1166
- return {
1167
- success: true,
1168
- data: { item }
1169
- };
1170
- } catch (error) {
1171
- if (error instanceof ContentMutationConflictError) return {
1172
- success: false,
1173
- error: {
1174
- code: "CONFLICT",
1175
- message: error.message
1176
- }
1177
- };
1178
- if (error instanceof ScheduledNotDueError) return {
1179
- success: false,
1180
- error: {
1181
- code: "NOT_DUE",
1182
- message: error.message
1183
- }
1184
- };
1185
- if (error instanceof EmDashValidationError) {
1186
- const details = error.details;
1187
- return {
1188
- success: false,
1189
- error: {
1190
- code: typeof details === "object" && details !== null && "code" in details && details.code === "SLUG_CONFLICT" ? "SLUG_CONFLICT" : "VALIDATION_ERROR",
1191
- message: error.message
1192
- }
1193
- };
1194
- }
1195
- const message = error instanceof Error ? error.message.toLowerCase() : "";
1196
- if ((message.includes("unique constraint failed") || message.includes("duplicate key")) && message.includes("slug")) return {
1197
- success: false,
1198
- error: {
1199
- code: "SLUG_CONFLICT",
1200
- message: `The staged slug is already used by another entry in collection '${collection}'`
1201
- }
1202
- };
1203
- console.error("Content publish error:", error);
1204
- return {
1205
- success: false,
1206
- error: {
1207
- code: "CONTENT_PUBLISH_ERROR",
1208
- message: "Failed to publish content"
1209
- }
1210
- };
1211
- }
1212
- }
1213
- /**
1214
- * Unpublish content (revert to draft).
1215
- *
1216
- * Wrapped in a transaction — unpublish may create a draft revision
1217
- * from the live version then update the status, which is multi-step.
1218
- */
1219
- async function handleContentUnpublish(db, collection, id) {
1220
- try {
1221
- const item = await withTransaction(db, async (trx) => {
1222
- const repo = new ContentRepository(trx);
1223
- const resolvedId = await resolveId(repo, collection, id) ?? id;
1224
- return repo.unpublish(collection, resolvedId);
1225
- });
1226
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1227
- return {
1228
- success: true,
1229
- data: { item }
1230
- };
1231
- } catch (error) {
1232
- if (error instanceof EmDashValidationError) return {
1233
- success: false,
1234
- error: {
1235
- code: "VALIDATION_ERROR",
1236
- message: error.message
1237
- }
1238
- };
1239
- console.error("Content unpublish error:", error);
1240
- return {
1241
- success: false,
1242
- error: {
1243
- code: "CONTENT_UNPUBLISH_ERROR",
1244
- message: "Failed to unpublish content"
1245
- }
1246
- };
1247
- }
1248
- }
1249
- /**
1250
- * Count scheduled content items
1251
- */
1252
- async function handleContentCountScheduled(db, collection) {
1253
- try {
1254
- return {
1255
- success: true,
1256
- data: { count: await new ContentRepository(db).countScheduled(collection) }
1257
- };
1258
- } catch (error) {
1259
- console.error("Content count scheduled error:", error);
1260
- return {
1261
- success: false,
1262
- error: {
1263
- code: "CONTENT_COUNT_ERROR",
1264
- message: "Failed to count scheduled content"
1265
- }
1266
- };
1267
- }
1268
- }
1269
- /**
1270
- * Discard draft changes (revert to live version)
1271
- */
1272
- async function handleContentDiscardDraft(db, collection, id) {
1273
- try {
1274
- const item = await withTransaction(db, async (trx) => {
1275
- const repo = new ContentRepository(trx);
1276
- const resolvedId = await resolveId(repo, collection, id) ?? id;
1277
- return repo.discardDraft(collection, resolvedId);
1278
- });
1279
- await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
1280
- return {
1281
- success: true,
1282
- data: { item }
1283
- };
1284
- } catch (error) {
1285
- if (error instanceof EmDashValidationError) return {
1286
- success: false,
1287
- error: {
1288
- code: "NOT_FOUND",
1289
- message: error.message
1290
- }
1291
- };
1292
- console.error("Content discard draft error:", error);
1293
- return {
1294
- success: false,
1295
- error: {
1296
- code: "CONTENT_DISCARD_DRAFT_ERROR",
1297
- message: "Failed to discard draft"
1298
- }
1299
- };
1300
- }
1301
- }
1302
- /**
1303
- * Compare live and draft revisions
1304
- */
1305
- async function handleContentCompare(db, collection, id) {
1306
- try {
1307
- const entry = await new ContentRepository(db).findByIdOrSlug(collection, id);
1308
- if (!entry) return {
1309
- success: false,
1310
- error: {
1311
- code: "NOT_FOUND",
1312
- message: `Content item not found: ${id}`
1313
- }
1314
- };
1315
- const revisionRepo = new RevisionRepository(db);
1316
- const live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;
1317
- const draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;
1318
- return {
1319
- success: true,
1320
- data: {
1321
- hasChanges: entry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,
1322
- live: live?.data ?? null,
1323
- draft: draft?.data ?? null
1324
- }
1325
- };
1326
- } catch (error) {
1327
- console.error("Content compare error:", error);
1328
- return {
1329
- success: false,
1330
- error: {
1331
- code: "CONTENT_COMPARE_ERROR",
1332
- message: "Failed to compare revisions"
1333
- }
1334
- };
1335
- }
1336
- }
1337
- /**
1338
- * Get all translations for a content item.
1339
- * Returns the item's translation group members with locale and status info.
1340
- */
1341
- async function handleContentTranslations(db, collection, id) {
1342
- try {
1343
- const repo = new ContentRepository(db);
1344
- const item = await repo.findByIdOrSlug(collection, id);
1345
- if (!item) return {
1346
- success: false,
1347
- error: {
1348
- code: "NOT_FOUND",
1349
- message: `Content item not found: ${id}`
1350
- }
1351
- };
1352
- if (!item.translationGroup) return {
1353
- success: true,
1354
- data: {
1355
- translationGroup: item.id,
1356
- translations: [{
1357
- id: item.id,
1358
- locale: item.locale,
1359
- slug: item.slug,
1360
- status: item.status,
1361
- updatedAt: item.updatedAt
1362
- }]
1363
- }
1364
- };
1365
- const translations = await repo.findTranslations(collection, item.translationGroup);
1366
- return {
1367
- success: true,
1368
- data: {
1369
- translationGroup: item.translationGroup,
1370
- translations: translations.map((t) => ({
1371
- id: t.id,
1372
- locale: t.locale,
1373
- slug: t.slug,
1374
- status: t.status,
1375
- updatedAt: t.updatedAt
1376
- }))
1377
- }
1378
- };
1379
- } catch (error) {
1380
- if (error instanceof Error) console.error("Content translations error:", error);
1381
- return {
1382
- success: false,
1383
- error: {
1384
- code: "CONTENT_TRANSLATIONS_ERROR",
1385
- message: "Failed to get translations"
1386
- }
1387
- };
1388
- }
1389
- }
1390
- /**
1391
- * Sync non-translatable fields to sibling locales.
1392
- *
1393
- * When a content item is updated and it belongs to a translation group,
1394
- * any non-translatable fields in the update data are written to all other
1395
- * rows in the same translation group within the same transaction.
1396
- *
1397
- * Non-translatable fields are **copied, not linked** — each row owns its
1398
- * own data. This keeps queries simple and avoids cross-row joins.
1399
- */
1400
- async function syncNonTranslatableFields(trx, collectionSlug, updatedItemId, translationGroup, data) {
1401
- const collection = await trx.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug).executeTakeFirst();
1402
- if (!collection) return;
1403
- const nonTranslatableSlugs = (await trx.selectFrom("_emdash_fields").select("slug").where("collection_id", "=", collection.id).where("translatable", "=", 0).execute()).map((f) => f.slug);
1404
- if (nonTranslatableSlugs.length === 0) return;
1405
- const syncData = {};
1406
- for (const slug of nonTranslatableSlugs) if (slug in data) syncData[slug] = data[slug];
1407
- if (Object.keys(syncData).length === 0) return;
1408
- validateIdentifier(collectionSlug, "collection slug");
1409
- const tableName = `ec_${collectionSlug}`;
1410
- const setClauses = Object.entries(syncData).map(([key, value]) => {
1411
- validateIdentifier(key, "field slug");
1412
- const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
1413
- return sql`${sql.ref(key)} = ${serialized}`;
1414
- });
1415
- await sql`
1416
- UPDATE ${sql.ref(tableName)}
1417
- SET ${sql.join(setClauses, sql`, `)}
1418
- WHERE translation_group = ${translationGroup}
1419
- AND id != ${updatedItemId}
1420
- `.execute(trx);
1421
- }
1422
- /**
1423
- * Resolve a `{ taxonomyName: [slug, ...] }` map to term IDs and replace the
1424
- * entry's assignments for each named taxonomy.
1425
- *
1426
- * Shared by handleContentCreate and handleContentUpdate so both MCP entry
1427
- * points behave identically. Slug resolution is scoped to `locale`; passing
1428
- * `undefined` lets `findBySlug` fall back to its default (lowest locale code)
1429
- * so callers on single-locale sites don't need to know the site's default.
1430
- *
1431
- * Throws EmDashValidationError on unknown slug or wrong shape; the calling
1432
- * handler translates that into a VALIDATION_ERROR response.
1433
- */
1434
- async function assignTaxonomies(trx, collection, entryId, locale, taxonomies) {
1435
- const taxRepo = new TaxonomyRepository(trx);
1436
- let anyChange = false;
1437
- for (const [taxonomyName, slugs] of Object.entries(taxonomies)) {
1438
- if (!Array.isArray(slugs)) throw new EmDashValidationError(`taxonomies.${taxonomyName} must be an array of term slugs`);
1439
- const termIds = [];
1440
- for (const slug of slugs) {
1441
- if (typeof slug !== "string" || slug.length === 0) throw new EmDashValidationError(`taxonomies.${taxonomyName} contains a non-string or empty slug`);
1442
- const term = await taxRepo.findBySlug(taxonomyName, slug, locale);
1443
- if (!term) throw new EmDashValidationError(`Unknown taxonomy term: ${taxonomyName}='${slug}'${locale ? ` (locale '${locale}')` : ""}`);
1444
- termIds.push(term.id);
1445
- }
1446
- await taxRepo.setTermsForEntry(collection, entryId, taxonomyName, termIds);
1447
- anyChange = true;
1448
- }
1449
- if (anyChange) invalidateTermCache();
1450
- }
1451
-
1452
- //#endregion
1453
- //#region src/api/handlers/manifest.ts
1454
- /**
1455
- * Manifest generation handlers
1456
- */
1457
- /** Pattern to add spaces before capital letters */
1458
- const CAMEL_CASE_PATTERN = /([A-Z])/g;
1459
- const FIRST_CHAR_PATTERN = /^./;
1460
- /**
1461
- * Generate admin manifest from collections
1462
- */
1463
- async function generateManifest(collections, plugins = {}) {
1464
- const manifestCollections = {};
1465
- for (const [name, definition] of Object.entries(collections)) {
1466
- const fields = extractFieldDescriptors(definition.schema);
1467
- manifestCollections[name] = {
1468
- label: definition.admin.label,
1469
- labelSingular: definition.admin.labelSingular || definition.admin.label,
1470
- supports: definition.admin.supports || [],
1471
- routable: definition.admin.routable ?? true,
1472
- fields
1473
- };
1474
- }
1475
- return {
1476
- version: "0.1.0",
1477
- hash: await hashString(JSON.stringify(manifestCollections)),
1478
- collections: manifestCollections,
1479
- plugins
1480
- };
1481
- }
1482
- /**
1483
- * Extract field descriptors from Zod schema
1484
- * Note: This is a simplified implementation that handles common types
1485
- */
1486
- function extractFieldDescriptors(schema) {
1487
- const fields = {};
1488
- const shape = typeof schema._def?.shape === "function" ? schema._def.shape() : schema.shape || {};
1489
- for (const [name, fieldSchema] of Object.entries(shape)) fields[name] = extractFieldType(name, fieldSchema);
1490
- return fields;
1491
- }
1492
- /**
1493
- * Extract field type from Zod schema
1494
- */
1495
- /** Type guard: check if a value is a non-null object */
1496
- function isObject(value) {
1497
- return typeof value === "object" && value !== null;
1498
- }
1499
- function extractFieldType(name, schema) {
1500
- if (!isObject(schema)) return {
1501
- kind: "string",
1502
- label: formatLabel(name)
1503
- };
1504
- if (schema.isPortableText) return {
1505
- kind: "portableText",
1506
- label: formatLabel(name)
1507
- };
1508
- if (schema.isImage) return {
1509
- kind: "image",
1510
- label: formatLabel(name)
1511
- };
1512
- if (schema.isReference) return {
1513
- kind: "reference",
1514
- label: formatLabel(name)
1515
- };
1516
- const def = isObject(schema._def) ? schema._def : void 0;
1517
- switch (typeof def?.typeName === "string" ? def.typeName : void 0) {
1518
- case "ZodString": return {
1519
- kind: "string",
1520
- label: formatLabel(name)
1521
- };
1522
- case "ZodNumber": return {
1523
- kind: "number",
1524
- label: formatLabel(name)
1525
- };
1526
- case "ZodBoolean": return {
1527
- kind: "boolean",
1528
- label: formatLabel(name)
1529
- };
1530
- case "ZodDate": return {
1531
- kind: "datetime",
1532
- label: formatLabel(name)
1533
- };
1534
- case "ZodEnum": {
1535
- const values = Array.isArray(def?.values) ? def.values : [];
1536
- return {
1537
- kind: "select",
1538
- label: formatLabel(name),
1539
- options: values.filter((v) => typeof v === "string").map((v) => ({
1540
- value: v,
1541
- label: v.charAt(0).toUpperCase() + v.slice(1)
1542
- }))
1543
- };
1544
- }
1545
- case "ZodArray": return {
1546
- kind: "array",
1547
- label: formatLabel(name)
1548
- };
1549
- case "ZodObject": return {
1550
- kind: "object",
1551
- label: formatLabel(name)
1552
- };
1553
- case "ZodOptional":
1554
- case "ZodDefault":
1555
- if (def?.innerType) return extractFieldType(name, def.innerType);
1556
- return {
1557
- kind: "string",
1558
- label: formatLabel(name)
1559
- };
1560
- default: return {
1561
- kind: "string",
1562
- label: formatLabel(name)
1563
- };
1564
- }
1565
- }
1566
- /**
1567
- * Format field name as label
1568
- */
1569
- function formatLabel(name) {
1570
- return name.replace(CAMEL_CASE_PATTERN, " $1").replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase()).trim();
1571
- }
1572
-
1573
- //#endregion
1574
- //#region src/api/handlers/revision.ts
1575
- /**
1576
- * List revisions for a content entry
1577
- */
1578
- async function handleRevisionList(db, collection, entryId, params = {}) {
1579
- try {
1580
- const repo = new RevisionRepository(db);
1581
- const [items, total] = await Promise.all([repo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }), repo.countByEntry(collection, entryId)]);
1582
- return {
1583
- success: true,
1584
- data: {
1585
- items,
1586
- total
1587
- }
1588
- };
1589
- } catch {
1590
- return {
1591
- success: false,
1592
- error: {
1593
- code: "REVISION_LIST_ERROR",
1594
- message: "Failed to list revisions"
1595
- }
1596
- };
1597
- }
1598
- }
1599
- /**
1600
- * Get a specific revision
1601
- */
1602
- async function handleRevisionGet(db, revisionId) {
1603
- try {
1604
- const item = await new RevisionRepository(db).findById(revisionId);
1605
- if (!item) return {
1606
- success: false,
1607
- error: {
1608
- code: "NOT_FOUND",
1609
- message: `Revision not found: ${revisionId}`
1610
- }
1611
- };
1612
- return {
1613
- success: true,
1614
- data: { item }
1615
- };
1616
- } catch {
1617
- return {
1618
- success: false,
1619
- error: {
1620
- code: "REVISION_GET_ERROR",
1621
- message: "Failed to get revision"
1622
- }
1623
- };
1624
- }
1625
- }
1626
- /**
1627
- * Restore a revision (updates content to this revision's data and creates new revision)
1628
- */
1629
- async function handleRevisionRestore(db, revisionId, callerUserId) {
1630
- try {
1631
- const revision = await new RevisionRepository(db).findById(revisionId);
1632
- if (!revision) return {
1633
- success: false,
1634
- error: {
1635
- code: "NOT_FOUND",
1636
- message: `Revision not found: ${revisionId}`
1637
- }
1638
- };
1639
- const { _slug, ...fieldData } = revision.data;
1640
- const { item, queuedRevisionId } = await withTransaction(db, async (trx) => {
1641
- const trxContentRepo = new ContentRepository(trx);
1642
- const trxRevisionRepo = new RevisionRepository(trx);
1643
- return {
1644
- item: await trxContentRepo.update(revision.collection, revision.entryId, {
1645
- data: fieldData,
1646
- slug: typeof _slug === "string" ? _slug : void 0
1647
- }),
1648
- queuedRevisionId: (await trxRevisionRepo.create({
1649
- collection: revision.collection,
1650
- entryId: revision.entryId,
1651
- data: revision.data,
1652
- authorId: callerUserId
1653
- })).id
1654
- };
1655
- });
1656
- const pruneRepo = new RevisionRepository(db);
1657
- after(async () => {
1658
- try {
1659
- await pruneRepo.pruneQueuedEntry(revision.collection, revision.entryId, queuedRevisionId, 50);
1660
- } catch (error) {
1661
- console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
1662
- }
1663
- });
1664
- return {
1665
- success: true,
1666
- data: { item }
1667
- };
1668
- } catch {
1669
- return {
1670
- success: false,
1671
- error: {
1672
- code: "REVISION_RESTORE_ERROR",
1673
- message: "Failed to restore revision"
1674
- }
1675
- };
1676
- }
1677
- }
1678
-
1679
- //#endregion
1680
- //#region src/api/handlers/media.ts
1681
- /**
1682
- * List media items
1683
- */
1684
- async function handleMediaList(db, params) {
1685
- try {
1686
- const result = await new MediaRepository(db).findMany({
1687
- cursor: params.cursor,
1688
- limit: Math.min(params.limit || 50, 100),
1689
- mimeType: params.mimeType,
1690
- q: params.q
1691
- });
1692
- return {
1693
- success: true,
1694
- data: {
1695
- items: result.items,
1696
- nextCursor: result.nextCursor
1697
- }
1698
- };
1699
- } catch (error) {
1700
- if (error instanceof InvalidCursorError) return {
1701
- success: false,
1702
- error: {
1703
- code: "INVALID_CURSOR",
1704
- message: error.message
1705
- }
1706
- };
1707
- return {
1708
- success: false,
1709
- error: {
1710
- code: "MEDIA_LIST_ERROR",
1711
- message: "Failed to list media"
1712
- }
1713
- };
1714
- }
1715
- }
1716
- /**
1717
- * Get single media item
1718
- */
1719
- async function handleMediaGet(db, id) {
1720
- try {
1721
- const item = await new MediaRepository(db).findById(id);
1722
- if (!item) return {
1723
- success: false,
1724
- error: {
1725
- code: "NOT_FOUND",
1726
- message: `Media item not found: ${id}`
1727
- }
1728
- };
1729
- return {
1730
- success: true,
1731
- data: { item }
1732
- };
1733
- } catch {
1734
- return {
1735
- success: false,
1736
- error: {
1737
- code: "MEDIA_GET_ERROR",
1738
- message: "Failed to get media"
1739
- }
1740
- };
1741
- }
1742
- }
1743
- /**
1744
- * Create media item (after file upload)
1745
- */
1746
- async function handleMediaCreate(db, input) {
1747
- try {
1748
- return {
1749
- success: true,
1750
- data: { item: await new MediaRepository(db).create(input) }
1751
- };
1752
- } catch {
1753
- return {
1754
- success: false,
1755
- error: {
1756
- code: "MEDIA_CREATE_ERROR",
1757
- message: "Failed to create media"
1758
- }
1759
- };
1760
- }
1761
- }
1762
- /**
1763
- * Update media metadata
1764
- */
1765
- async function handleMediaUpdate(db, id, input) {
1766
- try {
1767
- const item = await new MediaRepository(db).update(id, input);
1768
- if (!item) return {
1769
- success: false,
1770
- error: {
1771
- code: "NOT_FOUND",
1772
- message: `Media item not found: ${id}`
1773
- }
1774
- };
1775
- return {
1776
- success: true,
1777
- data: { item }
1778
- };
1779
- } catch {
1780
- return {
1781
- success: false,
1782
- error: {
1783
- code: "MEDIA_UPDATE_ERROR",
1784
- message: "Failed to update media"
1785
- }
1786
- };
1787
- }
1788
- }
1789
- /**
1790
- * Delete media item
1791
- */
1792
- async function handleMediaDelete(db, id) {
1793
- try {
1794
- const storageKey = await new MediaRepository(db).deleteWithStorageKey(id);
1795
- if (!storageKey) return {
1796
- success: false,
1797
- error: {
1798
- code: "NOT_FOUND",
1799
- message: `Media item not found: ${id}`
1800
- }
1801
- };
1802
- return {
1803
- success: true,
1804
- data: {
1805
- deleted: true,
1806
- storageKey
1807
- }
1808
- };
1809
- } catch {
1810
- return {
1811
- success: false,
1812
- error: {
1813
- code: "MEDIA_DELETE_ERROR",
1814
- message: "Failed to delete media"
1815
- }
1816
- };
1817
- }
1818
- }
1819
-
1820
- //#endregion
1821
- //#region src/api/handlers/plugins.ts
1822
- function marketplaceIconUrl(marketplaceUrl, pluginId) {
1823
- return `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;
1824
- }
1825
- /**
1826
- * Get plugin info from configured plugin and database state
1827
- */
1828
- function buildPluginInfo(plugin, state, marketplaceUrl) {
1829
- const status = state?.status ?? "active";
1830
- const enabled = status === "active";
1831
- const isMarketplace = (state?.source ?? "config") === "marketplace";
1832
- return {
1833
- id: plugin.id,
1834
- name: state?.displayName || plugin.id,
1835
- version: plugin.version,
1836
- package: void 0,
1837
- enabled,
1838
- status,
1839
- source: state?.source ?? "config",
1840
- marketplaceVersion: state?.marketplaceVersion ?? void 0,
1841
- registryPublisherDid: state?.registryPublisherDid ?? void 0,
1842
- registrySlug: state?.registrySlug ?? void 0,
1843
- capabilities: plugin.capabilities,
1844
- hasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,
1845
- hasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,
1846
- hasHooks: Object.keys(plugin.hooks ?? {}).length > 0,
1847
- hasSettings: Object.keys(plugin.admin.settingsSchema ?? {}).length > 0,
1848
- installedAt: state?.installedAt?.toISOString(),
1849
- activatedAt: state?.activatedAt?.toISOString() ?? void 0,
1850
- deactivatedAt: state?.deactivatedAt?.toISOString() ?? void 0,
1851
- description: state?.description ?? void 0,
1852
- iconUrl: isMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : void 0,
1853
- mcpToolsEnabled: state?.mcpToolsEnabled ?? false,
1854
- mcpTools: Object.entries(plugin.mcp?.tools ?? {}).flatMap(([name, tool]) => {
1855
- const permission = plugin.routes[tool.route]?.permission;
1856
- return permission ? [{
1857
- name,
1858
- description: tool.description,
1859
- route: tool.route,
1860
- permission,
1861
- destructive: tool.destructive ?? false
1862
- }] : [];
1863
- })
1864
- };
1865
- }
1866
- /**
1867
- * Build plugin info for a statically-sandboxed plugin entry
1868
- */
1869
- function buildSandboxedPluginInfo(entry, state) {
1870
- const status = state?.status ?? "active";
1871
- const enabled = status === "active";
1872
- return {
1873
- id: entry.id,
1874
- name: state?.displayName || entry.id,
1875
- version: entry.version,
1876
- package: void 0,
1877
- enabled,
1878
- status,
1879
- source: "config",
1880
- sandboxed: true,
1881
- capabilities: entry.capabilities,
1882
- hasAdminPages: (entry.adminPages?.length ?? 0) > 0,
1883
- hasDashboardWidgets: (entry.adminWidgets?.length ?? 0) > 0,
1884
- hasHooks: false,
1885
- hasSettings: Object.keys(entry.settingsSchema ?? {}).length > 0,
1886
- installedAt: state?.installedAt?.toISOString(),
1887
- activatedAt: state?.activatedAt?.toISOString() ?? void 0,
1888
- deactivatedAt: state?.deactivatedAt?.toISOString() ?? void 0,
1889
- description: state?.description ?? void 0,
1890
- mcpToolsEnabled: state?.mcpToolsEnabled ?? false,
1891
- mcpTools: entry.mcp?.tools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool) ?? []
1892
- };
1893
- }
1894
- /**
1895
- * List all configured plugins with their state
1896
- */
1897
- async function handlePluginList(db, configuredPlugins, sandboxedPluginEntries, marketplaceUrl, runtimeSettingsSchemaLookup) {
1898
- try {
1899
- const allStates = await new PluginStateRepository(db).getAll();
1900
- const stateMap = new Map(allStates.map((s) => [s.pluginId, s]));
1901
- const configuredIds = new Set(configuredPlugins.map((p) => p.id));
1902
- const items = configuredPlugins.map((plugin) => {
1903
- return buildPluginInfo(plugin, stateMap.get(plugin.id) ?? null, marketplaceUrl);
1904
- });
1905
- for (const entry of sandboxedPluginEntries) {
1906
- if (configuredIds.has(entry.id)) continue;
1907
- configuredIds.add(entry.id);
1908
- items.push(buildSandboxedPluginInfo(entry, stateMap.get(entry.id) ?? null));
1909
- }
1910
- for (const state of allStates) {
1911
- if (state.source !== "marketplace" && state.source !== "registry") continue;
1912
- if (configuredIds.has(state.pluginId)) continue;
1913
- items.push({
1914
- id: state.pluginId,
1915
- name: state.displayName || state.pluginId,
1916
- version: state.marketplaceVersion ?? state.version,
1917
- enabled: state.status === "active",
1918
- status: state.status,
1919
- source: state.source,
1920
- marketplaceVersion: state.marketplaceVersion ?? void 0,
1921
- registryPublisherDid: state.registryPublisherDid ?? void 0,
1922
- registrySlug: state.registrySlug ?? void 0,
1923
- capabilities: [],
1924
- hasAdminPages: false,
1925
- hasDashboardWidgets: false,
1926
- hasHooks: false,
1927
- hasSettings: Object.keys(runtimeSettingsSchemaLookup?.(state.pluginId) ?? {}).length > 0,
1928
- installedAt: state.installedAt?.toISOString(),
1929
- activatedAt: state.activatedAt?.toISOString() ?? void 0,
1930
- deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
1931
- description: state.description ?? void 0,
1932
- iconUrl: state.source === "marketplace" && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, state.pluginId) : void 0,
1933
- mcpToolsEnabled: state.mcpToolsEnabled,
1934
- mcpTools: []
1935
- });
1936
- }
1937
- return {
1938
- success: true,
1939
- data: { items }
1940
- };
1941
- } catch {
1942
- return {
1943
- success: false,
1944
- error: {
1945
- code: "PLUGIN_LIST_ERROR",
1946
- message: "Failed to list plugins"
1947
- }
1948
- };
1949
- }
1950
- }
1951
- /**
1952
- * Get a single plugin's info
1953
- */
1954
- async function handlePluginGet(db, configuredPlugins, sandboxedPluginEntries, pluginId, marketplaceUrl) {
1955
- try {
1956
- const stateRepo = new PluginStateRepository(db);
1957
- const plugin = configuredPlugins.find((p) => p.id === pluginId);
1958
- if (plugin) return {
1959
- success: true,
1960
- data: { item: buildPluginInfo(plugin, await stateRepo.get(pluginId), marketplaceUrl) }
1961
- };
1962
- const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
1963
- if (sandboxed) return {
1964
- success: true,
1965
- data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.get(pluginId)) }
1966
- };
1967
- return {
1968
- success: false,
1969
- error: {
1970
- code: "NOT_FOUND",
1971
- message: `Plugin not found: ${pluginId}`
1972
- }
1973
- };
1974
- } catch {
1975
- return {
1976
- success: false,
1977
- error: {
1978
- code: "PLUGIN_GET_ERROR",
1979
- message: "Failed to get plugin"
1980
- }
1981
- };
1982
- }
1983
- }
1984
- /**
1985
- * Build a minimal `PluginInfo` for a plugin that exists only as a
1986
- * `_plugin_state` row (marketplace or registry install), with no
1987
- * matching `configuredPlugins` entry. Runtime-installed plugins don't
1988
- * have ResolvedPlugin metadata until they're loaded into the sandbox,
1989
- * so the enable/disable response surfaces the state-row view as a
1990
- * stable shape the admin UI already understands.
1991
- */
1992
- function buildStateOnlyPluginInfo(state) {
1993
- return {
1994
- id: state.pluginId,
1995
- name: state.displayName || state.pluginId,
1996
- version: state.marketplaceVersion ?? state.version,
1997
- enabled: state.status === "active",
1998
- status: state.status,
1999
- source: state.source,
2000
- marketplaceVersion: state.marketplaceVersion ?? void 0,
2001
- registryPublisherDid: state.registryPublisherDid ?? void 0,
2002
- registrySlug: state.registrySlug ?? void 0,
2003
- capabilities: [],
2004
- hasAdminPages: false,
2005
- hasDashboardWidgets: false,
2006
- hasHooks: false,
2007
- hasSettings: false,
2008
- installedAt: state.installedAt?.toISOString(),
2009
- activatedAt: state.activatedAt?.toISOString() ?? void 0,
2010
- deactivatedAt: state.deactivatedAt?.toISOString() ?? void 0,
2011
- description: state.description ?? void 0,
2012
- mcpToolsEnabled: state.mcpToolsEnabled,
2013
- mcpTools: []
2014
- };
2015
- }
2016
- /**
2017
- * Enable a plugin
2018
- */
2019
- async function handlePluginEnable(db, configuredPlugins, sandboxedPluginEntries, pluginId) {
2020
- try {
2021
- const stateRepo = new PluginStateRepository(db);
2022
- const plugin = configuredPlugins.find((p) => p.id === pluginId);
2023
- if (plugin) return {
2024
- success: true,
2025
- data: { item: buildPluginInfo(plugin, await stateRepo.enable(pluginId, plugin.version)) }
2026
- };
2027
- const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2028
- if (sandboxed) return {
2029
- success: true,
2030
- data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.enable(pluginId, sandboxed.version)) }
2031
- };
2032
- const existing = await stateRepo.get(pluginId);
2033
- if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
2034
- success: false,
2035
- error: {
2036
- code: "NOT_FOUND",
2037
- message: `Plugin not found: ${pluginId}`
2038
- }
2039
- };
2040
- return {
2041
- success: true,
2042
- data: { item: buildStateOnlyPluginInfo(await stateRepo.enable(pluginId, existing.version)) }
2043
- };
2044
- } catch {
2045
- return {
2046
- success: false,
2047
- error: {
2048
- code: "PLUGIN_ENABLE_ERROR",
2049
- message: "Failed to enable plugin"
2050
- }
2051
- };
2052
- }
2053
- }
2054
- /**
2055
- * Disable a plugin
2056
- */
2057
- async function handlePluginDisable(db, configuredPlugins, sandboxedPluginEntries, pluginId) {
2058
- try {
2059
- const stateRepo = new PluginStateRepository(db);
2060
- const plugin = configuredPlugins.find((p) => p.id === pluginId);
2061
- if (plugin) return {
2062
- success: true,
2063
- data: { item: buildPluginInfo(plugin, await stateRepo.disable(pluginId, plugin.version)) }
2064
- };
2065
- const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2066
- if (sandboxed) return {
2067
- success: true,
2068
- data: { item: buildSandboxedPluginInfo(sandboxed, await stateRepo.disable(pluginId, sandboxed.version)) }
2069
- };
2070
- const existing = await stateRepo.get(pluginId);
2071
- if (!existing || existing.source !== "marketplace" && existing.source !== "registry") return {
2072
- success: false,
2073
- error: {
2074
- code: "NOT_FOUND",
2075
- message: `Plugin not found: ${pluginId}`
2076
- }
2077
- };
2078
- return {
2079
- success: true,
2080
- data: { item: buildStateOnlyPluginInfo(await stateRepo.disable(pluginId, existing.version)) }
2081
- };
2082
- } catch {
2083
- return {
2084
- success: false,
2085
- error: {
2086
- code: "PLUGIN_DISABLE_ERROR",
2087
- message: "Failed to disable plugin"
2088
- }
2089
- };
2090
- }
2091
- }
2092
-
2093
- //#endregion
2094
- //#region src/api/handlers/plugin-settings.ts
2095
- function settingsKey(pluginId, key) {
2096
- return `plugin:${pluginId}:settings:${key}`;
2097
- }
2098
- /**
2099
- * Resolve a plugin's settings schema from either the configured
2100
- * (in-process) plugin list or the statically-sandboxed entries.
2101
- * Returns null when the plugin doesn't exist, undefined-equivalent
2102
- * empty object when it declares no schema.
2103
- */
2104
- function getPluginSettingsSchema(configuredPlugins, sandboxedPluginEntries, pluginId) {
2105
- const plugin = configuredPlugins.find((p) => p.id === pluginId);
2106
- if (plugin) return plugin.admin.settingsSchema ?? {};
2107
- const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
2108
- if (sandboxed) return sandboxed.settingsSchema ?? {};
2109
- return null;
2110
- }
2111
- /**
2112
- * Validate a single value against its schema field.
2113
- * Returns an error message, or null when valid.
2114
- */
2115
- function validateValue(key, field, value) {
2116
- switch (field.type) {
2117
- case "string":
2118
- case "secret":
2119
- case "url":
2120
- case "email":
2121
- if (typeof value !== "string") return `Setting "${key}" must be a string`;
2122
- if (field.type === "url" && value !== "" && !URL.canParse(value)) return `Setting "${key}" must be a valid URL`;
2123
- if (field.type === "email" && value !== "" && !value.includes("@")) return `Setting "${key}" must be a valid email address`;
2124
- return null;
2125
- case "number":
2126
- if (typeof value !== "number" || Number.isNaN(value)) return `Setting "${key}" must be a number`;
2127
- if (field.min !== void 0 && value < field.min) return `Setting "${key}" must be at least ${field.min}`;
2128
- if (field.max !== void 0 && value > field.max) return `Setting "${key}" must be at most ${field.max}`;
2129
- return null;
2130
- case "boolean": return typeof value === "boolean" ? null : `Setting "${key}" must be a boolean`;
2131
- case "select":
2132
- if (typeof value !== "string" || !field.options.some((o) => o.value === value)) return `Setting "${key}" must be one of the defined options`;
2133
- return null;
2134
- default: return `Setting "${key}" has an unknown field type`;
2135
- }
2136
- }
2137
- async function buildSettingsResponse(optionsRepo, pluginId, schema) {
2138
- const keys = Object.keys(schema);
2139
- const stored = await optionsRepo.getMany(keys.map((key) => settingsKey(pluginId, key)));
2140
- const values = {};
2141
- const secretsSet = {};
2142
- for (const key of keys) {
2143
- const field = schema[key];
2144
- if (!field) continue;
2145
- const storedValue = stored.get(settingsKey(pluginId, key));
2146
- if (field.type === "secret") {
2147
- secretsSet[key] = typeof storedValue === "string" && storedValue.length > 0;
2148
- continue;
2149
- }
2150
- if (storedValue !== void 0 && storedValue !== null) values[key] = storedValue;
2151
- else if ("default" in field && field.default !== void 0) values[key] = field.default;
2152
- else values[key] = null;
2153
- }
2154
- return {
2155
- schema,
2156
- values,
2157
- secretsSet
2158
- };
2159
- }
2160
- /**
2161
- * Get a plugin's settings (schema + current values, secrets masked)
2162
- */
2163
- async function handlePluginSettingsGet(db, pluginId, schema) {
2164
- try {
2165
- return {
2166
- success: true,
2167
- data: await buildSettingsResponse(new OptionsRepository(db), pluginId, schema)
2168
- };
2169
- } catch {
2170
- return {
2171
- success: false,
2172
- error: {
2173
- code: ErrorCode.PLUGIN_SETTINGS_READ_ERROR,
2174
- message: "Failed to read plugin settings"
2175
- }
2176
- };
2177
- }
2178
- }
2179
- /**
2180
- * Update a plugin's settings.
2181
- *
2182
- * Only keys present in `updates` are written. A `null` value deletes the
2183
- * stored value (reverting to the schema default). Secret fields are
2184
- * write-only: they accept a new string value or `null` to clear, and the
2185
- * response never echoes them back.
2186
- */
2187
- async function handlePluginSettingsUpdate(db, pluginId, schema, updates) {
2188
- try {
2189
- for (const [key, value] of Object.entries(updates)) {
2190
- const field = schema[key];
2191
- if (!field) return {
2192
- success: false,
2193
- error: {
2194
- code: ErrorCode.VALIDATION_ERROR,
2195
- message: `Unknown setting "${key}" for plugin "${pluginId}"`
2196
- }
2197
- };
2198
- if (value === null) continue;
2199
- const error = validateValue(key, field, value);
2200
- if (error) return {
2201
- success: false,
2202
- error: {
2203
- code: ErrorCode.VALIDATION_ERROR,
2204
- message: error
2205
- }
2206
- };
2207
- }
2208
- return {
2209
- success: true,
2210
- data: await withTransaction(db, async (trx) => {
2211
- const txRepo = new OptionsRepository(trx);
2212
- for (const [key, value] of Object.entries(updates)) if (value === null) await txRepo.delete(settingsKey(pluginId, key));
2213
- else await txRepo.set(settingsKey(pluginId, key), value);
2214
- return buildSettingsResponse(txRepo, pluginId, schema);
2215
- })
2216
- };
2217
- } catch {
2218
- return {
2219
- success: false,
2220
- error: {
2221
- code: ErrorCode.PLUGIN_SETTINGS_UPDATE_ERROR,
2222
- message: "Failed to update plugin settings"
2223
- }
2224
- };
2225
- }
2226
- }
2227
-
2228
- //#endregion
2229
17
  //#region src/plugins/marketplace.ts
2230
18
  /**
2231
19
  * MarketplaceClient — HTTP client for the EmDash Plugin Marketplace
@@ -3307,9 +1095,368 @@ async function handleThemeGetDetail(marketplaceUrl, themeId) {
3307
1095
  }
3308
1096
  }
3309
1097
 
1098
+ //#endregion
1099
+ //#region src/registry/artifact-fetch.ts
1100
+ const TRAILING_DOT$1 = /\.+$/;
1101
+ const HEADER_END = new Uint8Array([
1102
+ 13,
1103
+ 10,
1104
+ 13,
1105
+ 10
1106
+ ]);
1107
+ const CRLF = new Uint8Array([13, 10]);
1108
+ const STATUS_LINE_RE = /^HTTP\/1\.[01] ([2-5][0-9]{2})(?: .*)?$/;
1109
+ const HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
1110
+ const FOLDED_HEADER_RE = /^[ \t]/;
1111
+ const CONTENT_LENGTH_RE = /^(0|[1-9][0-9]*)$/;
1112
+ const CHUNK_SIZE_RE = /^[0-9A-Fa-f]+$/;
1113
+ const MAX_HEADER_BYTES = 32 * 1024;
1114
+ const LOCALHOST_HOSTNAMES = new Set([
1115
+ "localhost",
1116
+ "localhost.localdomain",
1117
+ "ip6-localhost",
1118
+ "ip6-loopback"
1119
+ ]);
1120
+ let defaultTransport = null;
1121
+ function isLocalhostHostname(hostname) {
1122
+ const normalized = hostname.toLowerCase().replace(TRAILING_DOT$1, "");
1123
+ return LOCALHOST_HOSTNAMES.has(normalized) || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]" || normalized.startsWith("::ffff:127.") || normalized.startsWith("::ffff:7f00:");
1124
+ }
1125
+ async function resolveSafeArtifactTarget(urlString) {
1126
+ let url;
1127
+ try {
1128
+ url = new URL(urlString);
1129
+ } catch {
1130
+ throw new Error(`Invalid artifact URL: ${urlString}`);
1131
+ }
1132
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`Artifact URL protocol not allowed: ${url.protocol}`);
1133
+ if (url.username || url.password) throw new Error("Artifact URL must not contain embedded credentials");
1134
+ const hostname = stripIpv6Brackets(url.hostname.toLowerCase().replace(TRAILING_DOT$1, ""));
1135
+ const localhost = isLocalhostHostname(hostname);
1136
+ if (!import.meta.env.DEV) {
1137
+ if (url.protocol === "http:") throw new Error("Artifact URL must use https");
1138
+ if (localhost) throw new Error(`Artifact URL points to localhost: ${hostname}`);
1139
+ } else if (url.protocol === "http:" && !localhost) throw new Error("Artifact URL must use https (http allowed only for localhost in dev)");
1140
+ if (localhost) return {
1141
+ url,
1142
+ addresses: []
1143
+ };
1144
+ try {
1145
+ return await resolveAndValidateExternalUrlTarget(url.href);
1146
+ } catch (error) {
1147
+ if (error instanceof SsrfError) throw new Error(`Artifact URL rejected: ${error.message}`, { cause: error });
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ async function fetchRegistryArtifactUrl(urlString, options) {
1152
+ if (!Number.isSafeInteger(options.maxResponseBytes) || options.maxResponseBytes < 0) throw new TypeError("Registry artifact response limit is invalid");
1153
+ const target = await resolveSafeArtifactTarget(urlString);
1154
+ if (target.addresses.length === 0) return globalThis.fetch(target.url, {
1155
+ redirect: "manual",
1156
+ signal: options.signal
1157
+ });
1158
+ const result = await (defaultTransport ?? await createRuntimeRegistryArtifactTransport()).fetch({
1159
+ url: target.url,
1160
+ allowedAddresses: target.addresses,
1161
+ signal: options.signal,
1162
+ maxResponseBytes: options.maxResponseBytes
1163
+ });
1164
+ if (!target.addresses.includes(result.connectedAddress)) {
1165
+ await result.response.body?.cancel().catch(() => void 0);
1166
+ throw new Error("Registry artifact transport connected outside the validated address set");
1167
+ }
1168
+ return result.response;
1169
+ }
1170
+ async function createRuntimeRegistryArtifactTransport() {
1171
+ try {
1172
+ const connect = objectProperty(await import("cloudflare:sockets"), "connect");
1173
+ if (!isWorkerSocketConnect(connect)) throw new TypeError("Workers socket binding is invalid");
1174
+ return createWorkersRegistryArtifactTransport(connect);
1175
+ } catch {
1176
+ return createNodeRegistryArtifactTransport();
1177
+ }
1178
+ }
1179
+ function createWorkersRegistryArtifactTransport(connect) {
1180
+ return { async fetch(input) {
1181
+ let lastError;
1182
+ for (const address of input.allowedAddresses) {
1183
+ if (input.signal.aborted) throw new Error("Registry artifact request was aborted");
1184
+ let socket;
1185
+ let tls;
1186
+ try {
1187
+ socket = connect({
1188
+ hostname: address,
1189
+ port: parseHttpsPort(input.url)
1190
+ }, {
1191
+ secureTransport: "starttls",
1192
+ allowHalfOpen: false
1193
+ });
1194
+ await abortable(socket.opened, input.signal, () => socket?.close());
1195
+ tls = socket.startTls({ expectedServerHostname: stripIpv6Brackets(input.url.hostname) });
1196
+ await abortable(tls.opened, input.signal, () => tls?.close());
1197
+ const writer = tls.writable.getWriter();
1198
+ try {
1199
+ await abortable(writer.write(buildHttpRequest(input.url)), input.signal, () => tls?.close());
1200
+ } finally {
1201
+ writer.releaseLock();
1202
+ }
1203
+ const bytes = await readWorkerSocketResponse(tls, input.maxResponseBytes + MAX_HEADER_BYTES, input.signal);
1204
+ await tls.close().catch(() => void 0);
1205
+ return {
1206
+ response: parsedResponseToWebResponse(parsePinnedHttpResponse(bytes, input.maxResponseBytes)),
1207
+ connectedAddress: address
1208
+ };
1209
+ } catch (error) {
1210
+ await Promise.all([tls?.close().catch(() => void 0), socket?.close().catch(() => void 0)]);
1211
+ if (input.signal.aborted) throw error;
1212
+ lastError = error;
1213
+ }
1214
+ }
1215
+ throw new Error("Registry artifact transport could not connect to an approved address", { cause: lastError });
1216
+ } };
1217
+ }
1218
+ async function createNodeRegistryArtifactTransport() {
1219
+ const { request } = await import("node:https");
1220
+ return { async fetch(input) {
1221
+ let lastError;
1222
+ for (const address of input.allowedAddresses) try {
1223
+ return {
1224
+ response: await new Promise((resolve, reject) => {
1225
+ const chunks = [];
1226
+ let total = 0;
1227
+ const req = request({
1228
+ protocol: "https:",
1229
+ hostname: stripIpv6Brackets(input.url.hostname),
1230
+ port: parseHttpsPort(input.url),
1231
+ path: `${input.url.pathname}${input.url.search}`,
1232
+ method: "GET",
1233
+ agent: false,
1234
+ headers: {
1235
+ Host: input.url.host,
1236
+ Connection: "close",
1237
+ "Accept-Encoding": "identity"
1238
+ },
1239
+ lookup: (_hostname, _options, callback) => {
1240
+ callback(null, address, address.includes(":") ? 6 : 4);
1241
+ },
1242
+ signal: input.signal
1243
+ }, (upstream) => {
1244
+ const contentEncoding = upstream.headers["content-encoding"];
1245
+ if (contentEncoding && contentEncoding.toLowerCase() !== "identity") {
1246
+ upstream.destroy(/* @__PURE__ */ new Error("Registry artifact response content encoding is unsupported"));
1247
+ return;
1248
+ }
1249
+ upstream.on("data", (chunk) => {
1250
+ total += chunk.byteLength;
1251
+ if (total > input.maxResponseBytes) {
1252
+ upstream.destroy(/* @__PURE__ */ new RangeError("Registry artifact response exceeds its byte limit"));
1253
+ return;
1254
+ }
1255
+ chunks.push(new Uint8Array(chunk));
1256
+ });
1257
+ upstream.once("error", reject);
1258
+ upstream.once("end", () => {
1259
+ const bytes = concatBytes(chunks, total);
1260
+ const headers = new Headers();
1261
+ for (let index = 0; index < upstream.rawHeaders.length; index += 2) headers.append(upstream.rawHeaders[index], upstream.rawHeaders[index + 1]);
1262
+ resolve(parsedResponseToWebResponse({
1263
+ status: upstream.statusCode ?? 502,
1264
+ headers,
1265
+ body: bytes
1266
+ }));
1267
+ });
1268
+ });
1269
+ req.once("error", reject);
1270
+ req.end();
1271
+ }),
1272
+ connectedAddress: address
1273
+ };
1274
+ } catch (error) {
1275
+ if (input.signal.aborted) throw error;
1276
+ lastError = error;
1277
+ }
1278
+ throw new Error("Registry artifact transport could not connect to an approved address", { cause: lastError });
1279
+ } };
1280
+ }
1281
+ function parsePinnedHttpResponse(bytes, maxResponseBytes) {
1282
+ const headerEnd = findSequence(bytes, HEADER_END, 0);
1283
+ if (headerEnd === -1 || headerEnd > MAX_HEADER_BYTES) throw new Error("Registry artifact response headers are invalid or too large");
1284
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, headerEnd)).split("\r\n");
1285
+ const statusMatch = STATUS_LINE_RE.exec(lines.shift() ?? "");
1286
+ if (!statusMatch) throw new Error("Registry artifact response status line is invalid");
1287
+ const headers = new Headers();
1288
+ const rawHeaders = /* @__PURE__ */ new Map();
1289
+ for (const line of lines) {
1290
+ if (FOLDED_HEADER_RE.test(line)) throw new Error("Registry artifact response uses folded headers");
1291
+ const separator = line.indexOf(":");
1292
+ if (separator < 1) throw new Error("Registry artifact response header is invalid");
1293
+ const name = line.slice(0, separator);
1294
+ const value = line.slice(separator + 1).trim();
1295
+ if (!HEADER_NAME_RE.test(name) || containsInvalidHeaderValue(value)) throw new Error("Registry artifact response header is invalid");
1296
+ const normalized = name.toLowerCase();
1297
+ const values = rawHeaders.get(normalized) ?? [];
1298
+ values.push(value);
1299
+ rawHeaders.set(normalized, values);
1300
+ headers.append(name, value);
1301
+ }
1302
+ const contentEncoding = rawHeaders.get("content-encoding");
1303
+ if (contentEncoding && (contentEncoding.length !== 1 || contentEncoding[0]?.toLowerCase() !== "identity")) throw new Error("Registry artifact response content encoding is unsupported");
1304
+ const contentLength = rawHeaders.get("content-length");
1305
+ const transferEncoding = rawHeaders.get("transfer-encoding");
1306
+ if (contentLength && transferEncoding) throw new Error("Registry artifact response framing is ambiguous");
1307
+ const bodyBytes = bytes.subarray(headerEnd + HEADER_END.length);
1308
+ let body;
1309
+ if (transferEncoding) {
1310
+ if (transferEncoding.length !== 1 || transferEncoding[0]?.toLowerCase() !== "chunked") throw new Error("Registry artifact response transfer encoding is unsupported");
1311
+ body = decodeChunkedBody(bodyBytes, maxResponseBytes);
1312
+ } else if (contentLength) {
1313
+ if (contentLength.length !== 1 || !CONTENT_LENGTH_RE.test(contentLength[0])) throw new Error("Registry artifact response content length is invalid");
1314
+ const length = Number(contentLength[0]);
1315
+ if (!Number.isSafeInteger(length) || length !== bodyBytes.byteLength) throw new Error("Registry artifact response body length does not match its framing");
1316
+ body = new Uint8Array(bodyBytes);
1317
+ } else body = new Uint8Array(bodyBytes);
1318
+ if (body.byteLength > maxResponseBytes) throw new RangeError("Registry artifact response exceeds its byte limit");
1319
+ return {
1320
+ status: Number(statusMatch[1]),
1321
+ headers,
1322
+ body
1323
+ };
1324
+ }
1325
+ function parsedResponseToWebResponse(parsed) {
1326
+ const bodyAllowed = ![
1327
+ 204,
1328
+ 205,
1329
+ 304
1330
+ ].includes(parsed.status);
1331
+ let body = null;
1332
+ if (bodyAllowed) {
1333
+ body = new ArrayBuffer(parsed.body.byteLength);
1334
+ new Uint8Array(body).set(parsed.body);
1335
+ }
1336
+ return new Response(body, {
1337
+ status: parsed.status,
1338
+ headers: parsed.headers
1339
+ });
1340
+ }
1341
+ function buildHttpRequest(url) {
1342
+ return new TextEncoder().encode([
1343
+ `GET ${url.pathname}${url.search} HTTP/1.1`,
1344
+ `Host: ${url.host}`,
1345
+ "Connection: close",
1346
+ "Accept-Encoding: identity",
1347
+ "",
1348
+ ""
1349
+ ].join("\r\n"));
1350
+ }
1351
+ async function readWorkerSocketResponse(socket, maximumBytes, signal) {
1352
+ const reader = socket.readable.getReader();
1353
+ const chunks = [];
1354
+ let total = 0;
1355
+ try {
1356
+ for (;;) {
1357
+ const next = await abortable(reader.read(), signal, () => socket.close());
1358
+ if (next.done) break;
1359
+ total += next.value.byteLength;
1360
+ if (total > maximumBytes) throw new RangeError("Registry artifact response exceeds its byte limit");
1361
+ chunks.push(next.value);
1362
+ }
1363
+ } finally {
1364
+ reader.releaseLock();
1365
+ }
1366
+ return concatBytes(chunks, total);
1367
+ }
1368
+ function decodeChunkedBody(bytes, maximumBytes) {
1369
+ const chunks = [];
1370
+ let offset = 0;
1371
+ let total = 0;
1372
+ for (;;) {
1373
+ const lineEnd = findSequence(bytes, CRLF, offset);
1374
+ if (lineEnd === -1 || lineEnd - offset > 128) throw new Error("Registry artifact chunk size line is invalid");
1375
+ const sizeText = new TextDecoder().decode(bytes.subarray(offset, lineEnd)).split(";", 1)[0];
1376
+ if (!sizeText || !CHUNK_SIZE_RE.test(sizeText)) throw new Error("Registry artifact chunk size is invalid");
1377
+ const size = Number.parseInt(sizeText, 16);
1378
+ if (!Number.isSafeInteger(size)) throw new Error("Registry artifact chunk size is invalid");
1379
+ offset = lineEnd + CRLF.length;
1380
+ if (size === 0) {
1381
+ if (offset + CRLF.length !== bytes.length || bytes[offset] !== 13 || bytes[offset + 1] !== 10) throw new Error("Registry artifact chunk trailer is invalid");
1382
+ break;
1383
+ }
1384
+ if (offset + size + CRLF.length > bytes.length) throw new Error("Registry artifact chunk body is truncated");
1385
+ if (bytes[offset + size] !== 13 || bytes[offset + size + 1] !== 10) throw new Error("Registry artifact chunk delimiter is invalid");
1386
+ total += size;
1387
+ if (total > maximumBytes) throw new RangeError("Registry artifact response exceeds its byte limit");
1388
+ chunks.push(new Uint8Array(bytes.subarray(offset, offset + size)));
1389
+ offset += size + CRLF.length;
1390
+ }
1391
+ return concatBytes(chunks, total);
1392
+ }
1393
+ function findSequence(bytes, sequence, start) {
1394
+ outer: for (let offset = start; offset <= bytes.length - sequence.length; offset += 1) {
1395
+ for (let index = 0; index < sequence.length; index += 1) if (bytes[offset + index] !== sequence[index]) continue outer;
1396
+ return offset;
1397
+ }
1398
+ return -1;
1399
+ }
1400
+ function concatBytes(chunks, total) {
1401
+ const bytes = new Uint8Array(total);
1402
+ let offset = 0;
1403
+ for (const chunk of chunks) {
1404
+ bytes.set(chunk, offset);
1405
+ offset += chunk.byteLength;
1406
+ }
1407
+ return bytes;
1408
+ }
1409
+ function containsInvalidHeaderValue(value) {
1410
+ for (const character of value) {
1411
+ const code = character.codePointAt(0);
1412
+ if (code === 0 || code === 10 || code === 13) return true;
1413
+ }
1414
+ return false;
1415
+ }
1416
+ function parseHttpsPort(url) {
1417
+ const port = url.port === "" ? 443 : Number(url.port);
1418
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new TypeError("Registry artifact HTTPS port is invalid");
1419
+ return port;
1420
+ }
1421
+ function stripIpv6Brackets(hostname) {
1422
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1423
+ }
1424
+ function objectProperty(value, key) {
1425
+ if (typeof value !== "object" || value === null) return void 0;
1426
+ return Object.getOwnPropertyDescriptor(value, key)?.value;
1427
+ }
1428
+ function isWorkerSocketConnect(value) {
1429
+ return typeof value === "function";
1430
+ }
1431
+ async function abortable(operation, signal, onAbort) {
1432
+ if (signal.aborted) throw new Error("Registry artifact request was aborted");
1433
+ let abort;
1434
+ const aborted = new Promise((_resolve, reject) => {
1435
+ abort = () => {
1436
+ onAbort();
1437
+ reject(/* @__PURE__ */ new Error("Registry artifact request was aborted"));
1438
+ };
1439
+ signal.addEventListener("abort", abort, { once: true });
1440
+ });
1441
+ try {
1442
+ return await Promise.race([operation, aborted]);
1443
+ } finally {
1444
+ if (abort) signal.removeEventListener("abort", abort);
1445
+ }
1446
+ }
1447
+
3310
1448
  //#endregion
3311
1449
  //#region src/registry/config.ts
3312
1450
  /**
1451
+ * Helpers for normalizing the experimental registry integration option
1452
+ * (`config.experimental.registry` in `astro.config.mjs`) into the shape
1453
+ * exposed on the admin manifest.
1454
+ *
1455
+ * The integration option accepts a human-friendly duration string for
1456
+ * `policy.minimumReleaseAge` (`"48h"`, `"7d"`); the manifest exposes
1457
+ * seconds so the browser doesn't need a duration parser.
1458
+ */
1459
+ /**
3313
1460
  * Canonicalize a capabilities list for set-style comparison.
3314
1461
  *
3315
1462
  * Capabilities (the legacy declared-access shape used by the current
@@ -3366,7 +1513,8 @@ const DURATION_PATTERN = /^(\d+)(s|m|h|d|w)$/;
3366
1513
  /** Trailing slashes on the aggregator URL, stripped during normalization. */
3367
1514
  const TRAILING_SLASHES = /\/+$/;
3368
1515
  /** Trailing dot on a hostname, stripped before URL host comparisons. */
3369
- const TRAILING_DOT$1 = /\.$/;
1516
+ const TRAILING_DOT = /\.$/;
1517
+ const REGISTRY_PACKAGE_SLUG_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
3370
1518
  /**
3371
1519
  * Parse a duration string or raw second count into a non-negative
3372
1520
  * integer count of seconds. Throws on unrecognised input so config
@@ -3396,7 +1544,7 @@ function parseDurationSeconds(duration) {
3396
1544
  * required in production; `http://localhost` allowed only in dev.
3397
1545
  *
3398
1546
  * The aggregator's responses are the trust source for release records,
3399
- * checksums, labels, mirrors, and `indexedAt` (until full MST
1547
+ * checksums, labels, artifact cache descriptors, and `indexedAt` (until full MST
3400
1548
  * verification lands). Allowing plain HTTP here would let a network
3401
1549
  * attacker swap a release record and point the artifact URL at their
3402
1550
  * own HTTPS bundle, defeating the checksum trust chain because the
@@ -3411,7 +1559,7 @@ function validateAggregatorUrl(aggregatorUrl) {
3411
1559
  }
3412
1560
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`registry.aggregatorUrl must use http or https: ${aggregatorUrl}`);
3413
1561
  if (parsed.username || parsed.password) throw new Error("registry.aggregatorUrl must not contain embedded credentials (user:pass@)");
3414
- const rawHostname = parsed.hostname.toLowerCase().replace(TRAILING_DOT$1, "");
1562
+ const rawHostname = parsed.hostname.toLowerCase().replace(TRAILING_DOT, "");
3415
1563
  const hostname = rawHostname.startsWith("[") && rawHostname.endsWith("]") ? rawHostname.slice(1, -1) : rawHostname;
3416
1564
  const isLocalhost = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname.startsWith("::ffff:127.") || hostname.startsWith("::ffff:7f00:");
3417
1565
  if (!import.meta.env.DEV) {
@@ -3473,7 +1621,10 @@ function normalizeRegistryConfig(input) {
3473
1621
  const list = config.policy.minimumReleaseAgeExclude.map((entry) => {
3474
1622
  const trimmed = entry.trim();
3475
1623
  if (!trimmed) throw new Error("registry.policy.minimumReleaseAgeExclude entries cannot be empty");
3476
- return trimmed.toLowerCase();
1624
+ const lower = trimmed.toLowerCase();
1625
+ const [did, slug, ...extra] = lower.split("/");
1626
+ if (!did || !isDid(did) || extra.length > 0 || slug !== void 0 && !REGISTRY_PACKAGE_SLUG_PATTERN.test(slug)) throw new Error(`registry.policy.minimumReleaseAgeExclude entry must be a DID or <did>/<slug>: ${trimmed}`);
1627
+ return lower;
3477
1628
  });
3478
1629
  if (list.length > 0) {
3479
1630
  policy.minimumReleaseAgeExclude = list;
@@ -3582,11 +1733,10 @@ async function makeRegistryPluginId(publisherDid, slug) {
3582
1733
  * aggregator's `resolvePackage` XRPC.
3583
1734
  * 2. Look up the requested release (or the policy-filtered latest one)
3584
1735
  * via `getLatestRelease` / `listReleases`.
3585
- * 3. Reject the install if the aggregator surfaces a `security:yanked`
3586
- * hard-enforcement label or the release is below the configured
3587
- * minimum release age.
3588
- * 4. Fetch the bundle artifact, walking aggregator mirrors first and
3589
- * falling back to the publisher-declared URL.
1736
+ * 3. Require the aggregator's approved listing projection, then apply the
1737
+ * independent release-age and environment policies.
1738
+ * 4. Fetch the bundle artifact through an advertised record-scoped cache,
1739
+ * the publisher PDS, or the publisher-declared URL.
3590
1740
  * 5. Verify the artifact's multibase checksum against the signed
3591
1741
  * release record's `artifacts.package.checksum`.
3592
1742
  * 6. Extract `manifest.json` + `backend.js` + optional `admin.js` from
@@ -3606,10 +1756,10 @@ async function makeRegistryPluginId(publisherDid, slug) {
3606
1756
  * The artifact checksum is verified end-to-end against the value
3607
1757
  * in the (aggregator-relayed) release record, which is the actual
3608
1758
  * trust boundary for the bytes that end up in the sandbox.
3609
- * - `acceptLabelers` is forwarded as-is to the aggregator; this
3610
- * handler does not independently re-fetch and verify labels from
3611
- * each labeller's DID. Aggregator label envelope tampering is
3612
- * mitigated by the artifact checksum but not detected.
1759
+ * - Listing approval controls whether metadata is eligible for discovery.
1760
+ * It is never treated as approval of plugin code: checksum, bundle,
1761
+ * manifest, access, consent, sandbox, and environment gates remain
1762
+ * independent below.
3613
1763
  */
3614
1764
  const RELEASE_EXTENSION_NSID = "com.emdashcms.experimental.package.releaseExtension";
3615
1765
  /**
@@ -3703,28 +1853,20 @@ const MAX_ARTIFACT_BYTES = 512 * 1024;
3703
1853
  const MAX_REDIRECTS = 5;
3704
1854
  /**
3705
1855
  * Wall-clock cap on any single artifact fetch attempt (per URL).
3706
- * Defends against slow-loris mirrors that accept the connection but
1856
+ * Defends against slow-loris artifact sources that accept the connection but
3707
1857
  * never finish sending headers or body.
3708
1858
  */
3709
1859
  const ARTIFACT_FETCH_TIMEOUT_MS = 15e3;
3710
1860
  /**
3711
1861
  * Total wall-clock budget for the artifact-download phase across all
3712
- * mirrors and the declared URL. Even with the per-URL timeout, a
3713
- * malicious mirror list could otherwise tie up the install request for
1862
+ * advertised caches, the publisher PDS, and the declared URL. Even with the
1863
+ * per-URL timeout, unavailable sources could otherwise tie up the install request for
3714
1864
  * minutes; this caps total time at a budget interactive admins can
3715
1865
  * tolerate. Tuned so a fast happy path takes <1s of budget per
3716
1866
  * attempt and a worst case still completes in under a minute.
3717
1867
  */
3718
1868
  const ARTIFACT_TOTAL_BUDGET_MS = 45e3;
3719
1869
  /**
3720
- * Cap on the number of mirror URLs we try before falling back to the
3721
- * publisher-declared URL. Matches the aggregator lexicon's
3722
- * `mirrors` array length cap (16) but enforced here independently so
3723
- * a misbehaving aggregator can't slow-loris us through hundreds of
3724
- * URLs.
3725
- */
3726
- const MAX_MIRRORS = 16;
3727
- /**
3728
1870
  * Per-request timeout applied to every aggregator XRPC call
3729
1871
  * (`resolvePackage`, `getLatestRelease`, `listReleases`). Matches the
3730
1872
  * per-URL artifact-fetch cap. Without this, a slow-loris aggregator
@@ -3759,178 +1901,29 @@ function timedFetch(totalDeadline) {
3759
1901
  });
3760
1902
  };
3761
1903
  }
3762
- /**
3763
- * Localhost-equivalent hostnames the artifact fetcher rejects in
3764
- * production. The full literal-IP / DNS-rebinding blocklist lives in
3765
- * `#security/ssrf.js` and is invoked via `resolveAndValidateExternalUrl`
3766
- * below; this small set exists only because the artifact handler has
3767
- * a dev-mode escape hatch that lets `http://localhost` through.
3768
- */
3769
- const FORBIDDEN_HOSTNAMES = new Set([
3770
- "localhost",
3771
- "localhost.localdomain",
3772
- "ip6-localhost",
3773
- "ip6-loopback"
3774
- ]);
3775
- /** Trailing dot on a hostname, stripped before URL host comparisons. */
3776
- const TRAILING_DOT = /\.$/;
3777
- /** Hostnames that resolve to the local machine; rejected outright in production. */
3778
- function isLocalhostHostname(hostname) {
3779
- const stripped = hostname.toLowerCase().replace(TRAILING_DOT, "");
3780
- const h = stripped.startsWith("[") && stripped.endsWith("]") ? stripped.slice(1, -1) : stripped;
3781
- if (FORBIDDEN_HOSTNAMES.has(h)) return true;
3782
- if (h === "localhost") return true;
3783
- if (h.endsWith(".localhost")) return true;
3784
- if (h === "127.0.0.1" || h === "::1") return true;
3785
- if (h.startsWith("::ffff:127.") || h.startsWith("::ffff:7f00:")) return true;
3786
- return false;
3787
- }
3788
- /**
3789
- * Validate that `urlString` is a safe outbound target for artifact
3790
- * downloads. Rejects non-HTTPS (except localhost in dev), embedded
3791
- * credentials, any host that's a loopback / private / link-local
3792
- * literal address, and any hostname whose resolved A or AAAA records
3793
- * point at one of those addresses (closes the DNS-rebinding gap).
3794
- *
3795
- * Wraps `resolveAndValidateExternalUrl` from the import-pipeline SSRF
3796
- * module so both code paths share one DoH cache, one resolver, one
3797
- * blocklist, and one set of regression tests. Layers an
3798
- * artifact-specific protocol/dev-localhost policy on top.
3799
- *
3800
- * `import.meta.env.DEV` is a Vite/Astro compile-time constant, so
3801
- * production bundles cannot enable the dev escape hatch at runtime.
3802
- */
3803
- async function assertSafeArtifactUrl(urlString) {
3804
- let url;
3805
- try {
3806
- url = new URL(urlString);
3807
- } catch {
3808
- throw new Error(`Invalid artifact URL: ${urlString}`);
3809
- }
3810
- if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`Artifact URL protocol not allowed: ${url.protocol}`);
3811
- if (url.username || url.password) throw new Error("Artifact URL must not contain embedded credentials");
3812
- const rawHostname = url.hostname.toLowerCase().replace(TRAILING_DOT, "");
3813
- const hostname = rawHostname.startsWith("[") && rawHostname.endsWith("]") ? rawHostname.slice(1, -1) : rawHostname;
3814
- const localhost = isLocalhostHostname(hostname);
3815
- if (!import.meta.env.DEV) {
3816
- if (url.protocol === "http:") throw new Error("Artifact URL must use https");
3817
- if (localhost) throw new Error(`Artifact URL points to localhost: ${hostname}`);
3818
- } else if (url.protocol === "http:" && !localhost) throw new Error("Artifact URL must use https (http allowed only for localhost in dev)");
3819
- if (localhost) return url;
3820
- try {
3821
- return await resolveAndValidateExternalUrl(url.href);
3822
- } catch (err) {
3823
- if (err instanceof SsrfError) throw new Error(`Artifact URL rejected: ${err.message}`, { cause: err });
3824
- throw err;
3825
- }
3826
- }
3827
- /**
3828
- * Fetch one URL with manual redirect handling so every hop is
3829
- * URL-validated, a hard byte cap so a malicious response body cannot
3830
- * exhaust memory before the checksum check rejects it, and a wall-clock
3831
- * timeout that covers connect, headers, and body together. The timeout
3832
- * is the minimum of the per-URL cap and the remaining total budget so
3833
- * a late-arriving mirror still respects the install's global budget.
3834
- */
3835
- async function fetchWithLimits(initialUrl, totalDeadline) {
3836
- const now = Date.now();
3837
- const remaining = Math.max(0, totalDeadline - now);
3838
- if (remaining === 0) throw new Error("Artifact download budget exhausted");
3839
- const perUrlTimeout = Math.min(ARTIFACT_FETCH_TIMEOUT_MS, remaining);
3840
- const controller = new AbortController();
3841
- const timer = setTimeout(() => controller.abort(), perUrlTimeout);
3842
- try {
3843
- let current = await assertSafeArtifactUrl(initialUrl);
3844
- let response;
3845
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
3846
- response = await fetch(current.href, {
3847
- redirect: "manual",
3848
- signal: controller.signal
1904
+ async function fetchArtifact(artifactCaches, artifact, record, auth) {
1905
+ const result = await fetchReleaseArtifact({
1906
+ artifact,
1907
+ record,
1908
+ artifactCaches,
1909
+ ...auth === void 0 ? {} : { auth }
1910
+ }, {
1911
+ fetch: async (url, init) => {
1912
+ if (!init.signal) throw new Error("Registry artifact fetch requires an abort signal");
1913
+ return fetchRegistryArtifactUrl(url.href, {
1914
+ signal: init.signal,
1915
+ maxResponseBytes: MAX_ARTIFACT_BYTES
3849
1916
  });
3850
- if (response.status < 300 || response.status >= 400) break;
3851
- const location = response.headers.get("location");
3852
- if (!location) break;
3853
- if (hop === MAX_REDIRECTS) throw new Error(`Too many redirects fetching artifact (>${MAX_REDIRECTS})`);
3854
- current = await assertSafeArtifactUrl(new URL(location, current).href);
3855
- }
3856
- const finalResponse = response;
3857
- if (!finalResponse.ok) throw new Error(`HTTP ${finalResponse.status}`);
3858
- const lengthHeader = finalResponse.headers.get("content-length");
3859
- if (lengthHeader) {
3860
- const declared = Number(lengthHeader);
3861
- if (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) throw new Error(`Artifact too large (declared ${declared} bytes, limit ${MAX_ARTIFACT_BYTES})`);
3862
- }
3863
- const body = finalResponse.body;
3864
- if (!body) {
3865
- const buf = new Uint8Array(await finalResponse.arrayBuffer());
3866
- if (buf.byteLength > MAX_ARTIFACT_BYTES) throw new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);
3867
- return buf;
3868
- }
3869
- const reader = body.getReader();
3870
- const chunks = [];
3871
- let total = 0;
3872
- while (true) {
3873
- const { done, value } = await reader.read();
3874
- if (done) break;
3875
- if (!value) continue;
3876
- total += value.byteLength;
3877
- if (total > MAX_ARTIFACT_BYTES) {
3878
- try {
3879
- await reader.cancel();
3880
- } catch {}
3881
- throw new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);
3882
- }
3883
- chunks.push(value);
3884
- }
3885
- const out = new Uint8Array(total);
3886
- let offset = 0;
3887
- for (const chunk of chunks) {
3888
- out.set(chunk, offset);
3889
- offset += chunk.byteLength;
3890
- }
3891
- return out;
3892
- } finally {
3893
- clearTimeout(timer);
3894
- }
3895
- }
3896
- /**
3897
- * Strip query string and fragment from a URL for use in
3898
- * client-visible error messages. Registry artifacts are often hosted
3899
- * on storage backends that include presigned tokens in the query
3900
- * string; surfacing the raw URL on a failed install leaks those
3901
- * tokens into the admin's HTTP response and any log drain that
3902
- * captures the error chain. Origin + pathname is enough to identify
3903
- * the host and resource without exposing credentials.
3904
- *
3905
- * Falls back to a generic placeholder when the URL is malformed.
3906
- */
3907
- function redactUrlForError(raw) {
3908
- try {
3909
- const u = new URL(raw);
3910
- return `${u.origin}${u.pathname}`;
3911
- } catch {
3912
- return "<malformed url>";
3913
- }
3914
- }
3915
- /** Walk artifact source URLs in priority order and return the first that fetches successfully. */
3916
- async function fetchArtifact(mirrors, declaredUrl) {
3917
- const urls = [...mirrors.slice(0, MAX_MIRRORS), declaredUrl];
3918
- const clientErrors = [];
3919
- const totalDeadline = Date.now() + ARTIFACT_TOTAL_BUDGET_MS;
3920
- for (const url of urls) {
3921
- if (Date.now() >= totalDeadline) {
3922
- clientErrors.push("(total artifact download budget exhausted)");
3923
- break;
3924
- }
3925
- try {
3926
- return await fetchWithLimits(url, totalDeadline);
3927
- } catch (err) {
3928
- const message = err instanceof Error ? err.message : String(err);
3929
- console.warn(`[registry-install] Artifact fetch failed from ${url}:`, message);
3930
- clientErrors.push(`${redactUrlForError(url)}: ${message}`);
3931
- }
3932
- }
3933
- throw new Error(`Failed to download artifact from any source. Tried:\n ${clientErrors.join("\n ")}`);
1917
+ },
1918
+ resolveHostname: async (hostname) => (await resolveAndValidateExternalUrlTarget(`https://${hostname}`)).addresses,
1919
+ allowHttpLocalhost: import.meta.env.DEV,
1920
+ headerTimeoutMs: ARTIFACT_FETCH_TIMEOUT_MS,
1921
+ totalTimeoutMs: ARTIFACT_TOTAL_BUDGET_MS,
1922
+ maxBytes: MAX_ARTIFACT_BYTES,
1923
+ maxRedirects: MAX_REDIRECTS
1924
+ });
1925
+ if (!result.success) throw new Error(`Artifact retrieval failed (${result.error.code}): ${result.error.message}`);
1926
+ return result.value.bytes;
3934
1927
  }
3935
1928
  /**
3936
1929
  * Gate a release's `requires` constraints against the running host
@@ -3992,11 +1985,12 @@ async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigI
3992
1985
  };
3993
1986
  }
3994
1987
  const { did, slug, version: requestedVersion } = input;
3995
- const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
1988
+ const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
3996
1989
  const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
3997
1990
  const discovery = new DiscoveryClient({
3998
1991
  aggregatorUrl: registryConfig.aggregatorUrl,
3999
1992
  acceptLabelers: registryConfig.acceptLabelers,
1993
+ labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
4000
1994
  fetch: timedFetch(aggregatorDeadline)
4001
1995
  });
4002
1996
  if (!did.startsWith("did:") || did.split(":").length < 3) return {
@@ -4059,13 +2053,11 @@ async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigI
4059
2053
  }
4060
2054
  };
4061
2055
  const version = releaseView.version;
4062
- const yanked = (packageView.labels ?? []).some((l) => l.val === "security:yanked");
4063
- const releaseYanked = (releaseView.labels ?? []).some((l) => l.val === "security:yanked");
4064
- if (yanked || releaseYanked) return {
2056
+ if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) return {
4065
2057
  success: false,
4066
2058
  error: {
4067
2059
  code: "RELEASE_YANKED",
4068
- message: "This release has been withdrawn (security:yanked label)."
2060
+ message: "This release has been withdrawn"
4069
2061
  }
4070
2062
  };
4071
2063
  if (opts?.hostEnv) {
@@ -4139,16 +2131,21 @@ async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigI
4139
2131
  };
4140
2132
  }
4141
2133
  const release = releaseView.release;
4142
- const declaredUrl = release?.artifacts?.package?.url;
4143
- const declaredChecksum = release?.artifacts?.package?.checksum;
4144
- if (!declaredUrl || !declaredChecksum) return {
2134
+ const packageArtifact = release?.artifacts?.package;
2135
+ const declaredChecksum = packageArtifact?.checksum;
2136
+ if (!packageArtifact || !declaredChecksum) return {
4145
2137
  success: false,
4146
2138
  error: {
4147
2139
  code: "INVALID_RELEASE",
4148
- message: "Release record is missing artifact url or checksum"
2140
+ message: "Release record is missing its package artifact or checksum"
4149
2141
  }
4150
2142
  };
4151
- const artifactBytes = await fetchArtifact(releaseView.mirrors ?? [], declaredUrl);
2143
+ const artifactBytes = await fetchArtifact(releaseView.artifactCaches ?? [], packageArtifact, {
2144
+ did: publisherDid,
2145
+ collection: NSID.packageRelease,
2146
+ rkey: `${slug}:${version}`,
2147
+ cid: releaseView.cid
2148
+ }, release.auth);
4152
2149
  if (!await verifyChecksum(artifactBytes, declaredChecksum)) return {
4153
2150
  success: false,
4154
2151
  error: {
@@ -4267,13 +2264,22 @@ async function handleRegistryInstall(db, storage, sandboxRunner, registryConfigI
4267
2264
  message: `Aggregator returned a response that does not conform to its lexicon (${err.target})`
4268
2265
  }
4269
2266
  };
4270
- if (err instanceof ClientResponseError) return {
4271
- success: false,
4272
- error: {
4273
- code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
4274
- message: `Aggregator returned ${err.status}: ${err.error}`
4275
- }
4276
- };
2267
+ if (err instanceof ClientResponseError) {
2268
+ if (err.error === "ListingUnavailable") return {
2269
+ success: false,
2270
+ error: {
2271
+ code: "LISTING_UNAVAILABLE",
2272
+ message: "This plugin is unavailable under the active registry policy"
2273
+ }
2274
+ };
2275
+ return {
2276
+ success: false,
2277
+ error: {
2278
+ code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
2279
+ message: `Aggregator returned ${err.status}: ${err.error}`
2280
+ }
2281
+ };
2282
+ }
4277
2283
  if (err instanceof EmDashStorageError) return {
4278
2284
  success: false,
4279
2285
  error: {
@@ -4407,11 +2413,12 @@ async function handleRegistryUpdate(db, storage, sandboxRunner, registryConfigIn
4407
2413
  const oldVersion = existing.version;
4408
2414
  const publisherDid = existing.registryPublisherDid;
4409
2415
  const slug = existing.registrySlug;
4410
- const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
2416
+ const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
4411
2417
  const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
4412
2418
  const discovery = new DiscoveryClient({
4413
2419
  aggregatorUrl: registryConfig.aggregatorUrl,
4414
2420
  acceptLabelers: registryConfig.acceptLabelers,
2421
+ labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
4415
2422
  fetch: timedFetch(aggregatorDeadline)
4416
2423
  });
4417
2424
  const MAX_LIST_PAGES = 20;
@@ -4454,18 +2461,18 @@ async function handleRegistryUpdate(db, storage, sandboxRunner, registryConfigIn
4454
2461
  }
4455
2462
  };
4456
2463
  const newVersion = releaseView.version;
4457
- if (newVersion === oldVersion) return {
2464
+ if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) return {
4458
2465
  success: false,
4459
2466
  error: {
4460
- code: "ALREADY_UP_TO_DATE",
4461
- message: "Plugin is already at the requested version"
2467
+ code: "YANKED",
2468
+ message: "Release has been withdrawn"
4462
2469
  }
4463
2470
  };
4464
- if ((releaseView.labels ?? []).some((l) => l.val === "security:yanked")) return {
2471
+ if (newVersion === oldVersion) return {
4465
2472
  success: false,
4466
2473
  error: {
4467
- code: "YANKED",
4468
- message: "Release has been yanked by a trusted labeller"
2474
+ code: "ALREADY_UP_TO_DATE",
2475
+ message: "Plugin is already at the requested version"
4469
2476
  }
4470
2477
  };
4471
2478
  if (opts?.hostEnv) {
@@ -4475,19 +2482,21 @@ async function handleRegistryUpdate(db, storage, sandboxRunner, registryConfigIn
4475
2482
  error: envError
4476
2483
  };
4477
2484
  }
4478
- const declaredUrl = signedRelease.artifacts?.package?.url;
4479
- const declaredChecksum = signedRelease.artifacts?.package?.checksum;
4480
- if (!declaredUrl || !declaredChecksum) return {
2485
+ const packageArtifact = signedRelease.artifacts?.package;
2486
+ const declaredChecksum = packageArtifact?.checksum;
2487
+ if (!packageArtifact || !declaredChecksum) return {
4481
2488
  success: false,
4482
2489
  error: {
4483
2490
  code: "INVALID_RELEASE",
4484
- message: "Release record is missing artifact url or checksum"
2491
+ message: "Release record is missing its package artifact or checksum"
4485
2492
  }
4486
2493
  };
4487
- await assertSafeArtifactUrl(declaredUrl);
4488
- const mirrors = (releaseView.mirrors ?? []).slice(0, MAX_MIRRORS);
4489
- for (const mirror of mirrors) await assertSafeArtifactUrl(mirror);
4490
- const artifactBytes = await fetchArtifact(mirrors, declaredUrl);
2494
+ const artifactBytes = await fetchArtifact(releaseView.artifactCaches ?? [], packageArtifact, {
2495
+ did: publisherDid,
2496
+ collection: NSID.packageRelease,
2497
+ rkey: `${slug}:${newVersion}`,
2498
+ cid: releaseView.cid
2499
+ }, signedRelease.auth);
4491
2500
  if (!await verifyChecksum(artifactBytes, declaredChecksum)) return {
4492
2501
  success: false,
4493
2502
  error: {
@@ -4585,13 +2594,22 @@ async function handleRegistryUpdate(db, storage, sandboxRunner, registryConfigIn
4585
2594
  message: `Aggregator returned a response that does not conform to its lexicon (${err.target})`
4586
2595
  }
4587
2596
  };
4588
- if (err instanceof ClientResponseError) return {
4589
- success: false,
4590
- error: {
4591
- code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
4592
- message: `Aggregator returned ${err.status}: ${err.error}`
4593
- }
4594
- };
2597
+ if (err instanceof ClientResponseError) {
2598
+ if (err.error === "ListingUnavailable") return {
2599
+ success: false,
2600
+ error: {
2601
+ code: "LISTING_UNAVAILABLE",
2602
+ message: "This plugin is unavailable under the active registry policy"
2603
+ }
2604
+ };
2605
+ return {
2606
+ success: false,
2607
+ error: {
2608
+ code: err.status === 404 ? "AGGREGATOR_NOT_FOUND" : "AGGREGATOR_HTTP_ERROR",
2609
+ message: `Aggregator returned ${err.status}: ${err.error}`
2610
+ }
2611
+ };
2612
+ }
4595
2613
  if (err instanceof EmDashStorageError) return {
4596
2614
  success: false,
4597
2615
  error: {
@@ -4631,21 +2649,24 @@ async function handleRegistryUpdateCheck(db, registryConfigInput) {
4631
2649
  success: true,
4632
2650
  data: { items: [] }
4633
2651
  };
4634
- const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
2652
+ const { DiscoveryClient, registryLabelerPolicy } = await import("@emdash-cms/registry-client/discovery");
4635
2653
  const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
4636
2654
  const discovery = new DiscoveryClient({
4637
2655
  aggregatorUrl: registryConfig.aggregatorUrl,
4638
2656
  acceptLabelers: registryConfig.acceptLabelers,
2657
+ labelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),
4639
2658
  fetch: timedFetch(aggregatorDeadline)
4640
2659
  });
4641
2660
  const items = [];
4642
2661
  for (const plugin of registryPlugins) {
4643
2662
  if (!plugin.registryPublisherDid || !plugin.registrySlug) continue;
4644
2663
  try {
4645
- const latest = (await discovery.getLatestRelease({
2664
+ const releaseView = await discovery.getLatestRelease({
4646
2665
  did: plugin.registryPublisherDid,
4647
2666
  package: plugin.registrySlug
4648
- })).version;
2667
+ });
2668
+ if (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) continue;
2669
+ const latest = releaseView.version;
4649
2670
  if (!latest) continue;
4650
2671
  const installed = plugin.version;
4651
2672
  items.push({
@@ -4691,5 +2712,5 @@ async function handleRegistryUpdateCheck(db, registryConfigInput) {
4691
2712
  }
4692
2713
 
4693
2714
  //#endregion
4694
- export { handleContentUnschedule as $, handleMediaUpdate as A, handleContentDelete as B, handlePluginEnable as C, handleMediaDelete as D, handleMediaCreate as E, handleContentAuthors as F, handleContentList as G, handleContentDuplicate as H, handleContentCompare as I, handleContentPublish as J, handleContentListTrashed as K, handleContentCountScheduled as L, handleRevisionList as M, handleRevisionRestore as N, handleMediaGet as O, generateManifest as P, handleContentUnpublish as Q, handleContentCountTrashed as R, handlePluginDisable as S, handlePluginList as T, handleContentGet as U, handleContentDiscardDraft as V, handleContentGetIncludingTrashed as W, handleContentSchedule as X, handleContentRestore as Y, handleContentTranslations as Z, loadBundleFromR2 as _, handleRegistryUpdateCheck as a, handlePluginSettingsGet as b, validateAggregatorUrl as c, handleMarketplaceSearch as d, handleContentUpdate as et, handleMarketplaceUninstall as f, handleThemeSearch as g, handleThemeGetDetail as h, handleRegistryUpdate as i, handleRevisionGet as j, handleMediaList as k, handleMarketplaceGetPlugin as l, handleMarketplaceUpdateCheck as m, handleRegistryInstall as n, coerceRegistryConfig as o, handleMarketplaceUpdate as p, handleContentPermanentDelete as q, handleRegistryUninstall as r, normalizeRegistryConfig as s, assertSafeArtifactUrl as t, validateRev as tt, handleMarketplaceInstall as u, syncDeclaredStorageIndexes as v, handlePluginGet as w, handlePluginSettingsUpdate as x, getPluginSettingsSchema as y, handleContentCreate as z };
4695
- //# sourceMappingURL=api-BeBglVxU.mjs.map
2715
+ export { handleThemeSearch as _, verifyChecksum as a, validateAggregatorUrl as c, handleMarketplaceInstall as d, handleMarketplaceSearch as f, handleThemeGetDetail as g, handleMarketplaceUpdateCheck as h, handleRegistryUpdateCheck as i, fetchRegistryArtifactUrl as l, handleMarketplaceUpdate as m, handleRegistryUninstall as n, coerceRegistryConfig as o, handleMarketplaceUninstall as p, handleRegistryUpdate as r, normalizeRegistryConfig as s, handleRegistryInstall as t, handleMarketplaceGetPlugin as u, loadBundleFromR2 as v, syncDeclaredStorageIndexes as y };
2716
+ //# sourceMappingURL=registry-BiO5WwPq.mjs.map