emdash 0.29.0 → 0.31.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 (876) hide show
  1. package/dist/{adapters-DPAkL6QF.d.mts → adapters-C0ONzkUP.d.mts} +10 -1
  2. package/dist/adapters-C0ONzkUP.d.mts.map +1 -0
  3. package/dist/{allowed-origins-FXB-FplB.mjs → allowed-origins-CCEi9bPI.mjs} +2 -2
  4. package/dist/{allowed-origins-FXB-FplB.mjs.map → allowed-origins-CCEi9bPI.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.d.mts.map +1 -1
  7. package/dist/api/route-utils.mjs +19 -19
  8. package/dist/api/schemas/index.d.mts +2 -2
  9. package/dist/api/schemas/index.mjs +5 -4
  10. package/dist/{api-DVbHNApo.mjs → api-EibxGP-V.mjs} +260 -160
  11. package/dist/api-EibxGP-V.mjs.map +1 -0
  12. package/dist/api-tokens-Cvmixds7.mjs +3 -0
  13. package/dist/{api-tokens-UYBkl17d.mjs → api-tokens-uPt8UDpx.mjs} +2 -2
  14. package/dist/{api-tokens-UYBkl17d.mjs.map → api-tokens-uPt8UDpx.mjs.map} +1 -1
  15. package/dist/{apply-D23iUV4q.mjs → apply-fGHCAwWN.mjs} +42 -44
  16. package/dist/apply-fGHCAwWN.mjs.map +1 -0
  17. package/dist/astro/index.d.mts +12 -13
  18. package/dist/astro/index.d.mts.map +1 -1
  19. package/dist/astro/index.mjs +90 -11
  20. package/dist/astro/index.mjs.map +1 -1
  21. package/dist/astro/middleware/auth.d.mts +10 -11
  22. package/dist/astro/middleware/auth.d.mts.map +1 -1
  23. package/dist/astro/middleware/auth.mjs +56 -107
  24. package/dist/astro/middleware/auth.mjs.map +1 -1
  25. package/dist/astro/middleware/redirect.mjs +7 -7
  26. package/dist/astro/middleware/request-context.mjs +4 -4
  27. package/dist/astro/middleware/request-context.mjs.map +1 -1
  28. package/dist/astro/middleware/setup.mjs +1 -1
  29. package/dist/astro/middleware.d.mts +47 -3
  30. package/dist/astro/middleware.d.mts.map +1 -1
  31. package/dist/astro/middleware.mjs +410 -123
  32. package/dist/astro/middleware.mjs.map +1 -1
  33. package/dist/astro/routes/PluginRegistry.d.mts +5 -1
  34. package/dist/astro/routes/PluginRegistry.d.mts.map +1 -1
  35. package/dist/astro/routes/PluginRegistry.mjs +2 -1
  36. package/dist/astro/routes/PluginRegistry.mjs.map +1 -1
  37. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
  38. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  39. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
  40. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  41. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  42. package/dist/astro/routes/api/admin/api-tokens/index.mjs +7 -7
  43. package/dist/astro/routes/api/admin/api-tokens/index.mjs.map +1 -1
  44. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -5
  45. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +10 -9
  46. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  47. package/dist/astro/routes/api/admin/byline-fields/index.mjs +10 -9
  48. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +10 -9
  50. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  51. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +16 -15
  52. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +17 -22
  54. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/bylines/index.d.mts.map +1 -1
  56. package/dist/astro/routes/api/admin/bylines/index.mjs +20 -18
  57. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  58. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +13 -12
  59. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  60. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  61. package/dist/astro/routes/api/admin/comments/bulk.mjs +11 -10
  62. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  63. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  64. package/dist/astro/routes/api/admin/comments/index.mjs +11 -10
  65. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  66. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
  67. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
  68. package/dist/astro/routes/api/admin/media-usage/repair.mjs +12 -10
  69. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  71. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  72. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +41 -40
  73. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  74. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +41 -40
  75. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  76. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +46 -40
  77. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  78. package/dist/astro/routes/api/admin/plugins/_id_/mcp.d.mts +8 -0
  79. package/dist/astro/routes/api/admin/plugins/_id_/mcp.d.mts.map +1 -0
  80. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +39 -0
  81. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs.map +1 -0
  82. package/dist/astro/routes/api/admin/plugins/_id_/settings.d.mts +9 -0
  83. package/dist/astro/routes/api/admin/plugins/_id_/settings.d.mts.map +1 -0
  84. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +78 -0
  85. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -0
  86. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +40 -39
  87. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +43 -40
  90. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  91. package/dist/astro/routes/api/admin/plugins/index.mjs +46 -40
  92. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  94. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +40 -39
  95. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  97. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +46 -41
  98. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  99. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +40 -39
  100. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  101. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +40 -39
  102. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  104. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +44 -41
  105. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +40 -39
  107. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  108. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  109. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +45 -42
  110. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/updates.mjs +41 -40
  112. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  113. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +40 -39
  114. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  115. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  116. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +40 -39
  117. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  118. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
  119. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +4 -4
  120. package/dist/astro/routes/api/admin/users/_id_/index.mjs +9 -8
  121. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  122. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +5 -5
  123. package/dist/astro/routes/api/admin/users/index.mjs +9 -8
  124. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  125. package/dist/astro/routes/api/auth/dev-bypass.mjs +7 -7
  126. package/dist/astro/routes/api/auth/invite/accept.mjs +4 -4
  127. package/dist/astro/routes/api/auth/invite/complete.mjs +13 -12
  128. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  129. package/dist/astro/routes/api/auth/invite/index.mjs +10 -9
  130. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  131. package/dist/astro/routes/api/auth/invite/register-options.mjs +12 -11
  132. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  133. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  134. package/dist/astro/routes/api/auth/magic-link/send.mjs +12 -11
  135. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  136. package/dist/astro/routes/api/auth/magic-link/verify.mjs +5 -5
  137. package/dist/astro/routes/api/auth/me.mjs +9 -8
  138. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  139. package/dist/astro/routes/api/auth/mode.d.mts.map +1 -1
  140. package/dist/astro/routes/api/auth/mode.mjs +6 -3
  141. package/dist/astro/routes/api/auth/mode.mjs.map +1 -1
  142. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +15 -5
  143. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs.map +1 -1
  144. package/dist/astro/routes/api/auth/oauth/_provider_.d.mts.map +1 -1
  145. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +11 -5
  146. package/dist/astro/routes/api/auth/oauth/_provider_.mjs.map +1 -1
  147. package/dist/astro/routes/api/auth/passkey/_id_.mjs +8 -7
  148. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  149. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  150. package/dist/astro/routes/api/auth/passkey/options.mjs +13 -12
  151. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  152. package/dist/astro/routes/api/auth/passkey/register/options.mjs +11 -10
  153. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  154. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +12 -11
  155. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  156. package/dist/astro/routes/api/auth/passkey/verify.mjs +12 -11
  157. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  158. package/dist/astro/routes/api/auth/signup/complete.mjs +13 -12
  159. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  160. package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
  161. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  162. package/dist/astro/routes/api/auth/signup/verify.mjs +4 -4
  163. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +14 -13
  164. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  165. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +13 -12
  166. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  167. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +5 -5
  168. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs.map +1 -1
  169. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -4
  170. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  171. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +6 -5
  172. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs.map +1 -1
  173. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +5 -5
  174. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs.map +1 -1
  175. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +14 -13
  176. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs.map +1 -1
  177. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +10 -8
  178. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  179. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.d.mts.map +1 -1
  180. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +20 -19
  181. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  182. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +18 -17
  183. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  184. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -4
  185. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs.map +1 -1
  186. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +5 -5
  187. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs.map +1 -1
  188. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +10 -8
  189. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  190. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  191. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +19 -18
  192. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  193. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +5 -5
  194. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs.map +1 -1
  195. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -4
  196. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  197. package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
  198. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -9
  199. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  200. package/dist/astro/routes/api/content/_collection_/authors.mjs +5 -5
  201. package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -1
  202. package/dist/astro/routes/api/content/_collection_/index.d.mts.map +1 -1
  203. package/dist/astro/routes/api/content/_collection_/index.mjs +12 -10
  204. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  205. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  206. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  207. package/dist/astro/routes/api/dashboard.mjs +8 -8
  208. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  209. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  210. package/dist/astro/routes/api/import/probe.mjs +17 -16
  211. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  212. package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
  213. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -12
  214. package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
  215. package/dist/astro/routes/api/import/wordpress/execute.mjs +13 -13
  216. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -11
  217. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  218. package/dist/astro/routes/api/import/wordpress/media.mjs +11 -10
  219. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  220. package/dist/astro/routes/api/import/wordpress/prepare.mjs +11 -10
  221. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  222. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -12
  223. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
  224. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +11 -10
  225. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  226. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  227. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +17 -16
  228. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  229. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  230. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -12
  231. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -1
  232. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +24 -23
  233. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  234. package/dist/astro/routes/api/manifest.mjs +7 -8
  235. package/dist/astro/routes/api/manifest.mjs.map +1 -1
  236. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  237. package/dist/astro/routes/api/mcp.mjs +149 -45
  238. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  239. package/dist/astro/routes/api/media/_id_/confirm.mjs +9 -8
  240. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  241. package/dist/astro/routes/api/media/_id_/usage.d.mts +8 -0
  242. package/dist/astro/routes/api/media/_id_/usage.d.mts.map +1 -0
  243. package/dist/astro/routes/api/media/_id_/usage.mjs +37 -0
  244. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -0
  245. package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
  246. package/dist/astro/routes/api/media/_id_.mjs +26 -10
  247. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  248. package/dist/astro/routes/api/media/file/_...key_.mjs +4 -3
  249. package/dist/astro/routes/api/media/file/_...key_.mjs.map +1 -1
  250. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  251. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  252. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  253. package/dist/astro/routes/api/media/upload-url.mjs +10 -9
  254. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  255. package/dist/astro/routes/api/media.d.mts.map +1 -1
  256. package/dist/astro/routes/api/media.mjs +31 -11
  257. package/dist/astro/routes/api/media.mjs.map +1 -1
  258. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +10 -9
  259. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
  260. package/dist/astro/routes/api/menus/_name_/items.mjs +10 -9
  261. package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
  262. package/dist/astro/routes/api/menus/_name_/reorder.mjs +10 -9
  263. package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
  264. package/dist/astro/routes/api/menus/_name_/translations.mjs +10 -9
  265. package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
  266. package/dist/astro/routes/api/menus/_name_.mjs +10 -9
  267. package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
  268. package/dist/astro/routes/api/menus/index.mjs +10 -9
  269. package/dist/astro/routes/api/menus/index.mjs.map +1 -1
  270. package/dist/astro/routes/api/oauth/authorize.d.mts.map +1 -1
  271. package/dist/astro/routes/api/oauth/authorize.mjs +29 -16
  272. package/dist/astro/routes/api/oauth/authorize.mjs.map +1 -1
  273. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  274. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  275. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  276. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  277. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  278. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  279. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  280. package/dist/astro/routes/api/openapi.json.mjs +52 -24
  281. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  282. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  283. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +20 -32
  284. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  285. package/dist/astro/routes/api/redirects/404s/index.mjs +12 -11
  286. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  287. package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
  288. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  289. package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
  290. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  291. package/dist/astro/routes/api/redirects/index.mjs +13 -12
  292. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  293. package/dist/astro/routes/api/relations/_id_/index.mjs +16 -15
  294. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  295. package/dist/astro/routes/api/relations/_id_/translations.mjs +11 -11
  296. package/dist/astro/routes/api/relations/index.mjs +16 -15
  297. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  298. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  299. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -4
  300. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs.map +1 -1
  301. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +40 -39
  302. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  303. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +40 -39
  304. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  305. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +40 -39
  306. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  307. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +40 -39
  308. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  309. package/dist/astro/routes/api/schema/collections/index.mjs +40 -39
  310. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  311. package/dist/astro/routes/api/schema/index.mjs +9 -9
  312. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +40 -39
  313. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  314. package/dist/astro/routes/api/schema/orphans/index.mjs +40 -39
  315. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  316. package/dist/astro/routes/api/search/enable.mjs +13 -12
  317. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  318. package/dist/astro/routes/api/search/index.mjs +12 -11
  319. package/dist/astro/routes/api/search/index.mjs.map +1 -1
  320. package/dist/astro/routes/api/search/rebuild.mjs +13 -12
  321. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  322. package/dist/astro/routes/api/search/stats.mjs +8 -8
  323. package/dist/astro/routes/api/search/suggest.mjs +12 -11
  324. package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
  325. package/dist/astro/routes/api/sections/_slug_.mjs +12 -11
  326. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  327. package/dist/astro/routes/api/sections/index.mjs +12 -11
  328. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  329. package/dist/astro/routes/api/settings/backups/archives/_name_.d.mts +9 -0
  330. package/dist/astro/routes/api/settings/backups/archives/_name_.d.mts.map +1 -0
  331. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +48 -0
  332. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs.map +1 -0
  333. package/dist/astro/routes/api/settings/backups/archives/index.d.mts +8 -0
  334. package/dist/astro/routes/api/settings/backups/archives/index.d.mts.map +1 -0
  335. package/dist/astro/routes/api/settings/backups/archives/index.mjs +23 -0
  336. package/dist/astro/routes/api/settings/backups/archives/index.mjs.map +1 -0
  337. package/dist/astro/routes/api/settings/backups/export.d.mts +8 -0
  338. package/dist/astro/routes/api/settings/backups/export.d.mts.map +1 -0
  339. package/dist/astro/routes/api/settings/backups/export.mjs +35 -0
  340. package/dist/astro/routes/api/settings/backups/export.mjs.map +1 -0
  341. package/dist/astro/routes/api/settings/backups/index.d.mts +9 -0
  342. package/dist/astro/routes/api/settings/backups/index.d.mts.map +1 -0
  343. package/dist/astro/routes/api/settings/backups/index.mjs +52 -0
  344. package/dist/astro/routes/api/settings/backups/index.mjs.map +1 -0
  345. package/dist/astro/routes/api/settings/email.mjs +5 -5
  346. package/dist/astro/routes/api/settings.mjs +14 -13
  347. package/dist/astro/routes/api/settings.mjs.map +1 -1
  348. package/dist/astro/routes/api/setup/admin-verify.mjs +19 -15
  349. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  350. package/dist/astro/routes/api/setup/admin.mjs +18 -14
  351. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  352. package/dist/astro/routes/api/setup/dev-bypass.mjs +26 -26
  353. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  354. package/dist/astro/routes/api/setup/index.mjs +28 -27
  355. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  356. package/dist/astro/routes/api/setup/status.mjs +5 -5
  357. package/dist/astro/routes/api/snapshot.mjs +8 -215
  358. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  359. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +15 -14
  360. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  361. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +15 -14
  362. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  363. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +15 -14
  364. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  365. package/dist/astro/routes/api/taxonomies/index.mjs +15 -14
  366. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  367. package/dist/astro/routes/api/themes/preview.mjs +6 -6
  368. package/dist/astro/routes/api/typegen.mjs +6 -6
  369. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  370. package/dist/astro/routes/api/well-known/oauth-authorization-server.d.mts.map +1 -1
  371. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +4 -3
  372. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs.map +1 -1
  373. package/dist/astro/routes/api/well-known/oauth-protected-resource.d.mts.map +1 -1
  374. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +4 -3
  375. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs.map +1 -1
  376. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +9 -8
  377. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  378. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +12 -11
  379. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  380. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -11
  381. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  382. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -7
  383. package/dist/astro/routes/api/widget-areas/index.mjs +12 -11
  384. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  385. package/dist/astro/routes/api/widget-components.mjs +4 -4
  386. package/dist/astro/routes/robots.txt.mjs +7 -7
  387. package/dist/astro/routes/sitemap-_collection_.xml.mjs +11 -11
  388. package/dist/astro/routes/sitemap.xml.mjs +8 -8
  389. package/dist/astro/types.d.mts +38 -17
  390. package/dist/astro/types.d.mts.map +1 -1
  391. package/dist/auth/providers/github.d.mts +1 -1
  392. package/dist/auth/providers/google.d.mts +1 -1
  393. package/dist/{authorize-BmCwQCib.mjs → authorize-6WPZa1xZ.mjs} +7 -4
  394. package/dist/authorize-6WPZa1xZ.mjs.map +1 -0
  395. package/dist/backup-Qr9PaGUo.mjs +241 -0
  396. package/dist/backup-Qr9PaGUo.mjs.map +1 -0
  397. package/dist/{base64-C74Msx8h.mjs → base64-B-PsqheR.mjs} +8 -4
  398. package/dist/base64-B-PsqheR.mjs.map +1 -0
  399. package/dist/{byline-BWb5H-65.mjs → byline-BTOxUlFF.mjs} +6 -6
  400. package/dist/{byline-BWb5H-65.mjs.map → byline-BTOxUlFF.mjs.map} +1 -1
  401. package/dist/{byline-fields-DO6bAYK8.mjs → byline-fields-CdU_LTF1.mjs} +1 -1
  402. package/dist/{byline-fields-DO6bAYK8.mjs.map → byline-fields-CdU_LTF1.mjs.map} +1 -1
  403. package/dist/{bylines-CHojDMpj.mjs → bylines-FyZu6TbJ.mjs} +7 -7
  404. package/dist/{bylines-CHojDMpj.mjs.map → bylines-FyZu6TbJ.mjs.map} +1 -1
  405. package/dist/{bylines-BGuKFaBy.mjs → bylines-oVN0iPwX.mjs} +12 -8
  406. package/dist/bylines-oVN0iPwX.mjs.map +1 -0
  407. package/dist/{cache-BeR7RfN8.mjs → cache-DDwy3JDW.mjs} +2 -2
  408. package/dist/{cache-BeR7RfN8.mjs.map → cache-DDwy3JDW.mjs.map} +1 -1
  409. package/dist/{challenge-store-B426wQ6O.mjs → challenge-store-BFzgFRog.mjs} +1 -1
  410. package/dist/{challenge-store-B426wQ6O.mjs.map → challenge-store-BFzgFRog.mjs.map} +1 -1
  411. package/dist/{chunks-BU5_nrAC.mjs → chunks-Bq-5DPrD.mjs} +2 -2
  412. package/dist/{chunks-BU5_nrAC.mjs.map → chunks-Bq-5DPrD.mjs.map} +1 -1
  413. package/dist/cli/index.mjs +139 -32
  414. package/dist/cli/index.mjs.map +1 -1
  415. package/dist/client/cf-access.d.mts +1 -1
  416. package/dist/client/index.d.mts +54 -3
  417. package/dist/client/index.d.mts.map +1 -1
  418. package/dist/client/index.mjs +16 -4
  419. package/dist/client/index.mjs.map +1 -1
  420. package/dist/{comment-SWh31kS0.mjs → comment-B9cUud8H.mjs} +2 -2
  421. package/dist/{comment-SWh31kS0.mjs.map → comment-B9cUud8H.mjs.map} +1 -1
  422. package/dist/{comment-reaction-DgcpziXI.mjs → comment-reaction-VUXO981F.mjs} +2 -2
  423. package/dist/{comment-reaction-DgcpziXI.mjs.map → comment-reaction-VUXO981F.mjs.map} +1 -1
  424. package/dist/{comments-kZYGppMS.mjs → comments-PXj1E9Si.mjs} +3 -3
  425. package/dist/{comments-kZYGppMS.mjs.map → comments-PXj1E9Si.mjs.map} +1 -1
  426. package/dist/{components-CliOkZAs.mjs → components-DZkL4Fx0.mjs} +1 -1
  427. package/dist/{components-CliOkZAs.mjs.map → components-DZkL4Fx0.mjs.map} +1 -1
  428. package/dist/{config-CVssduLe.mjs → config-B6b_BobR.mjs} +6 -2
  429. package/dist/config-B6b_BobR.mjs.map +1 -0
  430. package/dist/{content-BO61Sx4j.mjs → content-BC43pICM.mjs} +53 -19
  431. package/dist/content-BC43pICM.mjs.map +1 -0
  432. package/dist/{content-refresh-BkkApzeS.mjs → content-refresh-CPMSauD3.mjs} +96 -4
  433. package/dist/content-refresh-CPMSauD3.mjs.map +1 -0
  434. package/dist/{context-DtSFYFuS.mjs → context-DOurRIrb.mjs} +97 -74
  435. package/dist/context-DOurRIrb.mjs.map +1 -0
  436. package/dist/{cron-YFJ7uNVA.mjs → cron-BlKIMD_e.mjs} +1 -1
  437. package/dist/{cron-YFJ7uNVA.mjs.map → cron-BlKIMD_e.mjs.map} +1 -1
  438. package/dist/{dashboard-xo_gPs_D.mjs → dashboard-CBm4zhc3.mjs} +4 -4
  439. package/dist/{dashboard-xo_gPs_D.mjs.map → dashboard-CBm4zhc3.mjs.map} +1 -1
  440. package/dist/database/pg-migration-lock.d.mts.map +1 -1
  441. package/dist/database/pg-migration-lock.mjs +1 -4
  442. package/dist/database/pg-migration-lock.mjs.map +1 -1
  443. package/dist/db/index.d.mts +3 -3
  444. package/dist/db/index.mjs +2 -2
  445. package/dist/db/index.mjs.map +1 -1
  446. package/dist/db/libsql.d.mts +1 -1
  447. package/dist/db/postgres.d.mts +1 -1
  448. package/dist/db/sqlite.d.mts +1 -1
  449. package/dist/{db-errors-BKuXh_fi.mjs → db-errors-CcWLaRiR.mjs} +4 -2
  450. package/dist/db-errors-CcWLaRiR.mjs.map +1 -0
  451. package/dist/{default-DY5ovGUG.mjs → default-C4v9KP56.mjs} +1 -1
  452. package/dist/{default-DY5ovGUG.mjs.map → default-C4v9KP56.mjs.map} +1 -1
  453. package/dist/{device-flow-CPXQ_QfW.mjs → device-flow-BeA6lUS1.mjs} +5 -6
  454. package/dist/device-flow-BeA6lUS1.mjs.map +1 -0
  455. package/dist/{dialect-helpers-Cue7QQOR.mjs → dialect-helpers-BtOwo2us.mjs} +60 -14
  456. package/dist/dialect-helpers-BtOwo2us.mjs.map +1 -0
  457. package/dist/{email-console-CjkQc7oY.mjs → email-console-C-9Ng8DM.mjs} +1 -1
  458. package/dist/{email-console-CjkQc7oY.mjs.map → email-console-C-9Ng8DM.mjs.map} +1 -1
  459. package/dist/error-DJ98TRbQ.mjs +90 -0
  460. package/dist/error-DJ98TRbQ.mjs.map +1 -0
  461. package/dist/{error-CfJMLSkd.mjs → errors-DtEXIQQV.mjs} +15 -84
  462. package/dist/errors-DtEXIQQV.mjs.map +1 -0
  463. package/dist/{escape-r8_GbEU2.mjs → escape-CFedIB1C.mjs} +1 -1
  464. package/dist/{escape-r8_GbEU2.mjs.map → escape-CFedIB1C.mjs.map} +1 -1
  465. package/dist/{field-defs-cache-BVrNCgSj.mjs → field-defs-cache-KfNJ_qLQ.mjs} +2 -2
  466. package/dist/{field-defs-cache-BVrNCgSj.mjs.map → field-defs-cache-KfNJ_qLQ.mjs.map} +1 -1
  467. package/dist/{fts-manager-BD9s77qh.mjs → fts-manager-hyCU9OOp.mjs} +3 -3
  468. package/dist/{fts-manager-BD9s77qh.mjs.map → fts-manager-hyCU9OOp.mjs.map} +1 -1
  469. package/dist/{hreflang-CgIxhH4k.d.mts → hreflang-CIxH_LKz.d.mts} +2 -2
  470. package/dist/{hreflang-CgIxhH4k.d.mts.map → hreflang-CIxH_LKz.d.mts.map} +1 -1
  471. package/dist/{hreflang-pAWUiPd_.mjs → hreflang-CbYeCzl9.mjs} +7 -7
  472. package/dist/{hreflang-pAWUiPd_.mjs.map → hreflang-CbYeCzl9.mjs.map} +1 -1
  473. package/dist/{import-2jHtLzsO.mjs → import-oDYCgNYQ.mjs} +4 -4
  474. package/dist/{import-2jHtLzsO.mjs.map → import-oDYCgNYQ.mjs.map} +1 -1
  475. package/dist/{index-DxjWLKGT.d.mts → index-BEui-cRd.d.mts} +3 -3
  476. package/dist/{index-DxjWLKGT.d.mts.map → index-BEui-cRd.d.mts.map} +1 -1
  477. package/dist/{index-BXOCyLj0.d.mts → index-BcEH_FfS.d.mts} +812 -16
  478. package/dist/index-BcEH_FfS.d.mts.map +1 -0
  479. package/dist/index.d.mts +18 -19
  480. package/dist/index.mjs +61 -60
  481. package/dist/{load-DgoWMdRX.mjs → load-Dg5gtWma.mjs} +2 -2
  482. package/dist/{load-DgoWMdRX.mjs.map → load-Dg5gtWma.mjs.map} +1 -1
  483. package/dist/{loader-Rb8QlGZp.mjs → loader-D4rigvjR.mjs} +170 -6
  484. package/dist/loader-D4rigvjR.mjs.map +1 -0
  485. package/dist/{manifest-schema-DeGNma2O.mjs → manifest-schema-bCq54i7F.mjs} +16 -2
  486. package/dist/manifest-schema-bCq54i7F.mjs.map +1 -0
  487. package/dist/media/image-endpoint.d.mts +30 -2
  488. package/dist/media/image-endpoint.d.mts.map +1 -1
  489. package/dist/media/image-endpoint.mjs +29 -3
  490. package/dist/media/image-endpoint.mjs.map +1 -1
  491. package/dist/media/index.d.mts +1 -1
  492. package/dist/media/local-runtime.d.mts +12 -13
  493. package/dist/media/local-runtime.d.mts.map +1 -1
  494. package/dist/media/local-runtime.mjs +6 -6
  495. package/dist/media-BJ6SRY3g.mjs +247 -0
  496. package/dist/media-BJ6SRY3g.mjs.map +1 -0
  497. package/dist/{media-allowlist-Dc05vKtt.mjs → media-allowlist-jUppPWFy.mjs} +1 -1
  498. package/dist/{media-allowlist-Dc05vKtt.mjs.map → media-allowlist-jUppPWFy.mjs.map} +1 -1
  499. package/dist/{media-D2ZFowpn.mjs → media-its1WLYv.mjs} +2 -2
  500. package/dist/{media-D2ZFowpn.mjs.map → media-its1WLYv.mjs.map} +1 -1
  501. package/dist/media-upload-DNu3QySc.mjs +145 -0
  502. package/dist/media-upload-DNu3QySc.mjs.map +1 -0
  503. package/dist/{media-url-DgIEYZr4.mjs → media-url-BCm5vBn6.mjs} +1 -1
  504. package/dist/{media-url-DgIEYZr4.mjs.map → media-url-BCm5vBn6.mjs.map} +1 -1
  505. package/dist/{media-usage-B__v9NMm.mjs → media-usage-DPQ3K4xB.mjs} +138 -5
  506. package/dist/{media-usage-B__v9NMm.mjs.map → media-usage-DPQ3K4xB.mjs.map} +1 -1
  507. package/dist/{menus-BpfevlJD.mjs → menus-BMGzWyWH.mjs} +15 -8
  508. package/dist/menus-BMGzWyWH.mjs.map +1 -0
  509. package/dist/{menus-BtvH7Z7t.mjs → menus-CPf_SW7u.mjs} +234 -13
  510. package/dist/menus-CPf_SW7u.mjs.map +1 -0
  511. package/dist/{mode-BVnhGZ2o.mjs → mode-fiXRMfeA.mjs} +1 -1
  512. package/dist/{mode-BVnhGZ2o.mjs.map → mode-fiXRMfeA.mjs.map} +1 -1
  513. package/dist/{oauth-authorization-BWqWTHBK.mjs → oauth-authorization-oqfbBuMH.mjs} +5 -6
  514. package/dist/oauth-authorization-oqfbBuMH.mjs.map +1 -0
  515. package/dist/{oauth-clients-43_XADoa.mjs → oauth-clients-BlrUHAsf.mjs} +1 -1
  516. package/dist/{oauth-clients-43_XADoa.mjs.map → oauth-clients-BlrUHAsf.mjs.map} +1 -1
  517. package/dist/{oauth-state-store-BRSUEkmx.mjs → oauth-state-store-DnvjRMy6.mjs} +2 -1
  518. package/dist/{oauth-state-store-BRSUEkmx.mjs.map → oauth-state-store-DnvjRMy6.mjs.map} +1 -1
  519. package/dist/{oauth-user-lookup-Ctgemb0k.mjs → oauth-user-lookup-oUllxvAv.mjs} +1 -1
  520. package/dist/{oauth-user-lookup-Ctgemb0k.mjs.map → oauth-user-lookup-oUllxvAv.mjs.map} +1 -1
  521. package/dist/object-cache/memory.d.mts +1 -1
  522. package/dist/{options-COXGTtqd.d.mts → options-BqMkWfYa.d.mts} +6 -6
  523. package/dist/{options-COXGTtqd.d.mts.map → options-BqMkWfYa.d.mts.map} +1 -1
  524. package/dist/page/index.d.mts +2 -2
  525. package/dist/page/index.d.mts.map +1 -1
  526. package/dist/page/index.mjs.map +1 -1
  527. package/dist/{parse-GLksnn-B.mjs → parse-53zyxlpv.mjs} +4 -12
  528. package/dist/{parse-GLksnn-B.mjs.map → parse-53zyxlpv.mjs.map} +1 -1
  529. package/dist/{passkey-config-CjA1UbDj.mjs → passkey-config-C-SKpc0-.mjs} +1 -1
  530. package/dist/{passkey-config-CjA1UbDj.mjs.map → passkey-config-C-SKpc0-.mjs.map} +1 -1
  531. package/dist/{placeholder-DR-eeZr-.d.mts → placeholder-Dop47JMM.d.mts} +1 -1
  532. package/dist/{placeholder-DR-eeZr-.d.mts.map → placeholder-Dop47JMM.d.mts.map} +1 -1
  533. package/dist/plugin-types.d.mts +21 -2
  534. package/dist/plugin-types.d.mts.map +1 -1
  535. package/dist/plugin-utils.d.mts +18 -17
  536. package/dist/plugin-utils.d.mts.map +1 -1
  537. package/dist/plugin-utils.mjs +8 -6
  538. package/dist/plugin-utils.mjs.map +1 -1
  539. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -11
  540. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  541. package/dist/plugins/adapt-sandbox-entry.mjs +15 -3
  542. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  543. package/dist/{portable-text-BAkgEKcv.mjs → portable-text-sDj92Kxv.mjs} +1 -1
  544. package/dist/{portable-text-BAkgEKcv.mjs.map → portable-text-sDj92Kxv.mjs.map} +1 -1
  545. package/dist/{preview-Rloa09Wp.mjs → preview-D4Jnbfx7.mjs} +2 -2
  546. package/dist/{preview-Rloa09Wp.mjs.map → preview-D4Jnbfx7.mjs.map} +1 -1
  547. package/dist/{public-url-C0cEvrRu.mjs → public-url-DSGTnJFw.mjs} +5 -3
  548. package/dist/public-url-DSGTnJFw.mjs.map +1 -0
  549. package/dist/{query-ZzvXyxWq.mjs → query-BbJQ-dGg.mjs} +15 -15
  550. package/dist/{query-ZzvXyxWq.mjs.map → query-BbJQ-dGg.mjs.map} +1 -1
  551. package/dist/{query-BxT95opY.mjs → query-DdFGHzQF.mjs} +3 -3
  552. package/dist/{query-BxT95opY.mjs.map → query-DdFGHzQF.mjs.map} +1 -1
  553. package/dist/{rate-limit-Cg4Rvc_-.mjs → rate-limit-D3IEiUi2.mjs} +2 -2
  554. package/dist/{rate-limit-Cg4Rvc_-.mjs.map → rate-limit-D3IEiUi2.mjs.map} +1 -1
  555. package/dist/{redirect-Ch3E98qZ.mjs → redirect-BoUac4NT.mjs} +10 -4
  556. package/dist/redirect-BoUac4NT.mjs.map +1 -0
  557. package/dist/{redirect-Ce_6Yb6z.mjs → redirect-Botom7X6.mjs} +1 -1
  558. package/dist/{redirect-Ce_6Yb6z.mjs.map → redirect-Botom7X6.mjs.map} +1 -1
  559. package/dist/{redirects-CXmVGRNZ.mjs → redirects-ChbpqKiS.mjs} +4 -4
  560. package/dist/{redirects-CXmVGRNZ.mjs.map → redirects-ChbpqKiS.mjs.map} +1 -1
  561. package/dist/{redirects-CeD7K7EX.mjs → redirects-D457qwt0.mjs} +10 -181
  562. package/dist/redirects-D457qwt0.mjs.map +1 -0
  563. package/dist/{registry-CyIXPi4E.mjs → registry-y21kjhfG.mjs} +97 -8
  564. package/dist/registry-y21kjhfG.mjs.map +1 -0
  565. package/dist/{relation-Ba68Tm4Z.mjs → relation-Du0r04E0.mjs} +3 -3
  566. package/dist/{relation-Ba68Tm4Z.mjs.map → relation-Du0r04E0.mjs.map} +1 -1
  567. package/dist/{relations-CbBpoOys.d.mts → relations-BKybBoOX.d.mts} +315 -9
  568. package/dist/relations-BKybBoOX.d.mts.map +1 -0
  569. package/dist/{relations-BmBmX4Eb.mjs → relations-CZMpBqEd.mjs} +3 -3
  570. package/dist/{relations-BmBmX4Eb.mjs.map → relations-CZMpBqEd.mjs.map} +1 -1
  571. package/dist/{relations-C8hVyEy0.mjs → relations-DtX1UMBH.mjs} +13 -7
  572. package/dist/relations-DtX1UMBH.mjs.map +1 -0
  573. package/dist/{request-meta-DCwktC5P.mjs → request-meta-DzXYYI-n.mjs} +2 -2
  574. package/dist/{request-meta-DCwktC5P.mjs.map → request-meta-DzXYYI-n.mjs.map} +1 -1
  575. package/dist/{resolve-Cd9dzclN.mjs → resolve-DFr4kq72.mjs} +2 -2
  576. package/dist/{resolve-Cd9dzclN.mjs.map → resolve-DFr4kq72.mjs.map} +1 -1
  577. package/dist/{runner-jt65bF7F.mjs → runner-BdeLEgad.mjs} +334 -200
  578. package/dist/runner-BdeLEgad.mjs.map +1 -0
  579. package/dist/{runner-CiNdIk3y.d.mts → runner-Dd8m8k9F.d.mts} +2 -2
  580. package/dist/{runner-CiNdIk3y.d.mts.map → runner-Dd8m8k9F.d.mts.map} +1 -1
  581. package/dist/runtime.d.mts +11 -12
  582. package/dist/runtime.d.mts.map +1 -1
  583. package/dist/runtime.mjs +4 -4
  584. package/dist/{schema-DwjufZ1S.mjs → schema-BH8P8vaC.mjs} +3 -3
  585. package/dist/{schema-DwjufZ1S.mjs.map → schema-BH8P8vaC.mjs.map} +1 -1
  586. package/dist/scopes-DTIkIaGA.mjs +27 -0
  587. package/dist/scopes-DTIkIaGA.mjs.map +1 -0
  588. package/dist/{search-Cl875CVE.mjs → search-D-Yhpmvs.mjs} +8 -7
  589. package/dist/search-D-Yhpmvs.mjs.map +1 -0
  590. package/dist/{secrets-CO_bEmII.mjs → secrets-CQ-69kF9.mjs} +25 -19
  591. package/dist/secrets-CQ-69kF9.mjs.map +1 -0
  592. package/dist/{sections-DTKzgoiS.mjs → sections-CzVBAKjw.mjs} +3 -3
  593. package/dist/{sections-DTKzgoiS.mjs.map → sections-CzVBAKjw.mjs.map} +1 -1
  594. package/dist/seed/index.d.mts +2 -2
  595. package/dist/seed/index.mjs +20 -20
  596. package/dist/seo/index.d.mts +14 -3
  597. package/dist/seo/index.d.mts.map +1 -1
  598. package/dist/seo/index.mjs +17 -17
  599. package/dist/seo/index.mjs.map +1 -1
  600. package/dist/{seo-BjrbXAd_.mjs → seo-C3GDfT0V.mjs} +1 -1
  601. package/dist/{seo-BjrbXAd_.mjs.map → seo-C3GDfT0V.mjs.map} +1 -1
  602. package/dist/{seo-BLm6SXzS.mjs → seo-kJzVAmmu.mjs} +2 -2
  603. package/dist/{seo-BLm6SXzS.mjs.map → seo-kJzVAmmu.mjs.map} +1 -1
  604. package/dist/{service-D6zDmKXy.mjs → service-CQnc276a.mjs} +3 -3
  605. package/dist/{service-D6zDmKXy.mjs.map → service-CQnc276a.mjs.map} +1 -1
  606. package/dist/{session-user-DoSs6YM8.mjs → session-user-DbHqKDKe.mjs} +1 -1
  607. package/dist/{session-user-DoSs6YM8.mjs.map → session-user-DbHqKDKe.mjs.map} +1 -1
  608. package/dist/{settings-BuQYvLfy.mjs → settings-C2PaBS49.mjs} +4 -4
  609. package/dist/{settings-BuQYvLfy.mjs.map → settings-C2PaBS49.mjs.map} +1 -1
  610. package/dist/{settings-CmS3TwXf.mjs → settings-Dtc0DU31.mjs} +3 -3
  611. package/dist/{settings-CmS3TwXf.mjs.map → settings-Dtc0DU31.mjs.map} +1 -1
  612. package/dist/{setup-complete-BPNgLHlU.mjs → setup-complete-CdIlqloh.mjs} +1 -1
  613. package/dist/{setup-complete-BPNgLHlU.mjs.map → setup-complete-CdIlqloh.mjs.map} +1 -1
  614. package/dist/{setup-nonce-DEPZX96T.mjs → setup-nonce-DqiIPr-J.mjs} +1 -1
  615. package/dist/{setup-nonce-DEPZX96T.mjs.map → setup-nonce-DqiIPr-J.mjs.map} +1 -1
  616. package/dist/{single-flight-cache-DEtW-OEG.mjs → single-flight-cache-C2exrGAi.mjs} +1 -1
  617. package/dist/{single-flight-cache-DEtW-OEG.mjs.map → single-flight-cache-C2exrGAi.mjs.map} +1 -1
  618. package/dist/{site-url-CcLy5D5s.mjs → site-url-NGJT6NTU.mjs} +1 -1
  619. package/dist/{site-url-CcLy5D5s.mjs.map → site-url-NGJT6NTU.mjs.map} +1 -1
  620. package/dist/snapshot-BdpUJKD-.mjs +216 -0
  621. package/dist/snapshot-BdpUJKD-.mjs.map +1 -0
  622. package/dist/{ssrf-TrKYJAy0.mjs → ssrf-CviKqWmq.mjs} +1 -1
  623. package/dist/{ssrf-TrKYJAy0.mjs.map → ssrf-CviKqWmq.mjs.map} +1 -1
  624. package/dist/state-xxv6ZTMv.mjs +128 -0
  625. package/dist/state-xxv6ZTMv.mjs.map +1 -0
  626. package/dist/{status-D3Gy1Zc6.mjs → status-COfycGIU.mjs} +1 -1
  627. package/dist/{status-D3Gy1Zc6.mjs.map → status-COfycGIU.mjs.map} +1 -1
  628. package/dist/storage/local.d.mts +1 -1
  629. package/dist/storage/local.mjs +1 -1
  630. package/dist/storage/s3.d.mts +1 -1
  631. package/dist/storage/s3.mjs +1 -1
  632. package/dist/{taxonomies-pUkuD6nj.mjs → taxonomies-HI9fnTdp.mjs} +8 -8
  633. package/dist/{taxonomies-pUkuD6nj.mjs.map → taxonomies-HI9fnTdp.mjs.map} +1 -1
  634. package/dist/{taxonomies-ClS4RKaZ.mjs → taxonomies-rMeBzOjX.mjs} +26 -17
  635. package/dist/taxonomies-rMeBzOjX.mjs.map +1 -0
  636. package/dist/{taxonomy-BkxbUMSw.mjs → taxonomy-0mCI25HN.mjs} +51 -13
  637. package/dist/taxonomy-0mCI25HN.mjs.map +1 -0
  638. package/dist/{tokens-BV6fgl5A.mjs → tokens-DVltpO2D.mjs} +2 -2
  639. package/dist/{tokens-BV6fgl5A.mjs.map → tokens-DVltpO2D.mjs.map} +1 -1
  640. package/dist/{transport-DTFhhPOg.mjs → transport-CKrlqSpD.mjs} +1 -1
  641. package/dist/{transport-DTFhhPOg.mjs.map → transport-CKrlqSpD.mjs.map} +1 -1
  642. package/dist/{transport-DmlmzD2U.d.mts → transport-Vd7VdqHv.d.mts} +1 -1
  643. package/dist/{transport-DmlmzD2U.d.mts.map → transport-Vd7VdqHv.d.mts.map} +1 -1
  644. package/dist/{trusted-proxy-CSgZVESw.mjs → trusted-proxy-CwjQj0YG.mjs} +1 -1
  645. package/dist/{trusted-proxy-CSgZVESw.mjs.map → trusted-proxy-CwjQj0YG.mjs.map} +1 -1
  646. package/dist/{types-B6-N9cBB.d.mts → types-B4vm81VH.d.mts} +1 -1
  647. package/dist/{types-B6-N9cBB.d.mts.map → types-B4vm81VH.d.mts.map} +1 -1
  648. package/dist/{types-Cm4Cz-8M.d.mts → types-B97jg3aW.d.mts} +6 -1
  649. package/dist/{types-Cm4Cz-8M.d.mts.map → types-B97jg3aW.d.mts.map} +1 -1
  650. package/dist/{types-BNZsPL9z.mjs → types-BD40g7zz.mjs} +1 -1
  651. package/dist/{types-BNZsPL9z.mjs.map → types-BD40g7zz.mjs.map} +1 -1
  652. package/dist/{types-QnlQIyPE.d.mts → types-BNfPHfay.d.mts} +1 -1
  653. package/dist/{types-QnlQIyPE.d.mts.map → types-BNfPHfay.d.mts.map} +1 -1
  654. package/dist/{types-DLIrNtv5.d.mts → types-BfYUPJdP.d.mts} +2 -2
  655. package/dist/{types-DLIrNtv5.d.mts.map → types-BfYUPJdP.d.mts.map} +1 -1
  656. package/dist/{types-CtDVagM4.d.mts → types-BvB7gDOD.d.mts} +38 -4
  657. package/dist/types-BvB7gDOD.d.mts.map +1 -0
  658. package/dist/{types-B_pmP7Yq.d.mts → types-C7U0HlEr.d.mts} +1 -1
  659. package/dist/{types-B_pmP7Yq.d.mts.map → types-C7U0HlEr.d.mts.map} +1 -1
  660. package/dist/{types-Ch8pFMEm.mjs → types-CrRUFdej.mjs} +3 -3
  661. package/dist/{types-Ch8pFMEm.mjs.map → types-CrRUFdej.mjs.map} +1 -1
  662. package/dist/{types-momGysZk.d.mts → types-DPKhI5ib.d.mts} +9 -1
  663. package/dist/types-DPKhI5ib.d.mts.map +1 -0
  664. package/dist/{types-BCVuUPrL.d.mts → types-Kmaxdarz.d.mts} +1 -1
  665. package/dist/{types-BCVuUPrL.d.mts.map → types-Kmaxdarz.d.mts.map} +1 -1
  666. package/dist/{types-qljd1hsL.d.mts → types-iVeZ5g6N.d.mts} +1 -1
  667. package/dist/{types-qljd1hsL.d.mts.map → types-iVeZ5g6N.d.mts.map} +1 -1
  668. package/dist/{user-DTxX_Fha.mjs → user-aTXqmXBz.mjs} +3 -3
  669. package/dist/{user-DTxX_Fha.mjs.map → user-aTXqmXBz.mjs.map} +1 -1
  670. package/dist/{utils-Cz8PVrl4.mjs → utils-BbUgmeZG.mjs} +1 -1
  671. package/dist/{utils-Cz8PVrl4.mjs.map → utils-BbUgmeZG.mjs.map} +1 -1
  672. package/dist/{validate-C3CpWlNt.mjs → validate-CDT1jl6_.mjs} +11 -6
  673. package/dist/validate-CDT1jl6_.mjs.map +1 -0
  674. package/dist/{validate-9jLuE301.d.mts → validate-DULfgpJP.d.mts} +5 -5
  675. package/dist/{validate-9jLuE301.d.mts.map → validate-DULfgpJP.d.mts.map} +1 -1
  676. package/dist/{validation-DM2K_NJC.mjs → validation-DlD0E35W.mjs} +10 -10
  677. package/dist/{validation-DM2K_NJC.mjs.map → validation-DlD0E35W.mjs.map} +1 -1
  678. package/dist/version-Ci6bw6YK.mjs +7 -0
  679. package/dist/{version-BlIJvNAZ.mjs.map → version-Ci6bw6YK.mjs.map} +1 -1
  680. package/dist/{widgets-B_X6_UXJ.mjs → widgets-5fipNkMT.mjs} +3 -3
  681. package/dist/{widgets-B_X6_UXJ.mjs.map → widgets-5fipNkMT.mjs.map} +1 -1
  682. package/dist/{wxr-taxonomies-tADW4KTa.d.mts → wxr-taxonomies-B4tMuN5T.d.mts} +1 -1
  683. package/dist/{wxr-taxonomies-tADW4KTa.d.mts.map → wxr-taxonomies-B4tMuN5T.d.mts.map} +1 -1
  684. package/dist/{wxr-taxonomies-oNJB2S81.mjs → wxr-taxonomies-BmUnBNgz.mjs} +4 -4
  685. package/dist/{wxr-taxonomies-oNJB2S81.mjs.map → wxr-taxonomies-BmUnBNgz.mjs.map} +1 -1
  686. package/dist/{zod-generator-CrfV79De.mjs → zod-generator-CwkHWz4f.mjs} +2 -2
  687. package/dist/{zod-generator-CrfV79De.mjs.map → zod-generator-CwkHWz4f.mjs.map} +1 -1
  688. package/package.json +8 -8
  689. package/src/api/authorize.ts +10 -1
  690. package/src/api/error.ts +5 -5
  691. package/src/api/errors.ts +14 -0
  692. package/src/api/handlers/backup.ts +312 -0
  693. package/src/api/handlers/bylines.ts +5 -4
  694. package/src/api/handlers/content.ts +61 -9
  695. package/src/api/handlers/device-flow.ts +2 -3
  696. package/src/api/handlers/index.ts +16 -0
  697. package/src/api/handlers/marketplace.ts +38 -0
  698. package/src/api/handlers/media-upload.ts +210 -0
  699. package/src/api/handlers/media-usage.ts +211 -2
  700. package/src/api/handlers/menus.ts +11 -6
  701. package/src/api/handlers/oauth-authorization.ts +2 -6
  702. package/src/api/handlers/plugin-settings.ts +218 -0
  703. package/src/api/handlers/plugins.ts +41 -0
  704. package/src/api/handlers/registry.ts +39 -0
  705. package/src/api/handlers/relations.ts +7 -2
  706. package/src/api/handlers/snapshot.ts +21 -3
  707. package/src/api/handlers/taxonomies.ts +22 -14
  708. package/src/api/openapi/document.ts +48 -6
  709. package/src/api/parse.ts +1 -15
  710. package/src/api/public-url.ts +4 -2
  711. package/src/api/schemas/bylines.ts +4 -4
  712. package/src/api/schemas/common.ts +13 -8
  713. package/src/api/schemas/media-usage.ts +72 -0
  714. package/src/api/schemas/media.ts +32 -1
  715. package/src/api/schemas/menus.ts +2 -1
  716. package/src/api/schemas/relations.ts +3 -1
  717. package/src/api/schemas/taxonomies.ts +4 -2
  718. package/src/astro/integration/index.ts +38 -0
  719. package/src/astro/integration/routes.ts +35 -0
  720. package/src/astro/integration/runtime.ts +6 -0
  721. package/src/astro/integration/virtual-modules.ts +41 -7
  722. package/src/astro/integration/vite-config.ts +17 -1
  723. package/src/astro/middleware/auth.ts +70 -78
  724. package/src/astro/middleware/csp.ts +5 -2
  725. package/src/astro/middleware.ts +86 -31
  726. package/src/astro/public-plugin-api-routes.ts +2 -1
  727. package/src/astro/routes/PluginRegistry.tsx +5 -2
  728. package/src/astro/routes/admin.astro +56 -16
  729. package/src/astro/routes/api/admin/api-tokens/index.ts +2 -2
  730. package/src/astro/routes/api/admin/bylines/[id]/translations.ts +2 -5
  731. package/src/astro/routes/api/admin/bylines/index.ts +5 -8
  732. package/src/astro/routes/api/admin/plugins/[id]/index.ts +6 -0
  733. package/src/astro/routes/api/admin/plugins/[id]/mcp.ts +42 -0
  734. package/src/astro/routes/api/admin/plugins/[id]/settings.ts +80 -0
  735. package/src/astro/routes/api/admin/plugins/[id]/update.ts +2 -0
  736. package/src/astro/routes/api/admin/plugins/index.ts +9 -0
  737. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +2 -0
  738. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +2 -0
  739. package/src/astro/routes/api/admin/plugins/registry/install.ts +2 -0
  740. package/src/astro/routes/api/admin/plugins/updates.ts +2 -4
  741. package/src/astro/routes/api/auth/mode.ts +6 -14
  742. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +21 -4
  743. package/src/astro/routes/api/auth/oauth/[provider].ts +24 -6
  744. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +3 -4
  745. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +2 -3
  746. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +2 -3
  747. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +3 -4
  748. package/src/astro/routes/api/content/[collection]/[id]/references/[relation]/children.ts +4 -6
  749. package/src/astro/routes/api/content/[collection]/[id]/references/[relation]/parents.ts +2 -3
  750. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -4
  751. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +4 -6
  752. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +3 -4
  753. package/src/astro/routes/api/content/[collection]/[id].ts +3 -4
  754. package/src/astro/routes/api/content/[collection]/authors.ts +2 -3
  755. package/src/astro/routes/api/content/[collection]/index.ts +6 -8
  756. package/src/astro/routes/api/content/[collection]/trash.ts +2 -3
  757. package/src/astro/routes/api/manifest.ts +2 -9
  758. package/src/astro/routes/api/mcp.ts +2 -1
  759. package/src/astro/routes/api/media/[id]/usage.ts +30 -0
  760. package/src/astro/routes/api/media/[id].ts +17 -6
  761. package/src/astro/routes/api/media/file/[...key].ts +8 -0
  762. package/src/astro/routes/api/media.ts +21 -2
  763. package/src/astro/routes/api/oauth/authorize.ts +43 -15
  764. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +17 -3
  765. package/src/astro/routes/api/settings/backups/archives/[name].ts +78 -0
  766. package/src/astro/routes/api/settings/backups/archives/index.ts +33 -0
  767. package/src/astro/routes/api/settings/backups/export.ts +43 -0
  768. package/src/astro/routes/api/settings/backups/index.ts +76 -0
  769. package/src/astro/routes/api/setup/admin-verify.ts +8 -4
  770. package/src/astro/routes/api/setup/admin.ts +11 -4
  771. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +4 -1
  772. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +5 -1
  773. package/src/astro/types.ts +48 -2
  774. package/src/auth/api-tokens.ts +1 -0
  775. package/src/auth/oauth-state-store.ts +3 -0
  776. package/src/auth/providers/github-admin.tsx +5 -6
  777. package/src/auth/providers/google-admin.tsx +5 -6
  778. package/src/auth/scopes.ts +3 -9
  779. package/src/cli/client-factory.ts +0 -1
  780. package/src/cli/commands/bundle-utils.ts +50 -1
  781. package/src/cli/commands/bundle.ts +9 -2
  782. package/src/cli/commands/media.ts +127 -0
  783. package/src/client/index.ts +84 -2
  784. package/src/client/transport.ts +1 -1
  785. package/src/components/EmDashHead.astro +4 -8
  786. package/src/components/Gallery.astro +141 -26
  787. package/src/components/InlinePortableTextEditor.tsx +56 -35
  788. package/src/components/marks/Link.astro +5 -1
  789. package/src/config/secrets.ts +25 -19
  790. package/src/content/converters/gallery.ts +61 -0
  791. package/src/content/converters/portable-text-to-prosemirror.ts +19 -0
  792. package/src/content/converters/prosemirror-to-portable-text.ts +18 -0
  793. package/src/content/converters/types.ts +36 -0
  794. package/src/database/dialect-helpers.ts +80 -7
  795. package/src/database/migrations/051_content_taxonomies_denorm.ts +120 -0
  796. package/src/database/migrations/052_media_usage_read_index.ts +26 -0
  797. package/src/database/migrations/053_plugin_mcp_tools.ts +21 -0
  798. package/src/database/migrations/runner.ts +6 -0
  799. package/src/database/pg-migration-lock.ts +1 -4
  800. package/src/database/repositories/audit.ts +2 -1
  801. package/src/database/repositories/content.ts +98 -16
  802. package/src/database/repositories/media-usage.ts +250 -1
  803. package/src/database/repositories/plugin-storage.ts +29 -32
  804. package/src/database/repositories/redirect.ts +14 -2
  805. package/src/database/repositories/taxonomy.ts +57 -8
  806. package/src/database/types.ts +23 -0
  807. package/src/db/adapters.ts +9 -0
  808. package/src/emdash-runtime.ts +365 -83
  809. package/src/i18n/config.ts +8 -0
  810. package/src/i18n/repair-locale-casing.ts +51 -0
  811. package/src/loader.ts +292 -1
  812. package/src/mcp/server.ts +222 -5
  813. package/src/media/image-endpoint.ts +38 -2
  814. package/src/page/metadata.ts +27 -0
  815. package/src/plugin-types.ts +19 -0
  816. package/src/plugin-utils.ts +8 -6
  817. package/src/plugins/adapt-sandbox-entry.ts +30 -1
  818. package/src/plugins/context.ts +7 -0
  819. package/src/plugins/define-plugin.ts +16 -0
  820. package/src/plugins/manifest-schema.ts +28 -1
  821. package/src/plugins/routes.ts +27 -1
  822. package/src/plugins/sandbox/runner-options.ts +16 -0
  823. package/src/plugins/sandbox/types.ts +6 -1
  824. package/src/plugins/state.ts +17 -1
  825. package/src/plugins/storage-query.ts +67 -33
  826. package/src/plugins/types.ts +41 -0
  827. package/src/schema/registry.ts +141 -5
  828. package/src/search/query.ts +3 -2
  829. package/src/seed/apply.ts +37 -36
  830. package/src/seed/validate.ts +15 -4
  831. package/src/seo/index.ts +19 -4
  832. package/src/storage/types.ts +6 -0
  833. package/src/utils/base64.ts +7 -2
  834. package/src/utils/db-errors.ts +3 -1
  835. package/src/virtual-modules.d.ts +13 -0
  836. package/src/visual-editing/toolbar.ts +1 -1
  837. package/dist/adapters-DPAkL6QF.d.mts.map +0 -1
  838. package/dist/api-DVbHNApo.mjs.map +0 -1
  839. package/dist/api-tokens-Dk2GrJMY.mjs +0 -3
  840. package/dist/apply-D23iUV4q.mjs.map +0 -1
  841. package/dist/authorize-BmCwQCib.mjs.map +0 -1
  842. package/dist/base64-C74Msx8h.mjs.map +0 -1
  843. package/dist/bylines-BGuKFaBy.mjs.map +0 -1
  844. package/dist/config-CVssduLe.mjs.map +0 -1
  845. package/dist/content-BO61Sx4j.mjs.map +0 -1
  846. package/dist/content-refresh-BkkApzeS.mjs.map +0 -1
  847. package/dist/context-DtSFYFuS.mjs.map +0 -1
  848. package/dist/db-errors-BKuXh_fi.mjs.map +0 -1
  849. package/dist/device-flow-CPXQ_QfW.mjs.map +0 -1
  850. package/dist/dialect-helpers-Cue7QQOR.mjs.map +0 -1
  851. package/dist/error-CfJMLSkd.mjs.map +0 -1
  852. package/dist/index-BXOCyLj0.d.mts.map +0 -1
  853. package/dist/loader-Rb8QlGZp.mjs.map +0 -1
  854. package/dist/manifest-schema-DeGNma2O.mjs.map +0 -1
  855. package/dist/menus-BpfevlJD.mjs.map +0 -1
  856. package/dist/menus-BtvH7Z7t.mjs.map +0 -1
  857. package/dist/oauth-authorization-BWqWTHBK.mjs.map +0 -1
  858. package/dist/public-url-C0cEvrRu.mjs.map +0 -1
  859. package/dist/redirect-Ch3E98qZ.mjs.map +0 -1
  860. package/dist/redirects-CeD7K7EX.mjs.map +0 -1
  861. package/dist/registry-CyIXPi4E.mjs.map +0 -1
  862. package/dist/relations-C8hVyEy0.mjs.map +0 -1
  863. package/dist/relations-CbBpoOys.d.mts.map +0 -1
  864. package/dist/runner-jt65bF7F.mjs.map +0 -1
  865. package/dist/scheduled-publish-DlJHGcuL.d.mts +0 -11
  866. package/dist/scheduled-publish-DlJHGcuL.d.mts.map +0 -1
  867. package/dist/search-Cl875CVE.mjs.map +0 -1
  868. package/dist/secrets-CO_bEmII.mjs.map +0 -1
  869. package/dist/taxonomies-ClS4RKaZ.mjs.map +0 -1
  870. package/dist/taxonomy-BkxbUMSw.mjs.map +0 -1
  871. package/dist/types-CtDVagM4.d.mts.map +0 -1
  872. package/dist/types-momGysZk.d.mts.map +0 -1
  873. package/dist/validate-C3CpWlNt.mjs.map +0 -1
  874. package/dist/version-BlIJvNAZ.mjs +0 -7
  875. /package/dist/{ssrf-B5DOv5E5.mjs → ssrf-BB__xfNM.mjs} +0 -0
  876. /package/dist/{types-DeCZBZxn.mjs → types-CK0LxPUQ.mjs} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"verify.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/auth/passkey/register/verify.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/passkey/register/verify\n *\n * Verify and store a new passkey credential (authenticated user)\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { verifyRegistrationResponse, registerPasskey } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { passkeyRegisterVerifyBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nconst MAX_PASSKEYS = 10;\n\ninterface PasskeyResponse {\n\tid: string;\n\tname: string | null;\n\tdeviceType: \"singleDevice\" | \"multiDevice\";\n\tbackedUp: boolean;\n\tcreatedAt: string;\n\tlastUsedAt: string;\n}\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash, user } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Require authentication\n\tif (!user) {\n\t\treturn apiError(\"NOT_AUTHENTICATED\", \"Not authenticated\", 401);\n\t}\n\n\ttry {\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Check passkey limit again (in case of concurrent requests)\n\t\tconst count = await adapter.countCredentialsByUserId(user.id);\n\t\tif (count >= MAX_PASSKEYS) {\n\t\t\treturn apiError(\"PASSKEY_LIMIT\", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);\n\t\t}\n\n\t\t// Parse request body\n\t\tconst body = await parseBody(request, passkeyRegisterVerifyBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst optionsRepo = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await optionsRepo.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Verify the registration response\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\t\tconst verified = await verifyRegistrationResponse(\n\t\t\tpasskeyConfig,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Get passkey name - prefer body.name, then check stored pending name\n\t\tlet passKeyName: string | undefined = body.name ?? undefined;\n\t\tif (!passKeyName) {\n\t\t\tconst pending = await optionsRepo.get<{ name?: string }>(`emdash:passkey_pending:${user.id}`);\n\t\t\tif (pending?.name) {\n\t\t\t\tpassKeyName = pending.name;\n\t\t\t}\n\t\t}\n\n\t\t// Clean up pending state\n\t\tawait optionsRepo.delete(`emdash:passkey_pending:${user.id}`);\n\n\t\t// Register the passkey\n\t\tconst credential = await registerPasskey(adapter, user.id, verified, passKeyName);\n\n\t\t// Return the new passkey info\n\t\tconst passkey: PasskeyResponse = {\n\t\t\tid: credential.id,\n\t\t\tname: credential.name,\n\t\t\tdeviceType: credential.deviceType,\n\t\t\tbackedUp: credential.backedUp,\n\t\t\tcreatedAt: credential.createdAt.toISOString(),\n\t\t\tlastUsedAt: credential.lastUsedAt.toISOString(),\n\t\t};\n\n\t\treturn apiSuccess({ passkey });\n\t} catch (error) {\n\t\tconsole.error(\"Passkey registration verify error:\", error);\n\n\t\t// Handle specific errors\n\t\tconst message = error instanceof Error ? error.message : \"\";\n\n\t\t// Check for duplicate credential error\n\t\tif (message.includes(\"credential_exists\") || message.includes(\"already\")) {\n\t\t\treturn apiError(\"CREDENTIAL_EXISTS\", \"This passkey is already registered\", 400);\n\t\t}\n\n\t\t// Check for challenge errors\n\t\tif (message.includes(\"challenge\") || message.includes(\"expired\")) {\n\t\t\treturn apiError(\"CHALLENGE_EXPIRED\", \"Registration expired. Please try again.\", 400);\n\t\t}\n\n\t\treturn apiError(\"PASSKEY_REGISTER_ERROR\", \"Registration failed\", 500);\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAQA,MAAa,YAAY;AAczB,MAAM,eAAe;AAWrB,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,QAAQ,SAAS;AAEzB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAIpE,KAAI,CAAC,KACJ,QAAO,SAAS,qBAAqB,qBAAqB,IAAI;AAG/D,KAAI;EACH,MAAM,UAAU,oBAAoB,OAAO,GAAG;AAI9C,MADc,MAAM,QAAQ,yBAAyB,KAAK,GAAG,IAChD,aACZ,QAAO,SAAS,iBAAiB,cAAc,aAAa,oBAAoB,IAAI;EAIrF,MAAM,OAAO,MAAM,UAAU,SAAS,0BAA0B;AAChE,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAChC,MAAM,cAAc,IAAI,kBAAkB,OAAO,GAAG;EACpD,MAAM,WAAY,MAAM,YAAY,IAAY,oBAAoB,IAAK;EACzE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EACtD,MAAM,WAAW,MAAM,2BACtB,eACA,KAAK,YACL,eACA;EAGD,IAAI,cAAkC,KAAK,QAAQ;AACnD,MAAI,CAAC,aAAa;GACjB,MAAM,UAAU,MAAM,YAAY,IAAuB,0BAA0B,KAAK,KAAK;AAC7F,OAAI,SAAS,KACZ,eAAc,QAAQ;;AAKxB,QAAM,YAAY,OAAO,0BAA0B,KAAK,KAAK;EAG7D,MAAM,aAAa,MAAM,gBAAgB,SAAS,KAAK,IAAI,UAAU,YAAY;AAYjF,SAAO,WAAW,EAAE,SATa;GAChC,IAAI,WAAW;GACf,MAAM,WAAW;GACjB,YAAY,WAAW;GACvB,UAAU,WAAW;GACrB,WAAW,WAAW,UAAU,aAAa;GAC7C,YAAY,WAAW,WAAW,aAAa;GAC/C,EAE4B,CAAC;UACtB,OAAO;AACf,UAAQ,MAAM,sCAAsC,MAAM;EAG1D,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AAGzD,MAAI,QAAQ,SAAS,oBAAoB,IAAI,QAAQ,SAAS,UAAU,CACvE,QAAO,SAAS,qBAAqB,sCAAsC,IAAI;AAIhF,MAAI,QAAQ,SAAS,YAAY,IAAI,QAAQ,SAAS,UAAU,CAC/D,QAAO,SAAS,qBAAqB,2CAA2C,IAAI;AAGrF,SAAO,SAAS,0BAA0B,uBAAuB,IAAI"}
