emdash 0.15.0 → 0.16.1

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 (351) hide show
  1. package/dist/api/route-utils.mjs +10 -10
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-CLwG_3dh.mjs → api-BNKqxyFX.mjs} +54 -14
  4. package/dist/{api-CLwG_3dh.mjs.map → api-BNKqxyFX.mjs.map} +1 -1
  5. package/dist/{apply-wJhM_bwU.mjs → apply-BOPaD-s9.mjs} +16 -16
  6. package/dist/{apply-wJhM_bwU.mjs.map → apply-BOPaD-s9.mjs.map} +1 -1
  7. package/dist/astro/index.d.mts +3 -3
  8. package/dist/astro/index.d.mts.map +1 -1
  9. package/dist/astro/index.mjs +33 -1
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/middleware/auth.d.mts +3 -3
  12. package/dist/astro/middleware/auth.mjs +2 -2
  13. package/dist/astro/middleware/redirect.mjs +4 -4
  14. package/dist/astro/middleware/request-context.mjs +1 -1
  15. package/dist/astro/middleware.d.mts.map +1 -1
  16. package/dist/astro/middleware.mjs +66 -46
  17. package/dist/astro/middleware.mjs.map +1 -1
  18. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  19. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  20. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  21. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  22. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +8 -8
  23. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
  24. package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
  25. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
  26. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  27. package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
  28. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  29. package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
  30. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  31. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  32. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  33. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  34. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +27 -27
  35. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +27 -27
  36. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +27 -27
  37. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +27 -27
  38. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +27 -27
  39. package/dist/astro/routes/api/admin/plugins/index.mjs +27 -27
  40. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  41. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +27 -27
  42. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +27 -27
  43. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +27 -27
  44. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +27 -27
  45. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  46. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +41 -28
  47. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  48. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts +8 -0
  49. package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -0
  50. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +301 -0
  51. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -0
  52. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  53. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +46 -28
  54. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/plugins/updates.mjs +27 -27
  56. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +27 -27
  57. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  58. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +27 -27
  59. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  60. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  61. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  62. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  63. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  64. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  65. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  66. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  67. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  68. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  69. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  70. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  71. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  72. package/dist/astro/routes/api/auth/me.mjs +3 -3
  73. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  74. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  75. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  76. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  77. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  78. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  79. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  80. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  81. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  82. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  83. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  84. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  86. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  87. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  88. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  89. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  90. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  91. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  92. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
  93. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  94. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  95. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  96. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  97. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  98. package/dist/astro/routes/api/dashboard.mjs +7 -7
  99. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  100. package/dist/astro/routes/api/import/probe.mjs +4 -4
  101. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  102. package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
  103. package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
  104. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  105. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  106. package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts +11 -1
  107. package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.d.mts.map +1 -1
  108. package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs +17 -1
  109. package/dist/astro/routes/api/import/wordpress/rewrite-url-helpers.mjs.map +1 -1
  110. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
  111. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
  112. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  113. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  114. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
  115. package/dist/astro/routes/api/manifest.mjs +3 -3
  116. package/dist/astro/routes/api/mcp.mjs +26 -26
  117. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  118. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  119. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  120. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  121. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  122. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  123. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  124. package/dist/astro/routes/api/media.mjs +5 -5
  125. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  126. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  127. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  128. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  129. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  130. package/dist/astro/routes/api/menus/index.mjs +5 -5
  131. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  132. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  133. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  134. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  135. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  136. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  137. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  138. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  139. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  140. package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
  141. package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
  142. package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
  143. package/dist/astro/routes/api/redirects/index.mjs +7 -7
  144. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  145. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  146. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +27 -27
  147. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +27 -27
  148. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +27 -27
  149. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +27 -27
  150. package/dist/astro/routes/api/schema/collections/index.mjs +27 -27
  151. package/dist/astro/routes/api/schema/index.mjs +6 -6
  152. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +27 -27
  153. package/dist/astro/routes/api/schema/orphans/index.mjs +27 -27
  154. package/dist/astro/routes/api/search/enable.mjs +7 -7
  155. package/dist/astro/routes/api/search/index.mjs +6 -6
  156. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  157. package/dist/astro/routes/api/search/stats.mjs +6 -6
  158. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  159. package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
  160. package/dist/astro/routes/api/sections/index.mjs +6 -6
  161. package/dist/astro/routes/api/settings/email.mjs +4 -4
  162. package/dist/astro/routes/api/settings.mjs +8 -8
  163. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  164. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  165. package/dist/astro/routes/api/setup/dev-bypass.mjs +15 -15
  166. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  167. package/dist/astro/routes/api/setup/index.mjs +16 -16
  168. package/dist/astro/routes/api/setup/status.mjs +3 -3
  169. package/dist/astro/routes/api/snapshot.mjs +4 -4
  170. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  171. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
  172. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
  173. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
  174. package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
  175. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  176. package/dist/astro/routes/api/typegen.mjs +5 -5
  177. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  178. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
  179. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
  180. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  181. package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
  182. package/dist/astro/routes/api/widget-components.mjs +2 -2
  183. package/dist/astro/routes/robots.txt.mjs +4 -4
  184. package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
  185. package/dist/astro/routes/sitemap-_collection_.xml.mjs +58 -13
  186. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  187. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  188. package/dist/astro/types.d.mts +10 -3
  189. package/dist/astro/types.d.mts.map +1 -1
  190. package/dist/{authorize-Bkwe8kuL.mjs → authorize-Bn4S4DUT.mjs} +2 -2
  191. package/dist/{authorize-Bkwe8kuL.mjs.map → authorize-Bn4S4DUT.mjs.map} +1 -1
  192. package/dist/{byline-CTaWkMh5.mjs → byline-BDylH_m4.mjs} +3 -3
  193. package/dist/{byline-CTaWkMh5.mjs.map → byline-BDylH_m4.mjs.map} +1 -1
  194. package/dist/{bylines-H0Xh5TMy.mjs → bylines-B7TFEvFf.mjs} +2 -2
  195. package/dist/{bylines-H0Xh5TMy.mjs.map → bylines-B7TFEvFf.mjs.map} +1 -1
  196. package/dist/{bylines-DtDRNF1n.d.mts → bylines-DWLnr6-k.d.mts} +17 -17
  197. package/dist/{bylines-DtDRNF1n.d.mts.map → bylines-DWLnr6-k.d.mts.map} +1 -1
  198. package/dist/{bylines-BYHWU3T7.mjs → bylines-n6nykUyI.mjs} +6 -6
  199. package/dist/{bylines-BYHWU3T7.mjs.map → bylines-n6nykUyI.mjs.map} +1 -1
  200. package/dist/{cache-CNk1jIxp.mjs → cache-BcI1yUjR.mjs} +2 -2
  201. package/dist/{cache-CNk1jIxp.mjs.map → cache-BcI1yUjR.mjs.map} +1 -1
  202. package/dist/{chunks-BkfVdD-3.mjs → chunks-cYG4SnIP.mjs} +2 -2
  203. package/dist/{chunks-BkfVdD-3.mjs.map → chunks-cYG4SnIP.mjs.map} +1 -1
  204. package/dist/cli/index.mjs +61 -15
  205. package/dist/cli/index.mjs.map +1 -1
  206. package/dist/client/cf-access.d.mts +1 -1
  207. package/dist/client/index.d.mts +1 -1
  208. package/dist/{comment-_yzlBYPx.mjs → comment-C76G-9tz.mjs} +2 -2
  209. package/dist/{comment-_yzlBYPx.mjs.map → comment-C76G-9tz.mjs.map} +1 -1
  210. package/dist/{comments-DxID-rsd.mjs → comments-CCxFFGY1.mjs} +3 -3
  211. package/dist/{comments-DxID-rsd.mjs.map → comments-CCxFFGY1.mjs.map} +1 -1
  212. package/dist/{content-C0ooIs-f.mjs → content-8voQNTXX.mjs} +3 -3
  213. package/dist/{content-C0ooIs-f.mjs.map → content-8voQNTXX.mjs.map} +1 -1
  214. package/dist/{context-sAnCaUIR.mjs → context-B7qiYrz2.mjs} +7 -7
  215. package/dist/{context-sAnCaUIR.mjs.map → context-B7qiYrz2.mjs.map} +1 -1
  216. package/dist/{dashboard-Cqw3ay2X.mjs → dashboard-BeaFSPpx.mjs} +4 -4
  217. package/dist/{dashboard-Cqw3ay2X.mjs.map → dashboard-BeaFSPpx.mjs.map} +1 -1
  218. package/dist/db/index.d.mts +1 -1
  219. package/dist/db/index.mjs +1 -1
  220. package/dist/db/sqlite.mjs +1 -1
  221. package/dist/{db-errors-CGN9kJfo.mjs → db-errors-BiYqoX-n.mjs} +14 -2
  222. package/dist/db-errors-BiYqoX-n.mjs.map +1 -0
  223. package/dist/{error-CPh_8eLq.mjs → error-ChfADBuu.mjs} +5 -3
  224. package/dist/error-ChfADBuu.mjs.map +1 -0
  225. package/dist/errors-9P_FDrJ_.mjs +17 -0
  226. package/dist/errors-9P_FDrJ_.mjs.map +1 -0
  227. package/dist/{fts-manager-Mnrtn-r2.mjs → fts-manager-C_b-4x8u.mjs} +2 -2
  228. package/dist/{fts-manager-Mnrtn-r2.mjs.map → fts-manager-C_b-4x8u.mjs.map} +1 -1
  229. package/dist/{index-Bv1Wf1zB.d.mts → index-D_p_jIP1.d.mts} +153 -109
  230. package/dist/index-D_p_jIP1.d.mts.map +1 -0
  231. package/dist/index.d.mts +4 -4
  232. package/dist/index.mjs +38 -38
  233. package/dist/{load-DmXNVhst.mjs → load-CLFRjk9r.mjs} +2 -2
  234. package/dist/{load-DmXNVhst.mjs.map → load-CLFRjk9r.mjs.map} +1 -1
  235. package/dist/{loader-Chm5h7Gr.mjs → loader-D-vIJjfY.mjs} +86 -46
  236. package/dist/loader-D-vIJjfY.mjs.map +1 -0
  237. package/dist/media/local-runtime.d.mts +3 -3
  238. package/dist/media/local-runtime.mjs +4 -4
  239. package/dist/{media-oqRcNiQf.mjs → media-CKQd8AYU.mjs} +2 -2
  240. package/dist/{media-oqRcNiQf.mjs.map → media-CKQd8AYU.mjs.map} +1 -1
  241. package/dist/{menus-C75SSmRy.mjs → menus-C-nWT5Tu.mjs} +17 -11
  242. package/dist/menus-C-nWT5Tu.mjs.map +1 -0
  243. package/dist/{menus-Bjf5R1Qq.mjs → menus-arUNspyU.mjs} +2 -2
  244. package/dist/{menus-Bjf5R1Qq.mjs.map → menus-arUNspyU.mjs.map} +1 -1
  245. package/dist/{parse-3-caTKgt.mjs → parse-DHbXfvxO.mjs} +2 -2
  246. package/dist/{parse-3-caTKgt.mjs.map → parse-DHbXfvxO.mjs.map} +1 -1
  247. package/dist/plugin-utils.d.mts +25 -10
  248. package/dist/plugin-utils.d.mts.map +1 -1
  249. package/dist/plugin-utils.mjs +11 -10
  250. package/dist/plugin-utils.mjs.map +1 -1
  251. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  252. package/dist/{query-BJn8TOPk.mjs → query-7m6-l0f_.mjs} +21 -14
  253. package/dist/query-7m6-l0f_.mjs.map +1 -0
  254. package/dist/{rate-limit-D_-gAeJ0.mjs → rate-limit-D8RAXN8b.mjs} +2 -2
  255. package/dist/{rate-limit-D_-gAeJ0.mjs.map → rate-limit-D8RAXN8b.mjs.map} +1 -1
  256. package/dist/{redirect-CNv4mHX2.mjs → redirect-CjfDGrTd.mjs} +2 -2
  257. package/dist/{redirect-CNv4mHX2.mjs.map → redirect-CjfDGrTd.mjs.map} +1 -1
  258. package/dist/{redirects-B-CUZ1Xh.mjs → redirects-CowoEHdE.mjs} +3 -3
  259. package/dist/{redirects-B-CUZ1Xh.mjs.map → redirects-CowoEHdE.mjs.map} +1 -1
  260. package/dist/{registry-DqrAQDXH.mjs → registry-Cyp-dx6J.mjs} +4 -4
  261. package/dist/{registry-DqrAQDXH.mjs.map → registry-Cyp-dx6J.mjs.map} +1 -1
  262. package/dist/resolve-D6sM-SgF.mjs +143 -0
  263. package/dist/resolve-D6sM-SgF.mjs.map +1 -0
  264. package/dist/{runner-CNHRo1mT.d.mts → runner-DSQBurMS.d.mts} +7 -4
  265. package/dist/runner-DSQBurMS.d.mts.map +1 -0
  266. package/dist/{runner-CGlojznK.mjs → runner-Drnvs96u.mjs} +20 -24
  267. package/dist/{runner-CGlojznK.mjs.map → runner-Drnvs96u.mjs.map} +1 -1
  268. package/dist/runtime.d.mts +3 -3
  269. package/dist/runtime.mjs +2 -2
  270. package/dist/{schema-Djdlfi5G.mjs → schema-CI9mYPX3.mjs} +4 -4
  271. package/dist/{schema-Djdlfi5G.mjs.map → schema-CI9mYPX3.mjs.map} +1 -1
  272. package/dist/{search-By-NN3da.mjs → search-DKz_mGBP.mjs} +4 -4
  273. package/dist/{search-By-NN3da.mjs.map → search-DKz_mGBP.mjs.map} +1 -1
  274. package/dist/{sections-DcBIlOq1.mjs → sections-DBbCDIAT.mjs} +3 -3
  275. package/dist/{sections-DcBIlOq1.mjs.map → sections-DBbCDIAT.mjs.map} +1 -1
  276. package/dist/seed/index.mjs +13 -13
  277. package/dist/{seo-bjDoq9Eg.mjs → seo-BGCyDlkb.mjs} +2 -2
  278. package/dist/{seo-bjDoq9Eg.mjs.map → seo-BGCyDlkb.mjs.map} +1 -1
  279. package/dist/{seo-BoR4wCUh.mjs → seo-Dq707mNQ.mjs} +5 -3
  280. package/dist/seo-Dq707mNQ.mjs.map +1 -0
  281. package/dist/{service-BuuTdGAT.mjs → service-B0H7U1Y9.mjs} +2 -2
  282. package/dist/{service-BuuTdGAT.mjs.map → service-B0H7U1Y9.mjs.map} +1 -1
  283. package/dist/{settings-hcubRfkr.mjs → settings-BSXRtTzk.mjs} +3 -3
  284. package/dist/{settings-hcubRfkr.mjs.map → settings-BSXRtTzk.mjs.map} +1 -1
  285. package/dist/{settings-CJnKiWuR.mjs → settings-DfwNyQkf.mjs} +3 -3
  286. package/dist/{settings-CJnKiWuR.mjs.map → settings-DfwNyQkf.mjs.map} +1 -1
  287. package/dist/{taxonomies-CLs9HPE2.mjs → taxonomies-4vx0nmMr.mjs} +4 -4
  288. package/dist/{taxonomies-CLs9HPE2.mjs.map → taxonomies-4vx0nmMr.mjs.map} +1 -1
  289. package/dist/{taxonomies-WamPVA2x.mjs → taxonomies-CcvrMLbR.mjs} +7 -7
  290. package/dist/{taxonomies-WamPVA2x.mjs.map → taxonomies-CcvrMLbR.mjs.map} +1 -1
  291. package/dist/{taxonomy-D4Uc2LsZ.mjs → taxonomy-zqGQUqgu.mjs} +3 -3
  292. package/dist/{taxonomy-D4Uc2LsZ.mjs.map → taxonomy-zqGQUqgu.mjs.map} +1 -1
  293. package/dist/{transport-DOxLfUir.d.mts → transport-C2MGqtL6.d.mts} +1 -1
  294. package/dist/{transport-DOxLfUir.d.mts.map → transport-C2MGqtL6.d.mts.map} +1 -1
  295. package/dist/{types-ByV5sgsv.mjs → types-B0bmgwMG.mjs} +2 -2
  296. package/dist/{types-ByV5sgsv.mjs.map → types-B0bmgwMG.mjs.map} +1 -1
  297. package/dist/{user-D3BD5zdT.mjs → user-hUSOaIJy.mjs} +2 -2
  298. package/dist/{user-D3BD5zdT.mjs.map → user-hUSOaIJy.mjs.map} +1 -1
  299. package/dist/{validate-mz87i8_1.mjs → validate-IGltez8n.mjs} +2 -2
  300. package/dist/{validate-mz87i8_1.mjs.map → validate-IGltez8n.mjs.map} +1 -1
  301. package/dist/{validation-DKHhXjPr.mjs → validation-Bmymau7y.mjs} +6 -6
  302. package/dist/{validation-DKHhXjPr.mjs.map → validation-Bmymau7y.mjs.map} +1 -1
  303. package/dist/version-ITD3PlQd.mjs +7 -0
  304. package/dist/{version-Ct7C6RSo.mjs.map → version-ITD3PlQd.mjs.map} +1 -1
  305. package/dist/{widgets-lShIQXU5.mjs → widgets-yHQa4c6c.mjs} +2 -2
  306. package/dist/{widgets-lShIQXU5.mjs.map → widgets-yHQa4c6c.mjs.map} +1 -1
  307. package/dist/{zod-generator-dvxgmd1M.mjs → zod-generator-B80aap1J.mjs} +2 -2
  308. package/dist/{zod-generator-dvxgmd1M.mjs.map → zod-generator-B80aap1J.mjs.map} +1 -1
  309. package/package.json +7 -7
  310. package/src/api/errors.ts +2 -0
  311. package/src/api/handlers/index.ts +2 -0
  312. package/src/api/handlers/registry.ts +69 -1
  313. package/src/api/handlers/seo.ts +16 -1
  314. package/src/api/handlers/snapshot.ts +1 -1
  315. package/src/astro/integration/index.ts +26 -0
  316. package/src/astro/integration/routes.ts +5 -0
  317. package/src/astro/integration/runtime.ts +8 -0
  318. package/src/astro/middleware.ts +4 -0
  319. package/src/astro/public-plugin-api-routes.ts +41 -0
  320. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  321. package/src/astro/routes/api/admin/plugins/registry/artifact.ts +388 -0
  322. package/src/astro/routes/api/admin/plugins/registry/install.ts +7 -1
  323. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +22 -0
  324. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +5 -2
  325. package/src/astro/routes/sitemap-[collection].xml.ts +114 -14
  326. package/src/astro/types.ts +14 -0
  327. package/src/content/converters/portable-text-to-prosemirror.ts +35 -11
  328. package/src/database/connection.ts +3 -10
  329. package/src/database/errors.ts +14 -0
  330. package/src/database/index.ts +3 -1
  331. package/src/database/migrations/runner.ts +29 -21
  332. package/src/emdash-runtime.ts +1 -0
  333. package/src/i18n/resolve.ts +152 -0
  334. package/src/index.ts +2 -0
  335. package/src/loader.ts +133 -59
  336. package/src/plugin-utils.ts +23 -0
  337. package/src/query.ts +24 -5
  338. package/src/utils/db-errors.ts +24 -0
  339. package/dist/connection-2igzM-AT.mjs +0 -57
  340. package/dist/connection-2igzM-AT.mjs.map +0 -1
  341. package/dist/db-errors-CGN9kJfo.mjs.map +0 -1
  342. package/dist/error-CPh_8eLq.mjs.map +0 -1
  343. package/dist/index-Bv1Wf1zB.d.mts.map +0 -1
  344. package/dist/loader-Chm5h7Gr.mjs.map +0 -1
  345. package/dist/menus-C75SSmRy.mjs.map +0 -1
  346. package/dist/query-BJn8TOPk.mjs.map +0 -1
  347. package/dist/resolve-Cj98DuqN.mjs +0 -39
  348. package/dist/resolve-Cj98DuqN.mjs.map +0 -1
  349. package/dist/runner-CNHRo1mT.d.mts.map +0 -1
  350. package/dist/seo-BoR4wCUh.mjs.map +0 -1
  351. package/dist/version-Ct7C6RSo.mjs +0 -7
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Registry artifact proxy
3
+ *
4
+ * GET /_emdash/api/admin/plugins/registry/artifact?did=&slug=&version=&kind=&index=
5
+ *
6
+ * Proxies an icon / screenshot / banner image referenced by a registry
7
+ * release record so the admin UI can display it without cross-origin
8
+ * requests to arbitrary publisher hosting.
9
+ *
10
+ * Trust model (CRITICAL): the proxy never accepts an artifact URL from the
11
+ * client. The caller addresses an artifact by its coordinates
12
+ * `(did, slug, version, kind, index)`; the server resolves the *declared*
13
+ * URL from the validated release record fetched from the configured
14
+ * aggregator. The proxy can therefore only ever fetch a URL the publisher
15
+ * declared in their signed release — not an arbitrary caller-supplied URL.
16
+ *
17
+ * The publisher-declared URL is still untrusted (an attacker who controls a
18
+ * publisher record, or the aggregator, can point it anywhere), so the
19
+ * resolved URL passes through the SSRF defences (`assertSafeArtifactUrl`,
20
+ * re-validated on every redirect hop) before any fetch, and only allowlisted
21
+ * image content types are served back.
22
+ */
23
+
24
+ import type { Did } from "@atcute/lexicons";
25
+ import type { APIRoute } from "astro";
26
+
27
+ import { requirePerm } from "#api/authorize.js";
28
+ import { apiError } from "#api/error.js";
29
+ import { assertSafeArtifactUrl } from "#api/index.js";
30
+
31
+ import { coerceRegistryConfig, validateAggregatorUrl } from "../../../../../../registry/config.js";
32
+
33
+ export const prerender = false;
34
+
35
+ /**
36
+ * Image content types the proxy will pass through. Anything else is rejected.
37
+ *
38
+ * SVG is deliberately excluded: it is active content (an `<svg><script>`
39
+ * executes when navigated to as a top-level document), and the publisher
40
+ * supplies the bytes. Rather than serve it behind mitigations, we refuse it
41
+ * end-to-end — the publish CLI rejects SVG artifacts too, so a conforming
42
+ * release never references one. AVIF is included.
43
+ */
44
+ const ALLOWED_IMAGE_TYPES = new Set([
45
+ "image/png",
46
+ "image/jpeg",
47
+ "image/webp",
48
+ "image/gif",
49
+ "image/avif",
50
+ ]);
51
+
52
+ /** Artifact kinds the proxy can resolve. `screenshot` additionally needs `index`. */
53
+ const ALLOWED_KINDS = new Set(["icon", "banner", "screenshot"]);
54
+
55
+ /** Loose DID shape (`did:method:id`); the aggregator lexicon is authoritative. */
56
+ const DID_PATTERN = /^did:[a-z]+:.+/;
57
+ /** Slug grammar: ASCII letter then letters / digits / `-` / `_`. Mirrors the install route. */
58
+ const SLUG_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
59
+ /** Non-negative integer, for the screenshot index param. */
60
+ const INDEX_PATTERN = /^\d+$/;
61
+
62
+ /** Cap proxied images so a hostile host can't stream an unbounded body. */
63
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
64
+
65
+ /** Redirect hops to follow, re-validating each target against SSRF rules. */
66
+ const MAX_REDIRECTS = 5;
67
+
68
+ /** Wall-clock budget covering connect + headers + body for the artifact fetch. */
69
+ const FETCH_TIMEOUT_MS = 15_000;
70
+
71
+ /** Per-aggregator-request timeout and overall budget for release resolution. */
72
+ const AGGREGATOR_REQUEST_TIMEOUT_MS = 15_000;
73
+ const AGGREGATOR_TOTAL_BUDGET_MS = 30_000;
74
+
75
+ /** Bound the version search: 20 pages * 50 per page = 1000 releases worth. */
76
+ const MAX_LIST_PAGES = 20;
77
+
78
+ /** Build a fetch that enforces a per-request and per-budget timeout. Mirrors the install handler. */
79
+ function timedFetch(totalDeadline: number): typeof fetch {
80
+ return (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
81
+ const now = Date.now();
82
+ const remaining = Math.max(0, totalDeadline - now);
83
+ if (remaining === 0) {
84
+ return Promise.reject(new Error("Aggregator request budget exhausted"));
85
+ }
86
+ const timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), timeout);
89
+ const callerSignal = init?.signal;
90
+ if (callerSignal) {
91
+ if (callerSignal.aborted) controller.abort(callerSignal.reason);
92
+ else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason));
93
+ }
94
+ return fetch(input, { ...init, signal: controller.signal }).finally(() => {
95
+ clearTimeout(timer);
96
+ });
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Narrow one entry of a release's `artifacts` map to a usable image URL.
102
+ *
103
+ * The embedded `release` record is lexicon-validated at the DiscoveryClient
104
+ * boundary, but `artifacts` is an aggregator pass-through typed `unknown`, so
105
+ * the entry's shape is not guaranteed. Returns the `url` string only when the
106
+ * value is an object carrying a non-empty string `url`; everything else
107
+ * (missing key, wrong type, no `url`) yields `null`.
108
+ */
109
+ function declaredArtifactUrl(value: unknown): string | null {
110
+ if (!value || typeof value !== "object") return null;
111
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed to non-null object above; url checked below
112
+ const entry = value as Record<string, unknown>;
113
+ const url = entry.url;
114
+ if (typeof url !== "string" || url.length === 0) return null;
115
+ return url;
116
+ }
117
+
118
+ /**
119
+ * Resolve the declared artifact URL for `(kind, index)` from a release's
120
+ * `artifacts` map. Returns `null` when the requested artifact isn't present
121
+ * or doesn't carry a usable URL.
122
+ */
123
+ function resolveDeclaredUrl(artifacts: unknown, kind: string, index: number): string | null {
124
+ if (!artifacts || typeof artifacts !== "object") return null;
125
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed to non-null object above; each entry shape-narrowed by declaredArtifactUrl
126
+ const map = artifacts as Record<string, unknown>;
127
+
128
+ if (kind === "icon") return declaredArtifactUrl(map.icon);
129
+ if (kind === "banner") return declaredArtifactUrl(map.banner);
130
+ // kind === "screenshot"
131
+ const screenshots = map.screenshots;
132
+ if (!Array.isArray(screenshots)) return null;
133
+ if (index < 0 || index >= screenshots.length) return null;
134
+ return declaredArtifactUrl(screenshots[index]);
135
+ }
136
+
137
+ export const GET: APIRoute = async ({ url, locals }) => {
138
+ const { emdash, user } = locals;
139
+
140
+ if (!emdash?.db) {
141
+ return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
142
+ }
143
+
144
+ const denied = requirePerm(user, "plugins:read");
145
+ if (denied) return denied;
146
+
147
+ const did = url.searchParams.get("did");
148
+ const slug = url.searchParams.get("slug");
149
+ const kind = url.searchParams.get("kind");
150
+ const versionParam = url.searchParams.get("version");
151
+ const indexParam = url.searchParams.get("index");
152
+
153
+ if (!did || !slug || !kind) {
154
+ return apiError("INVALID_REQUEST", "Missing did, slug, or kind", 400);
155
+ }
156
+ if (did.length > 256 || !DID_PATTERN.test(did)) {
157
+ return apiError("INVALID_REQUEST", "Invalid did", 400);
158
+ }
159
+ if (slug.length > 64 || !SLUG_PATTERN.test(slug)) {
160
+ return apiError("INVALID_REQUEST", "Invalid slug", 400);
161
+ }
162
+ if (!ALLOWED_KINDS.has(kind)) {
163
+ return apiError("INVALID_REQUEST", "Invalid kind", 400);
164
+ }
165
+
166
+ let index = 0;
167
+ if (kind === "screenshot") {
168
+ if (indexParam === null) {
169
+ return apiError("INVALID_REQUEST", "Missing index for screenshot", 400);
170
+ }
171
+ if (!INDEX_PATTERN.test(indexParam)) {
172
+ return apiError("INVALID_REQUEST", "Invalid index", 400);
173
+ }
174
+ index = Number(indexParam);
175
+ if (!Number.isSafeInteger(index)) {
176
+ return apiError("INVALID_REQUEST", "Invalid index", 400);
177
+ }
178
+ }
179
+
180
+ let version: string | undefined;
181
+ if (versionParam !== null && versionParam.length > 0) {
182
+ if (versionParam.length > 64) {
183
+ return apiError("INVALID_REQUEST", "Invalid version", 400);
184
+ }
185
+ version = versionParam;
186
+ }
187
+
188
+ const registryConfig = coerceRegistryConfig(emdash.config.experimental?.registry);
189
+ if (!registryConfig) {
190
+ return apiError("REGISTRY_NOT_CONFIGURED", "Registry is not configured", 400);
191
+ }
192
+ try {
193
+ validateAggregatorUrl(registryConfig.aggregatorUrl);
194
+ } catch {
195
+ return apiError("REGISTRY_NOT_CONFIGURED", "Registry aggregator URL is invalid", 500);
196
+ }
197
+
198
+ // Resolve the publisher-declared artifact URL from the release record.
199
+ let declaredUrl: string;
200
+ try {
201
+ const resolved = await resolveArtifactUrl(registryConfig, did, slug, version, kind, index);
202
+ if (resolved === null) {
203
+ return apiError("ARTIFACT_NOT_FOUND", "Artifact not found", 404);
204
+ }
205
+ declaredUrl = resolved;
206
+ } catch {
207
+ return apiError("ARTIFACT_RESOLVE_FAILED", "Failed to resolve artifact", 502);
208
+ }
209
+
210
+ const controller = new AbortController();
211
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
212
+ try {
213
+ // `assertSafeArtifactUrl` validates scheme / credentials / loopback +
214
+ // resolves the hostname and rejects private / link-local / metadata
215
+ // targets (DNS-rebinding defence). It throws a plain Error on any
216
+ // block, so a rejection here means the URL is unsafe.
217
+ let current: URL;
218
+ try {
219
+ current = await assertSafeArtifactUrl(declaredUrl);
220
+ } catch {
221
+ return apiError("ARTIFACT_URL_REJECTED", "Artifact URL is not allowed", 400);
222
+ }
223
+
224
+ let response: Response;
225
+ for (let hop = 0; ; hop++) {
226
+ response = await fetch(current.href, { redirect: "manual", signal: controller.signal });
227
+ if (response.status < 300 || response.status >= 400) break;
228
+ const location = response.headers.get("location");
229
+ if (!location) break;
230
+ if (hop === MAX_REDIRECTS) {
231
+ return apiError("ARTIFACT_URL_REJECTED", "Too many redirects", 502);
232
+ }
233
+ let next: URL;
234
+ try {
235
+ next = await assertSafeArtifactUrl(new URL(location, current).href);
236
+ } catch {
237
+ return apiError("ARTIFACT_URL_REJECTED", "Redirect target is not allowed", 400);
238
+ }
239
+ current = next;
240
+ }
241
+
242
+ if (!response.ok) {
243
+ return apiError("ARTIFACT_FETCH_FAILED", "Failed to fetch artifact", 502);
244
+ }
245
+
246
+ // Content-Type allowlist: only image types are proxied. A non-image
247
+ // (HTML error page, JSON, octet-stream) is rejected so the admin
248
+ // never renders publisher-controlled markup from the EmDash origin.
249
+ const rawType = response.headers.get("content-type") ?? "";
250
+ const contentType = rawType.split(";", 1)[0]!.trim().toLowerCase();
251
+ if (!ALLOWED_IMAGE_TYPES.has(contentType)) {
252
+ return apiError("ARTIFACT_NOT_IMAGE", "Artifact is not an allowed image type", 415);
253
+ }
254
+
255
+ const declaredLength = response.headers.get("content-length");
256
+ if (declaredLength) {
257
+ const declared = Number(declaredLength);
258
+ if (Number.isFinite(declared) && declared > MAX_IMAGE_BYTES) {
259
+ return apiError("ARTIFACT_TOO_LARGE", "Artifact exceeds size limit", 413);
260
+ }
261
+ }
262
+
263
+ const bytes = await readCapped(response, MAX_IMAGE_BYTES);
264
+ if (bytes === null) {
265
+ return apiError("ARTIFACT_TOO_LARGE", "Artifact exceeds size limit", 413);
266
+ }
267
+
268
+ // Only the allowlisted Content-Type is forwarded — never copy other
269
+ // upstream headers. `private, no-store` keeps publisher images out of
270
+ // shared caches in the authenticated admin origin.
271
+ //
272
+ // SVG is not in the allowlist, so active-content bytes never reach
273
+ // here. `Content-Disposition: attachment`, the sandbox CSP, and
274
+ // `nosniff` remain as defence-in-depth: they force a download and
275
+ // neutralise script/plugins for any image type if a client navigates
276
+ // directly to the proxy URL.
277
+ return new Response(bytes, {
278
+ headers: {
279
+ "Content-Type": contentType,
280
+ "Cache-Control": "private, no-store",
281
+ "X-Content-Type-Options": "nosniff",
282
+ "Content-Disposition": "attachment",
283
+ "Content-Security-Policy": "default-src 'none'; sandbox",
284
+ },
285
+ });
286
+ } catch {
287
+ return apiError("ARTIFACT_FETCH_FAILED", "Failed to fetch artifact", 502);
288
+ } finally {
289
+ clearTimeout(timer);
290
+ }
291
+ };
292
+
293
+ /**
294
+ * Resolve the declared artifact URL for `(did, slug, version, kind, index)`
295
+ * from the aggregator's release record. Mirrors the install handler's release
296
+ * lookup. Returns `null` when the package/release/artifact isn't found.
297
+ *
298
+ * Self-contained to this route: the install/update handlers are intentionally
299
+ * left untouched, so a small amount of resolution-pattern duplication is
300
+ * accepted here.
301
+ */
302
+ async function resolveArtifactUrl(
303
+ registryConfig: { aggregatorUrl: string; acceptLabelers?: string },
304
+ did: string,
305
+ slug: string,
306
+ version: string | undefined,
307
+ kind: string,
308
+ index: number,
309
+ ): Promise<string | null> {
310
+ // Lazy-load the discovery client so the `@atcute/client` dependency only
311
+ // loads when the registry path is exercised.
312
+ const { DiscoveryClient } = await import("@emdash-cms/registry-client/discovery");
313
+
314
+ const aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;
315
+ const discovery = new DiscoveryClient({
316
+ aggregatorUrl: registryConfig.aggregatorUrl,
317
+ acceptLabelers: registryConfig.acceptLabelers,
318
+ fetch: timedFetch(aggregatorDeadline),
319
+ });
320
+
321
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DID shape validated by the route before this call
322
+ const publisherDid = did as Did;
323
+
324
+ const releaseView = await (async () => {
325
+ if (!version) {
326
+ return discovery.getLatestRelease({ did: publisherDid, package: slug });
327
+ }
328
+ let cursor: string | undefined;
329
+ const seenCursors = new Set<string>();
330
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
331
+ if (cursor !== undefined) {
332
+ if (seenCursors.has(cursor)) break;
333
+ seenCursors.add(cursor);
334
+ }
335
+ const result = await discovery.listReleases({
336
+ did: publisherDid,
337
+ package: slug,
338
+ cursor,
339
+ limit: 50,
340
+ });
341
+ for (const r of result.releases) {
342
+ if (r.version === version) return r;
343
+ }
344
+ if (!result.cursor) break;
345
+ cursor = result.cursor;
346
+ }
347
+ return undefined;
348
+ })();
349
+
350
+ if (!releaseView?.release) return null;
351
+
352
+ return resolveDeclaredUrl(releaseView.release.artifacts, kind, index);
353
+ }
354
+
355
+ /**
356
+ * Read a response body into memory, aborting once it exceeds `limit`. Returns
357
+ * `null` when the cap is breached (the streamed body lied about / omitted
358
+ * Content-Length). The cap is the real defence against an unbounded body.
359
+ */
360
+ async function readCapped(response: Response, limit: number): Promise<Uint8Array | null> {
361
+ const body = response.body;
362
+ if (!body) {
363
+ const buf = new Uint8Array(await response.arrayBuffer());
364
+ return buf.length > limit ? null : buf;
365
+ }
366
+ const reader = body.getReader();
367
+ const chunks: Uint8Array[] = [];
368
+ let total = 0;
369
+ while (true) {
370
+ const { done, value } = await reader.read();
371
+ if (done) break;
372
+ if (value) {
373
+ total += value.length;
374
+ if (total > limit) {
375
+ await reader.cancel();
376
+ return null;
377
+ }
378
+ chunks.push(value);
379
+ }
380
+ }
381
+ const combined = new Uint8Array(total);
382
+ let offset = 0;
383
+ for (const chunk of chunks) {
384
+ combined.set(chunk, offset);
385
+ offset += chunk.length;
386
+ }
387
+ return combined;
388
+ }
@@ -12,6 +12,7 @@
12
12
  * view time (handle is best-effort per the lexicon).
