emdash 0.20.0 → 0.22.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 (685) hide show
  1. package/dist/{adapters-BzIHV3sw.d.mts → adapters-hUy8vOB-.d.mts} +1 -1
  2. package/dist/{adapters-BzIHV3sw.d.mts.map → adapters-hUy8vOB-.d.mts.map} +1 -1
  3. package/dist/after-B1IIdH3Y.mjs +29 -0
  4. package/dist/after-B1IIdH3Y.mjs.map +1 -0
  5. package/dist/{allowed-origins-B1u7Qnvg.mjs → allowed-origins-DjlhyfNN.mjs} +2 -2
  6. package/dist/{allowed-origins-B1u7Qnvg.mjs.map → allowed-origins-DjlhyfNN.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -3
  8. package/dist/api/route-utils.mjs +20 -18
  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 +4 -3
  12. package/dist/{api-DStv36ik.mjs → api-BDuM4bEy.mjs} +31 -25
  13. package/dist/api-BDuM4bEy.mjs.map +1 -0
  14. package/dist/{api-tokens-DPfhPu5V.mjs → api-tokens-ViYKeIp9.mjs} +13 -3
  15. package/dist/api-tokens-ViYKeIp9.mjs.map +1 -0
  16. package/dist/{apply-Dr7snAMT.mjs → apply-wDqQoxX4.mjs} +18 -18
  17. package/dist/{apply-Dr7snAMT.mjs.map → apply-wDqQoxX4.mjs.map} +1 -1
  18. package/dist/astro/image-endpoint.d.mts +8 -0
  19. package/dist/astro/image-endpoint.d.mts.map +1 -0
  20. package/dist/astro/image-endpoint.mjs +55 -0
  21. package/dist/astro/image-endpoint.mjs.map +1 -0
  22. package/dist/astro/index.d.mts +36 -11
  23. package/dist/astro/index.d.mts.map +1 -1
  24. package/dist/astro/index.mjs +160 -35
  25. package/dist/astro/index.mjs.map +1 -1
  26. package/dist/astro/middleware/auth.d.mts +9 -9
  27. package/dist/astro/middleware/auth.d.mts.map +1 -1
  28. package/dist/astro/middleware/auth.mjs +12 -10
  29. package/dist/astro/middleware/auth.mjs.map +1 -1
  30. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  31. package/dist/astro/middleware/redirect.mjs +14 -5
  32. package/dist/astro/middleware/redirect.mjs.map +1 -1
  33. package/dist/astro/middleware/request-context.mjs +5 -4
  34. package/dist/astro/middleware/request-context.mjs.map +1 -1
  35. package/dist/astro/middleware/setup.mjs +1 -1
  36. package/dist/astro/middleware.d.mts +1 -1
  37. package/dist/astro/middleware.d.mts.map +1 -1
  38. package/dist/astro/middleware.mjs +157 -80
  39. package/dist/astro/middleware.mjs.map +1 -1
  40. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +7 -6
  41. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  42. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +7 -6
  43. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  44. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  45. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  46. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  47. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +10 -9
  48. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/byline-fields/index.mjs +10 -9
  50. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  51. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +10 -9
  52. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +17 -14
  54. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +17 -14
  56. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/bylines/index.mjs +17 -14
  58. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +15 -12
  60. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  61. package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -6
  62. package/dist/astro/routes/api/admin/comments/_id_.mjs.map +1 -1
  63. package/dist/astro/routes/api/admin/comments/bulk.mjs +12 -9
  64. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  65. package/dist/astro/routes/api/admin/comments/counts.mjs +8 -6
  66. package/dist/astro/routes/api/admin/comments/counts.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/comments/index.mjs +12 -9
  68. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  69. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
  70. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
  71. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  72. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  73. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +40 -37
  74. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +40 -37
  76. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  77. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +39 -36
  78. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  79. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +39 -36
  80. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  81. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +39 -36
  82. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/plugins/index.mjs +39 -36
  84. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  85. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  86. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +39 -36
  87. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +39 -36
  89. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  90. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +39 -36
  91. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +39 -36
  93. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  94. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +40 -37
  95. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +39 -36
  97. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +40 -37
  99. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/updates.mjs +39 -36
  101. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  102. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +39 -36
  103. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  104. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  105. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +39 -36
  106. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  107. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
  108. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  109. package/dist/astro/routes/api/admin/users/_id_/index.mjs +8 -7
  110. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +5 -5
  112. package/dist/astro/routes/api/admin/users/index.mjs +7 -6
  113. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  114. package/dist/astro/routes/api/auth/dev-bypass.mjs +6 -6
  115. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  116. package/dist/astro/routes/api/auth/invite/complete.mjs +12 -11
  117. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  118. package/dist/astro/routes/api/auth/invite/index.mjs +9 -8
  119. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  120. package/dist/astro/routes/api/auth/invite/register-options.mjs +11 -10
  121. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  122. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  123. package/dist/astro/routes/api/auth/magic-link/send.mjs +11 -10
  124. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  125. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  126. package/dist/astro/routes/api/auth/me.mjs +8 -7
  127. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  128. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  129. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  130. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  131. package/dist/astro/routes/api/auth/passkey/_id_.mjs +7 -6
  132. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  133. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  134. package/dist/astro/routes/api/auth/passkey/options.mjs +13 -12
  135. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  136. package/dist/astro/routes/api/auth/passkey/register/options.mjs +11 -10
  137. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  138. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +12 -11
  139. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  140. package/dist/astro/routes/api/auth/passkey/verify.mjs +12 -11
  141. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  142. package/dist/astro/routes/api/auth/signup/complete.mjs +12 -11
  143. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  144. package/dist/astro/routes/api/auth/signup/request.mjs +11 -10
  145. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  146. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  147. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +16 -13
  148. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  149. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.d.mts +9 -0
  150. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.d.mts.map +1 -0
  151. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +179 -0
  152. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -0
  153. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  154. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +4 -4
  155. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +4 -4
  156. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  157. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +13 -11
  158. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -1
  159. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +8 -7
  160. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  161. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +4 -4
  162. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  163. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -7
  164. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  165. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +17 -14
  166. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  167. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  168. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +4 -4
  169. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -7
  170. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  171. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  172. package/dist/astro/routes/api/content/_collection_/index.mjs +8 -7
  173. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  174. package/dist/astro/routes/api/content/_collection_/trash.mjs +8 -7
  175. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  176. package/dist/astro/routes/api/dashboard.mjs +10 -8
  177. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  178. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  179. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  180. package/dist/astro/routes/api/import/probe.mjs +12 -11
  181. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  182. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  183. package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
  184. package/dist/astro/routes/api/import/wordpress/execute.mjs +16 -14
  185. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  186. package/dist/astro/routes/api/import/wordpress/media.mjs +10 -9
  187. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  188. package/dist/astro/routes/api/import/wordpress/prepare.mjs +11 -10
  189. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  190. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +10 -9
  191. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  192. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  193. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +12 -11
  194. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  195. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  196. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  197. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +18 -15
  198. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  199. package/dist/astro/routes/api/manifest.mjs +5 -5
  200. package/dist/astro/routes/api/mcp.mjs +213 -45
  201. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  202. package/dist/astro/routes/api/media/_id_/confirm.mjs +8 -7
  203. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  204. package/dist/astro/routes/api/media/_id_.mjs +8 -7
  205. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  206. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  207. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  208. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  209. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  210. package/dist/astro/routes/api/media/upload-url.mjs +10 -9
  211. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  212. package/dist/astro/routes/api/media.mjs +13 -12
  213. package/dist/astro/routes/api/media.mjs.map +1 -1
  214. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +11 -8
  215. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
  216. package/dist/astro/routes/api/menus/_name_/items.mjs +11 -8
  217. package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
  218. package/dist/astro/routes/api/menus/_name_/reorder.mjs +11 -8
  219. package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
  220. package/dist/astro/routes/api/menus/_name_/translations.mjs +11 -8
  221. package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
  222. package/dist/astro/routes/api/menus/_name_.mjs +11 -8
  223. package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
  224. package/dist/astro/routes/api/menus/index.mjs +11 -8
  225. package/dist/astro/routes/api/menus/index.mjs.map +1 -1
  226. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  227. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  228. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  229. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  230. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  231. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  232. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  233. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  234. package/dist/astro/routes/api/openapi.json.mjs +5 -4
  235. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  236. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +5 -5
  237. package/dist/astro/routes/api/redirects/404s/index.mjs +11 -10
  238. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  239. package/dist/astro/routes/api/redirects/404s/summary.mjs +11 -10
  240. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  241. package/dist/astro/routes/api/redirects/_id_.mjs +12 -11
  242. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  243. package/dist/astro/routes/api/redirects/index.mjs +12 -11
  244. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  245. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  246. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +4 -4
  247. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +39 -36
  248. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  249. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +39 -36
  250. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  251. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +39 -36
  252. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  253. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +39 -36
  254. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  255. package/dist/astro/routes/api/schema/collections/index.mjs +39 -36
  256. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  257. package/dist/astro/routes/api/schema/index.mjs +7 -7
  258. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +39 -36
  259. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  260. package/dist/astro/routes/api/schema/orphans/index.mjs +39 -36
  261. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  262. package/dist/astro/routes/api/search/enable.mjs +11 -10
  263. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  264. package/dist/astro/routes/api/search/index.mjs +10 -9
  265. package/dist/astro/routes/api/search/index.mjs.map +1 -1
  266. package/dist/astro/routes/api/search/rebuild.mjs +11 -10
  267. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  268. package/dist/astro/routes/api/search/stats.mjs +7 -7
  269. package/dist/astro/routes/api/search/suggest.mjs +10 -9
  270. package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
  271. package/dist/astro/routes/api/sections/_slug_.mjs +10 -9
  272. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  273. package/dist/astro/routes/api/sections/index.mjs +10 -9
  274. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  275. package/dist/astro/routes/api/settings/email.mjs +6 -6
  276. package/dist/astro/routes/api/settings.mjs +16 -13
  277. package/dist/astro/routes/api/settings.mjs.map +1 -1
  278. package/dist/astro/routes/api/setup/admin-verify.mjs +13 -12
  279. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  280. package/dist/astro/routes/api/setup/admin.mjs +12 -11
  281. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  282. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  283. package/dist/astro/routes/api/setup/dev-bypass.mjs +29 -26
  284. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  285. package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
  286. package/dist/astro/routes/api/setup/index.mjs +29 -26
  287. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  288. package/dist/astro/routes/api/setup/status.mjs +5 -5
  289. package/dist/astro/routes/api/snapshot.d.mts.map +1 -1
  290. package/dist/astro/routes/api/snapshot.mjs +10 -8
  291. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  292. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -13
  293. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  294. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -13
  295. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  296. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +16 -13
  297. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  298. package/dist/astro/routes/api/taxonomies/index.mjs +16 -13
  299. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  300. package/dist/astro/routes/api/themes/preview.mjs +8 -7
  301. package/dist/astro/routes/api/themes/preview.mjs.map +1 -1
  302. package/dist/astro/routes/api/typegen.mjs +6 -6
  303. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  304. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  305. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  306. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +8 -7
  307. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  308. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +11 -10
  309. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  310. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +11 -10
  311. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  312. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -7
  313. package/dist/astro/routes/api/widget-areas/index.mjs +11 -10
  314. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  315. package/dist/astro/routes/api/widget-components.mjs +4 -4
  316. package/dist/astro/routes/robots.txt.mjs +10 -8
  317. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  318. package/dist/astro/routes/sitemap-_collection_.xml.mjs +13 -11
  319. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  320. package/dist/astro/routes/sitemap.xml.mjs +11 -9
  321. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  322. package/dist/astro/types.d.mts +16 -12
  323. package/dist/astro/types.d.mts.map +1 -1
  324. package/dist/auth/providers/github.d.mts +1 -1
  325. package/dist/auth/providers/google.d.mts +1 -1
  326. package/dist/{authorize-DsMSVSaY.mjs → authorize-C0EYoUeV.mjs} +2 -2
  327. package/dist/{authorize-DsMSVSaY.mjs.map → authorize-C0EYoUeV.mjs.map} +1 -1
  328. package/dist/{base64-CqR-7kqF.mjs → base64-CmWvODNW.mjs} +1 -1
  329. package/dist/{base64-CqR-7kqF.mjs.map → base64-CmWvODNW.mjs.map} +1 -1
  330. package/dist/{byline-DUx48sJp.mjs → byline-CC0WS4Gu.mjs} +40 -18
  331. package/dist/byline-CC0WS4Gu.mjs.map +1 -0
  332. package/dist/{byline-fields-8TMtkBnH.mjs → byline-fields-53i9Et0x.mjs} +3 -3
  333. package/dist/{byline-fields-8TMtkBnH.mjs.map → byline-fields-53i9Et0x.mjs.map} +1 -1
  334. package/dist/{byline-fields--WxSNS79.mjs → byline-fields-DNp0HNKc.mjs} +2 -2
  335. package/dist/{byline-fields--WxSNS79.mjs.map → byline-fields-DNp0HNKc.mjs.map} +1 -1
  336. package/dist/{byline-fields-DbibsvTl.d.mts → byline-fields-jy07PZJM.d.mts} +9 -4
  337. package/dist/byline-fields-jy07PZJM.d.mts.map +1 -0
  338. package/dist/{byline-registry-CWP7I71B.mjs → byline-registry-BOjqDOim.mjs} +3 -3
  339. package/dist/{byline-registry-CWP7I71B.mjs.map → byline-registry-BOjqDOim.mjs.map} +1 -1
  340. package/dist/{bylines-s8c2DXbH.mjs → bylines-CCO1CYnH.mjs} +8 -8
  341. package/dist/{bylines-s8c2DXbH.mjs.map → bylines-CCO1CYnH.mjs.map} +1 -1
  342. package/dist/{bylines-BdxWCnPL.mjs → bylines-DGekMGGm.mjs} +10 -4
  343. package/dist/{bylines-BdxWCnPL.mjs.map → bylines-DGekMGGm.mjs.map} +1 -1
  344. package/dist/{cache-B_HzASVT.mjs → cache-pXTpun6s.mjs} +3 -3
  345. package/dist/{cache-B_HzASVT.mjs.map → cache-pXTpun6s.mjs.map} +1 -1
  346. package/dist/{challenge-store-DXX3rfdI.mjs → challenge-store-C6E_kWTs.mjs} +1 -1
  347. package/dist/{challenge-store-DXX3rfdI.mjs.map → challenge-store-C6E_kWTs.mjs.map} +1 -1
  348. package/dist/{chunks-BerYVuve.mjs → chunks-SyjZaYwV.mjs} +2 -2
  349. package/dist/{chunks-BerYVuve.mjs.map → chunks-SyjZaYwV.mjs.map} +1 -1
  350. package/dist/cli/index.mjs +29 -27
  351. package/dist/cli/index.mjs.map +1 -1
  352. package/dist/client/cf-access.d.mts +1 -1
  353. package/dist/client/index.d.mts +1 -1
  354. package/dist/client/index.mjs +2 -1
  355. package/dist/client/index.mjs.map +1 -1
  356. package/dist/{comment-sqQxNpN3.mjs → comment-80UyJJUR.mjs} +11 -3
  357. package/dist/comment-80UyJJUR.mjs.map +1 -0
  358. package/dist/comment-reaction-B6AdcpXw.mjs +85 -0
  359. package/dist/comment-reaction-B6AdcpXw.mjs.map +1 -0
  360. package/dist/{comments-Vkivawyl.mjs → comments-CtPkT2tp.mjs} +3 -3
  361. package/dist/{comments-Vkivawyl.mjs.map → comments-CtPkT2tp.mjs.map} +1 -1
  362. package/dist/{components-CK0cuUoH.mjs → components-D6ZnrzbB.mjs} +1 -1
  363. package/dist/{components-CK0cuUoH.mjs.map → components-D6ZnrzbB.mjs.map} +1 -1
  364. package/dist/{content-BIlVx-RX.mjs → content-2PTDNnoh.mjs} +22 -7
  365. package/dist/content-2PTDNnoh.mjs.map +1 -0
  366. package/dist/{context-Y7BRkWes.mjs → context-f1__jpzF.mjs} +11 -11
  367. package/dist/{context-Y7BRkWes.mjs.map → context-f1__jpzF.mjs.map} +1 -1
  368. package/dist/{cron-BJ2ClIlj.mjs → cron-CQPxBjYs.mjs} +1 -1
  369. package/dist/{cron-BJ2ClIlj.mjs.map → cron-CQPxBjYs.mjs.map} +1 -1
  370. package/dist/{dashboard-2JgAMWxK.mjs → dashboard-BOESNcwE.mjs} +4 -4
  371. package/dist/{dashboard-2JgAMWxK.mjs.map → dashboard-BOESNcwE.mjs.map} +1 -1
  372. package/dist/database/instrumentation.d.mts +19 -0
  373. package/dist/database/instrumentation.d.mts.map +1 -1
  374. package/dist/database/instrumentation.mjs +8 -1
  375. package/dist/database/instrumentation.mjs.map +1 -1
  376. package/dist/db/index.d.mts +3 -3
  377. package/dist/db/index.mjs +1 -1
  378. package/dist/db/libsql.d.mts +1 -1
  379. package/dist/db/postgres.d.mts +1 -1
  380. package/dist/db/sqlite.d.mts +1 -1
  381. package/dist/{db-errors-CtzxKBxe.mjs → db-errors-CrC5w3Jz.mjs} +1 -1
  382. package/dist/{db-errors-CtzxKBxe.mjs.map → db-errors-CrC5w3Jz.mjs.map} +1 -1
  383. package/dist/{default-IlBaTFxM.mjs → default-CpBEPn6a.mjs} +1 -1
  384. package/dist/{default-IlBaTFxM.mjs.map → default-CpBEPn6a.mjs.map} +1 -1
  385. package/dist/{device-flow-R23SIbQ2.mjs → device-flow-Bn-qiDrw.mjs} +5 -5
  386. package/dist/{device-flow-R23SIbQ2.mjs.map → device-flow-Bn-qiDrw.mjs.map} +1 -1
  387. package/dist/{email-console-DHT2Fbpj.mjs → email-console-B9VS6eN4.mjs} +1 -1
  388. package/dist/{email-console-DHT2Fbpj.mjs.map → email-console-B9VS6eN4.mjs.map} +1 -1
  389. package/dist/{error-RwM4dD35.mjs → error-BQ1pxs0L.mjs} +2 -2
  390. package/dist/{error-RwM4dD35.mjs.map → error-BQ1pxs0L.mjs.map} +1 -1
  391. package/dist/{escape-Ds07EEyu.mjs → escape-BtbJWyaW.mjs} +1 -1
  392. package/dist/{escape-Ds07EEyu.mjs.map → escape-BtbJWyaW.mjs.map} +1 -1
  393. package/dist/{fts-manager-1RgHmopc.mjs → fts-manager-DtiWqwNJ.mjs} +2 -2
  394. package/dist/{fts-manager-1RgHmopc.mjs.map → fts-manager-DtiWqwNJ.mjs.map} +1 -1
  395. package/dist/{hash-9w3pd3-m.mjs → hash-BowcuPwv.mjs} +1 -1
  396. package/dist/{hash-9w3pd3-m.mjs.map → hash-BowcuPwv.mjs.map} +1 -1
  397. package/dist/{import-Dh8bWmyq.mjs → import-DqSGOlmR.mjs} +4 -4
  398. package/dist/{import-Dh8bWmyq.mjs.map → import-DqSGOlmR.mjs.map} +1 -1
  399. package/dist/{index-B1keaX5Y.d.mts → index-C2dLNT6S.d.mts} +169 -14
  400. package/dist/index-C2dLNT6S.d.mts.map +1 -0
  401. package/dist/{index-DR56od45.d.mts → index-ZDEwR7qW.d.mts} +3 -3
  402. package/dist/{index-DR56od45.d.mts.map → index-ZDEwR7qW.d.mts.map} +1 -1
  403. package/dist/index.d.mts +18 -17
  404. package/dist/index.mjs +63 -59
  405. package/dist/init-lock-6b309ZrF.mjs +56 -0
  406. package/dist/init-lock-6b309ZrF.mjs.map +1 -0
  407. package/dist/{load-BBetCvLC.mjs → load-Dtoo7KcJ.mjs} +2 -2
  408. package/dist/{load-BBetCvLC.mjs.map → load-Dtoo7KcJ.mjs.map} +1 -1
  409. package/dist/{loader-ZN1ll-d-.mjs → loader-C8Z48Uof.mjs} +4 -4
  410. package/dist/{loader-ZN1ll-d-.mjs.map → loader-C8Z48Uof.mjs.map} +1 -1
  411. package/dist/{manifest-schema-BtwbL_vj.mjs → manifest-schema-DDSbwkAL.mjs} +1 -1
  412. package/dist/{manifest-schema-BtwbL_vj.mjs.map → manifest-schema-DDSbwkAL.mjs.map} +1 -1
  413. package/dist/media/image-endpoint.d.mts +74 -0
  414. package/dist/media/image-endpoint.d.mts.map +1 -0
  415. package/dist/media/image-endpoint.mjs +162 -0
  416. package/dist/media/image-endpoint.mjs.map +1 -0
  417. package/dist/media/index.d.mts +1 -1
  418. package/dist/media/index.mjs +2 -2
  419. package/dist/media/local-runtime.d.mts +11 -11
  420. package/dist/media/local-runtime.mjs +9 -7
  421. package/dist/media/local-runtime.mjs.map +1 -1
  422. package/dist/{media-JOf3pNkw.mjs → media-Bw0sA6rb.mjs} +2 -2
  423. package/dist/{media-JOf3pNkw.mjs.map → media-Bw0sA6rb.mjs.map} +1 -1
  424. package/dist/{media-allowlist-Dknq-OFY.mjs → media-allowlist-DunzrKFM.mjs} +2 -2
  425. package/dist/{media-allowlist-Dknq-OFY.mjs.map → media-allowlist-DunzrKFM.mjs.map} +1 -1
  426. package/dist/{media-url-VClf8glU.mjs → media-url-S22B6aPr.mjs} +1 -1
  427. package/dist/{media-url-VClf8glU.mjs.map → media-url-S22B6aPr.mjs.map} +1 -1
  428. package/dist/{menus-DX4_E01q.mjs → menus-CYs0WZoL.mjs} +20 -6
  429. package/dist/menus-CYs0WZoL.mjs.map +1 -0
  430. package/dist/{menus-DrQLusqj.mjs → menus-DOs1MQJg.mjs} +120 -20
  431. package/dist/menus-DOs1MQJg.mjs.map +1 -0
  432. package/dist/{mime-CCEzze7W.mjs → mime-DYpsOAny.mjs} +1 -1
  433. package/dist/{mime-CCEzze7W.mjs.map → mime-DYpsOAny.mjs.map} +1 -1
  434. package/dist/{mode-CO2vQHfq.mjs → mode-CFh8Dw8G.mjs} +1 -1
  435. package/dist/{mode-CO2vQHfq.mjs.map → mode-CFh8Dw8G.mjs.map} +1 -1
  436. package/dist/{normalize-CK5o04zr.mjs → normalize-Uam4_Vkr.mjs} +1 -1
  437. package/dist/{normalize-CK5o04zr.mjs.map → normalize-Uam4_Vkr.mjs.map} +1 -1
  438. package/dist/{oauth-authorization-Bw4NdF_S.mjs → oauth-authorization-BN-t83Uy.mjs} +5 -5
  439. package/dist/{oauth-authorization-Bw4NdF_S.mjs.map → oauth-authorization-BN-t83Uy.mjs.map} +1 -1
  440. package/dist/{oauth-clients-BGGFp57s.mjs → oauth-clients-R6I_V0qz.mjs} +1 -1
  441. package/dist/{oauth-clients-BGGFp57s.mjs.map → oauth-clients-R6I_V0qz.mjs.map} +1 -1
  442. package/dist/{oauth-state-store-97x0xtN2.mjs → oauth-state-store-Csoc6kkR.mjs} +1 -1
  443. package/dist/{oauth-state-store-97x0xtN2.mjs.map → oauth-state-store-Csoc6kkR.mjs.map} +1 -1
  444. package/dist/{oauth-user-lookup-B_vnZHKO.mjs → oauth-user-lookup-d7VYjTpx.mjs} +1 -1
  445. package/dist/{oauth-user-lookup-B_vnZHKO.mjs.map → oauth-user-lookup-d7VYjTpx.mjs.map} +1 -1
  446. package/dist/object-cache/memory.d.mts +15 -0
  447. package/dist/object-cache/memory.d.mts.map +1 -0
  448. package/dist/object-cache/memory.mjs +56 -0
  449. package/dist/object-cache/memory.mjs.map +1 -0
  450. package/dist/object-cache-SEb2IM4Z.mjs +373 -0
  451. package/dist/object-cache-SEb2IM4Z.mjs.map +1 -0
  452. package/dist/{options-DyYIYpPd.d.mts → options-DFFpvNJU.d.mts} +3 -3
  453. package/dist/{options-DyYIYpPd.d.mts.map → options-DFFpvNJU.d.mts.map} +1 -1
  454. package/dist/{options-BPCVnesz.mjs → options-DTTML-Tx.mjs} +1 -1
  455. package/dist/{options-BPCVnesz.mjs.map → options-DTTML-Tx.mjs.map} +1 -1
  456. package/dist/page/index.d.mts +2 -2
  457. package/dist/{parse-CrGndy1A.mjs → parse-qrHlnzTy.mjs} +2 -2
  458. package/dist/{parse-CrGndy1A.mjs.map → parse-qrHlnzTy.mjs.map} +1 -1
  459. package/dist/{passkey-config-C3QgnQnU.mjs → passkey-config-BJTbcnI5.mjs} +1 -1
  460. package/dist/{passkey-config-C3QgnQnU.mjs.map → passkey-config-BJTbcnI5.mjs.map} +1 -1
  461. package/dist/{patterns-p-RBdTbM.mjs → patterns-BKmjvM7K.mjs} +1 -1
  462. package/dist/{patterns-p-RBdTbM.mjs.map → patterns-BKmjvM7K.mjs.map} +1 -1
  463. package/dist/{placeholder-BZxr8W1j.mjs → placeholder-CIDCpyEY.mjs} +1 -1
  464. package/dist/{placeholder-BZxr8W1j.mjs.map → placeholder-CIDCpyEY.mjs.map} +1 -1
  465. package/dist/{placeholder-CVBv5z8k.d.mts → placeholder-CirPauNG.d.mts} +1 -1
  466. package/dist/{placeholder-CVBv5z8k.d.mts.map → placeholder-CirPauNG.d.mts.map} +1 -1
  467. package/dist/plugin-types.d.mts +1 -1
  468. package/dist/plugin-utils.d.mts +9 -9
  469. package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
  470. package/dist/plugins/adapt-sandbox-entry.mjs +4 -2
  471. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  472. package/dist/{transport-CmpLD7W3.mjs → portable-text-BIFhrU6T.mjs} +2 -135
  473. package/dist/portable-text-BIFhrU6T.mjs.map +1 -0
  474. package/dist/{preview-BfuRkVKW.mjs → preview-IDye9SPQ.mjs} +2 -2
  475. package/dist/{preview-BfuRkVKW.mjs.map → preview-IDye9SPQ.mjs.map} +1 -1
  476. package/dist/{public-url-BFVC2OTJ.mjs → public-url-8lFyQ8ZF.mjs} +1 -1
  477. package/dist/{public-url-BFVC2OTJ.mjs.map → public-url-8lFyQ8ZF.mjs.map} +1 -1
  478. package/dist/{query-CbUcI4Xk.mjs → query-B1RVFUNM.mjs} +159 -31
  479. package/dist/query-B1RVFUNM.mjs.map +1 -0
  480. package/dist/{rate-limit-C7hjdkS5.mjs → rate-limit-DfNAqIWW.mjs} +2 -2
  481. package/dist/{rate-limit-C7hjdkS5.mjs.map → rate-limit-DfNAqIWW.mjs.map} +1 -1
  482. package/dist/{redirect-B_q19j4v.mjs → redirect-B4wuX2A5.mjs} +1 -1
  483. package/dist/{redirect-B_q19j4v.mjs.map → redirect-B4wuX2A5.mjs.map} +1 -1
  484. package/dist/{redirect-CRWIt8Zj.mjs → redirect-Bifv_yHv.mjs} +3 -3
  485. package/dist/{redirect-CRWIt8Zj.mjs.map → redirect-Bifv_yHv.mjs.map} +1 -1
  486. package/dist/{redirects-DxVoR7PI.mjs → redirects-CT_vb3NV.mjs} +24 -18
  487. package/dist/redirects-CT_vb3NV.mjs.map +1 -0
  488. package/dist/{redirects-CCbCqCCd.mjs → redirects-DUvXgzCX.mjs} +14 -10
  489. package/dist/redirects-DUvXgzCX.mjs.map +1 -0
  490. package/dist/{registry-brYh-rAT.mjs → registry-CjK96fHD.mjs} +6 -6
  491. package/dist/{registry-brYh-rAT.mjs.map → registry-CjK96fHD.mjs.map} +1 -1
  492. package/dist/{request-cache-D32LpnmI.mjs → request-cache-KCNHp_RJ.mjs} +1 -1
  493. package/dist/{request-cache-D32LpnmI.mjs.map → request-cache-KCNHp_RJ.mjs.map} +1 -1
  494. package/dist/{request-meta-7ByVLxB-.mjs → request-meta-ei3ATilC.mjs} +2 -2
  495. package/dist/{request-meta-7ByVLxB-.mjs.map → request-meta-ei3ATilC.mjs.map} +1 -1
  496. package/dist/{resolve-BqYMVG0D.mjs → resolve-CpbBglV0.mjs} +1 -1
  497. package/dist/{resolve-BqYMVG0D.mjs.map → resolve-CpbBglV0.mjs.map} +1 -1
  498. package/dist/{runner--4wMWwKM.mjs → runner-DYYnW530.mjs} +201 -170
  499. package/dist/runner-DYYnW530.mjs.map +1 -0
  500. package/dist/{runner-DTdhuI9i.d.mts → runner-jdXtFEd3.d.mts} +2 -2
  501. package/dist/{runner-DTdhuI9i.d.mts.map → runner-jdXtFEd3.d.mts.map} +1 -1
  502. package/dist/runtime.d.mts +10 -10
  503. package/dist/runtime.mjs +3 -3
  504. package/dist/{schema-C1E70ug_.mjs → schema-5z2Jfnbm.mjs} +13 -8
  505. package/dist/schema-5z2Jfnbm.mjs.map +1 -0
  506. package/dist/{search-B3SGZw91.mjs → search-D9FkoM-k.mjs} +4 -4
  507. package/dist/{search-B3SGZw91.mjs.map → search-D9FkoM-k.mjs.map} +1 -1
  508. package/dist/{secrets-ChPTmy9x.mjs → secrets-BUE5UQys.mjs} +23 -13
  509. package/dist/secrets-BUE5UQys.mjs.map +1 -0
  510. package/dist/{sections-D_lVzwRZ.mjs → sections-DWYZo6HK.mjs} +3 -3
  511. package/dist/{sections-D_lVzwRZ.mjs.map → sections-DWYZo6HK.mjs.map} +1 -1
  512. package/dist/seed/index.d.mts +2 -2
  513. package/dist/seed/index.mjs +22 -20
  514. package/dist/seo/index.d.mts +1 -1
  515. package/dist/seo/index.mjs +1 -1
  516. package/dist/{seo-B5e6y9Wk.mjs → seo-Cu47j3mJ.mjs} +5 -2
  517. package/dist/seo-Cu47j3mJ.mjs.map +1 -0
  518. package/dist/{seo-D_LPkOtu.mjs → seo-DRnqnstF.mjs} +1 -1
  519. package/dist/{seo-D_LPkOtu.mjs.map → seo-DRnqnstF.mjs.map} +1 -1
  520. package/dist/{service-ChDcsTBs.mjs → service-DTmtqCnw.mjs} +3 -3
  521. package/dist/{service-ChDcsTBs.mjs.map → service-DTmtqCnw.mjs.map} +1 -1
  522. package/dist/session-user-DmtPMNa1.mjs +51 -0
  523. package/dist/session-user-DmtPMNa1.mjs.map +1 -0
  524. package/dist/{settings-Cv47v9u8.mjs → settings-DUrVDN0c.mjs} +3 -3
  525. package/dist/{settings-Cv47v9u8.mjs.map → settings-DUrVDN0c.mjs.map} +1 -1
  526. package/dist/settings-yaxu9mU-.mjs +245 -0
  527. package/dist/settings-yaxu9mU-.mjs.map +1 -0
  528. package/dist/{setup-complete-yvPE4OsP.mjs → setup-complete-BWFHzgvZ.mjs} +2 -2
  529. package/dist/{setup-complete-yvPE4OsP.mjs.map → setup-complete-BWFHzgvZ.mjs.map} +1 -1
  530. package/dist/{setup-nonce-C9aFzb94.mjs → setup-nonce-DFWG-J5c.mjs} +1 -1
  531. package/dist/{setup-nonce-C9aFzb94.mjs.map → setup-nonce-DFWG-J5c.mjs.map} +1 -1
  532. package/dist/single-flight-cache-B5p5cNOL.mjs +104 -0
  533. package/dist/single-flight-cache-B5p5cNOL.mjs.map +1 -0
  534. package/dist/{site-url-CnHlmAs9.mjs → site-url-BOTPgmeM.mjs} +2 -2
  535. package/dist/{site-url-CnHlmAs9.mjs.map → site-url-BOTPgmeM.mjs.map} +1 -1
  536. package/dist/{slugify-Cjh1ssOZ.mjs → slugify-Cce3dTdg.mjs} +1 -1
  537. package/dist/{slugify-Cjh1ssOZ.mjs.map → slugify-Cce3dTdg.mjs.map} +1 -1
  538. package/dist/{ssrf-BsVGIE0Z.mjs → ssrf-B0VeRLrp.mjs} +1 -1
  539. package/dist/{ssrf-BsVGIE0Z.mjs.map → ssrf-B0VeRLrp.mjs.map} +1 -1
  540. package/dist/status-CB2basWJ.mjs +30 -0
  541. package/dist/status-CB2basWJ.mjs.map +1 -0
  542. package/dist/storage/local.d.mts +1 -1
  543. package/dist/storage/local.mjs +2 -2
  544. package/dist/storage/s3.d.mts +1 -1
  545. package/dist/storage/s3.mjs +1 -1
  546. package/dist/{taxonomies-BdAmbOwx.mjs → taxonomies-Be9B1jEj.mjs} +156 -122
  547. package/dist/taxonomies-Be9B1jEj.mjs.map +1 -0
  548. package/dist/{taxonomies-BILwiyGk.mjs → taxonomies-CJmkEpdS.mjs} +7 -7
  549. package/dist/{taxonomies-BILwiyGk.mjs.map → taxonomies-CJmkEpdS.mjs.map} +1 -1
  550. package/dist/{taxonomy-CdllE4oq.mjs → taxonomy-B2HQuVf5.mjs} +19 -6
  551. package/dist/taxonomy-B2HQuVf5.mjs.map +1 -0
  552. package/dist/{tokens-Bx2afeT-.mjs → tokens-BvrJvIB6.mjs} +2 -2
  553. package/dist/{tokens-Bx2afeT-.mjs.map → tokens-BvrJvIB6.mjs.map} +1 -1
  554. package/dist/{transaction-x2tJQ-A1.mjs → transaction-qfqpPVpu.mjs} +1 -1
  555. package/dist/{transaction-x2tJQ-A1.mjs.map → transaction-qfqpPVpu.mjs.map} +1 -1
  556. package/dist/transport-B4rXnSTf.mjs +135 -0
  557. package/dist/transport-B4rXnSTf.mjs.map +1 -0
  558. package/dist/{transport-B7PPP2CC.d.mts → transport-k7YjfmEn.d.mts} +1 -1
  559. package/dist/{transport-B7PPP2CC.d.mts.map → transport-k7YjfmEn.d.mts.map} +1 -1
  560. package/dist/{trusted-proxy-B4AfnoAp.mjs → trusted-proxy-DY1_UqHr.mjs} +1 -1
  561. package/dist/{trusted-proxy-B4AfnoAp.mjs.map → trusted-proxy-DY1_UqHr.mjs.map} +1 -1
  562. package/dist/{types-CPAPl93j.d.mts → types-CIKX2HO5.d.mts} +2 -2
  563. package/dist/{types-CPAPl93j.d.mts.map → types-CIKX2HO5.d.mts.map} +1 -1
  564. package/dist/{types-D4kUqbHh.d.mts → types-CKNXt63n.d.mts} +1 -1
  565. package/dist/{types-D4kUqbHh.d.mts.map → types-CKNXt63n.d.mts.map} +1 -1
  566. package/dist/{types-DTniiNto.d.mts → types-CWGYDYqF.d.mts} +2 -2
  567. package/dist/{types-DTniiNto.d.mts.map → types-CWGYDYqF.d.mts.map} +1 -1
  568. package/dist/{types-BH8-30hc.d.mts → types-CgvhU-hF.d.mts} +1 -1
  569. package/dist/{types-BH8-30hc.d.mts.map → types-CgvhU-hF.d.mts.map} +1 -1
  570. package/dist/{types-BFgrqwSk.d.mts → types-CmivgZbp.d.mts} +1 -1
  571. package/dist/{types-BFgrqwSk.d.mts.map → types-CmivgZbp.d.mts.map} +1 -1
  572. package/dist/{types-BPzXTV9x.d.mts → types-Czg1S3gB.d.mts} +9 -1
  573. package/dist/{types-BPzXTV9x.d.mts.map → types-Czg1S3gB.d.mts.map} +1 -1
  574. package/dist/types-DCNNfeCv.d.mts +124 -0
  575. package/dist/types-DCNNfeCv.d.mts.map +1 -0
  576. package/dist/{types-BUUVn1zr.d.mts → types-DPCpkzvS.d.mts} +1 -1
  577. package/dist/{types-BUUVn1zr.d.mts.map → types-DPCpkzvS.d.mts.map} +1 -1
  578. package/dist/{types-S15DXXNi.d.mts → types-DXLVzV2w.d.mts} +1 -1
  579. package/dist/{types-S15DXXNi.d.mts.map → types-DXLVzV2w.d.mts.map} +1 -1
  580. package/dist/{types-BXSUSAjt.mjs → types-DXOIbece.mjs} +3 -3
  581. package/dist/{types-BXSUSAjt.mjs.map → types-DXOIbece.mjs.map} +1 -1
  582. package/dist/{types-DpFmlNyB.mjs → types-DYF2_Vf4.mjs} +1 -1
  583. package/dist/{types-DpFmlNyB.mjs.map → types-DYF2_Vf4.mjs.map} +1 -1
  584. package/dist/{types-DZk_y-MU.mjs → types-tM44hEcf.mjs} +1 -1
  585. package/dist/{types-DZk_y-MU.mjs.map → types-tM44hEcf.mjs.map} +1 -1
  586. package/dist/{user-C0um7wrg.mjs → user-Db-Rti_z.mjs} +3 -3
  587. package/dist/{user-C0um7wrg.mjs.map → user-Db-Rti_z.mjs.map} +1 -1
  588. package/dist/{utils-C4Ih4DML.mjs → utils-LbHcSRTj.mjs} +2 -2
  589. package/dist/{utils-C4Ih4DML.mjs.map → utils-LbHcSRTj.mjs.map} +1 -1
  590. package/dist/{validate-Bz4vqcX1.mjs → validate-Bwl64sxv.mjs} +3 -3
  591. package/dist/{validate-Bz4vqcX1.mjs.map → validate-Bwl64sxv.mjs.map} +1 -1
  592. package/dist/{validate-CNwkPWzz.d.mts → validate-CE2M0nvx.d.mts} +5 -5
  593. package/dist/{validate-CNwkPWzz.d.mts.map → validate-CE2M0nvx.d.mts.map} +1 -1
  594. package/dist/{validation-DgGTJm3u.mjs → validation-C_yOvFaO.mjs} +6 -6
  595. package/dist/{validation-DgGTJm3u.mjs.map → validation-C_yOvFaO.mjs.map} +1 -1
  596. package/dist/version-RnYXYjzD.mjs +7 -0
  597. package/dist/{version-D-5txk2m.mjs.map → version-RnYXYjzD.mjs.map} +1 -1
  598. package/dist/{widgets-DZfmAbE4.mjs → widgets-8GONuRHo.mjs} +4 -4
  599. package/dist/{widgets-DZfmAbE4.mjs.map → widgets-8GONuRHo.mjs.map} +1 -1
  600. package/dist/{zod-generator-Djo_VHCt.mjs → zod-generator-D481JjzD.mjs} +3 -3
  601. package/dist/{zod-generator-Djo_VHCt.mjs.map → zod-generator-D481JjzD.mjs.map} +1 -1
  602. package/package.json +18 -6
  603. package/src/api/handlers/api-tokens.ts +19 -0
  604. package/src/api/handlers/comment-reactions.ts +165 -0
  605. package/src/api/handlers/redirects.ts +24 -13
  606. package/src/api/handlers/schema.ts +8 -0
  607. package/src/api/schemas/comments.ts +10 -0
  608. package/src/api/schemas/redirects.ts +11 -4
  609. package/src/astro/image-endpoint.ts +84 -0
  610. package/src/astro/index.ts +6 -0
  611. package/src/astro/integration/index.ts +127 -30
  612. package/src/astro/integration/routes.ts +51 -9
  613. package/src/astro/integration/runtime.ts +65 -1
  614. package/src/astro/integration/virtual-modules.ts +32 -0
  615. package/src/astro/integration/vite-config.ts +16 -0
  616. package/src/astro/middleware/auth.ts +4 -3
  617. package/src/astro/middleware/redirect.ts +12 -0
  618. package/src/astro/middleware/stream-end-metrics.ts +11 -1
  619. package/src/astro/middleware.ts +41 -3
  620. package/src/astro/object-cache/adapters.ts +50 -0
  621. package/src/astro/prefetch.ts +77 -0
  622. package/src/astro/routes/api/comments/[collection]/[contentId]/reactions.ts +97 -0
  623. package/src/astro/routes/api/setup/dev-bypass.ts +5 -1
  624. package/src/astro/routes/api/snapshot.ts +3 -1
  625. package/src/astro/session-user.ts +57 -0
  626. package/src/astro/types.ts +1 -0
  627. package/src/bylines/field-defs-cache.ts +70 -20
  628. package/src/cli/commands/doctor.ts +1 -1
  629. package/src/comments/query.ts +78 -7
  630. package/src/comments/ranking.ts +58 -0
  631. package/src/components/Comments.astro +136 -1
  632. package/src/config/secrets.ts +28 -14
  633. package/src/database/instrumentation.ts +21 -1
  634. package/src/database/migrations/044_comment_reactions.ts +56 -0
  635. package/src/database/migrations/runner.ts +2 -0
  636. package/src/database/repositories/byline.ts +13 -0
  637. package/src/database/repositories/comment-reaction.ts +132 -0
  638. package/src/database/repositories/comment.ts +10 -0
  639. package/src/database/repositories/content.ts +25 -3
  640. package/src/database/repositories/menu.ts +13 -1
  641. package/src/database/repositories/seo.ts +3 -0
  642. package/src/database/repositories/taxonomy.ts +13 -1
  643. package/src/database/types.ts +9 -0
  644. package/src/emdash-runtime.ts +23 -9
  645. package/src/index.ts +22 -0
  646. package/src/mcp/server.ts +307 -20
  647. package/src/media/image-endpoint.ts +167 -0
  648. package/src/menus/index.ts +11 -4
  649. package/src/object-cache/codec.ts +73 -0
  650. package/src/object-cache/index.ts +495 -0
  651. package/src/object-cache/memory.ts +91 -0
  652. package/src/object-cache/types.ts +124 -0
  653. package/src/plugins/adapt-sandbox-entry.ts +11 -1
  654. package/src/query.ts +196 -17
  655. package/src/redirects/status.ts +27 -0
  656. package/src/schema/query.ts +11 -4
  657. package/src/settings/index.ts +30 -17
  658. package/src/taxonomies/index.ts +237 -165
  659. package/src/utils/{isolate-cache.ts → single-flight-cache.ts} +26 -21
  660. package/src/virtual-modules.d.ts +11 -0
  661. package/dist/api-DStv36ik.mjs.map +0 -1
  662. package/dist/api-tokens-DPfhPu5V.mjs.map +0 -1
  663. package/dist/byline-DUx48sJp.mjs.map +0 -1
  664. package/dist/byline-fields-DbibsvTl.d.mts.map +0 -1
  665. package/dist/comment-sqQxNpN3.mjs.map +0 -1
  666. package/dist/content-BIlVx-RX.mjs.map +0 -1
  667. package/dist/index-B1keaX5Y.d.mts.map +0 -1
  668. package/dist/menus-DX4_E01q.mjs.map +0 -1
  669. package/dist/menus-DrQLusqj.mjs.map +0 -1
  670. package/dist/query-CbUcI4Xk.mjs.map +0 -1
  671. package/dist/redirects-CCbCqCCd.mjs.map +0 -1
  672. package/dist/redirects-DxVoR7PI.mjs.map +0 -1
  673. package/dist/runner--4wMWwKM.mjs.map +0 -1
  674. package/dist/schema-C1E70ug_.mjs.map +0 -1
  675. package/dist/secrets-ChPTmy9x.mjs.map +0 -1
  676. package/dist/seo-B5e6y9Wk.mjs.map +0 -1
  677. package/dist/settings-DfxiWY_s.mjs +0 -411
  678. package/dist/settings-DfxiWY_s.mjs.map +0 -1
  679. package/dist/taxonomies-BdAmbOwx.mjs.map +0 -1
  680. package/dist/taxonomy-CdllE4oq.mjs.map +0 -1
  681. package/dist/transport-CmpLD7W3.mjs.map +0 -1
  682. package/dist/version-D-5txk2m.mjs +0 -7
  683. /package/dist/{api-tokens-Oq39ba-Z.mjs → api-tokens-BVH-H8Z9.mjs} +0 -0
  684. /package/dist/{ssrf-BvgVcfNQ.mjs → ssrf-Cz1e2QPn.mjs} +0 -0
  685. /package/dist/{types-CZI4E3qG.mjs → types-Bfr2Pj6z.mjs} +0 -0
@@ -1,14 +1,15 @@
1
- import "../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../types-BXSUSAjt.mjs";
3
- import { t as OptionsRepository } from "../../../../../options-BPCVnesz.mjs";
4
- import { n as apiSuccess, t as apiError } from "../../../../../error-RwM4dD35.mjs";
5
- import { n as parseBody, t as isParseError } from "../../../../../parse-CrGndy1A.mjs";
6
- import "../../../../../redirects-CCbCqCCd.mjs";
7
- import { k as signupRequestBody } from "../../../../../byline-fields-8TMtkBnH.mjs";
1
+ import "../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../types-DXOIbece.mjs";
3
+ import { t as OptionsRepository } from "../../../../../options-DTTML-Tx.mjs";
4
+ import { n as apiSuccess, t as apiError } from "../../../../../error-BQ1pxs0L.mjs";
5
+ import { n as parseBody, t as isParseError } from "../../../../../parse-qrHlnzTy.mjs";
6
+ import "../../../../../redirects-DUvXgzCX.mjs";
7
+ import { k as signupRequestBody } from "../../../../../byline-fields-53i9Et0x.mjs";
8
+ import "../../../../../status-CB2basWJ.mjs";
8
9
  import "../../../../../api/schemas/index.mjs";
9
- import { t as getTrustedProxyHeaders } from "../../../../../trusted-proxy-B4AfnoAp.mjs";
10
- import { t as getSiteBaseUrl } from "../../../../../site-url-CnHlmAs9.mjs";
11
- import { n as getClientIp, t as checkRateLimit } from "../../../../../rate-limit-C7hjdkS5.mjs";
10
+ import { t as getTrustedProxyHeaders } from "../../../../../trusted-proxy-DY1_UqHr.mjs";
11
+ import { t as getSiteBaseUrl } from "../../../../../site-url-BOTPgmeM.mjs";
12
+ import { n as getClientIp, t as checkRateLimit } from "../../../../../rate-limit-DfNAqIWW.mjs";
12
13
  import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
13
14
  import { requestSignup } from "@emdash-cms/auth";
14
15
 
@@ -1 +1 @@
1
- {"version":3,"file":"request.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/signup/request.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/signup/request\n *\n * Request self-signup. Sends verification email if domain is allowed.\n * Always returns 200 to prevent email enumeration.\n *\n * Rate limited: 3 requests per 5 minutes per IP. Mirrors magic-link/send.\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { requestSignup } from \"@emdash-cms/auth\";\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\n\nimport { apiError, apiSuccess } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { signupRequestBody } from \"#api/schemas.js\";\nimport { getSiteBaseUrl } from \"#api/site-url.js\";\nimport { checkRateLimit, getClientIp } from \"#auth/rate-limit.js\";\nimport { getTrustedProxyHeaders } from \"#auth/trusted-proxy.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\n// Generic response body used for both the real success path and the\n// rate-limited / domain-disallowed paths. Keeping them identical prevents\n// the caller from distinguishing between them.\nconst GENERIC_SUCCESS = {\n\tsuccess: true,\n\tmessage: \"If your email domain is allowed, you'll receive a verification email.\",\n};\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Check if email pipeline is available\n\tif (!emdash.email?.isAvailable()) {\n\t\treturn apiError(\n\t\t\t\"EMAIL_NOT_CONFIGURED\",\n\t\t\t\"Email not configured. Self-signup is unavailable.\",\n\t\t\t503,\n\t\t);\n\t}\n\n\ttry {\n\t\t// Parse the body first — this avoids burning a rate-limit slot on\n\t\t// malformed input and keeps the timing of the rate-limited and\n\t\t// real paths aligned.\n\t\tconst body = await parseBody(request, signupRequestBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Rate limit: 3 requests per 300 seconds per IP. Matches magic-link/send.\n\t\tconst ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));\n\t\tconst rateLimit = await checkRateLimit(emdash.db, ip, \"signup/request\", 3, 300);\n\t\tif (!rateLimit.allowed) {\n\t\t\t// Return success-shaped response to avoid revealing rate limiting\n\t\t\t// (and by extension, the fact that the caller is probing).\n\t\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t\t}\n\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Get site config for signup email\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) || \"EmDash\";\n\n\t\t// Use stored site URL to prevent Host header spoofing in signup emails\n\t\tconst baseUrl = await getSiteBaseUrl(emdash.db, request);\n\n\t\t// Request signup - this handles all checks internally and fails silently\n\t\t// if domain not allowed or user exists (to prevent enumeration)\n\t\tawait requestSignup(\n\t\t\t{\n\t\t\t\tbaseUrl,\n\t\t\t\tsiteName,\n\t\t\t\temail: (message) => emdash.email!.send(message, \"system\"),\n\t\t\t},\n\t\t\tadapter,\n\t\t\tbody.email.toLowerCase().trim(),\n\t\t);\n\n\t\t// Always return success to prevent email enumeration\n\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t} catch (error) {\n\t\tconsole.error(\"Signup request error:\", error);\n\n\t\t// Don't reveal internal errors - just return generic success\n\t\t// to prevent information leakage\n\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;AAWA,MAAa,YAAY;AAgBzB,MAAM,kBAAkB;CACvB,SAAS;CACT,SAAS;CACT;AAED,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAIpE,KAAI,CAAC,OAAO,OAAO,aAAa,CAC/B,QAAO,SACN,wBACA,qDACA,IACA;AAGF,KAAI;EAIH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,KAAK,YAAY,SAAS,uBAAuB,OAAO,OAAO,CAAC;AAEtE,MAAI,EADc,MAAM,eAAe,OAAO,IAAI,IAAI,kBAAkB,GAAG,IAAI,EAChE,QAGd,QAAO,WAAW,gBAAgB;EAGnC,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAI9C,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;AAOrE,QAAM,cACL;GACC,SANc,MAAM,eAAe,OAAO,IAAI,QAAQ;GAOtD;GACA,QAAQ,YAAY,OAAO,MAAO,KAAK,SAAS,SAAS;GACzD,EACD,SACA,KAAK,MAAM,aAAa,CAAC,MAAM,CAC/B;AAGD,SAAO,WAAW,gBAAgB;UAC1B,OAAO;AACf,UAAQ,MAAM,yBAAyB,MAAM;AAI7C,SAAO,WAAW,gBAAgB"}
1
+ {"version":3,"file":"request.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/signup/request.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/signup/request\n *\n * Request self-signup. Sends verification email if domain is allowed.\n * Always returns 200 to prevent email enumeration.\n *\n * Rate limited: 3 requests per 5 minutes per IP. Mirrors magic-link/send.\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { requestSignup } from \"@emdash-cms/auth\";\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\n\nimport { apiError, apiSuccess } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { signupRequestBody } from \"#api/schemas.js\";\nimport { getSiteBaseUrl } from \"#api/site-url.js\";\nimport { checkRateLimit, getClientIp } from \"#auth/rate-limit.js\";\nimport { getTrustedProxyHeaders } from \"#auth/trusted-proxy.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\n// Generic response body used for both the real success path and the\n// rate-limited / domain-disallowed paths. Keeping them identical prevents\n// the caller from distinguishing between them.\nconst GENERIC_SUCCESS = {\n\tsuccess: true,\n\tmessage: \"If your email domain is allowed, you'll receive a verification email.\",\n};\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Check if email pipeline is available\n\tif (!emdash.email?.isAvailable()) {\n\t\treturn apiError(\n\t\t\t\"EMAIL_NOT_CONFIGURED\",\n\t\t\t\"Email not configured. Self-signup is unavailable.\",\n\t\t\t503,\n\t\t);\n\t}\n\n\ttry {\n\t\t// Parse the body first — this avoids burning a rate-limit slot on\n\t\t// malformed input and keeps the timing of the rate-limited and\n\t\t// real paths aligned.\n\t\tconst body = await parseBody(request, signupRequestBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Rate limit: 3 requests per 300 seconds per IP. Matches magic-link/send.\n\t\tconst ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));\n\t\tconst rateLimit = await checkRateLimit(emdash.db, ip, \"signup/request\", 3, 300);\n\t\tif (!rateLimit.allowed) {\n\t\t\t// Return success-shaped response to avoid revealing rate limiting\n\t\t\t// (and by extension, the fact that the caller is probing).\n\t\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t\t}\n\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Get site config for signup email\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) || \"EmDash\";\n\n\t\t// Use stored site URL to prevent Host header spoofing in signup emails\n\t\tconst baseUrl = await getSiteBaseUrl(emdash.db, request);\n\n\t\t// Request signup - this handles all checks internally and fails silently\n\t\t// if domain not allowed or user exists (to prevent enumeration)\n\t\tawait requestSignup(\n\t\t\t{\n\t\t\t\tbaseUrl,\n\t\t\t\tsiteName,\n\t\t\t\temail: (message) => emdash.email!.send(message, \"system\"),\n\t\t\t},\n\t\t\tadapter,\n\t\t\tbody.email.toLowerCase().trim(),\n\t\t);\n\n\t\t// Always return success to prevent email enumeration\n\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t} catch (error) {\n\t\tconsole.error(\"Signup request error:\", error);\n\n\t\t// Don't reveal internal errors - just return generic success\n\t\t// to prevent information leakage\n\t\treturn apiSuccess(GENERIC_SUCCESS);\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAWA,MAAa,YAAY;AAgBzB,MAAM,kBAAkB;CACvB,SAAS;CACT,SAAS;CACT;AAED,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAIpE,KAAI,CAAC,OAAO,OAAO,aAAa,CAC/B,QAAO,SACN,wBACA,qDACA,IACA;AAGF,KAAI;EAIH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,KAAK,YAAY,SAAS,uBAAuB,OAAO,OAAO,CAAC;AAEtE,MAAI,EADc,MAAM,eAAe,OAAO,IAAI,IAAI,kBAAkB,GAAG,IAAI,EAChE,QAGd,QAAO,WAAW,gBAAgB;EAGnC,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAI9C,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;AAOrE,QAAM,cACL;GACC,SANc,MAAM,eAAe,OAAO,IAAI,QAAQ;GAOtD;GACA,QAAQ,YAAY,OAAO,MAAO,KAAK,SAAS,SAAS;GACzD,EACD,SACA,KAAK,MAAM,aAAa,CAAC,MAAM,CAC/B;AAGD,SAAO,WAAW,gBAAgB;UAC1B,OAAO;AACf,UAAQ,MAAM,yBAAyB,MAAM;AAI7C,SAAO,WAAW,gBAAgB"}
@@ -1,6 +1,6 @@
1
- import "../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../types-BXSUSAjt.mjs";
3
- import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-RwM4dD35.mjs";
1
+ import "../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../types-DXOIbece.mjs";
3
+ import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-BQ1pxs0L.mjs";
4
4
  import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
5
5
  import { SignupError, roleFromLevel, validateSignupToken } from "@emdash-cms/auth";
6
6
 
@@ -1,18 +1,21 @@
1
1
  import { t as validateIdentifier } from "../../../../../../validate-VPnKoIzW.mjs";
2
- import "../../../../../../base64-CqR-7kqF.mjs";
3
- import "../../../../../../types-BXSUSAjt.mjs";
4
- import { t as CommentRepository } from "../../../../../../comment-sqQxNpN3.mjs";
5
- import "../../../../../../options-BPCVnesz.mjs";
6
- import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
7
- import { n as parseBody, t as isParseError } from "../../../../../../parse-CrGndy1A.mjs";
8
- import { yt as createCommentBody } from "../../../../../../redirects-CCbCqCCd.mjs";
9
- import "../../../../../../byline-fields-8TMtkBnH.mjs";
2
+ import "../../../../../../after-B1IIdH3Y.mjs";
3
+ import "../../../../../../object-cache-SEb2IM4Z.mjs";
4
+ import "../../../../../../base64-CmWvODNW.mjs";
5
+ import "../../../../../../types-DXOIbece.mjs";
6
+ import { t as CommentRepository } from "../../../../../../comment-80UyJJUR.mjs";
7
+ import "../../../../../../options-DTTML-Tx.mjs";
8
+ import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
9
+ import { n as parseBody, t as isParseError } from "../../../../../../parse-qrHlnzTy.mjs";
10
+ import { yt as createCommentBody } from "../../../../../../redirects-DUvXgzCX.mjs";
11
+ import "../../../../../../byline-fields-53i9Et0x.mjs";
12
+ import "../../../../../../status-CB2basWJ.mjs";
10
13
  import "../../../../../../api/schemas/index.mjs";
11
- import { t as extractRequestMeta } from "../../../../../../request-meta-7ByVLxB-.mjs";
12
- import { i as resolveSecretsCached } from "../../../../../../secrets-ChPTmy9x.mjs";
13
- import { c as hashIp, s as handleCommentList, t as checkRateLimit } from "../../../../../../comments-Vkivawyl.mjs";
14
- import { t as getSiteBaseUrl } from "../../../../../../site-url-CnHlmAs9.mjs";
15
- import { i as sendCommentNotification, t as createComment } from "../../../../../../service-ChDcsTBs.mjs";
14
+ import { t as extractRequestMeta } from "../../../../../../request-meta-ei3ATilC.mjs";
15
+ import { i as resolveSecretsCached } from "../../../../../../secrets-BUE5UQys.mjs";
16
+ import { c as hashIp, s as handleCommentList, t as checkRateLimit } from "../../../../../../comments-CtPkT2tp.mjs";
17
+ import { t as getSiteBaseUrl } from "../../../../../../site-url-BOTPgmeM.mjs";
18
+ import { i as sendCommentNotification, t as createComment } from "../../../../../../service-DTmtqCnw.mjs";
16
19
 
17
20
  //#region src/astro/routes/api/comments/[collection]/[contentId]/index.ts
18
21
  const prerender = false;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/index.ts"],"sourcesContent":["/**\n * Public comment endpoints\n *\n * GET /_emdash/api/comments/:collection/:contentId - List approved comments\n * POST /_emdash/api/comments/:collection/:contentId - Submit a comment\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { apiError, apiSuccess, handleError, requireDb, unwrapResult } from \"#api/error.js\";\nimport { handleCommentList, checkRateLimit, hashIp } from \"#api/handlers/comments.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { createCommentBody } from \"#api/schemas.js\";\nimport { getSiteBaseUrl } from \"#api/site-url.js\";\nimport { sendCommentNotification } from \"#comments/notifications.js\";\nimport { createComment, type CommentHookRunner } from \"#comments/service.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\nimport { CommentRepository } from \"#db/repositories/comment.js\";\nimport { validateIdentifier } from \"#db/validate.js\";\nimport { extractRequestMeta } from \"#plugins/request-meta.js\";\nimport type { CollectionCommentSettings, ModerationDecision } from \"#plugins/types.js\";\n\nexport const prerender = false;\n\n/**\n * List approved comments for a content item (public, no auth required)\n */\nexport const GET: APIRoute = async ({ params, url, locals }) => {\n\tconst { emdash } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\tconst limit = Math.min(Number(url.searchParams.get(\"limit\") || 50), 100);\n\t\tconst cursor = url.searchParams.get(\"cursor\") ?? undefined;\n\t\tconst threaded = url.searchParams.get(\"threaded\") === \"true\";\n\n\t\t// Check collection exists and has comments enabled\n\t\tconst collectionRow = await emdash.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"comments_enabled\"])\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!collectionRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", `Collection '${collection}' not found`, 404);\n\t\t}\n\n\t\tif (!collectionRow.comments_enabled) {\n\t\t\treturn apiError(\"COMMENTS_DISABLED\", \"Comments are not enabled for this collection\", 403);\n\t\t}\n\n\t\tconst result = await handleCommentList(emdash.db, collection, contentId, {\n\t\t\tlimit,\n\t\t\tcursor,\n\t\t\tthreaded,\n\t\t});\n\n\t\treturn unwrapResult(result);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to list comments\", \"COMMENT_LIST_ERROR\");\n\t}\n};\n\n/**\n * Submit a comment (public, gated by anti-spam checks)\n */\nexport const POST: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\t// Parse and validate input\n\t\tconst body = await parseBody(request, createCommentBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Check collection exists and has comments enabled\n\t\tconst collectionRow = await emdash.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\n\t\t\t\t\"comments_enabled\",\n\t\t\t\t\"comments_moderation\",\n\t\t\t\t\"comments_closed_after_days\",\n\t\t\t\t\"comments_auto_approve_users\",\n\t\t\t])\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!collectionRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", `Collection '${collection}' not found`, 404);\n\t\t}\n\n\t\tif (!collectionRow.comments_enabled) {\n\t\t\treturn apiError(\"COMMENTS_DISABLED\", \"Comments are not enabled for this collection\", 403);\n\t\t}\n\n\t\t// Verify the content item exists, is published, and not soft-deleted\n\t\tvalidateIdentifier(collection, \"collection\");\n\t\tconst contentRow = await emdash.db\n\t\t\t.selectFrom(`ec_${collection}` as never)\n\t\t\t.select([\"id\" as never, \"slug\" as never, \"author_id\" as never, \"published_at\" as never])\n\t\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t\t.where(\"status\" as never, \"=\", \"published\" as never)\n\t\t\t.where(\"deleted_at\" as never, \"is\", null as never)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!contentRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", \"Content not found\", 404);\n\t\t}\n\n\t\t// Check if comments are closed (published_at + closed_after_days)\n\t\tif (collectionRow.comments_closed_after_days > 0) {\n\t\t\tconst publishedAt = (contentRow as { published_at: string | null }).published_at;\n\t\t\tif (publishedAt) {\n\t\t\t\tconst closedDate = new Date(publishedAt);\n\t\t\t\tclosedDate.setDate(closedDate.getDate() + collectionRow.comments_closed_after_days);\n\t\t\t\tif (new Date() > closedDate) {\n\t\t\t\t\treturn apiError(\"COMMENTS_CLOSED\", \"Comments are closed for this content\", 403);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Anti-spam: Honeypot — hidden field filled only by bots\n\t\tif (body.website_url) {\n\t\t\t// Silently accept — don't reveal the honeypot to bots\n\t\t\treturn apiSuccess({ status: \"pending\", message: \"Comment submitted for review\" });\n\t\t}\n\n\t\t// Anti-spam: Rate limiting\n\t\tconst meta = extractRequestMeta(request, emdash.config);\n\t\tconst { ipSalt } = await resolveSecretsCached(emdash.db);\n\t\tlet ipHash: string;\n\t\tif (meta.ip) {\n\t\t\tipHash = await hashIp(meta.ip, ipSalt);\n\t\t} else {\n\t\t\t// No trusted IP — fail closed by bucketing all unidentifiable\n\t\t\t// requests together. A larger limit reflects the shared bucket.\n\t\t\t//\n\t\t\t// Self-hosted operators behind a reverse proxy should set\n\t\t\t// `trustedProxyHeaders` in the EmDash config (or the\n\t\t\t// EMDASH_TRUSTED_PROXY_HEADERS env var) so this path isn't hit\n\t\t\t// for legitimate traffic. UA-hashing was previously used here\n\t\t\t// but was trivially rotatable — the shared bucket is stricter\n\t\t\t// and forces operators toward a real fix.\n\t\t\tipHash = \"unknown\";\n\t\t}\n\t\tconst unknownBucketLimit = ipHash === \"unknown\" ? 20 : undefined;\n\t\tconst rateLimited = await checkRateLimit(emdash.db, ipHash, unknownBucketLimit);\n\t\tif (rateLimited) {\n\t\t\treturn apiError(\"RATE_LIMITED\", \"Too many comments. Please try again later.\", 429);\n\t\t}\n\n\t\t// Build collection settings\n\t\tconst collectionSettings: CollectionCommentSettings = {\n\t\t\tcommentsEnabled: collectionRow.comments_enabled === 1,\n\t\t\tcommentsModeration:\n\t\t\t\tcollectionRow.comments_moderation as CollectionCommentSettings[\"commentsModeration\"],\n\t\t\tcommentsClosedAfterDays: collectionRow.comments_closed_after_days,\n\t\t\tcommentsAutoApproveUsers: collectionRow.comments_auto_approve_users === 1,\n\t\t};\n\n\t\t// Determine author fields — authenticated user overrides form input\n\t\tlet authorName = body.authorName;\n\t\tlet authorEmail = body.authorEmail;\n\t\tlet authorUserId: string | null = null;\n\n\t\tif (user) {\n\t\t\tauthorName = user.name || authorName;\n\t\t\tauthorEmail = user.email;\n\t\t\tauthorUserId = user.id;\n\t\t}\n\n\t\t// Validate parent exists and belongs to the same content.\n\t\t// Enforce 1-level nesting: if the parent is itself a reply, attach to its root.\n\t\tlet resolvedParentId = body.parentId ?? null;\n\t\tif (body.parentId) {\n\t\t\tconst repo = new CommentRepository(emdash.db);\n\t\t\tconst parent = await repo.findById(body.parentId);\n\t\t\tif (!parent) {\n\t\t\t\treturn apiError(\"VALIDATION_ERROR\", \"Parent comment not found\", 400);\n\t\t\t}\n\t\t\tif (parent.collection !== collection || parent.contentId !== contentId) {\n\t\t\t\treturn apiError(\"VALIDATION_ERROR\", \"Parent comment belongs to different content\", 400);\n\t\t\t}\n\t\t\t// Flatten: if parent is a reply, use its parent (the root) instead\n\t\t\tresolvedParentId = parent.parentId ?? parent.id;\n\t\t}\n\n\t\t// Wire the comment service to the real hook pipeline\n\t\tconst hookRunner: CommentHookRunner = {\n\t\t\tasync runBeforeCreate(event) {\n\t\t\t\treturn emdash.hooks.runCommentBeforeCreate(event);\n\t\t\t},\n\t\t\tasync runModerate(event) {\n\t\t\t\tconst result = await emdash.hooks.invokeExclusiveHook(\"comment:moderate\", event);\n\t\t\t\tif (!result) return { status: \"pending\" as const, reason: \"No moderator configured\" };\n\t\t\t\tif (result.error) {\n\t\t\t\t\tconsole.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);\n\t\t\t\t\treturn { status: \"pending\" as const, reason: \"Moderation error\" };\n\t\t\t\t}\n\t\t\t\treturn result.result as ModerationDecision;\n\t\t\t},\n\t\t\tfireAfterCreate(event) {\n\t\t\t\temdash.hooks\n\t\t\t\t\t.runCommentAfterCreate(event)\n\t\t\t\t\t.catch((err) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterCreate error:\",\n\t\t\t\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t\tfireAfterModerate(event) {\n\t\t\t\temdash.hooks\n\t\t\t\t\t.runCommentAfterModerate(event)\n\t\t\t\t\t.catch((err) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterModerate error:\",\n\t\t\t\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\t// Build content info for afterCreate hooks (e.g. email notifications)\n\t\tconst typedContent = contentRow as {\n\t\t\tid: string;\n\t\t\tslug: string;\n\t\t\tauthor_id: string | null;\n\t\t};\n\t\tlet contentAuthor: { id: string; name: string | null; email: string } | undefined;\n\t\tif (typedContent.author_id) {\n\t\t\tconst authorRow = await emdash.db\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t\t.where(\"id\", \"=\", typedContent.author_id)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (authorRow && authorRow.email_verified) {\n\t\t\t\tcontentAuthor = {\n\t\t\t\t\tid: authorRow.id,\n\t\t\t\t\tname: authorRow.name,\n\t\t\t\t\temail: authorRow.email,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst result = await createComment(\n\t\t\temdash.db,\n\t\t\t{\n\t\t\t\tcollection,\n\t\t\t\tcontentId,\n\t\t\t\tparentId: resolvedParentId,\n\t\t\t\tauthorName,\n\t\t\t\tauthorEmail,\n\t\t\t\tauthorUserId,\n\t\t\t\tbody: body.body,\n\t\t\t\tipHash,\n\t\t\t\tuserAgent: meta.userAgent,\n\t\t\t},\n\t\t\tcollectionSettings,\n\t\t\thookRunner,\n\t\t\t{\n\t\t\t\tid: typedContent.id,\n\t\t\t\tcollection,\n\t\t\t\tslug: typedContent.slug,\n\t\t\t\tauthor: contentAuthor,\n\t\t\t},\n\t\t);\n\n\t\tif (!result) {\n\t\t\treturn apiError(\"COMMENT_REJECTED\", \"Comment was rejected\", 403);\n\t\t}\n\n\t\t// Send notification to content author (awaited so it completes before\n\t\t// the response is sent — required for Cloudflare Workers where the\n\t\t// isolate terminates after the response).\n\t\tif (result.comment.status === \"approved\" && emdash.email && contentAuthor) {\n\t\t\ttry {\n\t\t\t\tconst adminBaseUrl = await getSiteBaseUrl(emdash.db, request);\n\t\t\t\tawait sendCommentNotification({\n\t\t\t\t\temail: emdash.email,\n\t\t\t\t\tcomment: result.comment,\n\t\t\t\t\tcontentAuthor,\n\t\t\t\t\tadminBaseUrl,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[comments] notification error:\", err instanceof Error ? err.message : err);\n\t\t\t}\n\t\t}\n\n\t\treturn apiSuccess(\n\t\t\t{\n\t\t\t\tid: result.comment.id,\n\t\t\t\tstatus: result.comment.status,\n\t\t\t\tmessage:\n\t\t\t\t\tresult.comment.status === \"approved\"\n\t\t\t\t\t\t? \"Comment published\"\n\t\t\t\t\t\t: \"Comment submitted for review\",\n\t\t\t},\n\t\t\t201,\n\t\t);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to submit comment\", \"COMMENT_CREATE_ERROR\");\n\t}\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/index.ts"],"sourcesContent":["/**\n * Public comment endpoints\n *\n * GET /_emdash/api/comments/:collection/:contentId - List approved comments\n * POST /_emdash/api/comments/:collection/:contentId - Submit a comment\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { apiError, apiSuccess, handleError, requireDb, unwrapResult } from \"#api/error.js\";\nimport { handleCommentList, checkRateLimit, hashIp } from \"#api/handlers/comments.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { createCommentBody } from \"#api/schemas.js\";\nimport { getSiteBaseUrl } from \"#api/site-url.js\";\nimport { sendCommentNotification } from \"#comments/notifications.js\";\nimport { createComment, type CommentHookRunner } from \"#comments/service.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\nimport { CommentRepository } from \"#db/repositories/comment.js\";\nimport { validateIdentifier } from \"#db/validate.js\";\nimport { extractRequestMeta } from \"#plugins/request-meta.js\";\nimport type { CollectionCommentSettings, ModerationDecision } from \"#plugins/types.js\";\n\nexport const prerender = false;\n\n/**\n * List approved comments for a content item (public, no auth required)\n */\nexport const GET: APIRoute = async ({ params, url, locals }) => {\n\tconst { emdash } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\tconst limit = Math.min(Number(url.searchParams.get(\"limit\") || 50), 100);\n\t\tconst cursor = url.searchParams.get(\"cursor\") ?? undefined;\n\t\tconst threaded = url.searchParams.get(\"threaded\") === \"true\";\n\n\t\t// Check collection exists and has comments enabled\n\t\tconst collectionRow = await emdash.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"comments_enabled\"])\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!collectionRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", `Collection '${collection}' not found`, 404);\n\t\t}\n\n\t\tif (!collectionRow.comments_enabled) {\n\t\t\treturn apiError(\"COMMENTS_DISABLED\", \"Comments are not enabled for this collection\", 403);\n\t\t}\n\n\t\tconst result = await handleCommentList(emdash.db, collection, contentId, {\n\t\t\tlimit,\n\t\t\tcursor,\n\t\t\tthreaded,\n\t\t});\n\n\t\treturn unwrapResult(result);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to list comments\", \"COMMENT_LIST_ERROR\");\n\t}\n};\n\n/**\n * Submit a comment (public, gated by anti-spam checks)\n */\nexport const POST: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\t// Parse and validate input\n\t\tconst body = await parseBody(request, createCommentBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Check collection exists and has comments enabled\n\t\tconst collectionRow = await emdash.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\n\t\t\t\t\"comments_enabled\",\n\t\t\t\t\"comments_moderation\",\n\t\t\t\t\"comments_closed_after_days\",\n\t\t\t\t\"comments_auto_approve_users\",\n\t\t\t])\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!collectionRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", `Collection '${collection}' not found`, 404);\n\t\t}\n\n\t\tif (!collectionRow.comments_enabled) {\n\t\t\treturn apiError(\"COMMENTS_DISABLED\", \"Comments are not enabled for this collection\", 403);\n\t\t}\n\n\t\t// Verify the content item exists, is published, and not soft-deleted\n\t\tvalidateIdentifier(collection, \"collection\");\n\t\tconst contentRow = await emdash.db\n\t\t\t.selectFrom(`ec_${collection}` as never)\n\t\t\t.select([\"id\" as never, \"slug\" as never, \"author_id\" as never, \"published_at\" as never])\n\t\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t\t.where(\"status\" as never, \"=\", \"published\" as never)\n\t\t\t.where(\"deleted_at\" as never, \"is\", null as never)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!contentRow) {\n\t\t\treturn apiError(\"NOT_FOUND\", \"Content not found\", 404);\n\t\t}\n\n\t\t// Check if comments are closed (published_at + closed_after_days)\n\t\tif (collectionRow.comments_closed_after_days > 0) {\n\t\t\tconst publishedAt = (contentRow as { published_at: string | null }).published_at;\n\t\t\tif (publishedAt) {\n\t\t\t\tconst closedDate = new Date(publishedAt);\n\t\t\t\tclosedDate.setDate(closedDate.getDate() + collectionRow.comments_closed_after_days);\n\t\t\t\tif (new Date() > closedDate) {\n\t\t\t\t\treturn apiError(\"COMMENTS_CLOSED\", \"Comments are closed for this content\", 403);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Anti-spam: Honeypot — hidden field filled only by bots\n\t\tif (body.website_url) {\n\t\t\t// Silently accept — don't reveal the honeypot to bots\n\t\t\treturn apiSuccess({ status: \"pending\", message: \"Comment submitted for review\" });\n\t\t}\n\n\t\t// Anti-spam: Rate limiting\n\t\tconst meta = extractRequestMeta(request, emdash.config);\n\t\tconst { ipSalt } = await resolveSecretsCached(emdash.db);\n\t\tlet ipHash: string;\n\t\tif (meta.ip) {\n\t\t\tipHash = await hashIp(meta.ip, ipSalt);\n\t\t} else {\n\t\t\t// No trusted IP — fail closed by bucketing all unidentifiable\n\t\t\t// requests together. A larger limit reflects the shared bucket.\n\t\t\t//\n\t\t\t// Self-hosted operators behind a reverse proxy should set\n\t\t\t// `trustedProxyHeaders` in the EmDash config (or the\n\t\t\t// EMDASH_TRUSTED_PROXY_HEADERS env var) so this path isn't hit\n\t\t\t// for legitimate traffic. UA-hashing was previously used here\n\t\t\t// but was trivially rotatable — the shared bucket is stricter\n\t\t\t// and forces operators toward a real fix.\n\t\t\tipHash = \"unknown\";\n\t\t}\n\t\tconst unknownBucketLimit = ipHash === \"unknown\" ? 20 : undefined;\n\t\tconst rateLimited = await checkRateLimit(emdash.db, ipHash, unknownBucketLimit);\n\t\tif (rateLimited) {\n\t\t\treturn apiError(\"RATE_LIMITED\", \"Too many comments. Please try again later.\", 429);\n\t\t}\n\n\t\t// Build collection settings\n\t\tconst collectionSettings: CollectionCommentSettings = {\n\t\t\tcommentsEnabled: collectionRow.comments_enabled === 1,\n\t\t\tcommentsModeration:\n\t\t\t\tcollectionRow.comments_moderation as CollectionCommentSettings[\"commentsModeration\"],\n\t\t\tcommentsClosedAfterDays: collectionRow.comments_closed_after_days,\n\t\t\tcommentsAutoApproveUsers: collectionRow.comments_auto_approve_users === 1,\n\t\t};\n\n\t\t// Determine author fields — authenticated user overrides form input\n\t\tlet authorName = body.authorName;\n\t\tlet authorEmail = body.authorEmail;\n\t\tlet authorUserId: string | null = null;\n\n\t\tif (user) {\n\t\t\tauthorName = user.name || authorName;\n\t\t\tauthorEmail = user.email;\n\t\t\tauthorUserId = user.id;\n\t\t}\n\n\t\t// Validate parent exists and belongs to the same content.\n\t\t// Enforce 1-level nesting: if the parent is itself a reply, attach to its root.\n\t\tlet resolvedParentId = body.parentId ?? null;\n\t\tif (body.parentId) {\n\t\t\tconst repo = new CommentRepository(emdash.db);\n\t\t\tconst parent = await repo.findById(body.parentId);\n\t\t\tif (!parent) {\n\t\t\t\treturn apiError(\"VALIDATION_ERROR\", \"Parent comment not found\", 400);\n\t\t\t}\n\t\t\tif (parent.collection !== collection || parent.contentId !== contentId) {\n\t\t\t\treturn apiError(\"VALIDATION_ERROR\", \"Parent comment belongs to different content\", 400);\n\t\t\t}\n\t\t\t// Flatten: if parent is a reply, use its parent (the root) instead\n\t\t\tresolvedParentId = parent.parentId ?? parent.id;\n\t\t}\n\n\t\t// Wire the comment service to the real hook pipeline\n\t\tconst hookRunner: CommentHookRunner = {\n\t\t\tasync runBeforeCreate(event) {\n\t\t\t\treturn emdash.hooks.runCommentBeforeCreate(event);\n\t\t\t},\n\t\t\tasync runModerate(event) {\n\t\t\t\tconst result = await emdash.hooks.invokeExclusiveHook(\"comment:moderate\", event);\n\t\t\t\tif (!result) return { status: \"pending\" as const, reason: \"No moderator configured\" };\n\t\t\t\tif (result.error) {\n\t\t\t\t\tconsole.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);\n\t\t\t\t\treturn { status: \"pending\" as const, reason: \"Moderation error\" };\n\t\t\t\t}\n\t\t\t\treturn result.result as ModerationDecision;\n\t\t\t},\n\t\t\tfireAfterCreate(event) {\n\t\t\t\temdash.hooks\n\t\t\t\t\t.runCommentAfterCreate(event)\n\t\t\t\t\t.catch((err) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterCreate error:\",\n\t\t\t\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t\tfireAfterModerate(event) {\n\t\t\t\temdash.hooks\n\t\t\t\t\t.runCommentAfterModerate(event)\n\t\t\t\t\t.catch((err) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterModerate error:\",\n\t\t\t\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\t// Build content info for afterCreate hooks (e.g. email notifications)\n\t\tconst typedContent = contentRow as {\n\t\t\tid: string;\n\t\t\tslug: string;\n\t\t\tauthor_id: string | null;\n\t\t};\n\t\tlet contentAuthor: { id: string; name: string | null; email: string } | undefined;\n\t\tif (typedContent.author_id) {\n\t\t\tconst authorRow = await emdash.db\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t\t.where(\"id\", \"=\", typedContent.author_id)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (authorRow && authorRow.email_verified) {\n\t\t\t\tcontentAuthor = {\n\t\t\t\t\tid: authorRow.id,\n\t\t\t\t\tname: authorRow.name,\n\t\t\t\t\temail: authorRow.email,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst result = await createComment(\n\t\t\temdash.db,\n\t\t\t{\n\t\t\t\tcollection,\n\t\t\t\tcontentId,\n\t\t\t\tparentId: resolvedParentId,\n\t\t\t\tauthorName,\n\t\t\t\tauthorEmail,\n\t\t\t\tauthorUserId,\n\t\t\t\tbody: body.body,\n\t\t\t\tipHash,\n\t\t\t\tuserAgent: meta.userAgent,\n\t\t\t},\n\t\t\tcollectionSettings,\n\t\t\thookRunner,\n\t\t\t{\n\t\t\t\tid: typedContent.id,\n\t\t\t\tcollection,\n\t\t\t\tslug: typedContent.slug,\n\t\t\t\tauthor: contentAuthor,\n\t\t\t},\n\t\t);\n\n\t\tif (!result) {\n\t\t\treturn apiError(\"COMMENT_REJECTED\", \"Comment was rejected\", 403);\n\t\t}\n\n\t\t// Send notification to content author (awaited so it completes before\n\t\t// the response is sent — required for Cloudflare Workers where the\n\t\t// isolate terminates after the response).\n\t\tif (result.comment.status === \"approved\" && emdash.email && contentAuthor) {\n\t\t\ttry {\n\t\t\t\tconst adminBaseUrl = await getSiteBaseUrl(emdash.db, request);\n\t\t\t\tawait sendCommentNotification({\n\t\t\t\t\temail: emdash.email,\n\t\t\t\t\tcomment: result.comment,\n\t\t\t\t\tcontentAuthor,\n\t\t\t\t\tadminBaseUrl,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[comments] notification error:\", err instanceof Error ? err.message : err);\n\t\t\t}\n\t\t}\n\n\t\treturn apiSuccess(\n\t\t\t{\n\t\t\t\tid: result.comment.id,\n\t\t\t\tstatus: result.comment.status,\n\t\t\t\tmessage:\n\t\t\t\t\tresult.comment.status === \"approved\"\n\t\t\t\t\t\t? \"Comment published\"\n\t\t\t\t\t\t: \"Comment submitted for review\",\n\t\t\t},\n\t\t\t201,\n\t\t);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to submit comment\", \"COMMENT_CREATE_ERROR\");\n\t}\n};\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { APIRoute } from "astro";
2
+
3
+ //#region src/astro/routes/api/comments/[collection]/[contentId]/reactions.d.ts
4
+ declare const prerender = false;
5
+ declare const GET: APIRoute;
6
+ declare const POST: APIRoute;
7
+ //#endregion
8
+ export { GET, POST, prerender };
9
+ //# sourceMappingURL=reactions.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.d.mts","names":[],"sources":["../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/reactions.ts"],"mappings":";;;cAuBa,SAAA;AAAA,cAEA,GAAA,EAAK,QAAA;AAAA,cAgCL,IAAA,EAAM,QAAA"}
@@ -0,0 +1,179 @@
1
+ import "../../../../../../after-B1IIdH3Y.mjs";
2
+ import { s as invalidateCommentObjectCache } from "../../../../../../object-cache-SEb2IM4Z.mjs";
3
+ import "../../../../../../base64-CmWvODNW.mjs";
4
+ import "../../../../../../types-DXOIbece.mjs";
5
+ import "../../../../../../comment-80UyJJUR.mjs";
6
+ import "../../../../../../options-DTTML-Tx.mjs";
7
+ import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
8
+ import { n as parseBody, t as isParseError } from "../../../../../../parse-qrHlnzTy.mjs";
9
+ import { bt as createReactionBody } from "../../../../../../redirects-DUvXgzCX.mjs";
10
+ import "../../../../../../byline-fields-53i9Et0x.mjs";
11
+ import "../../../../../../status-CB2basWJ.mjs";
12
+ import "../../../../../../api/schemas/index.mjs";
13
+ import { t as extractRequestMeta } from "../../../../../../request-meta-ei3ATilC.mjs";
14
+ import { t as CommentReactionRepository } from "../../../../../../comment-reaction-B6AdcpXw.mjs";
15
+ import { i as resolveSecretsCached } from "../../../../../../secrets-BUE5UQys.mjs";
16
+ import { c as hashIp } from "../../../../../../comments-CtPkT2tp.mjs";
17
+
18
+ //#region src/api/handlers/comment-reactions.ts
19
+ /** Max reactions a single voter may register per window before throttling. */
20
+ const REACTION_RATE_LIMIT = 30;
21
+ const REACTION_RATE_WINDOW_MINUTES = 10;
22
+ /**
23
+ * Reactions the system accepts. Positive-only for now (matches the shipped
24
+ * widget); kept as an allowlist so a voter can't spam arbitrary reaction
25
+ * strings and bloat a comment's count map. Extend (or make configurable) as
26
+ * the UI grows.
27
+ */
28
+ const ALLOWED_REACTIONS = new Set(["like"]);
29
+ /**
30
+ * Toggle a reaction for a voter on an approved comment belonging to the given
31
+ * content item. Rate-limited per voter.
32
+ */
33
+ async function handleReactionToggle(db, params) {
34
+ try {
35
+ const { collection, contentId, commentId, reaction, voterHash } = params;
36
+ if (!ALLOWED_REACTIONS.has(reaction)) return {
37
+ success: false,
38
+ error: {
39
+ code: "VALIDATION_ERROR",
40
+ message: "Unsupported reaction"
41
+ }
42
+ };
43
+ const comment = await db.selectFrom("_emdash_comments").select(["id", "status"]).where("id", "=", commentId).where("collection", "=", collection).where("content_id", "=", contentId).executeTakeFirst();
44
+ if (!comment) return {
45
+ success: false,
46
+ error: {
47
+ code: "NOT_FOUND",
48
+ message: "Comment not found"
49
+ }
50
+ };
51
+ if (comment.status !== "approved") return {
52
+ success: false,
53
+ error: {
54
+ code: "COMMENT_NOT_APPROVED",
55
+ message: "Cannot react to this comment"
56
+ }
57
+ };
58
+ const repo = new CommentReactionRepository(db);
59
+ if (await repo.countRecentByVoter(voterHash, REACTION_RATE_WINDOW_MINUTES) >= REACTION_RATE_LIMIT) return {
60
+ success: false,
61
+ error: {
62
+ code: "RATE_LIMITED",
63
+ message: "Too many reactions. Please try again later."
64
+ }
65
+ };
66
+ const { reacted } = await repo.toggle({
67
+ commentId,
68
+ reaction,
69
+ voterHash
70
+ });
71
+ const countsMap = await repo.countsForComments([commentId]);
72
+ invalidateCommentObjectCache();
73
+ return {
74
+ success: true,
75
+ data: {
76
+ commentId,
77
+ reaction,
78
+ reacted,
79
+ counts: countsMap.get(commentId) ?? {}
80
+ }
81
+ };
82
+ } catch {
83
+ return {
84
+ success: false,
85
+ error: {
86
+ code: "REACTION_TOGGLE_ERROR",
87
+ message: "Failed to toggle reaction"
88
+ }
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Read aggregate reaction counts for every approved comment on a content item,
94
+ * plus (optionally) which reactions the current voter has active.
95
+ */
96
+ async function handleReactionCounts(db, collection, contentId, voterHash) {
97
+ try {
98
+ const ids = (await db.selectFrom("_emdash_comments").select("id").where("collection", "=", collection).where("content_id", "=", contentId).where("status", "=", "approved").execute()).map((c) => c.id);
99
+ const repo = new CommentReactionRepository(db);
100
+ const countsMap = await repo.countsForComments(ids);
101
+ const reactions = {};
102
+ for (const [id, counts] of countsMap) reactions[id] = counts;
103
+ const data = { reactions };
104
+ if (voterHash) {
105
+ const viewerMap = await repo.viewerReactions(ids, voterHash);
106
+ const viewer = {};
107
+ for (const [id, list] of viewerMap) viewer[id] = list;
108
+ data.viewer = viewer;
109
+ }
110
+ return {
111
+ success: true,
112
+ data
113
+ };
114
+ } catch {
115
+ return {
116
+ success: false,
117
+ error: {
118
+ code: "REACTION_COUNTS_ERROR",
119
+ message: "Failed to read reactions"
120
+ }
121
+ };
122
+ }
123
+ }
124
+
125
+ //#endregion
126
+ //#region src/astro/routes/api/comments/[collection]/[contentId]/reactions.ts
127
+ const prerender = false;
128
+ const GET = async ({ params, request, locals }) => {
129
+ const { emdash } = locals;
130
+ const { collection, contentId } = params;
131
+ if (!collection || !contentId) return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
132
+ const dbErr = requireDb(emdash?.db);
133
+ if (dbErr) return dbErr;
134
+ try {
135
+ const meta = extractRequestMeta(request, emdash.config);
136
+ let voterHash = "unknown";
137
+ if (meta.ip) {
138
+ const { ipSalt } = await resolveSecretsCached(emdash.db);
139
+ voterHash = await hashIp(meta.ip, ipSalt);
140
+ }
141
+ return unwrapResult(await handleReactionCounts(emdash.db, collection, contentId, voterHash));
142
+ } catch (error) {
143
+ return handleError(error, "Failed to read reactions", "REACTION_COUNTS_ERROR");
144
+ }
145
+ };
146
+ const POST = async ({ params, request, locals }) => {
147
+ const { emdash } = locals;
148
+ const { collection, contentId } = params;
149
+ if (!collection || !contentId) return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
150
+ const dbErr = requireDb(emdash?.db);
151
+ if (dbErr) return dbErr;
152
+ try {
153
+ const body = await parseBody(request, createReactionBody);
154
+ if (isParseError(body)) return body;
155
+ if (body.website_url) return apiSuccess({
156
+ reacted: false,
157
+ counts: {}
158
+ });
159
+ const meta = extractRequestMeta(request, emdash.config);
160
+ let voterHash = "unknown";
161
+ if (meta.ip) {
162
+ const { ipSalt } = await resolveSecretsCached(emdash.db);
163
+ voterHash = await hashIp(meta.ip, ipSalt);
164
+ }
165
+ return unwrapResult(await handleReactionToggle(emdash.db, {
166
+ collection,
167
+ contentId,
168
+ commentId: body.commentId,
169
+ reaction: body.reaction,
170
+ voterHash
171
+ }), 200);
172
+ } catch (error) {
173
+ return handleError(error, "Failed to toggle reaction", "REACTION_TOGGLE_ERROR");
174
+ }
175
+ };
176
+
177
+ //#endregion
178
+ export { GET, POST, prerender };
179
+ //# sourceMappingURL=reactions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.mjs","names":[],"sources":["../../../../../../../src/api/handlers/comment-reactions.ts","../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/reactions.ts"],"sourcesContent":["/**\n * Comment reaction handlers (Tier 1 of the best-in-class comments RFC).\n *\n * Business logic for toggling reactions and reading aggregate counts. Route\n * files stay thin wrappers; these return `ApiResult<T>`.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport {\n\tCommentReactionRepository,\n\ttype ReactionCounts,\n} from \"#db/repositories/comment-reaction.js\";\nimport type { Database } from \"#db/types.js\";\n\nimport { invalidateCommentObjectCache } from \"../../object-cache/index.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/** Max reactions a single voter may register per window before throttling. */\nconst REACTION_RATE_LIMIT = 30;\nconst REACTION_RATE_WINDOW_MINUTES = 10;\n\n/**\n * Reactions the system accepts. Positive-only for now (matches the shipped\n * widget); kept as an allowlist so a voter can't spam arbitrary reaction\n * strings and bloat a comment's count map. Extend (or make configurable) as\n * the UI grows.\n */\nconst ALLOWED_REACTIONS: ReadonlySet<string> = new Set([\"like\"]);\n\nexport interface ReactionToggleResult {\n\tcommentId: string;\n\treaction: string;\n\t/** true if the reaction was added, false if an existing one was removed */\n\treacted: boolean;\n\t/** updated counts for this comment after the toggle */\n\tcounts: ReactionCounts;\n}\n\nexport interface ReactionCountsResult {\n\t/** comment id -> reaction counts */\n\treactions: Record<string, ReactionCounts>;\n\t/** comment id -> reactions the current voter has active (omitted if anonymous) */\n\tviewer?: Record<string, string[]>;\n}\n\n/**\n * Toggle a reaction for a voter on an approved comment belonging to the given\n * content item. Rate-limited per voter.\n */\nexport async function handleReactionToggle(\n\tdb: Kysely<Database>,\n\tparams: {\n\t\tcollection: string;\n\t\tcontentId: string;\n\t\tcommentId: string;\n\t\treaction: string;\n\t\tvoterHash: string;\n\t},\n): Promise<ApiResult<ReactionToggleResult>> {\n\ttry {\n\t\tconst { collection, contentId, commentId, reaction, voterHash } = params;\n\n\t\tif (!ALLOWED_REACTIONS.has(reaction)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: \"Unsupported reaction\" },\n\t\t\t};\n\t\t}\n\n\t\t// The comment must exist, be approved, and belong to this content item.\n\t\tconst comment = await db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select([\"id\", \"status\"])\n\t\t\t.where(\"id\", \"=\", commentId)\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!comment) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Comment not found\" } };\n\t\t}\n\t\tif (comment.status !== \"approved\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"COMMENT_NOT_APPROVED\", message: \"Cannot react to this comment\" },\n\t\t\t};\n\t\t}\n\n\t\tconst repo = new CommentReactionRepository(db);\n\n\t\tconst recent = await repo.countRecentByVoter(voterHash, REACTION_RATE_WINDOW_MINUTES);\n\t\tif (recent >= REACTION_RATE_LIMIT) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"RATE_LIMITED\", message: \"Too many reactions. Please try again later.\" },\n\t\t\t};\n\t\t}\n\n\t\tconst { reacted } = await repo.toggle({ commentId, reaction, voterHash });\n\t\tconst countsMap = await repo.countsForComments([commentId]);\n\n\t\t// Reaction counts (and `best` ordering) are folded into cached getComments\n\t\t// reads, so a toggle must orphan them.\n\t\tinvalidateCommentObjectCache();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { commentId, reaction, reacted, counts: countsMap.get(commentId) ?? {} },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"REACTION_TOGGLE_ERROR\", message: \"Failed to toggle reaction\" },\n\t\t};\n\t}\n}\n\n/**\n * Read aggregate reaction counts for every approved comment on a content item,\n * plus (optionally) which reactions the current voter has active.\n */\nexport async function handleReactionCounts(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n\tvoterHash?: string,\n): Promise<ApiResult<ReactionCountsResult>> {\n\ttry {\n\t\tconst comments = await db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.execute();\n\n\t\tconst ids = comments.map((c) => c.id);\n\t\tconst repo = new CommentReactionRepository(db);\n\n\t\tconst countsMap = await repo.countsForComments(ids);\n\t\tconst reactions: Record<string, ReactionCounts> = {};\n\t\tfor (const [id, counts] of countsMap) {\n\t\t\treactions[id] = counts;\n\t\t}\n\n\t\tconst data: ReactionCountsResult = { reactions };\n\n\t\tif (voterHash) {\n\t\t\tconst viewerMap = await repo.viewerReactions(ids, voterHash);\n\t\t\tconst viewer: Record<string, string[]> = {};\n\t\t\tfor (const [id, list] of viewerMap) {\n\t\t\t\tviewer[id] = list;\n\t\t\t}\n\t\t\tdata.viewer = viewer;\n\t\t}\n\n\t\treturn { success: true, data };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"REACTION_COUNTS_ERROR\", message: \"Failed to read reactions\" },\n\t\t};\n\t}\n}\n","/**\n * Public comment reaction endpoints (Tier 1 of the best-in-class comments RFC).\n *\n * GET /_emdash/api/comments/:collection/:contentId/reactions\n * - Aggregate reaction counts for the content's approved comments, plus the\n * current visitor's active reactions.\n * POST /_emdash/api/comments/:collection/:contentId/reactions\n * - Toggle a reaction on a comment. Public, honeypot- and rate-limit-gated.\n *\n * Inherits the `/_emdash/api/comments/` public prefix (no auth); the POST still\n * requires the `X-EmDash-Request: 1` CSRF header like the comment POST.\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { apiError, apiSuccess, handleError, requireDb, unwrapResult } from \"#api/error.js\";\nimport { handleReactionCounts, handleReactionToggle } from \"#api/handlers/comment-reactions.js\";\nimport { hashIp } from \"#api/handlers/comments.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { createReactionBody } from \"#api/schemas.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\nimport { extractRequestMeta } from \"#plugins/request-meta.js\";\n\nexport const prerender = false;\n\nexport const GET: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\t// Salted voter hash from request IP (same primitive as comment ip_hash).\n\t\t// Behind Cloudflare (CF-Connecting-IP) or a configured trusted proxy this\n\t\t// is per-visitor. Without a trusted IP it collapses to a shared \"unknown\"\n\t\t// bucket, so reaction dedup degrades for those visitors — a real\n\t\t// per-visitor token is Tier 2 (visitor identity). Operators should set\n\t\t// trustedProxyHeaders; see the comment ingest route for the same note.\n\t\tconst meta = extractRequestMeta(request, emdash.config);\n\t\tlet voterHash = \"unknown\";\n\t\tif (meta.ip) {\n\t\t\tconst { ipSalt } = await resolveSecretsCached(emdash.db);\n\t\t\tvoterHash = await hashIp(meta.ip, ipSalt);\n\t\t}\n\n\t\tconst result = await handleReactionCounts(emdash.db, collection, contentId, voterHash);\n\t\treturn unwrapResult(result);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to read reactions\", \"REACTION_COUNTS_ERROR\");\n\t}\n};\n\nexport const POST: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash } = locals;\n\tconst { collection, contentId } = params;\n\n\tif (!collection || !contentId) {\n\t\treturn apiError(\"VALIDATION_ERROR\", \"Collection and content ID required\", 400);\n\t}\n\n\tconst dbErr = requireDb(emdash?.db);\n\tif (dbErr) return dbErr;\n\n\ttry {\n\t\tconst body = await parseBody(request, createReactionBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Anti-spam: honeypot — hidden field filled only by bots. Silently accept.\n\t\tif (body.website_url) {\n\t\t\treturn apiSuccess({ reacted: false, counts: {} });\n\t\t}\n\n\t\tconst meta = extractRequestMeta(request, emdash.config);\n\t\tlet voterHash = \"unknown\";\n\t\tif (meta.ip) {\n\t\t\tconst { ipSalt } = await resolveSecretsCached(emdash.db);\n\t\t\tvoterHash = await hashIp(meta.ip, ipSalt);\n\t\t}\n\n\t\tconst result = await handleReactionToggle(emdash.db, {\n\t\t\tcollection,\n\t\t\tcontentId,\n\t\t\tcommentId: body.commentId,\n\t\t\treaction: body.reaction,\n\t\t\tvoterHash,\n\t\t});\n\n\t\treturn unwrapResult(result, 200);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to toggle reaction\", \"REACTION_TOGGLE_ERROR\");\n\t}\n};\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { n as requirePerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { n as requirePerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/compare.ts
7
7
  const prerender = false;
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { t as requireOwnerPerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { t as requireOwnerPerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/discard-draft.ts
7
7
  const prerender = false;
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { n as requirePerm, t as requireOwnerPerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { n as requirePerm, t as requireOwnerPerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/duplicate.ts
7
7
  const prerender = false;
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { n as requirePerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { n as requirePerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/permanent.ts
7
7
  const prerender = false;
@@ -1,16 +1,18 @@
1
1
  import { n as getI18nConfig } from "../../../../../../config-CVssduLe.mjs";
2
- import "../../../../../../base64-CqR-7kqF.mjs";
3
- import "../../../../../../types-BXSUSAjt.mjs";
4
- import "../../../../../../options-BPCVnesz.mjs";
5
- import { a as unwrapResult, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
6
- import { r as parseOptionalBody, t as isParseError } from "../../../../../../parse-CrGndy1A.mjs";
7
- import { an as contentPreviewUrlBody } from "../../../../../../redirects-CCbCqCCd.mjs";
8
- import "../../../../../../byline-fields-8TMtkBnH.mjs";
2
+ import "../../../../../../after-B1IIdH3Y.mjs";
3
+ import "../../../../../../base64-CmWvODNW.mjs";
4
+ import "../../../../../../types-DXOIbece.mjs";
5
+ import "../../../../../../options-DTTML-Tx.mjs";
6
+ import { a as unwrapResult, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
7
+ import { r as parseOptionalBody, t as isParseError } from "../../../../../../parse-qrHlnzTy.mjs";
8
+ import { on as contentPreviewUrlBody } from "../../../../../../redirects-DUvXgzCX.mjs";
9
+ import "../../../../../../byline-fields-53i9Et0x.mjs";
10
+ import "../../../../../../status-CB2basWJ.mjs";
9
11
  import "../../../../../../api/schemas/index.mjs";
10
- import "../../../../../../tokens-Bx2afeT-.mjs";
11
- import { i as getPreviewUrl } from "../../../../../../preview-BfuRkVKW.mjs";
12
- import { i as resolveSecretsCached } from "../../../../../../secrets-ChPTmy9x.mjs";
13
- import { n as requirePerm } from "../../../../../../authorize-DsMSVSaY.mjs";
12
+ import "../../../../../../tokens-BvrJvIB6.mjs";
13
+ import { i as getPreviewUrl } from "../../../../../../preview-IDye9SPQ.mjs";
14
+ import { i as resolveSecretsCached } from "../../../../../../secrets-BUE5UQys.mjs";
15
+ import { n as requirePerm } from "../../../../../../authorize-C0EYoUeV.mjs";
14
16
 
15
17
  //#region src/astro/routes/api/content/[collection]/[id]/preview-url.ts
16
18
  const prerender = false;
@@ -1 +1 @@
1
- {"version":3,"file":"preview-url.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/preview-url.ts"],"sourcesContent":["/**\n * Preview URL endpoint - generates a signed preview URL for content\n *\n * POST /_emdash/api/content/{collection}/{id}/preview-url\n *\n * Request body:\n * {\n * expiresIn?: string | number; // Default: \"1h\"\n * pathPattern?: string; // Default: \"/{collection}/{id}\" (or EMDASH_PREVIEW_PATH_PATTERN)\n * }\n *\n * Response:\n * {\n * url: string; // The preview URL with token\n * expiresAt: number; // Unix timestamp when token expires\n * }\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, apiSuccess, handleError, unwrapResult } from \"#api/error.js\";\nimport { parseOptionalBody, isParseError } from \"#api/parse.js\";\nimport { contentPreviewUrlBody } from \"#api/schemas.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\nimport { getPreviewUrl } from \"#preview/index.js\";\n\nimport { getI18nConfig } from \"../../../../../../i18n/config.js\";\n\nexport const prerender = false;\n\nconst DURATION_PATTERN = /^(\\d+)([smhdw])$/;\n\nexport const POST: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst denied = requirePerm(user, \"content:read_drafts\");\n\tif (denied) return denied;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Resolve the preview secret. Env override wins; otherwise a stable\n\t// site-specific value is read from (or generated into) the options table.\n\t// The resolver always returns a usable secret, so this path can no\n\t// longer be silently disabled by a missing env var.\n\tconst { previewSecret } = await resolveSecretsCached(emdash.db);\n\n\t// Verify the content exists. The fetched item also yields the entry's\n\t// locale, used below to resolve the `{locale}` placeholder.\n\tlet entryLocale: string | null = null;\n\tif (emdash?.handleContentGet) {\n\t\tconst result = await emdash.handleContentGet(collection, id);\n\t\tif (!result.success) return unwrapResult(result);\n\t\tentryLocale = result.data?.item?.locale ?? null;\n\t}\n\n\t// Parse request body\n\tconst body = await parseOptionalBody(request, contentPreviewUrlBody, {});\n\tif (isParseError(body)) return body;\n\n\tconst expiresIn = body.expiresIn || \"1h\";\n\t// Allow a project-wide default `pathPattern` so the admin's \"View on site\"\n\t// link can match the site's actual route shape without each call having\n\t// to override the default `/{collection}/{id}`.\n\tconst defaultPathPattern = import.meta.env.EMDASH_PREVIEW_PATH_PATTERN || \"/{collection}/{id}\";\n\tconst pathPattern = body.pathPattern || defaultPathPattern;\n\n\t// Resolve the locale segment substituted for `{locale}`: empty when the\n\t// entry is in the default locale and `prefixDefaultLocale` is `false`,\n\t// the entry's own locale otherwise.\n\tconst i18n = getI18nConfig();\n\tlet localeSegment = \"\";\n\tif (entryLocale && i18n) {\n\t\tconst isDefault = entryLocale === i18n.defaultLocale;\n\t\tlocaleSegment = isDefault && !i18n.prefixDefaultLocale ? \"\" : entryLocale;\n\t} else if (entryLocale) {\n\t\tlocaleSegment = entryLocale;\n\t}\n\n\t// Calculate expiry timestamp\n\tconst expiresInSeconds = typeof expiresIn === \"number\" ? expiresIn : parseExpiresIn(expiresIn);\n\tconst expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n\n\ttry {\n\t\tconst url = await getPreviewUrl({\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tsecret: previewSecret,\n\t\t\texpiresIn,\n\t\t\tpathPattern,\n\t\t\tlocale: localeSegment,\n\t\t});\n\n\t\treturn apiSuccess({ url, expiresAt });\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to generate preview URL\", \"TOKEN_ERROR\");\n\t}\n};\n\n/**\n * Parse duration string to seconds\n */\nfunction parseExpiresIn(duration: string): number {\n\tconst match = duration.match(DURATION_PATTERN);\n\tif (!match) {\n\t\treturn 3600; // Default 1 hour\n\t}\n\n\tconst value = parseInt(match[1], 10);\n\tconst unit = match[2];\n\n\tswitch (unit) {\n\t\tcase \"s\":\n\t\t\treturn value;\n\t\tcase \"m\":\n\t\t\treturn value * 60;\n\t\tcase \"h\":\n\t\t\treturn value * 60 * 60;\n\t\tcase \"d\":\n\t\t\treturn value * 60 * 60 * 24;\n\t\tcase \"w\":\n\t\t\treturn value * 60 * 60 * 24 * 7;\n\t\tdefault:\n\t\t\treturn 3600;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,MAAa,YAAY;AAEzB,MAAM,mBAAmB;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,SAAS,aAAa;CACpE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,SAAS,YAAY,MAAM,sBAAsB;AACvD,KAAI,OAAQ,QAAO;CACnB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAOpE,MAAM,EAAE,kBAAkB,MAAM,qBAAqB,OAAO,GAAG;CAI/D,IAAI,cAA6B;AACjC,KAAI,QAAQ,kBAAkB;EAC7B,MAAM,SAAS,MAAM,OAAO,iBAAiB,YAAY,GAAG;AAC5D,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAChD,gBAAc,OAAO,MAAM,MAAM,UAAU;;CAI5C,MAAM,OAAO,MAAM,kBAAkB,SAAS,uBAAuB,EAAE,CAAC;AACxE,KAAI,aAAa,KAAK,CAAE,QAAO;CAE/B,MAAM,YAAY,KAAK,aAAa;CAIpC,MAAM,qBAAqB,OAAO,KAAK,IAAI,+BAA+B;CAC1E,MAAM,cAAc,KAAK,eAAe;CAKxC,MAAM,OAAO,eAAe;CAC5B,IAAI,gBAAgB;AACpB,KAAI,eAAe,KAElB,iBADkB,gBAAgB,KAAK,iBACV,CAAC,KAAK,sBAAsB,KAAK;UACpD,YACV,iBAAgB;CAIjB,MAAM,mBAAmB,OAAO,cAAc,WAAW,YAAY,eAAe,UAAU;CAC9F,MAAM,YAAY,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,GAAG;AAElD,KAAI;AAUH,SAAO,WAAW;GAAE,KATR,MAAM,cAAc;IAC/B;IACA;IACA,QAAQ;IACR;IACA;IACA,QAAQ;IACR,CAAC;GAEuB;GAAW,CAAC;UAC7B,OAAO;AACf,SAAO,YAAY,OAAO,kCAAkC,cAAc;;;;;;AAO5E,SAAS,eAAe,UAA0B;CACjD,MAAM,QAAQ,SAAS,MAAM,iBAAiB;AAC9C,KAAI,CAAC,MACJ,QAAO;CAGR,MAAM,QAAQ,SAAS,MAAM,IAAI,GAAG;AAGpC,SAFa,MAAM,IAEnB;EACC,KAAK,IACJ,QAAO;EACR,KAAK,IACJ,QAAO,QAAQ;EAChB,KAAK,IACJ,QAAO,QAAQ,KAAK;EACrB,KAAK,IACJ,QAAO,QAAQ,KAAK,KAAK;EAC1B,KAAK,IACJ,QAAO,QAAQ,KAAK,KAAK,KAAK;EAC/B,QACC,QAAO"}
1
+ {"version":3,"file":"preview-url.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/preview-url.ts"],"sourcesContent":["/**\n * Preview URL endpoint - generates a signed preview URL for content\n *\n * POST /_emdash/api/content/{collection}/{id}/preview-url\n *\n * Request body:\n * {\n * expiresIn?: string | number; // Default: \"1h\"\n * pathPattern?: string; // Default: \"/{collection}/{id}\" (or EMDASH_PREVIEW_PATH_PATTERN)\n * }\n *\n * Response:\n * {\n * url: string; // The preview URL with token\n * expiresAt: number; // Unix timestamp when token expires\n * }\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, apiSuccess, handleError, unwrapResult } from \"#api/error.js\";\nimport { parseOptionalBody, isParseError } from \"#api/parse.js\";\nimport { contentPreviewUrlBody } from \"#api/schemas.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\nimport { getPreviewUrl } from \"#preview/index.js\";\n\nimport { getI18nConfig } from \"../../../../../../i18n/config.js\";\n\nexport const prerender = false;\n\nconst DURATION_PATTERN = /^(\\d+)([smhdw])$/;\n\nexport const POST: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst denied = requirePerm(user, \"content:read_drafts\");\n\tif (denied) return denied;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Resolve the preview secret. Env override wins; otherwise a stable\n\t// site-specific value is read from (or generated into) the options table.\n\t// The resolver always returns a usable secret, so this path can no\n\t// longer be silently disabled by a missing env var.\n\tconst { previewSecret } = await resolveSecretsCached(emdash.db);\n\n\t// Verify the content exists. The fetched item also yields the entry's\n\t// locale, used below to resolve the `{locale}` placeholder.\n\tlet entryLocale: string | null = null;\n\tif (emdash?.handleContentGet) {\n\t\tconst result = await emdash.handleContentGet(collection, id);\n\t\tif (!result.success) return unwrapResult(result);\n\t\tentryLocale = result.data?.item?.locale ?? null;\n\t}\n\n\t// Parse request body\n\tconst body = await parseOptionalBody(request, contentPreviewUrlBody, {});\n\tif (isParseError(body)) return body;\n\n\tconst expiresIn = body.expiresIn || \"1h\";\n\t// Allow a project-wide default `pathPattern` so the admin's \"View on site\"\n\t// link can match the site's actual route shape without each call having\n\t// to override the default `/{collection}/{id}`.\n\tconst defaultPathPattern = import.meta.env.EMDASH_PREVIEW_PATH_PATTERN || \"/{collection}/{id}\";\n\tconst pathPattern = body.pathPattern || defaultPathPattern;\n\n\t// Resolve the locale segment substituted for `{locale}`: empty when the\n\t// entry is in the default locale and `prefixDefaultLocale` is `false`,\n\t// the entry's own locale otherwise.\n\tconst i18n = getI18nConfig();\n\tlet localeSegment = \"\";\n\tif (entryLocale && i18n) {\n\t\tconst isDefault = entryLocale === i18n.defaultLocale;\n\t\tlocaleSegment = isDefault && !i18n.prefixDefaultLocale ? \"\" : entryLocale;\n\t} else if (entryLocale) {\n\t\tlocaleSegment = entryLocale;\n\t}\n\n\t// Calculate expiry timestamp\n\tconst expiresInSeconds = typeof expiresIn === \"number\" ? expiresIn : parseExpiresIn(expiresIn);\n\tconst expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n\n\ttry {\n\t\tconst url = await getPreviewUrl({\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tsecret: previewSecret,\n\t\t\texpiresIn,\n\t\t\tpathPattern,\n\t\t\tlocale: localeSegment,\n\t\t});\n\n\t\treturn apiSuccess({ url, expiresAt });\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to generate preview URL\", \"TOKEN_ERROR\");\n\t}\n};\n\n/**\n * Parse duration string to seconds\n */\nfunction parseExpiresIn(duration: string): number {\n\tconst match = duration.match(DURATION_PATTERN);\n\tif (!match) {\n\t\treturn 3600; // Default 1 hour\n\t}\n\n\tconst value = parseInt(match[1], 10);\n\tconst unit = match[2];\n\n\tswitch (unit) {\n\t\tcase \"s\":\n\t\t\treturn value;\n\t\tcase \"m\":\n\t\t\treturn value * 60;\n\t\tcase \"h\":\n\t\t\treturn value * 60 * 60;\n\t\tcase \"d\":\n\t\t\treturn value * 60 * 60 * 24;\n\t\tcase \"w\":\n\t\t\treturn value * 60 * 60 * 24 * 7;\n\t\tdefault:\n\t\t\treturn 3600;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,MAAa,YAAY;AAEzB,MAAM,mBAAmB;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,SAAS,aAAa;CACpE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,SAAS,YAAY,MAAM,sBAAsB;AACvD,KAAI,OAAQ,QAAO;CACnB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAOpE,MAAM,EAAE,kBAAkB,MAAM,qBAAqB,OAAO,GAAG;CAI/D,IAAI,cAA6B;AACjC,KAAI,QAAQ,kBAAkB;EAC7B,MAAM,SAAS,MAAM,OAAO,iBAAiB,YAAY,GAAG;AAC5D,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAChD,gBAAc,OAAO,MAAM,MAAM,UAAU;;CAI5C,MAAM,OAAO,MAAM,kBAAkB,SAAS,uBAAuB,EAAE,CAAC;AACxE,KAAI,aAAa,KAAK,CAAE,QAAO;CAE/B,MAAM,YAAY,KAAK,aAAa;CAIpC,MAAM,qBAAqB,OAAO,KAAK,IAAI,+BAA+B;CAC1E,MAAM,cAAc,KAAK,eAAe;CAKxC,MAAM,OAAO,eAAe;CAC5B,IAAI,gBAAgB;AACpB,KAAI,eAAe,KAElB,iBADkB,gBAAgB,KAAK,iBACV,CAAC,KAAK,sBAAsB,KAAK;UACpD,YACV,iBAAgB;CAIjB,MAAM,mBAAmB,OAAO,cAAc,WAAW,YAAY,eAAe,UAAU;CAC9F,MAAM,YAAY,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,GAAG;AAElD,KAAI;AAUH,SAAO,WAAW;GAAE,KATR,MAAM,cAAc;IAC/B;IACA;IACA,QAAQ;IACR;IACA;IACA,QAAQ;IACR,CAAC;GAEuB;GAAW,CAAC;UAC7B,OAAO;AACf,SAAO,YAAY,OAAO,kCAAkC,cAAc;;;;;;AAO5E,SAAS,eAAe,UAA0B;CACjD,MAAM,QAAQ,SAAS,MAAM,iBAAiB;AAC9C,KAAI,CAAC,MACJ,QAAO;CAGR,MAAM,QAAQ,SAAS,MAAM,IAAI,GAAG;AAGpC,SAFa,MAAM,IAEnB;EACC,KAAK,IACJ,QAAO;EACR,KAAK,IACJ,QAAO,QAAQ;EAChB,KAAK,IACJ,QAAO,QAAQ,KAAK;EACrB,KAAK,IACJ,QAAO,QAAQ,KAAK,KAAK;EAC1B,KAAK,IACJ,QAAO,QAAQ,KAAK,KAAK,KAAK;EAC/B,QACC,QAAO"}
@@ -1,11 +1,12 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { r as parseOptionalBody, t as isParseError } from "../../../../../../parse-CrGndy1A.mjs";
5
- import { on as contentPublishBody } from "../../../../../../redirects-CCbCqCCd.mjs";
6
- import "../../../../../../byline-fields-8TMtkBnH.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { r as parseOptionalBody, t as isParseError } from "../../../../../../parse-qrHlnzTy.mjs";
5
+ import { sn as contentPublishBody } from "../../../../../../redirects-DUvXgzCX.mjs";
6
+ import "../../../../../../byline-fields-53i9Et0x.mjs";
7
+ import "../../../../../../status-CB2basWJ.mjs";
7
8
  import "../../../../../../api/schemas/index.mjs";
8
- import { t as requireOwnerPerm } from "../../../../../../authorize-DsMSVSaY.mjs";
9
+ import { t as requireOwnerPerm } from "../../../../../../authorize-C0EYoUeV.mjs";
9
10
  import { hasPermission } from "@emdash-cms/auth";
10
11
 
11
12
  //#region src/astro/routes/api/content/[collection]/[id]/publish.ts
@@ -1 +1 @@
1
- {"version":3,"file":"publish.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/publish.ts"],"sourcesContent":["/**\n * Publish content - promotes draft to live\n *\n * POST /_emdash/api/content/{collection}/{id}/publish\n *\n * Optional JSON body: { publishedAt?: string }\n * publishedAt — ISO 8601 datetime to backdate the publish (e.g. when\n * migrating content). Writing publishedAt requires content:publish_any.\n * Without it, the existing published_at is preserved on re-publish and\n * falls back to the current time on first publish.\n */\n\nimport { hasPermission } from \"@emdash-cms/auth\";\nimport type { APIRoute } from \"astro\";\n\nimport { requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\nimport { isParseError, parseOptionalBody } from \"#api/parse.js\";\nimport { contentPublishBody } from \"#api/schemas.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, request, locals, url, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.handleContentPublish || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Body is optional — empty body means use the legacy behavior (preserve\n\t// or default published_at). Pass `publishedAt` to backdate.\n\tconst body = await parseOptionalBody(request, contentPublishBody, {});\n\tif (isParseError(body)) return body;\n\n\tconst locale = url.searchParams.get(\"locale\") || undefined;\n\n\t// Fetch item to check ownership\n\tconst existing = await emdash.handleContentGet(collection, id, locale);\n\tif (!existing.success) {\n\t\treturn apiError(\n\t\t\texisting.error?.code ?? \"UNKNOWN_ERROR\",\n\t\t\texisting.error?.message ?? \"Unknown error\",\n\t\t\tmapErrorStatus(existing.error?.code),\n\t\t);\n\t}\n\n\tconst existingData =\n\t\texisting.data && typeof existing.data === \"object\"\n\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above\n\t\t\t\t(existing.data as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst existingItem =\n\t\texistingData?.item && typeof existingData.item === \"object\"\n\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above\n\t\t\t\t(existingData.item as Record<string, unknown>)\n\t\t\t: existingData;\n\tconst authorId = typeof existingItem?.authorId === \"string\" ? existingItem.authorId : \"\";\n\tconst denied = requireOwnerPerm(user, authorId, \"content:publish_own\", \"content:publish_any\");\n\tif (denied) return denied;\n\n\t// Schema narrows `publishedAt` to `string | undefined`; null is rejected\n\t// at the schema layer (publish has no semantic meaning for \"clear\").\n\tconst publishedAt = body?.publishedAt;\n\n\t// Backdating overwrites historical record — gate behind publish_any\n\t// regardless of ownership.\n\tif (publishedAt !== undefined && !hasPermission(user, \"content:publish_any\")) {\n\t\treturn apiError(\n\t\t\t\"FORBIDDEN\",\n\t\t\t\"Setting publishedAt requires content:publish_any permission\",\n\t\t\t403,\n\t\t);\n\t}\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\n\tconst result = await emdash.handleContentPublish(collection, resolvedId, {\n\t\tpublishedAt,\n\t});\n\n\tif (!result.success) return unwrapResult(result);\n\n\tif (cache?.enabled) await cache.invalidate({ tags: [collection, resolvedId] });\n\n\treturn unwrapResult(result);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,SAAS,QAAQ,KAAK,YAAY;CAChF,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,wBAAwB,CAAC,QAAQ,iBAC7C,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAKpE,MAAM,OAAO,MAAM,kBAAkB,SAAS,oBAAoB,EAAE,CAAC;AACrE,KAAI,aAAa,KAAK,CAAE,QAAO;CAE/B,MAAM,SAAS,IAAI,aAAa,IAAI,SAAS,IAAI;CAGjD,MAAM,WAAW,MAAM,OAAO,iBAAiB,YAAY,IAAI,OAAO;AACtE,KAAI,CAAC,SAAS,QACb,QAAO,SACN,SAAS,OAAO,QAAQ,iBACxB,SAAS,OAAO,WAAW,iBAC3B,eAAe,SAAS,OAAO,KAAK,CACpC;CAGF,MAAM,eACL,SAAS,QAAQ,OAAO,SAAS,SAAS,WAEvC,SAAS,OACT;CACJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAEJ,MAAM,SAAS,iBAAiB,MADf,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IACtC,uBAAuB,sBAAsB;AAC7F,KAAI,OAAQ,QAAO;CAInB,MAAM,cAAc,MAAM;AAI1B,KAAI,gBAAgB,UAAa,CAAC,cAAc,MAAM,sBAAsB,CAC3E,QAAO,SACN,aACA,+DACA,IACA;CAGF,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAE5E,MAAM,SAAS,MAAM,OAAO,qBAAqB,YAAY,YAAY,EACxE,aACA,CAAC;AAEF,KAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAEhD,KAAI,OAAO,QAAS,OAAM,MAAM,WAAW,EAAE,MAAM,CAAC,YAAY,WAAW,EAAE,CAAC;AAE9E,QAAO,aAAa,OAAO"}
1
+ {"version":3,"file":"publish.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/publish.ts"],"sourcesContent":["/**\n * Publish content - promotes draft to live\n *\n * POST /_emdash/api/content/{collection}/{id}/publish\n *\n * Optional JSON body: { publishedAt?: string }\n * publishedAt — ISO 8601 datetime to backdate the publish (e.g. when\n * migrating content). Writing publishedAt requires content:publish_any.\n * Without it, the existing published_at is preserved on re-publish and\n * falls back to the current time on first publish.\n */\n\nimport { hasPermission } from \"@emdash-cms/auth\";\nimport type { APIRoute } from \"astro\";\n\nimport { requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\nimport { isParseError, parseOptionalBody } from \"#api/parse.js\";\nimport { contentPublishBody } from \"#api/schemas.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, request, locals, url, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.handleContentPublish || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Body is optional — empty body means use the legacy behavior (preserve\n\t// or default published_at). Pass `publishedAt` to backdate.\n\tconst body = await parseOptionalBody(request, contentPublishBody, {});\n\tif (isParseError(body)) return body;\n\n\tconst locale = url.searchParams.get(\"locale\") || undefined;\n\n\t// Fetch item to check ownership\n\tconst existing = await emdash.handleContentGet(collection, id, locale);\n\tif (!existing.success) {\n\t\treturn apiError(\n\t\t\texisting.error?.code ?? \"UNKNOWN_ERROR\",\n\t\t\texisting.error?.message ?? \"Unknown error\",\n\t\t\tmapErrorStatus(existing.error?.code),\n\t\t);\n\t}\n\n\tconst existingData =\n\t\texisting.data && typeof existing.data === \"object\"\n\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above\n\t\t\t\t(existing.data as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst existingItem =\n\t\texistingData?.item && typeof existingData.item === \"object\"\n\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above\n\t\t\t\t(existingData.item as Record<string, unknown>)\n\t\t\t: existingData;\n\tconst authorId = typeof existingItem?.authorId === \"string\" ? existingItem.authorId : \"\";\n\tconst denied = requireOwnerPerm(user, authorId, \"content:publish_own\", \"content:publish_any\");\n\tif (denied) return denied;\n\n\t// Schema narrows `publishedAt` to `string | undefined`; null is rejected\n\t// at the schema layer (publish has no semantic meaning for \"clear\").\n\tconst publishedAt = body?.publishedAt;\n\n\t// Backdating overwrites historical record — gate behind publish_any\n\t// regardless of ownership.\n\tif (publishedAt !== undefined && !hasPermission(user, \"content:publish_any\")) {\n\t\treturn apiError(\n\t\t\t\"FORBIDDEN\",\n\t\t\t\"Setting publishedAt requires content:publish_any permission\",\n\t\t\t403,\n\t\t);\n\t}\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\n\tconst result = await emdash.handleContentPublish(collection, resolvedId, {\n\t\tpublishedAt,\n\t});\n\n\tif (!result.success) return unwrapResult(result);\n\n\tif (cache?.enabled) await cache.invalidate({ tags: [collection, resolvedId] });\n\n\treturn unwrapResult(result);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,SAAS,QAAQ,KAAK,YAAY;CAChF,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,wBAAwB,CAAC,QAAQ,iBAC7C,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAKpE,MAAM,OAAO,MAAM,kBAAkB,SAAS,oBAAoB,EAAE,CAAC;AACrE,KAAI,aAAa,KAAK,CAAE,QAAO;CAE/B,MAAM,SAAS,IAAI,aAAa,IAAI,SAAS,IAAI;CAGjD,MAAM,WAAW,MAAM,OAAO,iBAAiB,YAAY,IAAI,OAAO;AACtE,KAAI,CAAC,SAAS,QACb,QAAO,SACN,SAAS,OAAO,QAAQ,iBACxB,SAAS,OAAO,WAAW,iBAC3B,eAAe,SAAS,OAAO,KAAK,CACpC;CAGF,MAAM,eACL,SAAS,QAAQ,OAAO,SAAS,SAAS,WAEvC,SAAS,OACT;CACJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAEJ,MAAM,SAAS,iBAAiB,MADf,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IACtC,uBAAuB,sBAAsB;AAC7F,KAAI,OAAQ,QAAO;CAInB,MAAM,cAAc,MAAM;AAI1B,KAAI,gBAAgB,UAAa,CAAC,cAAc,MAAM,sBAAsB,CAC3E,QAAO,SACN,aACA,+DACA,IACA;CAGF,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAE5E,MAAM,SAAS,MAAM,OAAO,qBAAqB,YAAY,YAAY,EACxE,aACA,CAAC;AAEF,KAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAEhD,KAAI,OAAO,QAAS,OAAM,MAAM,WAAW,EAAE,MAAM,CAAC,YAAY,WAAW,EAAE,CAAC;AAE9E,QAAO,aAAa,OAAO"}
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { t as requireOwnerPerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, o as mapErrorStatus, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { t as requireOwnerPerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/restore.ts
7
7
  const prerender = false;
@@ -1,7 +1,7 @@
1
- import "../../../../../../base64-CqR-7kqF.mjs";
2
- import "../../../../../../types-BXSUSAjt.mjs";
3
- import { a as unwrapResult, t as apiError } from "../../../../../../error-RwM4dD35.mjs";
4
- import { n as requirePerm } from "../../../../../../authorize-DsMSVSaY.mjs";
1
+ import "../../../../../../base64-CmWvODNW.mjs";
2
+ import "../../../../../../types-DXOIbece.mjs";
3
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-BQ1pxs0L.mjs";
4
+ import { n as requirePerm } from "../../../../../../authorize-C0EYoUeV.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/revisions.ts
7
7
  const prerender = false;