1
+ {"version":3,"file":"verify.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/auth/passkey/register/verify.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/passkey/register/verify\n *\n * Verify and store a new passkey credential (authenticated user)\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { verifyRegistrationResponse, registerPasskey } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { passkeyRegisterVerifyBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nconst MAX_PASSKEYS = 10;\n\ninterface PasskeyResponse {\n\tid: string;\n\tname: string | null;\n\tdeviceType: \"singleDevice\" | \"multiDevice\";\n\tbackedUp: boolean;\n\tcreatedAt: string;\n\tlastUsedAt: string;\n}\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash, user } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Require authentication\n\tif (!user) {\n\t\treturn apiError(\"NOT_AUTHENTICATED\", \"Not authenticated\", 401);\n\t}\n\n\ttry {\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Check passkey limit again (in case of concurrent requests)\n\t\tconst count = await adapter.countCredentialsByUserId(user.id);\n\t\tif (count >= MAX_PASSKEYS) {\n\t\t\treturn apiError(\"PASSKEY_LIMIT\", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);\n\t\t}\n\n\t\t// Parse request body\n\t\tconst body = await parseBody(request, passkeyRegisterVerifyBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst optionsRepo = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await optionsRepo.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Verify the registration response\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\t\tconst verified = await verifyRegistrationResponse(\n\t\t\tpasskeyConfig,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Get passkey name - prefer body.name, then check stored pending name\n\t\tlet passKeyName: string | undefined = body.name ?? undefined;\n\t\tif (!passKeyName) {\n\t\t\tconst pending = await optionsRepo.get<{ name?: string }>(`emdash:passkey_pending:${user.id}`);\n\t\t\tif (pending?.name) {\n\t\t\t\tpassKeyName = pending.name;\n\t\t\t}\n\t\t}\n\n\t\t// Clean up pending state\n\t\tawait optionsRepo.delete(`emdash:passkey_pending:${user.id}`);\n\n\t\t// Register the passkey\n\t\tconst credential = await registerPasskey(adapter, user.id, verified, passKeyName);\n\n\t\t// Return the new passkey info\n\t\tconst passkey: PasskeyResponse = {\n\t\t\tid: credential.id,\n\t\t\tname: credential.name,\n\t\t\tdeviceType: credential.deviceType,\n\t\t\tbackedUp: credential.backedUp,\n\t\t\tcreatedAt: credential.createdAt.toISOString(),\n\t\t\tlastUsedAt: credential.lastUsedAt.toISOString(),\n\t\t};\n\n\t\treturn apiSuccess({ passkey });\n\t} catch (error) {\n\t\tconsole.error(\"Passkey registration verify error:\", error);\n\n\t\t// Handle specific errors\n\t\tconst message = error instanceof Error ? error.message : \"\";\n\n\t\t// Check for duplicate credential error\n\t\tif (message.includes(\"credential_exists\") || message.includes(\"already\")) {\n\t\t\treturn apiError(\"CREDENTIAL_EXISTS\", \"This passkey is already registered\", 400);\n\t\t}\n\n\t\t// Check for challenge errors\n\t\tif (message.includes(\"challenge\") || message.includes(\"expired\")) {\n\t\t\treturn apiError(\"CHALLENGE_EXPIRED\", \"Registration expired. Please try again.\", 400);\n\t\t}\n\n\t\treturn apiError(\"PASSKEY_REGISTER_ERROR\", \"Registration failed\", 500);\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAQA,MAAa,YAAY;AAczB,MAAM,eAAe;AAWrB,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,QAAQ,SAAS;AAEzB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAIpE,KAAI,CAAC,KACJ,QAAO,SAAS,qBAAqB,qBAAqB,IAAI;AAG/D,KAAI;EACH,MAAM,UAAU,oBAAoB,OAAO,GAAG;AAI9C,MADc,MAAM,QAAQ,yBAAyB,KAAK,GAAG,IAChD,aACZ,QAAO,SAAS,iBAAiB,cAAc,aAAa,oBAAoB,IAAI;EAIrF,MAAM,OAAO,MAAM,UAAU,SAAS,0BAA0B;AAChE,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAChC,MAAM,cAAc,IAAI,kBAAkB,OAAO,GAAG;EACpD,MAAM,WAAY,MAAM,YAAY,IAAY,oBAAoB,IAAK;EACzE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EACtD,MAAM,WAAW,MAAM,2BACtB,eACA,KAAK,YACL,eACA;EAGD,IAAI,cAAkC,KAAK,QAAQ;AACnD,MAAI,CAAC,aAAa;GACjB,MAAM,UAAU,MAAM,YAAY,IAAuB,0BAA0B,KAAK,KAAK;AAC7F,OAAI,SAAS,KACZ,eAAc,QAAQ;;AAKxB,QAAM,YAAY,OAAO,0BAA0B,KAAK,KAAK;EAG7D,MAAM,aAAa,MAAM,gBAAgB,SAAS,KAAK,IAAI,UAAU,YAAY;AAYjF,SAAO,WAAW,EAAE,SATa;GAChC,IAAI,WAAW;GACf,MAAM,WAAW;GACjB,YAAY,WAAW;GACvB,UAAU,WAAW;GACrB,WAAW,WAAW,UAAU,aAAa;GAC7C,YAAY,WAAW,WAAW,aAAa;GAC/C,EAE4B,CAAC;UACtB,OAAO;AACf,UAAQ,MAAM,sCAAsC,MAAM;EAG1D,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AAGzD,MAAI,QAAQ,SAAS,oBAAoB,IAAI,QAAQ,SAAS,UAAU,CACvE,QAAO,SAAS,qBAAqB,sCAAsC,IAAI;AAIhF,MAAI,QAAQ,SAAS,YAAY,IAAI,QAAQ,SAAS,UAAU,CAC/D,QAAO,SAAS,qBAAqB,2CAA2C,IAAI;AAGrF,SAAO,SAAS,0BAA0B,uBAAuB,IAAI"}
@@ -1,16 +1,17 @@
1
- import "../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../types-Ch8pFMEm.mjs";
1
+ import "../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../types-CrRUFdej.mjs";
3
3
  import { t as OptionsRepository } from "../../../../../options-BlmBHTvX.mjs";