13
13
  */
14
14
 
15
+ import { hostEnvFromVersions } from "@emdash-cms/registry-client/env";
15
16
  import type { APIRoute } from "astro";
16
17
  import { z } from "zod";
17
18
 
@@ -20,6 +21,8 @@ import { apiError, handleError, unwrapResult } from "#api/error.js";
20
21
  import { handleRegistryInstall } from "#api/index.js";
21
22
  import { isParseError, parseBody } from "#api/parse.js";
22
23
 
24
+ import { VERSION } from "../../../../../../version.js";
25
+
23
26
  export const prerender = false;
24
27
 
25
28
  const installBodySchema = z.object({
@@ -91,7 +94,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
91
94
  version: body.version,
92
95
  acknowledgedDeclaredAccess: body.acknowledgedDeclaredAccess,
93
96
  },
94
- { configuredPluginIds: reservedPluginIds },
97
+ {
98
+ configuredPluginIds: reservedPluginIds,
99
+ hostEnv: hostEnvFromVersions(VERSION, emdash.config.astroVersion),
100
+ },
95
101
  );
96
102
 
97
103
  if (!result.success) return unwrapResult(result);
@@ -27,6 +27,28 @@ export function buildBaseUrlMap(urlMap: Record<string, string>): Map<string, str
27
27
  return baseMap;
