emdash 0.17.1 → 0.18.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 (285) hide show
  1. package/dist/api/route-utils.mjs +11 -11
  2. package/dist/{api-Dmz40c2V.mjs → api-Cs7DAACP.mjs} +12 -12
  3. package/dist/{api-Dmz40c2V.mjs.map → api-Cs7DAACP.mjs.map} +1 -1
  4. package/dist/{apply-CuuZG6op.mjs → apply-BWMV4Zmw.mjs} +16 -16
  5. package/dist/{apply-CuuZG6op.mjs.map → apply-BWMV4Zmw.mjs.map} +1 -1
  6. package/dist/astro/index.mjs +1 -1
  7. package/dist/astro/middleware/auth.mjs +2 -2
  8. package/dist/astro/middleware/redirect.mjs +5 -5
  9. package/dist/astro/middleware.d.mts.map +1 -1
  10. package/dist/astro/middleware.mjs +274 -91
  11. package/dist/astro/middleware.mjs.map +1 -1
  12. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  13. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  14. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  15. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  16. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  17. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  18. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  19. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  20. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +9 -9
  21. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
  22. package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
  23. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
  24. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  25. package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
  26. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  27. package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
  28. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  29. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  30. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  31. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  32. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +26 -26
  33. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +26 -26
  34. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
  35. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +26 -26
  36. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +26 -26
  37. package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
  38. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  39. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
  40. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +26 -26
  41. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
  42. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +26 -26
  43. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +27 -27
  44. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +26 -26
  45. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +27 -27
  46. package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
  47. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
  48. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  49. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
  50. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  51. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  52. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  53. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  54. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  55. package/dist/astro/routes/api/auth/dev-bypass.mjs +4 -4
  56. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  57. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  58. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  59. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  60. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  61. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  62. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  63. package/dist/astro/routes/api/auth/me.mjs +4 -4
  64. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  65. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  66. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  67. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  68. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  69. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  70. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  71. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  72. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  73. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  74. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  75. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  76. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  77. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  78. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  79. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  80. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  81. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  82. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  83. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +9 -9
  84. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  86. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  87. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  88. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  89. package/dist/astro/routes/api/dashboard.mjs +7 -7
  90. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  91. package/dist/astro/routes/api/import/probe.mjs +4 -4
  92. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  93. package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
  94. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  95. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  96. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
  97. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  98. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +6 -6
  99. package/dist/astro/routes/api/manifest.mjs +3 -3
  100. package/dist/astro/routes/api/mcp.mjs +26 -26
  101. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  102. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  103. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  104. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  105. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  106. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  107. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  108. package/dist/astro/routes/api/media.mjs +5 -5
  109. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  110. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  111. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  112. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  113. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  114. package/dist/astro/routes/api/menus/index.mjs +5 -5
  115. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  116. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  117. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  118. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  119. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  120. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  121. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  122. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  123. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  124. package/dist/astro/routes/api/redirects/404s/index.mjs +7 -7
  125. package/dist/astro/routes/api/redirects/404s/summary.mjs +7 -7
  126. package/dist/astro/routes/api/redirects/_id_.mjs +8 -8
  127. package/dist/astro/routes/api/redirects/index.mjs +8 -8
  128. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  129. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  130. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +26 -26
  131. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
  132. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
  133. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
  134. package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
  135. package/dist/astro/routes/api/schema/index.mjs +7 -7
  136. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
  137. package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
  138. package/dist/astro/routes/api/search/enable.mjs +8 -8
  139. package/dist/astro/routes/api/search/index.mjs +7 -7
  140. package/dist/astro/routes/api/search/rebuild.mjs +8 -8
  141. package/dist/astro/routes/api/search/stats.mjs +7 -7
  142. package/dist/astro/routes/api/search/suggest.mjs +7 -7
  143. package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
  144. package/dist/astro/routes/api/sections/index.mjs +7 -7
  145. package/dist/astro/routes/api/settings/email.mjs +4 -4
  146. package/dist/astro/routes/api/settings.mjs +9 -9
  147. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  148. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  149. package/dist/astro/routes/api/setup/dev-bypass.mjs +16 -16
  150. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  151. package/dist/astro/routes/api/setup/index.mjs +17 -17
  152. package/dist/astro/routes/api/setup/status.mjs +3 -3
  153. package/dist/astro/routes/api/snapshot.mjs +3 -3
  154. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
  155. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
  156. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
  157. package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
  158. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  159. package/dist/astro/routes/api/typegen.mjs +5 -5
  160. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  161. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
  162. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
  163. package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -6
  164. package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
  165. package/dist/astro/routes/api/widget-components.mjs +2 -2
  166. package/dist/astro/routes/robots.txt.mjs +5 -5
  167. package/dist/astro/routes/sitemap-_collection_.xml.mjs +5 -5
  168. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  169. package/dist/{authorize-_wWM_44T.mjs → authorize-CotM4Yiu.mjs} +2 -2
  170. package/dist/{authorize-_wWM_44T.mjs.map → authorize-CotM4Yiu.mjs.map} +1 -1
  171. package/dist/{byline-BrIVWLm-.mjs → byline-CWQ9aSoz.mjs} +4 -4
  172. package/dist/{byline-BrIVWLm-.mjs.map → byline-CWQ9aSoz.mjs.map} +1 -1
  173. package/dist/{bylines-C_POWmGT.mjs → bylines-BJSva1Un.mjs} +4 -4
  174. package/dist/{bylines-C_POWmGT.mjs.map → bylines-BJSva1Un.mjs.map} +1 -1
  175. package/dist/{bylines-sqExMElV.mjs → bylines-LJMgENMI.mjs} +3 -3
  176. package/dist/{bylines-sqExMElV.mjs.map → bylines-LJMgENMI.mjs.map} +1 -1
  177. package/dist/{cache-wsDkA8ru.mjs → cache-lZL7SgVb.mjs} +2 -2
  178. package/dist/{cache-wsDkA8ru.mjs.map → cache-lZL7SgVb.mjs.map} +1 -1
  179. package/dist/{chunks-BAYkM-CF.mjs → chunks-BU-vP9Dh.mjs} +2 -2
  180. package/dist/{chunks-BAYkM-CF.mjs.map → chunks-BU-vP9Dh.mjs.map} +1 -1
  181. package/dist/cli/index.mjs +14 -14
  182. package/dist/{comment-Cd29aktf.mjs → comment-C4jVbCM8.mjs} +2 -2
  183. package/dist/{comment-Cd29aktf.mjs.map → comment-C4jVbCM8.mjs.map} +1 -1
  184. package/dist/{comments-B7ufhkxN.mjs → comments-BTAbC0Ek.mjs} +3 -3
  185. package/dist/{comments-B7ufhkxN.mjs.map → comments-BTAbC0Ek.mjs.map} +1 -1
  186. package/dist/{content-BbqKo3Kc.mjs → content-CyqOmOzm.mjs} +3 -3
  187. package/dist/{content-BbqKo3Kc.mjs.map → content-CyqOmOzm.mjs.map} +1 -1
  188. package/dist/{context-BsF1rhoI.mjs → context-DZ7bEh5-.mjs} +8 -8
  189. package/dist/{context-BsF1rhoI.mjs.map → context-DZ7bEh5-.mjs.map} +1 -1
  190. package/dist/{dashboard-BwIX9r-X.mjs → dashboard-B5WQpNTP.mjs} +4 -4
  191. package/dist/{dashboard-BwIX9r-X.mjs.map → dashboard-B5WQpNTP.mjs.map} +1 -1
  192. package/dist/db/index.mjs +2 -2
  193. package/dist/{dialect-helpers-BKCvISIQ.mjs → dialect-helpers-DRI5pyY3.mjs} +3 -3
  194. package/dist/dialect-helpers-DRI5pyY3.mjs.map +1 -0
  195. package/dist/{error-npZWBSb7.mjs → error-DJOsMVSt.mjs} +2 -2
  196. package/dist/{error-npZWBSb7.mjs.map → error-DJOsMVSt.mjs.map} +1 -1
  197. package/dist/{fts-manager-DmUAk-kQ.mjs → fts-manager-DR1ERA0c.mjs} +3 -3
  198. package/dist/{fts-manager-DmUAk-kQ.mjs.map → fts-manager-DR1ERA0c.mjs.map} +1 -1
  199. package/dist/index-CjKdMZ3U.d.mts.map +1 -1
  200. package/dist/index.mjs +35 -35
  201. package/dist/{load-DsoLq7ex.mjs → load-6ZrRhepW.mjs} +2 -2
  202. package/dist/{load-DsoLq7ex.mjs.map → load-6ZrRhepW.mjs.map} +1 -1
  203. package/dist/{loader-CJ6lWO0d.mjs → loader-Dyx8dhFV.mjs} +4 -4
  204. package/dist/{loader-CJ6lWO0d.mjs.map → loader-Dyx8dhFV.mjs.map} +1 -1
  205. package/dist/media/local-runtime.mjs +5 -5
  206. package/dist/{media-jk_HzzOl.mjs → media-C-oovGCG.mjs} +2 -2
  207. package/dist/{media-jk_HzzOl.mjs.map → media-C-oovGCG.mjs.map} +1 -1
  208. package/dist/{menus-CyMO6GBx.mjs → menus-BKkxXCmd.mjs} +30 -11
  209. package/dist/menus-BKkxXCmd.mjs.map +1 -0
  210. package/dist/{menus-B-5-3aon.mjs → menus-DugoYwTX.mjs} +2 -2
  211. package/dist/{menus-B-5-3aon.mjs.map → menus-DugoYwTX.mjs.map} +1 -1
  212. package/dist/{parse-4zO5Y2DL.mjs → parse-BBkFmLVr.mjs} +2 -2
  213. package/dist/{parse-4zO5Y2DL.mjs.map → parse-BBkFmLVr.mjs.map} +1 -1
  214. package/dist/{query-Bt52mHXp.mjs → query-Ctlq1aOk.mjs} +10 -10
  215. package/dist/{query-Bt52mHXp.mjs.map → query-Ctlq1aOk.mjs.map} +1 -1
  216. package/dist/{rate-limit-D6VQqBk_.mjs → rate-limit-CH6W6ikK.mjs} +2 -2
  217. package/dist/{rate-limit-D6VQqBk_.mjs.map → rate-limit-CH6W6ikK.mjs.map} +1 -1
  218. package/dist/{redirect-BZUJltlj.mjs → redirect-C6tJA7tk.mjs} +3 -3
  219. package/dist/{redirect-BZUJltlj.mjs.map → redirect-C6tJA7tk.mjs.map} +1 -1
  220. package/dist/{redirects-DnYuqsEf.mjs → redirects-CacE9eQa.mjs} +3 -3
  221. package/dist/{redirects-DnYuqsEf.mjs.map → redirects-CacE9eQa.mjs.map} +1 -1
  222. package/dist/{registry-Dn6gsx3L.mjs → registry-CIDxZbhh.mjs} +5 -5
  223. package/dist/{registry-Dn6gsx3L.mjs.map → registry-CIDxZbhh.mjs.map} +1 -1
  224. package/dist/runner-DM1yR5qd.d.mts.map +1 -1
  225. package/dist/{runner-eAgyIkeg.mjs → runner-pt6Wl-l-.mjs} +11 -6
  226. package/dist/runner-pt6Wl-l-.mjs.map +1 -0
  227. package/dist/runtime.mjs +3 -3
  228. package/dist/{schema--mYZX4D7.mjs → schema-B4tk0HAG.mjs} +4 -4
  229. package/dist/{schema--mYZX4D7.mjs.map → schema-B4tk0HAG.mjs.map} +1 -1
  230. package/dist/{search-C6U_NvZI.mjs → search-f-fNfwab.mjs} +4 -4
  231. package/dist/{search-C6U_NvZI.mjs.map → search-f-fNfwab.mjs.map} +1 -1
  232. package/dist/{sections-Ba-rJLKb.mjs → sections-biElLfT9.mjs} +3 -3
  233. package/dist/{sections-Ba-rJLKb.mjs.map → sections-biElLfT9.mjs.map} +1 -1
  234. package/dist/seed/index.mjs +14 -14
  235. package/dist/seo/index.mjs +1 -0
  236. package/dist/seo/index.mjs.map +1 -1
  237. package/dist/{seo-BTzb5ksq.mjs → seo-BR39kvTF.mjs} +2 -2
  238. package/dist/{seo-BTzb5ksq.mjs.map → seo-BR39kvTF.mjs.map} +1 -1
  239. package/dist/{service-Cn-kIfZn.mjs → service-BhR2acnc.mjs} +2 -2
  240. package/dist/{service-Cn-kIfZn.mjs.map → service-BhR2acnc.mjs.map} +1 -1
  241. package/dist/{settings-C65OSm41.mjs → settings-D_NJvjgN.mjs} +3 -3
  242. package/dist/{settings-C65OSm41.mjs.map → settings-D_NJvjgN.mjs.map} +1 -1
  243. package/dist/{settings-ChlQbwU0.mjs → settings-b5zW1R1T.mjs} +3 -3
  244. package/dist/{settings-ChlQbwU0.mjs.map → settings-b5zW1R1T.mjs.map} +1 -1
  245. package/dist/{taxonomies-ByLlXrv5.mjs → taxonomies-Crtzy4MT.mjs} +8 -7
  246. package/dist/taxonomies-Crtzy4MT.mjs.map +1 -0
  247. package/dist/{taxonomies-CbO6v7EE.mjs → taxonomies-Mhn9rjTQ.mjs} +4 -4
  248. package/dist/{taxonomies-CbO6v7EE.mjs.map → taxonomies-Mhn9rjTQ.mjs.map} +1 -1
  249. package/dist/{taxonomy-BBK-UAEo.mjs → taxonomy-DTZrIQpi.mjs} +3 -3
  250. package/dist/{taxonomy-BBK-UAEo.mjs.map → taxonomy-DTZrIQpi.mjs.map} +1 -1
  251. package/dist/{types-SF1DwGf2.mjs → types-K3MDsxpy.mjs} +2 -2
  252. package/dist/{types-SF1DwGf2.mjs.map → types-K3MDsxpy.mjs.map} +1 -1
  253. package/dist/{user-X4rtyO4Y.mjs → user-DzEUl5zA.mjs} +2 -2
  254. package/dist/{user-X4rtyO4Y.mjs.map → user-DzEUl5zA.mjs.map} +1 -1
  255. package/dist/{validate-DactmcJG.mjs → validate-JCXcsqiY.mjs} +2 -2
  256. package/dist/{validate-DactmcJG.mjs.map → validate-JCXcsqiY.mjs.map} +1 -1
  257. package/dist/{validation-BYA4i85b.mjs → validation-Bq-VyKJg.mjs} +6 -6
  258. package/dist/{validation-BYA4i85b.mjs.map → validation-Bq-VyKJg.mjs.map} +1 -1
  259. package/dist/version-CnS-Cr8A.mjs +7 -0
  260. package/dist/{version-CWbvq9LG.mjs.map → version-CnS-Cr8A.mjs.map} +1 -1
  261. package/dist/{widgets-DG-1jxnz.mjs → widgets-Bap1eS1X.mjs} +2 -2
  262. package/dist/{widgets-DG-1jxnz.mjs.map → widgets-Bap1eS1X.mjs.map} +1 -1
  263. package/dist/{zod-generator-BNAObjSt.mjs → zod-generator-BSDpkqSH.mjs} +4 -3
  264. package/dist/zod-generator-BSDpkqSH.mjs.map +1 -0
  265. package/package.json +7 -7
  266. package/src/astro/middleware/stream-end-metrics.ts +96 -0
  267. package/src/astro/middleware.ts +114 -40
  268. package/src/components/EmDashImage.astro +1 -0
  269. package/src/database/dialect-helpers.ts +8 -2
  270. package/src/database/migrations/019_i18n.ts +2 -2
  271. package/src/database/migrations/runner.ts +7 -2
  272. package/src/emdash-runtime.ts +177 -126
  273. package/src/menus/index.ts +27 -9
  274. package/src/plugins/hooks.ts +35 -6
  275. package/src/plugins/manager.ts +1 -0
  276. package/src/schema/zod-generator.ts +6 -2
  277. package/src/seo/index.ts +10 -1
  278. package/src/taxonomies/index.ts +12 -8
  279. package/src/utils/init-lock.ts +143 -0
  280. package/dist/dialect-helpers-BKCvISIQ.mjs.map +0 -1
  281. package/dist/menus-CyMO6GBx.mjs.map +0 -1
  282. package/dist/runner-eAgyIkeg.mjs.map +0 -1
  283. package/dist/taxonomies-ByLlXrv5.mjs.map +0 -1
  284. package/dist/version-CWbvq9LG.mjs +0 -7
  285. package/dist/zod-generator-BNAObjSt.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"cache-wsDkA8ru.mjs","names":[],"sources":["../src/redirects/cache.ts"],"sourcesContent":["/**\n * Redirect rule cache.\n *\n * Module-level cache for enabled redirect rules. The middleware populates this\n * on first request; route handlers invalidate it on writes.\n *\n * Both exact-match and pattern rules are loaded from one query and cached\n * together: exact rules indexed by source path in a Map, pattern rules\n * pre-compiled into an array. A single warm request issues zero database\n * queries; a cold isolate issues one.\n *\n * This module deliberately has NO Astro imports so it can be safely imported\n * from handlers, seed, CLI, and tests without dragging in `astro:middleware`.\n */\n\nimport type { Redirect } from \"../database/repositories/redirect.js\";\nimport type { CompiledPattern } from \"./patterns.js\";\nimport { compilePattern, interpolateDestination, matchPattern } from \"./patterns.js\";\n\nexport interface CachedRedirectRule {\n\tredirect: Redirect;\n\tcompiled: CompiledPattern;\n}\n\nexport interface CachedRedirects {\n\t/** Exact-match rules indexed by source path (`source` -> `Redirect`). */\n\texact: Map<string, Redirect>;\n\t/** Pattern rules with their compiled regexes, preserving insertion order. */\n\tpatterns: CachedRedirectRule[];\n}\n\n/**\n * Cached enabled redirects.\n * null = not yet populated, object = cached.\n */\nlet cachedRedirects: CachedRedirects | null = null;\n\n/**\n * Invalidate the cached redirects (both exact and pattern).\n * Call when redirects are created, updated, or deleted.\n */\nexport function invalidateRedirectCache(): void {\n\tcachedRedirects = null;\n}\n\n/**\n * Get the cached redirects, or null if the cache is cold.\n */\nexport function getCachedRedirects(): CachedRedirects | null {\n\treturn cachedRedirects;\n}\n\n/**\n * Populate the cache from a list of enabled redirects (both exact and\n * pattern). The caller is responsible for passing only enabled rows — the\n * cache stores them as-is.\n */\nexport function setCachedRedirects(redirects: Redirect[]): CachedRedirects {\n\tconst exact = new Map<string, Redirect>();\n\tconst patterns: CachedRedirectRule[] = [];\n\tfor (const r of redirects) {\n\t\tif (r.isPattern) {\n\t\t\tpatterns.push({ redirect: r, compiled: compilePattern(r.source) });\n\t\t} else {\n\t\t\texact.set(r.source, r);\n\t\t}\n\t}\n\tcachedRedirects = { exact, patterns };\n\treturn cachedRedirects;\n}\n\n/**\n * Match a path against the cached pattern rules.\n * Returns the resolved destination and matching redirect, or null.\n */\nexport function matchCachedPatterns(\n\trules: CachedRedirectRule[],\n\tpathname: string,\n): { redirect: Redirect; destination: string } | null {\n\tfor (const { redirect, compiled } of rules) {\n\t\tconst params = matchPattern(compiled, pathname);\n\t\tif (params) {\n\t\t\tconst dest = interpolateDestination(redirect.destination, params);\n\t\t\treturn { redirect, destination: dest };\n\t\t}\n\t}\n\treturn null;\n}\n"],"mappings":";;;;;;;;;;;;;;AAmCA,IAAI,kBAA0C;;;;;AAM9C,SAAgB,0BAAgC;AAC/C,mBAAkB;;;;;AAMnB,SAAgB,qBAA6C;AAC5D,QAAO;;;;;;;AAQR,SAAgB,mBAAmB,WAAwC;CAC1E,MAAM,wBAAQ,IAAI,KAAuB;CACzC,MAAM,WAAiC,EAAE;AACzC,MAAK,MAAM,KAAK,UACf,KAAI,EAAE,UACL,UAAS,KAAK;EAAE,UAAU;EAAG,UAAU,eAAe,EAAE,OAAO;EAAE,CAAC;KAElE,OAAM,IAAI,EAAE,QAAQ,EAAE;AAGxB,mBAAkB;EAAE;EAAO;EAAU;AACrC,QAAO;;;;;;AAOR,SAAgB,oBACf,OACA,UACqD;AACrD,MAAK,MAAM,EAAE,UAAU,cAAc,OAAO;EAC3C,MAAM,SAAS,aAAa,UAAU,SAAS;AAC/C,MAAI,OAEH,QAAO;GAAE;GAAU,aADN,uBAAuB,SAAS,aAAa,OAAO;GAC3B;;AAGxC,QAAO"}