4
- import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-CfJMLSkd.mjs";
5
- import { n as parseBody, t as isParseError } from "../../../../../parse-GLksnn-B.mjs";
6
- import "../../../../../redirects-CeD7K7EX.mjs";
7
- import { L as passkeyVerifyBody } from "../../../../../relations-BmBmX4Eb.mjs";
8
- import "../../../../../status-D3Gy1Zc6.mjs";
4
+ import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
5
+ import { n as parseBody, t as isParseError } from "../../../../../parse-53zyxlpv.mjs";
6
+ import "../../../../../media-BJ6SRY3g.mjs";
7
+ import "../../../../../redirects-D457qwt0.mjs";
8
+ import { L as passkeyVerifyBody } from "../../../../../relations-CZMpBqEd.mjs";
9
+ import "../../../../../status-COfycGIU.mjs";
9
10
  import "../../../../../api/schemas/index.mjs";
10
- import { n as getPublicOrigin } from "../../../../../public-url-C0cEvrRu.mjs";
11
- import { n as validateAllowedOrigins, t as getConfiguredAllowedOrigins } from "../../../../../allowed-origins-FXB-FplB.mjs";
12
- import { n as createChallengeStore } from "../../../../../challenge-store-B426wQ6O.mjs";
13
- import { t as getPasskeyConfig } from "../../../../../passkey-config-CjA1UbDj.mjs";
11
+ import { n as getPublicOrigin } from "../../../../../public-url-DSGTnJFw.mjs";
12
+ import { n as validateAllowedOrigins, t as getConfiguredAllowedOrigins } from "../../../../../allowed-origins-CCEi9bPI.mjs";
13
+ import { n as createChallengeStore } from "../../../../../challenge-store-BFzgFRog.mjs";
14
+ import { t as getPasskeyConfig } from "../../../../../passkey-config-C-SKpc0-.mjs";
14
15
  import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