28
28
  }
29
29
 
30
+ /**
31
+ * Extract the URL to match from a stored media field value.
32
+ *
33
+ * Image/file columns hold a JSON-stringified MediaValue
34
+ * (e.g. `{"provider":"external","id":"","src":"https://.../hero.jpg"}`), but legacy
35
+ * rows may hold a bare URL string. Returns the inner `src` for a MediaValue, otherwise
36
+ * the value unchanged. Without this, the whole JSON blob is passed to findMatchingUrl()
37
+ * and the embedded URL is never matched.
38
+ */
39
+ export function extractMediaUrl(value: string): string {
40
+ try {
41
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape validated below
42
+ const parsed = JSON.parse(value) as { src?: unknown };
43
+ if (parsed && typeof parsed.src === "string") {
44
+ return parsed.src;
45
+ }
46
+ } catch {
47
+ // Not JSON — treat the column value as a bare URL.
48
+ }
49
+ return value;
50
+ }
51
+
30
52
  /**
31
53
  * Find matching new URL for a given URL, checking exact, base, and WordPress image-size matches
32
54
  */
@@ -24,6 +24,7 @@ import type { EmDashHandlers } from "#types";
24
24
 
25
25
  import {
26
26
  buildBaseUrlMap,
27
+ extractMediaUrl,
27
28
  findMatchingUrl,
28
29
  rewritePortableTextUrls,
29
30
  rewriteStringUrls,
@@ -174,8 +175,10 @@ async function rewriteUrls(
174
175
  const value = row[field.slug];
175
176
  if (!value || typeof value !== "string") continue;
176
177
 
177
- // Try to find a matching rewritten URL
178
- const newUrl = findMatchingUrl(value, urlMap, baseMap);
178
+ // Values are stored as JSON MediaValue objects (e.g. featured_image from
179
+ // import normalizes to {"provider":"external","src":"<wp url>"}). Match on the
180
+ // inner `src`, falling back to the raw value for legacy bare-URL rows.
181
+ const newUrl = findMatchingUrl(extractMediaUrl(value), urlMap, baseMap);
179
182
  if (newUrl) {
180
183
  // Normalize into a proper MediaValue instead of storing a bare URL
181
184
  try {
@@ -5,13 +5,25 @@
5
5
  *
6
6
  * Uses the collection's url_pattern to build URLs. Falls back to
7
7
  * /{collection}/{slug} when no pattern is configured.
8
+ *
9
+ * i18n behaviour: when Astro i18n is enabled, the locale prefix is
10
+ * applied via Astro's own `getRelativeLocaleUrl` (which honours
11
+ * `prefixDefaultLocale`, custom `path` mappings, and other `routing`
12
+ * config). Each translation row is emitted as its own `<url>` with
13
+ * `<xhtml:link rel="alternate" hreflang="...">` entries pointing to
14
+ * its siblings (grouped by `translation_group`). The default-locale
15
+ * variant is also linked as `hreflang="x-default"`.
8
16
  */
9
17
 
10
18
  import type { APIRoute } from "astro";
11
19
 
12
20
  import { handleSitemapData } from "#api/handlers/seo.js";
21
+ import { getPublicOrigin } from "#api/public-url.js";
13
22
  import { getSiteSettingsWithDb } from "#settings/index.js";
14
23
 
24
+ import { getI18nConfig, isI18nEnabled } from "../../i18n/config.js";
25
+ import { interpolateUrlPattern, localizePath } from "../../i18n/resolve.js";
26
+
15
27
  export const prerender = false;
16
28
 
17
29
  const TRAILING_SLASH_RE = /\/$/;
@@ -20,8 +32,6 @@ const LT_RE = /</g;
20
32
  const GT_RE = />/g;
21
33
  const QUOT_RE = /"/g;
22
34
  const APOS_RE = /'/g;
23
- const SLUG_PLACEHOLDER = "{slug}";
24
- const ID_PLACEHOLDER = "{id}";
25
35
 
26
36
  export const GET: APIRoute = async ({ params, locals, url }) => {
27
37
  const { emdash } = locals;
@@ -36,7 +46,10 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
36
46
 
37
47
  try {
38
48
  const settings = await getSiteSettingsWithDb(emdash.db);
39
- const siteUrl = (settings.url || url.origin).replace(TRAILING_SLASH_RE, "");
49
+ const siteUrl = (settings.url || getPublicOrigin(url, emdash?.config)).replace(
50
+ TRAILING_SLASH_RE,
51
+ "",
52
+ );
40
53
 
41
54
  const result = await handleSitemapData(emdash.db, collectionSlug);
42
55
 
@@ -55,25 +68,112 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
55
68
  });
56
69
  }
57
70
 
58
- const lines: string[] = [
59
- '<?xml version="1.0" encoding="UTF-8"?>',
60
- '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
61
- ];
71
+ const i18nEnabled = isI18nEnabled();
72
+ const i18nConfig = getI18nConfig();
62
73
 
74
+ // Group entries by `translation_group` so each <url> can advertise
75
+ // its sibling translations via xhtml:link. Rows without a group
76
+ // (legacy/single-locale data) are emitted individually.
77
+ type Entry = (typeof col.entries)[number];
78
+ const groups = new Map<string, Entry[]>();
79
+ const ungrouped: Entry[] = [];
63
80
  for (const entry of col.entries) {
64
- const slug = entry.slug || entry.id;
65
- const path = col.urlPattern
66
- ? col.urlPattern
67
- .replace(SLUG_PLACEHOLDER, encodeURIComponent(slug))
68
- .replace(ID_PLACEHOLDER, encodeURIComponent(entry.id))
69
- : `/${encodeURIComponent(col.collection)}/${encodeURIComponent(slug)}`;
81
+ if (i18nEnabled && entry.translationGroup) {
82
+ const list = groups.get(entry.translationGroup);
83
+ if (list) list.push(entry);
84
+ else groups.set(entry.translationGroup, [entry]);
85
+ } else {
86
+ ungrouped.push(entry);
87
+ }
88
+ }
70
89
 
71
- const loc = `${siteUrl}${path}`;
90
+ // Resolve every URL up-front so we can reference sibling URLs
91
+ // while emitting hreflang alternates without re-resolving.
92
+ // `localizePath` returns `null` when the row's locale isn't in
93
+ // the configured `i18n.locales` list -- the site can't serve a
94
+ // route for it, so the entry is dropped from the sitemap and
95
+ // omitted from sibling alternates.
96
+ const urlByEntry = new Map<string, string | null>();
97
+ const resolveEntryUrl = async (entry: Entry): Promise<string | null> => {
98
+ if (urlByEntry.has(entry.id)) return urlByEntry.get(entry.id) ?? null;
99
+ const path = interpolateUrlPattern({
100
+ pattern: col.urlPattern,
101
+ collection: col.collection,
102
+ slug: entry.slug || entry.id,
103
+ id: entry.id,
104
+ });
105
+ const localized = await localizePath(path, entry.locale);
106
+ const absolute = localized === null ? null : `${siteUrl}${localized}`;
107
+ urlByEntry.set(entry.id, absolute);
108
+ return absolute;
109
+ };
110
+
111
+ const useXhtml = i18nEnabled;
112
+ const lines: string[] = ['<?xml version="1.0" encoding="UTF-8"?>'];
113
+ lines.push(
114
+ useXhtml
115
+ ? '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
116
+ : '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
117
+ );
118
+
119
+ const writeUrl = async (entry: Entry, siblings: Entry[] | null) => {
120
+ const loc = await resolveEntryUrl(entry);
121
+ // Skip rows whose locale isn't in the configured `i18n.locales`
122
+ // list. Linking to a route the site can't serve is worse than
123
+ // no link at all (search engines hit a 404 and downrank).
124
+ if (loc === null) return;
72
125
 
73
126
  lines.push(" <url>");
74
127
  lines.push(` <loc>${escapeXml(loc)}</loc>`);
75
128
  lines.push(` <lastmod>${escapeXml(entry.updatedAt)}</lastmod>`);
129
+
130
+ if (useXhtml && siblings && siblings.length > 1) {
131
+ // Emit one xhtml:link per sibling (including self -- Google
132
+ // recommends including the page's own hreflang annotation).
133
+ // Siblings with unroutable locales are skipped here too.
134
+ for (const sib of siblings) {
135
+ const sibLoc = await resolveEntryUrl(sib);
136
+ if (sibLoc === null) continue;
137
+ lines.push(
138
+ ` <xhtml:link rel="alternate" hreflang="${escapeXml(sib.locale)}" href="${escapeXml(sibLoc)}" />`,
139
+ );
140
+ }
141
+
142
+ // x-default: prefer the default-locale sibling, otherwise
143
+ // the first sibling with a routable URL. Stable order:
144
+ // rows arrive sorted by updated_at DESC from the handler.
145
+ const defaultSibling =
146
+ i18nConfig && siblings.find((s) => s.locale === i18nConfig.defaultLocale);
147
+ let xDefaultLoc: string | null = null;
148
+ if (defaultSibling) {
149
+ xDefaultLoc = await resolveEntryUrl(defaultSibling);
150
+ }
151
+ if (xDefaultLoc === null) {
152
+ for (const sib of siblings) {
153
+ const sibLoc = await resolveEntryUrl(sib);
154
+ if (sibLoc !== null) {
155
+ xDefaultLoc = sibLoc;
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ if (xDefaultLoc !== null) {
161
+ lines.push(
162
+ ` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(xDefaultLoc)}" />`,
163
+ );
164
+ }
165
+ }
166
+
76
167
  lines.push(" </url>");
168
+ };
169
+
170
+ for (const siblings of groups.values()) {
171
+ for (const entry of siblings) {
172
+ await writeUrl(entry, siblings);
173
+ }
174
+ }
175
+ for (const entry of ungrouped) {
176
+ await writeUrl(entry, null);
77
177
  }
78
178
 
79
179
  lines.push("</urlset>");
@@ -108,6 +108,12 @@ export interface EmDashManifest {
108
108
  version: string;
109
109
  commit?: string;
110
110
  hash: string;
111
+ /**
112
+ * Version of Astro the host project is built with. Present when the
113
+ * integration could resolve it. Surfaced so the admin can evaluate a
114
+ * registry plugin's `env:astro` requirement against the running host.
115
+ */
116
+ astroVersion?: string;
111
117
  collections: Record<string, ManifestCollection>;
112
118
  plugins: Record<string, ManifestPlugin>;
113
119
  /**
@@ -385,6 +391,14 @@ export interface EmDashHandlers {
385
391
  request: Request,
386
392
  ) => Promise<HandlerResponse>;
387
393
 
394
+ // Public-only plugin API route handler for SSR page components.
395
+ handlePublicPluginApiRoute: (
396
+ pluginId: string,
397
+ method: string,
398
+ path: string,
399
+ request: Request,
400
+ ) => Promise<HandlerResponse>;
401
+
388
402
  // Plugin route metadata (for auth decisions before dispatch)
389
403
  getPluginRouteMeta: (pluginId: string, path: string) => { public: boolean } | null;
390
404