1
+ {"version":3,"file":"cache-lZL7SgVb.mjs","names":[],"sources":["../src/redirects/cache.ts"],"sourcesContent":["/**\n * Redirect rule cache.\n *\n * Module-level cache for enabled redirect rules. The middleware populates this\n * on first request; route handlers invalidate it on writes.\n *\n * Both exact-match and pattern rules are loaded from one query and cached\n * together: exact rules indexed by source path in a Map, pattern rules\n * pre-compiled into an array. A single warm request issues zero database\n * queries; a cold isolate issues one.\n *\n * This module deliberately has NO Astro imports so it can be safely imported\n * from handlers, seed, CLI, and tests without dragging in `astro:middleware`.\n */\n\nimport type { Redirect } from \"../database/repositories/redirect.js\";\nimport type { CompiledPattern } from \"./patterns.js\";\nimport { compilePattern, interpolateDestination, matchPattern } from \"./patterns.js\";\n\nexport interface CachedRedirectRule {\n\tredirect: Redirect;\n\tcompiled: CompiledPattern;\n}\n\nexport interface CachedRedirects {\n\t/** Exact-match rules indexed by source path (`source` -> `Redirect`). */\n\texact: Map<string, Redirect>;\n\t/** Pattern rules with their compiled regexes, preserving insertion order. */\n\tpatterns: CachedRedirectRule[];\n}\n\n/**\n * Cached enabled redirects.\n * null = not yet populated, object = cached.\n */\nlet cachedRedirects: CachedRedirects | null = null;\n\n/**\n * Invalidate the cached redirects (both exact and pattern).\n * Call when redirects are created, updated, or deleted.\n */\nexport function invalidateRedirectCache(): void {\n\tcachedRedirects = null;\n}\n\n/**\n * Get the cached redirects, or null if the cache is cold.\n */\nexport function getCachedRedirects(): CachedRedirects | null {\n\treturn cachedRedirects;\n}\n\n/**\n * Populate the cache from a list of enabled redirects (both exact and\n * pattern). The caller is responsible for passing only enabled rows — the\n * cache stores them as-is.\n */\nexport function setCachedRedirects(redirects: Redirect[]): CachedRedirects {\n\tconst exact = new Map<string, Redirect>();\n\tconst patterns: CachedRedirectRule[] = [];\n\tfor (const r of redirects) {\n\t\tif (r.isPattern) {\n\t\t\tpatterns.push({ redirect: r, compiled: compilePattern(r.source) });\n\t\t} else {\n\t\t\texact.set(r.source, r);\n\t\t}\n\t}\n\tcachedRedirects = { exact, patterns };\n\treturn cachedRedirects;\n}\n\n/**\n * Match a path against the cached pattern rules.\n * Returns the resolved destination and matching redirect, or null.\n */\nexport function matchCachedPatterns(\n\trules: CachedRedirectRule[],\n\tpathname: string,\n): { redirect: Redirect; destination: string } | null {\n\tfor (const { redirect, compiled } of rules) {\n\t\tconst params = matchPattern(compiled, pathname);\n\t\tif (params) {\n\t\t\tconst dest = interpolateDestination(redirect.destination, params);\n\t\t\treturn { redirect, destination: dest };\n\t\t}\n\t}\n\treturn null;\n}\n"],"mappings":";;;;;;;;;;;;;;AAmCA,IAAI,kBAA0C;;;;;AAM9C,SAAgB,0BAAgC;AAC/C,mBAAkB;;;;;AAMnB,SAAgB,qBAA6C;AAC5D,QAAO;;;;;;;AAQR,SAAgB,mBAAmB,WAAwC;CAC1E,MAAM,wBAAQ,IAAI,KAAuB;CACzC,MAAM,WAAiC,EAAE;AACzC,MAAK,MAAM,KAAK,UACf,KAAI,EAAE,UACL,UAAS,KAAK;EAAE,UAAU;EAAG,UAAU,eAAe,EAAE,OAAO;EAAE,CAAC;KAElE,OAAM,IAAI,EAAE,QAAQ,EAAE;AAGxB,mBAAkB;EAAE;EAAO;EAAU;AACrC,QAAO;;;;;;AAOR,SAAgB,oBACf,OACA,UACqD;AACrD,MAAK,MAAM,EAAE,UAAU,cAAc,OAAO;EAC3C,MAAM,SAAS,aAAa,UAAU,SAAS;AAC/C,MAAI,OAEH,QAAO;GAAE;GAAU,aADN,uBAAuB,SAAS,aAAa,OAAO;GAC3B;;AAGxC,QAAO"}
@@ -1,4 +1,4 @@
1
- import { i as __exportAll } from "./runner-eAgyIkeg.mjs";
1
+ import { a as __exportAll } from "./runner-pt6Wl-l-.mjs";
2
2
 