15
16
  import { PasskeyAuthenticationError, authenticateWithPasskey } from "@emdash-cms/auth/passkey";
16
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"verify.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/passkey/verify.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/passkey/verify\n *\n * Verify a passkey authentication and create a session\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { authenticateWithPasskey, PasskeyAuthenticationError } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess, handleError } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { passkeyVerifyBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nexport const POST: APIRoute = async ({ request, locals, session }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\tconst body = await parseBody(request, passkeyVerifyBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Authenticate with passkey\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\n\t\tconst user = await authenticateWithPasskey(\n\t\t\tpasskeyConfig,\n\t\t\tadapter,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Create session\n\t\tif (session) {\n\t\t\tsession.set(\"user\", { id: user.id });\n\t\t}\n\n\t\treturn apiSuccess({\n\t\t\tsuccess: true,\n\t\t\tuser: {\n\t\t\t\tid: user.id,\n\t\t\t\temail: user.email,\n\t\t\t\tname: user.name,\n\t\t\t\trole: user.role,\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof PasskeyAuthenticationError) {\n\t\t\treturn apiError(\"UNAUTHORIZED\", \"Authentication failed\", 401);\n\t\t}\n\n\t\treturn handleError(error, \"Authentication failed\", \"PASSKEY_VERIFY_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAQA,MAAa,YAAY;AAczB,MAAa,OAAiB,OAAO,EAAE,SAAS,QAAQ,cAAc;CACrE,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAGpE,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAEhC,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;EACrE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAC9C,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EAEtD,MAAM,OAAO,MAAM,wBAClB,eACA,SACA,KAAK,YACL,eACA;AAGD,MAAI,QACH,SAAQ,IAAI,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAGrC,SAAO,WAAW;GACjB,SAAS;GACT,MAAM;IACL,IAAI,KAAK;IACT,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,MAAM,KAAK;IACX;GACD,CAAC;UACM,OAAO;AACf,MAAI,iBAAiB,2BACpB,QAAO,SAAS,gBAAgB,yBAAyB,IAAI;AAG9D,SAAO,YAAY,OAAO,yBAAyB,uBAAuB"}
1
+ {"version":3,"file":"verify.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/passkey/verify.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/passkey/verify\n *\n * Verify a passkey authentication and create a session\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { authenticateWithPasskey, PasskeyAuthenticationError } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess, handleError } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { passkeyVerifyBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nexport const POST: APIRoute = async ({ request, locals, session }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\tconst body = await parseBody(request, passkeyVerifyBody);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Authenticate with passkey\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\n\t\tconst user = await authenticateWithPasskey(\n\t\t\tpasskeyConfig,\n\t\t\tadapter,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Create session\n\t\tif (session) {\n\t\t\tsession.set(\"user\", { id: user.id });\n\t\t}\n\n\t\treturn apiSuccess({\n\t\t\tsuccess: true,\n\t\t\tuser: {\n\t\t\t\tid: user.id,\n\t\t\t\temail: user.email,\n\t\t\t\tname: user.name,\n\t\t\t\trole: user.role,\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof PasskeyAuthenticationError) {\n\t\t\treturn apiError(\"UNAUTHORIZED\", \"Authentication failed\", 401);\n\t\t}\n\n\t\treturn handleError(error, \"Authentication failed\", \"PASSKEY_VERIFY_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAQA,MAAa,YAAY;AAczB,MAAa,OAAiB,OAAO,EAAE,SAAS,QAAQ,cAAc;CACrE,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAGpE,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAEhC,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;EACrE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAC9C,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EAEtD,MAAM,OAAO,MAAM,wBAClB,eACA,SACA,KAAK,YACL,eACA;AAGD,MAAI,QACH,SAAQ,IAAI,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAGrC,SAAO,WAAW;GACjB,SAAS;GACT,MAAM;IACL,IAAI,KAAK;IACT,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,MAAM,KAAK;IACX;GACD,CAAC;UACM,OAAO;AACf,MAAI,iBAAiB,2BACpB,QAAO,SAAS,gBAAgB,yBAAyB,IAAI;AAG9D,SAAO,YAAY,OAAO,yBAAyB,uBAAuB"}
@@ -1,18 +1,19 @@
1
- import "../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../types-Ch8pFMEm.mjs";
1
+ import "../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../types-CrRUFdej.mjs";
3
3
  import { t as OptionsRepository } from "../../../../../options-BlmBHTvX.mjs";
4
- import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-CfJMLSkd.mjs";
5
- import { n as parseBody, t as isParseError } from "../../../../../parse-GLksnn-B.mjs";
6
- import "../../../../../redirects-CeD7K7EX.mjs";
7
- import { R as signupCompleteBody } from "../../../../../relations-BmBmX4Eb.mjs";
8
- import "../../../../../status-D3Gy1Zc6.mjs";
4
+ import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
5
+ import { n as parseBody, t as isParseError } from "../../../../../parse-53zyxlpv.mjs";
6
+ import "../../../../../media-BJ6SRY3g.mjs";
7
+ import "../../../../../redirects-D457qwt0.mjs";
8
+ import { R as signupCompleteBody } from "../../../../../relations-CZMpBqEd.mjs";
9
+ import "../../../../../status-COfycGIU.mjs";
9
10
  import "../../../../../api/schemas/index.mjs";
10
- import { n as getPublicOrigin } from "../../../../../public-url-C0cEvrRu.mjs";
11
- import { n as validateAllowedOrigins, t as getConfiguredAllowedOrigins } from "../../../../../allowed-origins-FXB-FplB.mjs";
12
- import { n as createChallengeStore } from "../../../../../challenge-store-B426wQ6O.mjs";
13
- import { t as getPasskeyConfig } from "../../../../../passkey-config-CjA1UbDj.mjs";
14
- import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
11
+ import { n as getPublicOrigin } from "../../../../../public-url-DSGTnJFw.mjs";
12
+ import { n as validateAllowedOrigins, t as getConfiguredAllowedOrigins } from "../../../../../allowed-origins-CCEi9bPI.mjs";
13
+ import { n as createChallengeStore } from "../../../../../challenge-store-BFzgFRog.mjs";
14
+ import { t as getPasskeyConfig } from "../../../../../passkey-config-C-SKpc0-.mjs";
15
15
  import { SignupError, completeSignup } from "@emdash-cms/auth";