3
3
  //#region src/utils/chunks.ts
4
4
  var chunks_exports = /* @__PURE__ */ __exportAll({
@@ -22,4 +22,4 @@ const SQL_BATCH_SIZE = 50;
22
22
 
23
23
  //#endregion
24
24
  export { chunks as n, chunks_exports as r, SQL_BATCH_SIZE as t };
25
- //# sourceMappingURL=chunks-BAYkM-CF.mjs.map
25
+ //# sourceMappingURL=chunks-BU-vP9Dh.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"chunks-BAYkM-CF.mjs","names":[],"sources":["../src/utils/chunks.ts"],"sourcesContent":["/**\n * Split an array into chunks of at most `size` elements.\n *\n * Used to keep SQL `IN (?, ?, …)` clauses within Cloudflare D1's\n * bound-parameter limit (~100 per statement).\n */\nexport function chunks<T>(arr: T[], size: number): T[][] {\n\tif (arr.length === 0) return [];\n\tconst result: T[][] = [];\n\tfor (let i = 0; i < arr.length; i += size) {\n\t\tresult.push(arr.slice(i, i + size));\n\t}\n\treturn result;\n}\n\n/** Conservative default chunk size for SQL IN clauses (well within D1's limit). */\nexport const SQL_BATCH_SIZE = 50;\n"],"mappings":";;;;;;;;;;;;;AAMA,SAAgB,OAAU,KAAU,MAAqB;AACxD,KAAI,IAAI,WAAW,EAAG,QAAO,EAAE;CAC/B,MAAM,SAAgB,EAAE;AACxB,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,KACpC,QAAO,KAAK,IAAI,MAAM,GAAG,IAAI,KAAK,CAAC;AAEpC,QAAO;;;AAIR,MAAa,iBAAiB"}
1
+ {"version":3,"file":"chunks-BU-vP9Dh.mjs","names":[],"sources":["../src/utils/chunks.ts"],"sourcesContent":["/**\n * Split an array into chunks of at most `size` elements.\n *\n * Used to keep SQL `IN (?, ?, …)` clauses within Cloudflare D1's\n * bound-parameter limit (~100 per statement).\n */\nexport function chunks<T>(arr: T[], size: number): T[][] {\n\tif (arr.length === 0) return [];\n\tconst result: T[][] = [];\n\tfor (let i = 0; i < arr.length; i += size) {\n\t\tresult.push(arr.slice(i, i + size));\n\t}\n\treturn result;\n}\n\n/** Conservative default chunk size for SQL IN clauses (well within D1's limit). */\nexport const SQL_BATCH_SIZE = 50;\n"],"mappings":";;;;;;;;;;;;;AAMA,SAAgB,OAAU,KAAU,MAAqB;AACxD,KAAI,IAAI,WAAW,EAAG,QAAO,EAAE;CAC/B,MAAM,SAAgB,EAAE;AACxB,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,KACpC,QAAO,KAAK,IAAI,MAAM,GAAG,IAAI,KAAK,CAAC;AAEpC,QAAO;;;AAIR,MAAa,iBAAiB"}
@@ -1,32 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { i as __exportAll, r as runMigrations, t as getMigrationStatus } from "../runner-eAgyIkeg.mjs";
2
+ import { a as __exportAll, i as runMigrations, n as getMigrationStatus } from "../runner-pt6Wl-l-.mjs";
3
3
  import { t as EmDashDatabaseError } from "../errors-9P_FDrJ_.mjs";
4
4
  import { t as validateIdentifier } from "../validate-VPnKoIzW.mjs";
5
- import { c as listTablesLike } from "../dialect-helpers-BKCvISIQ.mjs";
5
+ import { c as listTablesLike } from "../dialect-helpers-DRI5pyY3.mjs";
6
6
  import { r as isI18nEnabled } from "../config-CVssduLe.mjs";
7
7
  import { n as slugify } from "../slugify-Cjh1ssOZ.mjs";
8
- import { t as ContentRepository } from "../content-BbqKo3Kc.mjs";
8
+ import { t as ContentRepository } from "../content-CyqOmOzm.mjs";
9
9
  import { i as encodeBase64url } from "../base64-CqR-7kqF.mjs";
10
- import "../types-SF1DwGf2.mjs";
11
- import { t as MediaRepository } from "../media-jk_HzzOl.mjs";
12
- import { t as TaxonomyRepository } from "../taxonomy-BBK-UAEo.mjs";
10
+ import "../types-K3MDsxpy.mjs";
11
+ import { t as MediaRepository } from "../media-C-oovGCG.mjs";
12
+ import { t as TaxonomyRepository } from "../taxonomy-DTZrIQpi.mjs";
13
13
  import { t as OptionsRepository } from "../options-BL4X94qY.mjs";
14
- import "../redirect-BZUJltlj.mjs";
14
+ import "../redirect-C6tJA7tk.mjs";
15
15
  import "../request-cache-BYMs-BGX.mjs";
16
16
  import "../byline-registry-CxK5g559.mjs";
17
- import { t as BylineRepository } from "../byline-BrIVWLm-.mjs";
17
+ import { t as BylineRepository } from "../byline-CWQ9aSoz.mjs";
18
18
  import { n as isMissingTableError } from "../db-errors-CtzxKBxe.mjs";
19
- import "../fts-manager-DmUAk-kQ.mjs";
20
- import { n as SchemaRegistry } from "../registry-Dn6gsx3L.mjs";
19
+ import "../fts-manager-DR1ERA0c.mjs";
20
+ import { n as SchemaRegistry } from "../registry-CIDxZbhh.mjs";
21
21
  import { kyselyLogOption } from "../database/instrumentation.mjs";
22
- import "../loader-CJ6lWO0d.mjs";
23
- import "../settings-ChlQbwU0.mjs";
22
+ import "../loader-Dyx8dhFV.mjs";
23
+ import "../settings-b5zW1R1T.mjs";
24
24
  import { i as pluginManifestSchema } from "../manifest-schema-Cj-YrzrF.mjs";
25
25
  import { n as isDeprecatedCapability, t as CAPABILITY_RENAMES } from "../types-Cj2S6FuC.mjs";
26
26
  import "../ssrf-BsVGIE0Z.mjs";
27
27
  import "../ssrf-BvgVcfNQ.mjs";
28
- import { t as validateSeed } from "../validate-DactmcJG.mjs";
29
- import { t as applySeed } from "../apply-CuuZG6op.mjs";
28
+ import { t as validateSeed } from "../validate-JCXcsqiY.mjs";
29
+ import { t as applySeed } from "../apply-BWMV4Zmw.mjs";
30
30
  import { n as fingerprintKey, r as generateEncryptionKey, t as EmDashSecretsError } from "../secrets-YYbTgB1w.mjs";
31
31
  import { LocalStorage } from "../storage/local.mjs";
32
32
  import { o as convertDataForRead } from "../transport--Ck3RBin.mjs";
@@ -1,4 +1,4 @@
1
- import { i as encodeCursor, r as decodeCursor } from "./types-SF1DwGf2.mjs";
1
+ import { i as encodeCursor, r as decodeCursor } from "./types-K3MDsxpy.mjs";
2
2
  import { sql } from "kysely";
3
3
  import { ulid } from "ulidx";
4
4
 
@@ -244,4 +244,4 @@ function safeJsonParse(value) {
244
244
 
245
245
  //#endregion
246
246
  export { CommentRepository as t };
247
- //# sourceMappingURL=comment-Cd29aktf.mjs.map
247
+ //# sourceMappingURL=comment-C4jVbCM8.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comment-Cd29aktf.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n/** Matches LIKE wildcard characters and the escape character itself */\nconst LIKE_ESCAPE_RE = /[%_\\\\]/g;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type CommentStatus = \"pending\" | \"approved\" | \"spam\" | \"trash\";\n\nexport interface Comment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: CommentStatus;\n\tipHash: string | null;\n\tuserAgent: string | null;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/** Public-facing comment shape — no private fields */\nexport interface PublicComment {\n\tid: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tisRegisteredUser: boolean;\n\tbody: string;\n\tcreatedAt: string;\n\treplies?: PublicComment[];\n}\n\nexport interface CreateCommentInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tstatus?: CommentStatus;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n\tmoderationMetadata?: Record<string, unknown> | null;\n}\n\nexport interface CommentFindOptions {\n\tstatus?: CommentStatus;\n\tcollection?: string;\n\tsearch?: string;\n\tlimit?: number;\n\tcursor?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class CommentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new comment\n\t */\n\tasync create(input: CreateCommentInput): Promise<Comment> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tcollection: input.collection,\n\t\t\t\tcontent_id: input.contentId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tauthor_name: input.authorName,\n\t\t\t\tauthor_email: input.authorEmail,\n\t\t\t\tauthor_user_id: input.authorUserId ?? null,\n\t\t\t\tbody: input.body,\n\t\t\t\tstatus: input.status ?? \"pending\",\n\t\t\t\tip_hash: input.ipHash ?? null,\n\t\t\t\tuser_agent: input.userAgent ?? null,\n\t\t\t\tmoderation_metadata: input.moderationMetadata\n\t\t\t\t\t? JSON.stringify(input.moderationMetadata)\n\t\t\t\t\t: null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst comment = await this.findById(id);\n\t\tif (!comment) {\n\t\t\tthrow new Error(\"Failed to create comment\");\n\t\t}\n\t\treturn comment;\n\t}\n\n\t/**\n\t * Find comment by ID\n\t */\n\tasync findById(id: string): Promise<Comment | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToComment(row) : null;\n\t}\n\n\t/**\n\t * Find comments for a content item with optional status filter.\n\t * Results are ordered by created_at ASC (oldest first) for display.\n\t */\n\tasync findByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\toptions: { status?: CommentStatus; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (options.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.status);\n\t\t}\n\n\t\t// Cursor pagination (ascending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \">\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Find comments by status (moderation inbox).\n\t * Results are ordered by created_at DESC (newest first).\n\t */\n\tasync findByStatus(\n\t\tstatus: CommentStatus,\n\t\toptions: { collection?: string; search?: string; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db.selectFrom(\"_emdash_comments\").selectAll().where(\"status\", \"=\", status);\n\n\t\tif (options.collection) {\n\t\t\tquery = query.where(\"collection\", \"=\", options.collection);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Escape LIKE wildcards to prevent them acting as SQL pattern characters\n\t\t\tconst escaped = options.search.replace(LIKE_ESCAPE_RE, (ch) => `\\\\${ch}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`author_name LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`author_email LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`body LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Cursor pagination (descending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update comment status\n\t */\n\tasync updateStatus(id: string, status: CommentStatus): Promise<Comment | null> {\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Bulk update comment statuses\n\t */\n\tasync bulkUpdateStatus(ids: string[], status: CommentStatus): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numUpdatedRows ?? 0);\n\t}\n\n\t/**\n\t * Hard-delete a single comment. Replies cascade via FK.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Bulk hard-delete comments\n\t */\n\tasync bulkDelete(ids: string[]): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete all comments for a content item (cascade on content deletion)\n\t */\n\tasync deleteByContent(collection: string, contentId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Count comments for a content item, optionally filtered by status\n\t */\n\tasync countByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\tstatus?: CommentStatus,\n\t): Promise<number> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (status) {\n\t\t\tquery = query.where(\"status\", \"=\", status);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Count comments grouped by status (for inbox badges)\n\t *\n\t * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes\n\t * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)\n\t * instead of a full table GROUP BY scan.\n\t */\n\tasync countByStatus(): Promise<Record<CommentStatus, number>> {\n\t\t// Execute four parallel COUNT queries, each using its partial index\n\t\tconst [pending, approved, spam, trash] = await Promise.all([\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"spam\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"trash\")\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn {\n\t\t\tpending: Number(pending?.count ?? 0),\n\t\t\tapproved: Number(approved?.count ?? 0),\n\t\t\tspam: Number(spam?.count ?? 0),\n\t\t\ttrash: Number(trash?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Count approved comments from a given email address.\n\t * Used for \"first time commenter\" moderation logic.\n\t */\n\tasync countApprovedByEmail(email: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"author_email\", \"=\", email)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Update the moderation metadata JSON on a comment\n\t */\n\tasync updateModerationMetadata(id: string, metadata: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ moderation_metadata: JSON.stringify(metadata) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t// ---------------------------------------------------------------------------\n\t// Helpers\n\t// ---------------------------------------------------------------------------\n\n\t/**\n\t * Assemble a flat list of comments into a threaded structure (1-level nesting)\n\t */\n\tstatic assembleThreads(comments: Comment[]): Comment[] {\n\t\tconst roots: Comment[] = [];\n\t\tconst childrenMap = new Map<string, Comment[]>();\n\n\t\tfor (const comment of comments) {\n\t\t\tif (comment.parentId) {\n\t\t\t\tconst siblings = childrenMap.get(comment.parentId) ?? [];\n\t\t\t\tsiblings.push(comment);\n\t\t\t\tchildrenMap.set(comment.parentId, siblings);\n\t\t\t} else {\n\t\t\t\troots.push(comment);\n\t\t\t}\n\t\t}\n\n\t\t// Attach children as a non-standard property — callers map to PublicComment.replies\n\t\treturn roots.map((root) => ({\n\t\t\t...root,\n\t\t\t_replies: childrenMap.get(root.id) ?? [],\n\t\t})) as Comment[];\n\t}\n\n\t/**\n\t * Convert a Comment to its public-facing shape\n\t */\n\tstatic toPublicComment(comment: Comment & { _replies?: Comment[] }): PublicComment {\n\t\tconst pub: PublicComment = {\n\t\t\tid: comment.id,\n\t\t\tparentId: comment.parentId,\n\t\t\tauthorName: comment.authorName,\n\t\t\tisRegisteredUser: comment.authorUserId !== null,\n\t\t\tbody: comment.body,\n\t\t\tcreatedAt: comment.createdAt,\n\t\t};\n\n\t\tif (comment._replies && comment._replies.length > 0) {\n\t\t\tpub.replies = comment._replies.map((r) => CommentRepository.toPublicComment(r));\n\t\t}\n\n\t\treturn pub;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- selectAll returns runtime row\n\tprivate rowToComment(row: any): Comment {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tcontentId: row.content_id,\n\t\t\tparentId: row.parent_id,\n\t\t\tauthorName: row.author_name,\n\t\t\tauthorEmail: row.author_email,\n\t\t\tauthorUserId: row.author_user_id,\n\t\t\tbody: row.body,\n\t\t\tstatus: row.status as CommentStatus,\n\t\t\tipHash: row.ip_hash,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tmoderationMetadata: row.moderation_metadata ? safeJsonParse(row.moderation_metadata) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Module helpers\n// ---------------------------------------------------------------------------\n\nfunction safeJsonParse(value: string): Record<string, unknown> | null {\n\ttry {\n\t\treturn JSON.parse(value) as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"comment-C4jVbCM8.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n/** Matches LIKE wildcard characters and the escape character itself */\nconst LIKE_ESCAPE_RE = /[%_\\\\]/g;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type CommentStatus = \"pending\" | \"approved\" | \"spam\" | \"trash\";\n\nexport interface Comment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: CommentStatus;\n\tipHash: string | null;\n\tuserAgent: string | null;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/** Public-facing comment shape — no private fields */\nexport interface PublicComment {\n\tid: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tisRegisteredUser: boolean;\n\tbody: string;\n\tcreatedAt: string;\n\treplies?: PublicComment[];\n}\n\nexport interface CreateCommentInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tstatus?: CommentStatus;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n\tmoderationMetadata?: Record<string, unknown> | null;\n}\n\nexport interface CommentFindOptions {\n\tstatus?: CommentStatus;\n\tcollection?: string;\n\tsearch?: string;\n\tlimit?: number;\n\tcursor?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class CommentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new comment\n\t */\n\tasync create(input: CreateCommentInput): Promise<Comment> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tcollection: input.collection,\n\t\t\t\tcontent_id: input.contentId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tauthor_name: input.authorName,\n\t\t\t\tauthor_email: input.authorEmail,\n\t\t\t\tauthor_user_id: input.authorUserId ?? null,\n\t\t\t\tbody: input.body,\n\t\t\t\tstatus: input.status ?? \"pending\",\n\t\t\t\tip_hash: input.ipHash ?? null,\n\t\t\t\tuser_agent: input.userAgent ?? null,\n\t\t\t\tmoderation_metadata: input.moderationMetadata\n\t\t\t\t\t? JSON.stringify(input.moderationMetadata)\n\t\t\t\t\t: null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst comment = await this.findById(id);\n\t\tif (!comment) {\n\t\t\tthrow new Error(\"Failed to create comment\");\n\t\t}\n\t\treturn comment;\n\t}\n\n\t/**\n\t * Find comment by ID\n\t */\n\tasync findById(id: string): Promise<Comment | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToComment(row) : null;\n\t}\n\n\t/**\n\t * Find comments for a content item with optional status filter.\n\t * Results are ordered by created_at ASC (oldest first) for display.\n\t */\n\tasync findByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\toptions: { status?: CommentStatus; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (options.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.status);\n\t\t}\n\n\t\t// Cursor pagination (ascending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \">\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Find comments by status (moderation inbox).\n\t * Results are ordered by created_at DESC (newest first).\n\t */\n\tasync findByStatus(\n\t\tstatus: CommentStatus,\n\t\toptions: { collection?: string; search?: string; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db.selectFrom(\"_emdash_comments\").selectAll().where(\"status\", \"=\", status);\n\n\t\tif (options.collection) {\n\t\t\tquery = query.where(\"collection\", \"=\", options.collection);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Escape LIKE wildcards to prevent them acting as SQL pattern characters\n\t\t\tconst escaped = options.search.replace(LIKE_ESCAPE_RE, (ch) => `\\\\${ch}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`author_name LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`author_email LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`body LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Cursor pagination (descending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update comment status\n\t */\n\tasync updateStatus(id: string, status: CommentStatus): Promise<Comment | null> {\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Bulk update comment statuses\n\t */\n\tasync bulkUpdateStatus(ids: string[], status: CommentStatus): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numUpdatedRows ?? 0);\n\t}\n\n\t/**\n\t * Hard-delete a single comment. Replies cascade via FK.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Bulk hard-delete comments\n\t */\n\tasync bulkDelete(ids: string[]): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete all comments for a content item (cascade on content deletion)\n\t */\n\tasync deleteByContent(collection: string, contentId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Count comments for a content item, optionally filtered by status\n\t */\n\tasync countByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\tstatus?: CommentStatus,\n\t): Promise<number> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (status) {\n\t\t\tquery = query.where(\"status\", \"=\", status);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Count comments grouped by status (for inbox badges)\n\t *\n\t * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes\n\t * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)\n\t * instead of a full table GROUP BY scan.\n\t */\n\tasync countByStatus(): Promise<Record<CommentStatus, number>> {\n\t\t// Execute four parallel COUNT queries, each using its partial index\n\t\tconst [pending, approved, spam, trash] = await Promise.all([\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"spam\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"trash\")\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn {\n\t\t\tpending: Number(pending?.count ?? 0),\n\t\t\tapproved: Number(approved?.count ?? 0),\n\t\t\tspam: Number(spam?.count ?? 0),\n\t\t\ttrash: Number(trash?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Count approved comments from a given email address.\n\t * Used for \"first time commenter\" moderation logic.\n\t */\n\tasync countApprovedByEmail(email: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"author_email\", \"=\", email)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Update the moderation metadata JSON on a comment\n\t */\n\tasync updateModerationMetadata(id: string, metadata: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ moderation_metadata: JSON.stringify(metadata) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t// ---------------------------------------------------------------------------\n\t// Helpers\n\t// ---------------------------------------------------------------------------\n\n\t/**\n\t * Assemble a flat list of comments into a threaded structure (1-level nesting)\n\t */\n\tstatic assembleThreads(comments: Comment[]): Comment[] {\n\t\tconst roots: Comment[] = [];\n\t\tconst childrenMap = new Map<string, Comment[]>();\n\n\t\tfor (const comment of comments) {\n\t\t\tif (comment.parentId) {\n\t\t\t\tconst siblings = childrenMap.get(comment.parentId) ?? [];\n\t\t\t\tsiblings.push(comment);\n\t\t\t\tchildrenMap.set(comment.parentId, siblings);\n\t\t\t} else {\n\t\t\t\troots.push(comment);\n\t\t\t}\n\t\t}\n\n\t\t// Attach children as a non-standard property — callers map to PublicComment.replies\n\t\treturn roots.map((root) => ({\n\t\t\t...root,\n\t\t\t_replies: childrenMap.get(root.id) ?? [],\n\t\t})) as Comment[];\n\t}\n\n\t/**\n\t * Convert a Comment to its public-facing shape\n\t */\n\tstatic toPublicComment(comment: Comment & { _replies?: Comment[] }): PublicComment {\n\t\tconst pub: PublicComment = {\n\t\t\tid: comment.id,\n\t\t\tparentId: comment.parentId,\n\t\t\tauthorName: comment.authorName,\n\t\t\tisRegisteredUser: comment.authorUserId !== null,\n\t\t\tbody: comment.body,\n\t\t\tcreatedAt: comment.createdAt,\n\t\t};\n\n\t\tif (comment._replies && comment._replies.length > 0) {\n\t\t\tpub.replies = comment._replies.map((r) => CommentRepository.toPublicComment(r));\n\t\t}\n\n\t\treturn pub;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- selectAll returns runtime row\n\tprivate rowToComment(row: any): Comment {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tcontentId: row.content_id,\n\t\t\tparentId: row.parent_id,\n\t\t\tauthorName: row.author_name,\n\t\t\tauthorEmail: row.author_email,\n\t\t\tauthorUserId: row.author_user_id,\n\t\t\tbody: row.body,\n\t\t\tstatus: row.status as CommentStatus,\n\t\t\tipHash: row.ip_hash,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tmoderationMetadata: row.moderation_metadata ? safeJsonParse(row.moderation_metadata) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Module helpers\n// ---------------------------------------------------------------------------\n\nfunction safeJsonParse(value: string): Record<string, unknown> | null {\n\ttry {\n\t\treturn JSON.parse(value) as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { n as InvalidCursorError } from "./types-SF1DwGf2.mjs";
2
- import { t as CommentRepository } from "./comment-Cd29aktf.mjs";
1
+ import { n as InvalidCursorError } from "./types-K3MDsxpy.mjs";
2
+ import { t as CommentRepository } from "./comment-C4jVbCM8.mjs";
3
3
 
4
4
  //#region src/api/handlers/comments.ts
5
5
  async function handleCommentList(db, collection, contentId, options = {}) {
@@ -201,4 +201,4 @@ async function hashIp(ip, salt) {
201
201
 
202
202
  //#endregion
203
203
  export { handleCommentGet as a, hashIp as c, handleCommentDelete as i, handleCommentBulk as n, handleCommentInbox as o, handleCommentCounts as r, handleCommentList as s, checkRateLimit as t };
204
- //# sourceMappingURL=comments-B7ufhkxN.mjs.map
204
+ //# sourceMappingURL=comments-BTAbC0Ek.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comments-B7ufhkxN.mjs","names":[],"sources":["../src/api/handlers/comments.ts"],"sourcesContent":["/**\n * Comment handlers — business logic for comment API routes.\n *\n * Standalone functions that return ApiResult<T>. Routes are thin wrappers.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport type { Comment, CommentStatus, PublicComment } from \"../../database/repositories/comment.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Public: List approved comments for content\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n\toptions: { limit?: number; cursor?: string; threaded?: boolean } = {},\n): Promise<ApiResult<{ items: PublicComment[]; nextCursor?: string; total: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\t// Get total approved count\n\t\tconst total = await repo.countByContent(collection, contentId, \"approved\");\n\n\t\tlet publicItems: PublicComment[];\n\t\tlet nextCursor: string | undefined;\n\n\t\tif (options.threaded) {\n\t\t\t// Threaded mode: fetch all approved comments (capped) so threading\n\t\t\t// doesn't lose children that would fall on later pages.\n\t\t\tconst MAX_THREADED = 500;\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: MAX_THREADED,\n\t\t\t});\n\t\t\tconst threaded = CommentRepository.assembleThreads(result.items);\n\t\t\tpublicItems = threaded.map((c) => CommentRepository.toPublicComment(c));\n\t\t\t// No cursor for threaded mode — all comments returned at once\n\t\t} else {\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: options.limit,\n\t\t\t\tcursor: options.cursor,\n\t\t\t});\n\t\t\tpublicItems = result.items.map((c) => CommentRepository.toPublicComment(c));\n\t\t\tnextCursor = result.nextCursor;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: publicItems,\n\t\t\t\tnextCursor,\n\t\t\t\ttotal,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Moderation inbox\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentInbox(\n\tdb: Kysely<Database>,\n\toptions: {\n\t\tstatus?: CommentStatus;\n\t\tcollection?: string;\n\t\tsearch?: string;\n\t\tlimit?: number;\n\t\tcursor?: string;\n\t} = {},\n): Promise<ApiResult<{ items: Comment[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst status = options.status ?? \"pending\";\n\n\t\tconst result = await repo.findByStatus(status, {\n\t\t\tcollection: options.collection,\n\t\t\tsearch: options.search,\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment inbox error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_INBOX_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Status counts for inbox badges\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentCounts(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<Record<CommentStatus, number>>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst counts = await repo.countByStatus();\n\t\treturn { success: true, data: counts };\n\t} catch (error) {\n\t\tconsole.error(\"Comment counts error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_COUNTS_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment counts\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Get single comment detail\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst comment = await repo.findById(id);\n\n\t\tif (!comment) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: comment };\n\t} catch (error) {\n\t\tconsole.error(\"Comment get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Change comment status\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentStatusChange(\n\tdb: Kysely<Database>,\n\tid: string,\n\tstatus: CommentStatus,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst updated = await repo.updateStatus(id, status);\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: updated };\n\t} catch (error) {\n\t\tconsole.error(\"Comment status change error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_STATUS_ERROR\",\n\t\t\t\tmessage: \"Failed to update comment status\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Hard delete comment\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Bulk operations\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentBulk(\n\tdb: Kysely<Database>,\n\tids: string[],\n\taction: \"approve\" | \"spam\" | \"trash\" | \"delete\",\n): Promise<ApiResult<{ affected: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\tlet affected: number;\n\t\tif (action === \"delete\") {\n\t\t\taffected = await repo.bulkDelete(ids);\n\t\t} else {\n\t\t\tconst statusMap: Record<string, CommentStatus> = {\n\t\t\t\tapprove: \"approved\",\n\t\t\t\tspam: \"spam\",\n\t\t\t\ttrash: \"trash\",\n\t\t\t};\n\t\t\taffected = await repo.bulkUpdateStatus(ids, statusMap[action]);\n\t\t}\n\n\t\treturn { success: true, data: { affected } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment bulk error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_BULK_ERROR\",\n\t\t\t\tmessage: \"Failed to perform bulk operation\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Anti-spam: Rate limiting\n// ---------------------------------------------------------------------------\n\n/**\n * Check if an IP has exceeded the comment rate limit.\n * Uses ip_hash in the comments table — no separate counter storage.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tipHash: string,\n\tmaxPerWindow: number = 5,\n\twindowMinutes: number = 10,\n): Promise<boolean> {\n\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\n\t// Count recent comments from this IP\n\tconst result = await db\n\t\t.selectFrom(\"_emdash_comments\")\n\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t.where(\"ip_hash\", \"=\", ipHash)\n\t\t.where(\"created_at\", \">\", cutoff)\n\t\t.executeTakeFirst();\n\n\tconst count = Number(result?.count ?? 0);\n\treturn count >= maxPerWindow;\n}\n\n/**\n * Hash an IP address for storage (never store cleartext IPs).\n *\n * Uses full SHA-256 with a site-specific salt to prevent rainbow-table\n * recovery of IPs. The salt must be provided by the caller — typically\n * via `resolveSecretsCached(db).ipSalt` from `#config/secrets.js`. The\n * salt is generated and persisted on first need so it's stable across\n * requests within a deployment but unique per install.\n */\nexport async function hashIp(ip: string, salt: string): Promise<string> {\n\tconst data = `ip:${salt}:${ip}`;\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(data));\n\treturn Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"comments-BTAbC0Ek.mjs","names":[],"sources":["../src/api/handlers/comments.ts"],"sourcesContent":["/**\n * Comment handlers — business logic for comment API routes.\n *\n * Standalone functions that return ApiResult<T>. Routes are thin wrappers.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport type { Comment, CommentStatus, PublicComment } from \"../../database/repositories/comment.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Public: List approved comments for content\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n\toptions: { limit?: number; cursor?: string; threaded?: boolean } = {},\n): Promise<ApiResult<{ items: PublicComment[]; nextCursor?: string; total: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\t// Get total approved count\n\t\tconst total = await repo.countByContent(collection, contentId, \"approved\");\n\n\t\tlet publicItems: PublicComment[];\n\t\tlet nextCursor: string | undefined;\n\n\t\tif (options.threaded) {\n\t\t\t// Threaded mode: fetch all approved comments (capped) so threading\n\t\t\t// doesn't lose children that would fall on later pages.\n\t\t\tconst MAX_THREADED = 500;\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: MAX_THREADED,\n\t\t\t});\n\t\t\tconst threaded = CommentRepository.assembleThreads(result.items);\n\t\t\tpublicItems = threaded.map((c) => CommentRepository.toPublicComment(c));\n\t\t\t// No cursor for threaded mode — all comments returned at once\n\t\t} else {\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: options.limit,\n\t\t\t\tcursor: options.cursor,\n\t\t\t});\n\t\t\tpublicItems = result.items.map((c) => CommentRepository.toPublicComment(c));\n\t\t\tnextCursor = result.nextCursor;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: publicItems,\n\t\t\t\tnextCursor,\n\t\t\t\ttotal,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Moderation inbox\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentInbox(\n\tdb: Kysely<Database>,\n\toptions: {\n\t\tstatus?: CommentStatus;\n\t\tcollection?: string;\n\t\tsearch?: string;\n\t\tlimit?: number;\n\t\tcursor?: string;\n\t} = {},\n): Promise<ApiResult<{ items: Comment[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst status = options.status ?? \"pending\";\n\n\t\tconst result = await repo.findByStatus(status, {\n\t\t\tcollection: options.collection,\n\t\t\tsearch: options.search,\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment inbox error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_INBOX_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Status counts for inbox badges\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentCounts(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<Record<CommentStatus, number>>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst counts = await repo.countByStatus();\n\t\treturn { success: true, data: counts };\n\t} catch (error) {\n\t\tconsole.error(\"Comment counts error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_COUNTS_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment counts\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Get single comment detail\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst comment = await repo.findById(id);\n\n\t\tif (!comment) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: comment };\n\t} catch (error) {\n\t\tconsole.error(\"Comment get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Change comment status\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentStatusChange(\n\tdb: Kysely<Database>,\n\tid: string,\n\tstatus: CommentStatus,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst updated = await repo.updateStatus(id, status);\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: updated };\n\t} catch (error) {\n\t\tconsole.error(\"Comment status change error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_STATUS_ERROR\",\n\t\t\t\tmessage: \"Failed to update comment status\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Hard delete comment\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Bulk operations\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentBulk(\n\tdb: Kysely<Database>,\n\tids: string[],\n\taction: \"approve\" | \"spam\" | \"trash\" | \"delete\",\n): Promise<ApiResult<{ affected: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\tlet affected: number;\n\t\tif (action === \"delete\") {\n\t\t\taffected = await repo.bulkDelete(ids);\n\t\t} else {\n\t\t\tconst statusMap: Record<string, CommentStatus> = {\n\t\t\t\tapprove: \"approved\",\n\t\t\t\tspam: \"spam\",\n\t\t\t\ttrash: \"trash\",\n\t\t\t};\n\t\t\taffected = await repo.bulkUpdateStatus(ids, statusMap[action]);\n\t\t}\n\n\t\treturn { success: true, data: { affected } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment bulk error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_BULK_ERROR\",\n\t\t\t\tmessage: \"Failed to perform bulk operation\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Anti-spam: Rate limiting\n// ---------------------------------------------------------------------------\n\n/**\n * Check if an IP has exceeded the comment rate limit.\n * Uses ip_hash in the comments table — no separate counter storage.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tipHash: string,\n\tmaxPerWindow: number = 5,\n\twindowMinutes: number = 10,\n): Promise<boolean> {\n\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\n\t// Count recent comments from this IP\n\tconst result = await db\n\t\t.selectFrom(\"_emdash_comments\")\n\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t.where(\"ip_hash\", \"=\", ipHash)\n\t\t.where(\"created_at\", \">\", cutoff)\n\t\t.executeTakeFirst();\n\n\tconst count = Number(result?.count ?? 0);\n\treturn count >= maxPerWindow;\n}\n\n/**\n * Hash an IP address for storage (never store cleartext IPs).\n *\n * Uses full SHA-256 with a site-specific salt to prevent rainbow-table\n * recovery of IPs. The salt must be provided by the caller — typically\n * via `resolveSecretsCached(db).ipSalt` from `#config/secrets.js`. The\n * salt is generated and persisted on first need so it's stable across\n * requests within a deployment but unique per install.\n */\nexport async function hashIp(ip: string, salt: string): Promise<string> {\n\tconst data = `ip:${salt}:${ip}`;\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(data));\n\treturn Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { i as __exportAll } from "./runner-eAgyIkeg.mjs";
1
+ import { a as __exportAll } from "./runner-pt6Wl-l-.mjs";
2
2
  import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
3
3
  import { n as slugify } from "./slugify-Cjh1ssOZ.mjs";
4
- import { i as encodeCursor, r as decodeCursor, t as EmDashValidationError } from "./types-SF1DwGf2.mjs";
4
+ import { i as encodeCursor, r as decodeCursor, t as EmDashValidationError } from "./types-K3MDsxpy.mjs";
5
5
  import { sql } from "kysely";
6
6
  import { monotonicFactory, ulid } from "ulidx";
7
7
 
@@ -898,4 +898,4 @@ var ContentRepository = class {
898
898
 
899
899
  //#endregion
900
900
  export { content_exports as n, RevisionRepository as r, ContentRepository as t };
901
- //# sourceMappingURL=content-BbqKo3Kc.mjs.map
901
+ //# sourceMappingURL=content-CyqOmOzm.mjs.map