16
+ import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
16
17
  import { registerPasskey, verifyRegistrationResponse } from "@emdash-cms/auth/passkey";
17
18
 
18
19
  //#region src/astro/routes/api/auth/signup/complete.ts
@@ -1 +1 @@
1
- {"version":3,"file":"complete.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/signup/complete.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/signup/complete\n *\n * Complete self-signup by registering a passkey for the new user.\n * This creates the user account and establishes a session.\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { completeSignup, SignupError } from \"@emdash-cms/auth\";\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { verifyRegistrationResponse, registerPasskey } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess, handleError } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { signupCompleteBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nexport const POST: APIRoute = async ({ request, locals, session }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\tconst body = await parseBody(request, signupCompleteBody);\n\t\tif (isParseError(body)) return body;\n\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Verify the passkey registration response\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\t\tconst verified = await verifyRegistrationResponse(\n\t\t\tpasskeyConfig,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Complete the signup - creates the user\n\t\tconst user = await completeSignup(adapter, body.token, {\n\t\t\tname: body.name,\n\t\t});\n\n\t\t// Register the passkey for the new user\n\t\tawait registerPasskey(adapter, user.id, verified, \"Initial passkey\");\n\n\t\t// Create session\n\t\tif (session) {\n\t\t\tsession.set(\"user\", { id: user.id });\n\t\t}\n\n\t\treturn apiSuccess({\n\t\t\tsuccess: true,\n\t\t\tuser: {\n\t\t\t\tid: user.id,\n\t\t\t\temail: user.email,\n\t\t\t\tname: user.name,\n\t\t\t\trole: user.role,\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof SignupError) {\n\t\t\tconst statusMap: Record<string, number> = {\n\t\t\t\tinvalid_token: 404,\n\t\t\t\ttoken_expired: 410,\n\t\t\t\tuser_exists: 409,\n\t\t\t\tdomain_not_allowed: 403,\n\t\t\t};\n\t\t\treturn apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);\n\t\t}\n\n\t\treturn handleError(error, \"Failed to complete signup\", \"SIGNUP_COMPLETE_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AASA,MAAa,YAAY;AAezB,MAAa,OAAiB,OAAO,EAAE,SAAS,QAAQ,cAAc;CACrE,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAGpE,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,mBAAmB;AACzD,MAAI,aAAa,KAAK,CAAE,QAAO;EAE/B,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAG9C,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAEhC,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;EACrE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EACtD,MAAM,WAAW,MAAM,2BACtB,eACA,KAAK,YACL,eACA;EAGD,MAAM,OAAO,MAAM,eAAe,SAAS,KAAK,OAAO,EACtD,MAAM,KAAK,MACX,CAAC;AAGF,QAAM,gBAAgB,SAAS,KAAK,IAAI,UAAU,kBAAkB;AAGpE,MAAI,QACH,SAAQ,IAAI,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAGrC,SAAO,WAAW;GACjB,SAAS;GACT,MAAM;IACL,IAAI,KAAK;IACT,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,MAAM,KAAK;IACX;GACD,CAAC;UACM,OAAO;AACf,MAAI,iBAAiB,YAOpB,QAAO,SAAS,MAAM,KAAK,aAAa,EAAE,MAAM,SANN;GACzC,eAAe;GACf,eAAe;GACf,aAAa;GACb,oBAAoB;GACpB,CACkE,MAAM,SAAS,IAAI;AAGvF,SAAO,YAAY,OAAO,6BAA6B,wBAAwB"}
1
+ {"version":3,"file":"complete.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/auth/signup/complete.ts"],"sourcesContent":["/**\n * POST /_emdash/api/auth/signup/complete\n *\n * Complete self-signup by registering a passkey for the new user.\n * This creates the user account and establishes a session.\n */\n\nimport type { APIRoute } from \"astro\";\n\nexport const prerender = false;\n\nimport { completeSignup, SignupError } from \"@emdash-cms/auth\";\nimport { createKyselyAdapter } from \"@emdash-cms/auth/adapters/kysely\";\nimport { verifyRegistrationResponse, registerPasskey } from \"@emdash-cms/auth/passkey\";\n\nimport { apiError, apiSuccess, handleError } from \"#api/error.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { signupCompleteBody } from \"#api/schemas.js\";\nimport { getConfiguredAllowedOrigins, validateAllowedOrigins } from \"#auth/allowed-origins.js\";\nimport { createChallengeStore } from \"#auth/challenge-store.js\";\nimport { getPasskeyConfig } from \"#auth/passkey-config.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nexport const POST: APIRoute = async ({ request, locals, session }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\tconst body = await parseBody(request, signupCompleteBody);\n\t\tif (isParseError(body)) return body;\n\n\t\tconst adapter = createKyselyAdapter(emdash.db);\n\n\t\t// Get passkey config\n\t\tconst url = new URL(request.url);\n\t\tconst options = new OptionsRepository(emdash.db);\n\t\tconst siteName = (await options.get<string>(\"emdash:site_title\")) ?? undefined;\n\t\tconst siteUrl = getPublicOrigin(url, emdash?.config);\n\t\tconst allowedOrigins = validateAllowedOrigins(\n\t\t\tsiteUrl,\n\t\t\tgetConfiguredAllowedOrigins(emdash?.config),\n\t\t);\n\t\tconst passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);\n\n\t\t// Verify the passkey registration response\n\t\tconst challengeStore = createChallengeStore(emdash.db);\n\t\tconst verified = await verifyRegistrationResponse(\n\t\t\tpasskeyConfig,\n\t\t\tbody.credential,\n\t\t\tchallengeStore,\n\t\t);\n\n\t\t// Complete the signup - creates the user\n\t\tconst user = await completeSignup(adapter, body.token, {\n\t\t\tname: body.name,\n\t\t});\n\n\t\t// Register the passkey for the new user\n\t\tawait registerPasskey(adapter, user.id, verified, \"Initial passkey\");\n\n\t\t// Create session\n\t\tif (session) {\n\t\t\tsession.set(\"user\", { id: user.id });\n\t\t}\n\n\t\treturn apiSuccess({\n\t\t\tsuccess: true,\n\t\t\tuser: {\n\t\t\t\tid: user.id,\n\t\t\t\temail: user.email,\n\t\t\t\tname: user.name,\n\t\t\t\trole: user.role,\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof SignupError) {\n\t\t\tconst statusMap: Record<string, number> = {\n\t\t\t\tinvalid_token: 404,\n\t\t\t\ttoken_expired: 410,\n\t\t\t\tuser_exists: 409,\n\t\t\t\tdomain_not_allowed: 403,\n\t\t\t};\n\t\t\treturn apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);\n\t\t}\n\n\t\treturn handleError(error, \"Failed to complete signup\", \"SIGNUP_COMPLETE_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AASA,MAAa,YAAY;AAezB,MAAa,OAAiB,OAAO,EAAE,SAAS,QAAQ,cAAc;CACrE,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAGpE,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,mBAAmB;AACzD,MAAI,aAAa,KAAK,CAAE,QAAO;EAE/B,MAAM,UAAU,oBAAoB,OAAO,GAAG;EAG9C,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAEhC,MAAM,WAAY,MADF,IAAI,kBAAkB,OAAO,GAAG,CAChB,IAAY,oBAAoB,IAAK;EACrE,MAAM,UAAU,gBAAgB,KAAK,QAAQ,OAAO;EAKpD,MAAM,gBAAgB,iBAAiB,KAAK,UAAU,SAJ/B,uBACtB,SACA,4BAA4B,QAAQ,OAAO,CAC3C,CAC6E;EAG9E,MAAM,iBAAiB,qBAAqB,OAAO,GAAG;EACtD,MAAM,WAAW,MAAM,2BACtB,eACA,KAAK,YACL,eACA;EAGD,MAAM,OAAO,MAAM,eAAe,SAAS,KAAK,OAAO,EACtD,MAAM,KAAK,MACX,CAAC;AAGF,QAAM,gBAAgB,SAAS,KAAK,IAAI,UAAU,kBAAkB;AAGpE,MAAI,QACH,SAAQ,IAAI,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAGrC,SAAO,WAAW;GACjB,SAAS;GACT,MAAM;IACL,IAAI,KAAK;IACT,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,MAAM,KAAK;IACX;GACD,CAAC;UACM,OAAO;AACf,MAAI,iBAAiB,YAOpB,QAAO,SAAS,MAAM,KAAK,aAAa,EAAE,MAAM,SANN;GACzC,eAAe;GACf,eAAe;GACf,aAAa;GACb,oBAAoB;GACpB,CACkE,MAAM,SAAS,IAAI;AAGvF,SAAO,YAAY,OAAO,6BAA6B,wBAAwB"}
@@ -1,17 +1,18 @@
1
- import "../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../types-Ch8pFMEm.mjs";
1
+ import "../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../types-CrRUFdej.mjs";
3
3
  import { t as OptionsRepository } from "../../../../../options-BlmBHTvX.mjs";
4
- import { n as apiSuccess, t as apiError } from "../../../../../error-CfJMLSkd.mjs";
5
- import { n as parseBody, t as isParseError } from "../../../../../parse-GLksnn-B.mjs";
6
- import "../../../../../redirects-CeD7K7EX.mjs";
7
- import { z as signupRequestBody } from "../../../../../relations-BmBmX4Eb.mjs";
8
- import "../../../../../status-D3Gy1Zc6.mjs";
4
+ import { n as apiSuccess, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
5
+ import { n as parseBody, t as isParseError } from "../../../../../parse-53zyxlpv.mjs";
6
+ import "../../../../../media-BJ6SRY3g.mjs";
7
+ import "../../../../../redirects-D457qwt0.mjs";
8
+ import { z as signupRequestBody } from "../../../../../relations-CZMpBqEd.mjs";
9
+ import "../../../../../status-COfycGIU.mjs";
9
10
  import "../../../../../api/schemas/index.mjs";
10
- import { t as getTrustedProxyHeaders } from "../../../../../trusted-proxy-CSgZVESw.mjs";
11
- import { t as getSiteBaseUrl } from "../../../../../site-url-CcLy5D5s.mjs";
12
- import { n as getClientIp, t as checkRateLimit } from "../../../../../rate-limit-Cg4Rvc_-.mjs";
13
- import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
11
+ import { t as getTrustedProxyHeaders } from "../../../../../trusted-proxy-CwjQj0YG.mjs";
12
+ import { t as getSiteBaseUrl } from "../../../../../site-url-NGJT6NTU.mjs";
13
+ import { n as getClientIp, t as checkRateLimit } from "../../../../../rate-limit-D3IEiUi2.mjs";
14
14
  import { requestSignup } from "@emdash-cms/auth";
15
+ import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
15
16
 
16
17
  //#region src/astro/routes/api/auth/signup/request.ts
17
18
  const prerender = false;
@@ -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,8 +1,8 @@
1
- import "../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../types-Ch8pFMEm.mjs";
3
- import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-CfJMLSkd.mjs";
4
- import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
1
+ import "../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../types-CrRUFdej.mjs";
3
+ import { n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
5
4
  import { SignupError, roleFromLevel, validateSignupToken } from "@emdash-cms/auth";
5
+ import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
6
6
 
7
7
  //#region src/astro/routes/api/auth/signup/verify.ts
8
8
  const prerender = false;
@@ -1,21 +1,22 @@
1
1
  import { t as validateIdentifier } from "../../../../../../validate-VPnKoIzW.mjs";
2
2
  import "../../../../../../after-B1IIdH3Y.mjs";
3
3
  import "../../../../../../object-cache-Bok5j2ae.mjs";
4
- import "../../../../../../base64-C74Msx8h.mjs";
5
- import "../../../../../../types-Ch8pFMEm.mjs";
6
- import { t as CommentRepository } from "../../../../../../comment-SWh31kS0.mjs";
4
+ import "../../../../../../base64-B-PsqheR.mjs";
5
+ import "../../../../../../types-CrRUFdej.mjs";
6
+ import { t as CommentRepository } from "../../../../../../comment-B9cUud8H.mjs";
7
7
  import "../../../../../../options-BlmBHTvX.mjs";
8
- import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-CfJMLSkd.mjs";
9
- import { n as parseBody, t as isParseError } from "../../../../../../parse-GLksnn-B.mjs";
10
- import { yt as createCommentBody } from "../../../../../../redirects-CeD7K7EX.mjs";
11
- import "../../../../../../relations-BmBmX4Eb.mjs";
12
- import "../../../../../../status-D3Gy1Zc6.mjs";
8
+ import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
9
+ import { n as parseBody, t as isParseError } from "../../../../../../parse-53zyxlpv.mjs";
10
+ import "../../../../../../media-BJ6SRY3g.mjs";
11
+ import { yt as createCommentBody } from "../../../../../../redirects-D457qwt0.mjs";
12
+ import "../../../../../../relations-CZMpBqEd.mjs";
13
+ import "../../../../../../status-COfycGIU.mjs";
13
14
  import "../../../../../../api/schemas/index.mjs";
14
- import { t as extractRequestMeta } from "../../../../../../request-meta-DCwktC5P.mjs";
15
- import { i as resolveSecretsCached } from "../../../../../../secrets-CO_bEmII.mjs";
16
- import { c as hashIp, s as handleCommentList, t as checkRateLimit } from "../../../../../../comments-kZYGppMS.mjs";
17
- import { t as getSiteBaseUrl } from "../../../../../../site-url-CcLy5D5s.mjs";
18
- import { i as sendCommentNotification, t as createComment } from "../../../../../../service-D6zDmKXy.mjs";
15
+ import { t as extractRequestMeta } from "../../../../../../request-meta-DzXYYI-n.mjs";
16
+ import { i as resolveSecretsCached } from "../../../../../../secrets-CQ-69kF9.mjs";
17
+ import { c as hashIp, s as handleCommentList, t as checkRateLimit } from "../../../../../../comments-PXj1E9Si.mjs";
18
+ import { t as getSiteBaseUrl } from "../../../../../../site-url-NGJT6NTU.mjs";
19
+ import { i as sendCommentNotification, t as createComment } from "../../../../../../service-CQnc276a.mjs";
19
20
 
20
21
  //#region src/comments/turnstile.ts
21
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../src/comments/turnstile.ts","../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/index.ts"],"sourcesContent":["/**\n * Server-side Turnstile verification for public comment submissions.\n *\n * The comment form widget (`CommentForm.astro`) submits a `turnstileToken`;\n * this verifies it against Cloudflare's siteverify API. Enforcement is\n * opt-in: it only runs when the operator configures the Turnstile secret\n * key (`EMDASH_TURNSTILE_SECRET_KEY` or `TURNSTILE_SECRET_KEY`), so\n * existing non-Turnstile sites are unaffected.\n *\n * Mirrors `verifyTurnstile` in `@emdash-cms/plugin-forms` (not imported —\n * core doesn't depend on plugin packages).\n */\n\nconst SITEVERIFY_URL = \"https://challenges.cloudflare.com/turnstile/v0/siteverify\";\n\n/**\n * Resolve the configured Turnstile secret key, or `\"\"` when Turnstile\n * enforcement is not configured.\n */\nexport function getTurnstileSecretKey(): string {\n\treturn import.meta.env.EMDASH_TURNSTILE_SECRET_KEY || import.meta.env.TURNSTILE_SECRET_KEY || \"\";\n}\n\n/**\n * Verify a Turnstile response token via siteverify.\n *\n * Fails closed: a missing token, a failed verification, and a siteverify\n * transport error all return `false` — when the operator has configured a\n * secret, an unverifiable submission must not be persisted.\n */\nexport async function verifyTurnstileToken(\n\ttoken: string | undefined,\n\tsecretKey: string,\n\tremoteIp?: string | null,\n): Promise<boolean> {\n\tif (!token) return false;\n\n\tconst body: Record<string, string> = { secret: secretKey, response: token };\n\tif (remoteIp) {\n\t\tbody.remoteip = remoteIp;\n\t}\n\n\ttry {\n\t\tconst res = await fetch(SITEVERIFY_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\t// Fail closed *quickly* if siteverify is slow — without a timeout\n\t\t\t// the comment POST would hang until the runtime kills it\n\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t});\n\t\tconst data: { success?: boolean; \"error-codes\"?: string[] } = await res.json();\n\t\tif (!data.success) {\n\t\t\tconsole.warn(\"[comments] Turnstile verification failed:\", data[\"error-codes\"] ?? []);\n\t\t}\n\t\treturn data.success === true;\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t\"[comments] Turnstile siteverify request failed:\",\n\t\t\terror instanceof Error ? error.message : error,\n\t\t);\n\t\treturn false;\n\t}\n}\n","/**\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 { getTurnstileSecretKey, verifyTurnstileToken } from \"#comments/turnstile.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// Anti-spam: Turnstile — enforced only when the operator configured a\n\t\t// secret key. Runs after the free checks (honeypot, rate limit) so\n\t\t// obvious spam never triggers a siteverify subrequest. Fails closed:\n\t\t// missing token, failed verification, and siteverify errors all reject.\n\t\tconst turnstileSecretKey = getTurnstileSecretKey();\n\t\tif (turnstileSecretKey) {\n\t\t\tconst verified = await verifyTurnstileToken(body.turnstileToken, turnstileSecretKey, meta.ip);\n\t\t\tif (!verified) {\n\t\t\t\treturn apiError(\"TURNSTILE_FAILED\", \"CAPTCHA verification failed\", 403);\n\t\t\t}\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/comments/turnstile.ts","../../../../../../../src/astro/routes/api/comments/[collection]/[contentId]/index.ts"],"sourcesContent":["/**\n * Server-side Turnstile verification for public comment submissions.\n *\n * The comment form widget (`CommentForm.astro`) submits a `turnstileToken`;\n * this verifies it against Cloudflare's siteverify API. Enforcement is\n * opt-in: it only runs when the operator configures the Turnstile secret\n * key (`EMDASH_TURNSTILE_SECRET_KEY` or `TURNSTILE_SECRET_KEY`), so\n * existing non-Turnstile sites are unaffected.\n *\n * Mirrors `verifyTurnstile` in `@emdash-cms/plugin-forms` (not imported —\n * core doesn't depend on plugin packages).\n */\n\nconst SITEVERIFY_URL = \"https://challenges.cloudflare.com/turnstile/v0/siteverify\";\n\n/**\n * Resolve the configured Turnstile secret key, or `\"\"` when Turnstile\n * enforcement is not configured.\n */\nexport function getTurnstileSecretKey(): string {\n\treturn import.meta.env.EMDASH_TURNSTILE_SECRET_KEY || import.meta.env.TURNSTILE_SECRET_KEY || \"\";\n}\n\n/**\n * Verify a Turnstile response token via siteverify.\n *\n * Fails closed: a missing token, a failed verification, and a siteverify\n * transport error all return `false` — when the operator has configured a\n * secret, an unverifiable submission must not be persisted.\n */\nexport async function verifyTurnstileToken(\n\ttoken: string | undefined,\n\tsecretKey: string,\n\tremoteIp?: string | null,\n): Promise<boolean> {\n\tif (!token) return false;\n\n\tconst body: Record<string, string> = { secret: secretKey, response: token };\n\tif (remoteIp) {\n\t\tbody.remoteip = remoteIp;\n\t}\n\n\ttry {\n\t\tconst res = await fetch(SITEVERIFY_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\t// Fail closed *quickly* if siteverify is slow — without a timeout\n\t\t\t// the comment POST would hang until the runtime kills it\n\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t});\n\t\tconst data: { success?: boolean; \"error-codes\"?: string[] } = await res.json();\n\t\tif (!data.success) {\n\t\t\tconsole.warn(\"[comments] Turnstile verification failed:\", data[\"error-codes\"] ?? []);\n\t\t}\n\t\treturn data.success === true;\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t\"[comments] Turnstile siteverify request failed:\",\n\t\t\terror instanceof Error ? error.message : error,\n\t\t);\n\t\treturn false;\n\t}\n}\n","/**\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 { getTurnstileSecretKey, verifyTurnstileToken } from \"#comments/turnstile.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// Anti-spam: Turnstile — enforced only when the operator configured a\n\t\t// secret key. Runs after the free checks (honeypot, rate limit) so\n\t\t// obvious spam never triggers a siteverify subrequest. Fails closed:\n\t\t// missing token, failed verification, and siteverify errors all reject.\n\t\tconst turnstileSecretKey = getTurnstileSecretKey();\n\t\tif (turnstileSecretKey) {\n\t\t\tconst verified = await verifyTurnstileToken(body.turnstileToken, turnstileSecretKey, meta.ip);\n\t\t\tif (!verified) {\n\t\t\t\treturn apiError(\"TURNSTILE_FAILED\", \"CAPTCHA verification failed\", 403);\n\t\t\t}\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,19 +1,20 @@
1
1
  import "../../../../../../after-B1IIdH3Y.mjs";
2
2
  import { s as invalidateCommentObjectCache } from "../../../../../../object-cache-Bok5j2ae.mjs";
3
- import "../../../../../../base64-C74Msx8h.mjs";
4
- import "../../../../../../types-Ch8pFMEm.mjs";
5
- import "../../../../../../comment-SWh31kS0.mjs";
3
+ import "../../../../../../base64-B-PsqheR.mjs";
4
+ import "../../../../../../types-CrRUFdej.mjs";
5
+ import "../../../../../../comment-B9cUud8H.mjs";
6
6
  import "../../../../../../options-BlmBHTvX.mjs";
7
- import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-CfJMLSkd.mjs";
8
- import { n as parseBody, t as isParseError } from "../../../../../../parse-GLksnn-B.mjs";
9
- import { bt as createReactionBody } from "../../../../../../redirects-CeD7K7EX.mjs";
10
- import "../../../../../../relations-BmBmX4Eb.mjs";
11
- import "../../../../../../status-D3Gy1Zc6.mjs";
7
+ import { a as unwrapResult, i as requireDb, n as apiSuccess, r as handleError, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
8
+ import { n as parseBody, t as isParseError } from "../../../../../../parse-53zyxlpv.mjs";
9
+ import "../../../../../../media-BJ6SRY3g.mjs";
10
+ import { bt as createReactionBody } from "../../../../../../redirects-D457qwt0.mjs";
11
+ import "../../../../../../relations-CZMpBqEd.mjs";
12
+ import "../../../../../../status-COfycGIU.mjs";
12
13
  import "../../../../../../api/schemas/index.mjs";
13
- import { t as extractRequestMeta } from "../../../../../../request-meta-DCwktC5P.mjs";
14
- import { t as CommentReactionRepository } from "../../../../../../comment-reaction-DgcpziXI.mjs";
15
- import { i as resolveSecretsCached } from "../../../../../../secrets-CO_bEmII.mjs";
16
- import { c as hashIp } from "../../../../../../comments-kZYGppMS.mjs";
14
+ import { t as extractRequestMeta } from "../../../../../../request-meta-DzXYYI-n.mjs";
15
+ import { t as CommentReactionRepository } from "../../../../../../comment-reaction-VUXO981F.mjs";
16
+ import { i as resolveSecretsCached } from "../../../../../../secrets-CQ-69kF9.mjs";
17
+ import { c as hashIp } from "../../../../../../comments-PXj1E9Si.mjs";
17
18
 
18
19
  //#region src/api/handlers/comment-reactions.ts
19
20
  /** Max reactions a single voter may register per window before throttling. */
@@ -1 +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
+ {"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,17 +1,17 @@
1
- import "../../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../../types-Ch8pFMEm.mjs";
3
- import { a as unwrapResult, t as apiError } from "../../../../../../error-CfJMLSkd.mjs";
4
- import { n as requirePerm } from "../../../../../../authorize-BmCwQCib.mjs";
1
+ import "../../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../../types-CrRUFdej.mjs";
3
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
4
+ import { r as requirePerm } from "../../../../../../authorize-6WPZa1xZ.mjs";
5
5
 
6
6
  //#region src/astro/routes/api/content/[collection]/[id]/compare.ts
7
7
  const prerender = false;
8
8
  const GET = async ({ params, locals }) => {
9
9
  const { emdash, user } = locals;
10
+ if (!emdash?.handleContentCompare) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
10
11
  const denied = requirePerm(user, "content:read_drafts");
11
12
  if (denied) return denied;
12
13
  const collection = params.collection;
13
14
  const id = params.id;
14
- if (!emdash?.handleContentCompare) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
15
15
  return unwrapResult(await emdash.handleContentCompare(collection, id));
16
16
  };
17
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"compare.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/compare.ts"],"sourcesContent":["/**\n * Compare live and draft revisions\n *\n * GET /_emdash/api/content/{collection}/{id}/compare\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const GET: APIRoute = async ({ params, 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?.handleContentCompare) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\tconst result = await emdash.handleContentCompare(collection, id);\n\n\treturn unwrapResult(result);\n};\n"],"mappings":";;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,MAAgB,OAAO,EAAE,QAAQ,aAAa;CAC1D,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,qBACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAKpE,QAAO,aAFQ,MAAM,OAAO,qBAAqB,YAAY,GAAG,CAErC"}
1
+ {"version":3,"file":"compare.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/compare.ts"],"sourcesContent":["/**\n * Compare live and draft revisions\n *\n * GET /_emdash/api/content/{collection}/{id}/compare\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const GET: APIRoute = async ({ params, locals }) => {\n\tconst { emdash, user } = locals;\n\tif (!emdash?.handleContentCompare) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\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\tconst result = await emdash.handleContentCompare(collection, id);\n\n\treturn unwrapResult(result);\n};\n"],"mappings":";;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,MAAgB,OAAO,EAAE,QAAQ,aAAa;CAC1D,MAAM,EAAE,QAAQ,SAAS;AACzB,KAAI,CAAC,QAAQ,qBACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAEpE,MAAM,SAAS,YAAY,MAAM,sBAAsB;AACvD,KAAI,OAAQ,QAAO;CACnB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAIlB,QAAO,aAFQ,MAAM,OAAO,qBAAqB,YAAY,GAAG,CAErC"}
@@ -1,7 +1,8 @@
1
- import "../../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../../types-Ch8pFMEm.mjs";
3
- import { a as unwrapResult, s as mapErrorStatus, t as apiError } from "../../../../../../error-CfJMLSkd.mjs";
4
- import { t as requireOwnerPerm } from "../../../../../../authorize-BmCwQCib.mjs";
1
+ import "../../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../../types-CrRUFdej.mjs";
3
+ import { n as mapErrorStatus } from "../../../../../../errors-DtEXIQQV.mjs";
4
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
5
+ import { n as requireOwnerPerm } from "../../../../../../authorize-6WPZa1xZ.mjs";
5
6
 
6
7
  //#region src/astro/routes/api/content/[collection]/[id]/discard-draft.ts
7
8
  const prerender = false;
@@ -1 +1 @@
1
- {"version":3,"file":"discard-draft.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/discard-draft.ts"],"sourcesContent":["/**\n * Discard draft changes - reverts to live version\n *\n * POST /_emdash/api/content/{collection}/{id}/discard-draft\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, locals, url, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.handleContentDiscardDraft || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\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\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\t// Handler returns { item, _rev } — extract the item for ownership check\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:edit_own\", \"content:edit_any\");\n\tif (denied) return denied;\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\n\tconst result = await emdash.handleContentDiscardDraft(collection, resolvedId);\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":";;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,QAAQ,KAAK,YAAY;CACvE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,6BAA6B,CAAC,QAAQ,iBAClD,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,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;CAEF,MAAM,eACL,SAAS,QAAQ,OAAO,SAAS,SAAS,WAEvC,SAAS,OACT;CAEJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAEJ,MAAM,SAAS,iBAAiB,MADf,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IACtC,oBAAoB,mBAAmB;AACvF,KAAI,OAAQ,QAAO;CAEnB,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAE5E,MAAM,SAAS,MAAM,OAAO,0BAA0B,YAAY,WAAW;AAE7E,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":"discard-draft.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/discard-draft.ts"],"sourcesContent":["/**\n * Discard draft changes - reverts to live version\n *\n * POST /_emdash/api/content/{collection}/{id}/discard-draft\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, locals, url, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.handleContentDiscardDraft || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\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\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\t// Handler returns { item, _rev } — extract the item for ownership check\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:edit_own\", \"content:edit_any\");\n\tif (denied) return denied;\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\n\tconst result = await emdash.handleContentDiscardDraft(collection, resolvedId);\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":";;;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,QAAQ,KAAK,YAAY;CACvE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,6BAA6B,CAAC,QAAQ,iBAClD,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,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;CAEF,MAAM,eACL,SAAS,QAAQ,OAAO,SAAS,SAAS,WAEvC,SAAS,OACT;CAEJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAEJ,MAAM,SAAS,iBAAiB,MADf,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IACtC,oBAAoB,mBAAmB;AACvF,KAAI,OAAQ,QAAO;CAEnB,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAE5E,MAAM,SAAS,MAAM,OAAO,0BAA0B,YAAY,WAAW;AAE7E,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,8 @@
1
- import "../../../../../../base64-C74Msx8h.mjs";
2
- import "../../../../../../types-Ch8pFMEm.mjs";
3
- import { a as unwrapResult, s as mapErrorStatus, t as apiError } from "../../../../../../error-CfJMLSkd.mjs";
4
- import { n as requirePerm, t as requireOwnerPerm } from "../../../../../../authorize-BmCwQCib.mjs";
1
+ import "../../../../../../base64-B-PsqheR.mjs";
2
+ import "../../../../../../types-CrRUFdej.mjs";
3
+ import { n as mapErrorStatus } from "../../../../../../errors-DtEXIQQV.mjs";
4
+ import { a as unwrapResult, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
5
+ import { n as requireOwnerPerm, r as requirePerm } from "../../../../../../authorize-6WPZa1xZ.mjs";
5
6
 
6
7
  //#region src/astro/routes/api/content/[collection]/[id]/duplicate.ts
7
8
  const prerender = false;
@@ -9,9 +10,9 @@ const POST = async ({ params, locals, cache }) => {
9
10
  const { emdash, user } = locals;
10
11
  const collection = params.collection;
11
12
  const id = params.id;
13
+ if (!emdash?.handleContentDuplicate || !emdash?.handleContentGet) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
12
14
  const denied = requirePerm(user, "content:create");
13
15
  if (denied) return denied;
14
- if (!emdash?.handleContentDuplicate || !emdash?.handleContentGet) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
15
16
  const existing = await emdash.handleContentGet(collection, id);
16
17
  if (!existing.success) return apiError(existing.error?.code ?? "UNKNOWN_ERROR", existing.error?.message ?? "Unknown error", mapErrorStatus(existing.error?.code));
17
18
  const existingData = existing.data && typeof existing.data === "object" ? existing.data : void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"duplicate.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/duplicate.ts"],"sourcesContent":["/**\n * Duplicate content endpoint - injected by EmDash integration\n *\n * POST /_emdash/api/content/{collection}/{id}/duplicate - Create a copy\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm, requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, locals, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tconst denied = requirePerm(user, \"content:create\");\n\tif (denied) return denied;\n\n\tif (!emdash?.handleContentDuplicate || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\t// Fetch item to check ownership — duplicating requires read access to the source\n\tconst existing = await emdash.handleContentGet(collection, id);\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\t// Handler returns { item, _rev } — extract the item for ownership check\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\t// Duplicating requires read access to the source — check ownership-based edit permissions\n\t// since content:read is flat (no own/any split). This ensures authors can only duplicate their own.\n\tconst readDenied = requireOwnerPerm(user, authorId, \"content:edit_own\", \"content:edit_any\");\n\tif (readDenied) return readDenied;\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\tconst result = await emdash.handleContentDuplicate(collection, resolvedId, user?.id);\n\n\tif (!result.success) return unwrapResult(result);\n\n\tif (cache?.enabled) await cache.invalidate({ tags: [collection] });\n\n\treturn unwrapResult(result, 201);\n};\n"],"mappings":";;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,QAAQ,YAAY;CAClE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;CAElB,MAAM,SAAS,YAAY,MAAM,iBAAiB;AAClD,KAAI,OAAQ,QAAO;AAEnB,KAAI,CAAC,QAAQ,0BAA0B,CAAC,QAAQ,iBAC/C,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAIpE,MAAM,WAAW,MAAM,OAAO,iBAAiB,YAAY,GAAG;AAC9D,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;CAEJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAIJ,MAAM,aAAa,iBAAiB,MAHnB,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IAGlC,oBAAoB,mBAAmB;AAC3F,KAAI,WAAY,QAAO;CAEvB,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAC5E,MAAM,SAAS,MAAM,OAAO,uBAAuB,YAAY,YAAY,MAAM,GAAG;AAEpF,KAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAEhD,KAAI,OAAO,QAAS,OAAM,MAAM,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;AAElE,QAAO,aAAa,QAAQ,IAAI"}
1
+ {"version":3,"file":"duplicate.mjs","names":[],"sources":["../../../../../../../src/astro/routes/api/content/[collection]/[id]/duplicate.ts"],"sourcesContent":["/**\n * Duplicate content endpoint - injected by EmDash integration\n *\n * POST /_emdash/api/content/{collection}/{id}/duplicate - Create a copy\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm, requireOwnerPerm } from \"#api/authorize.js\";\nimport { apiError, mapErrorStatus, unwrapResult } from \"#api/error.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ params, locals, cache }) => {\n\tconst { emdash, user } = locals;\n\tconst collection = params.collection!;\n\tconst id = params.id!;\n\n\tif (!emdash?.handleContentDuplicate || !emdash?.handleContentGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\tconst denied = requirePerm(user, \"content:create\");\n\tif (denied) return denied;\n\n\t// Fetch item to check ownership — duplicating requires read access to the source\n\tconst existing = await emdash.handleContentGet(collection, id);\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\t// Handler returns { item, _rev } — extract the item for ownership check\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\t// Duplicating requires read access to the source — check ownership-based edit permissions\n\t// since content:read is flat (no own/any split). This ensures authors can only duplicate their own.\n\tconst readDenied = requireOwnerPerm(user, authorId, \"content:edit_own\", \"content:edit_any\");\n\tif (readDenied) return readDenied;\n\n\tconst resolvedId = typeof existingItem?.id === \"string\" ? existingItem.id : id;\n\tconst result = await emdash.handleContentDuplicate(collection, resolvedId, user?.id);\n\n\tif (!result.success) return unwrapResult(result);\n\n\tif (cache?.enabled) await cache.invalidate({ tags: [collection] });\n\n\treturn unwrapResult(result, 201);\n};\n"],"mappings":";;;;;;;AAWA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,QAAQ,YAAY;CAClE,MAAM,EAAE,QAAQ,SAAS;CACzB,MAAM,aAAa,OAAO;CAC1B,MAAM,KAAK,OAAO;AAElB,KAAI,CAAC,QAAQ,0BAA0B,CAAC,QAAQ,iBAC/C,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAEpE,MAAM,SAAS,YAAY,MAAM,iBAAiB;AAClD,KAAI,OAAQ,QAAO;CAGnB,MAAM,WAAW,MAAM,OAAO,iBAAiB,YAAY,GAAG;AAC9D,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;CAEJ,MAAM,eACL,cAAc,QAAQ,OAAO,aAAa,SAAS,WAEhD,aAAa,OACb;CAIJ,MAAM,aAAa,iBAAiB,MAHnB,OAAO,cAAc,aAAa,WAAW,aAAa,WAAW,IAGlC,oBAAoB,mBAAmB;AAC3F,KAAI,WAAY,QAAO;CAEvB,MAAM,aAAa,OAAO,cAAc,OAAO,WAAW,aAAa,KAAK;CAC5E,MAAM,SAAS,MAAM,OAAO,uBAAuB,YAAY,YAAY,MAAM,GAAG;AAEpF,KAAI,CAAC,OAAO,QAAS,QAAO,aAAa,OAAO;AAEhD,KAAI,OAAO,QAAS,OAAM,MAAM,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;AAElE,QAAO,aAAa,QAAQ,IAAI"}