emdash 0.31.1 → 0.32.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 (554) hide show
  1. package/dist/api/route-utils.d.mts +3 -3
  2. package/dist/api/route-utils.mjs +16 -14
  3. package/dist/api/route-utils.mjs.map +1 -1
  4. package/dist/api/schemas/index.d.mts +2 -2
  5. package/dist/api/schemas/index.mjs +4 -4
  6. package/dist/{api-BkqFzIu4.mjs → api-DgPbMlng.mjs} +24 -20
  7. package/dist/api-DgPbMlng.mjs.map +1 -0
  8. package/dist/{api-tokens-uPt8UDpx.mjs → api-tokens-p-iMzvXR.mjs} +2 -2
  9. package/dist/{api-tokens-uPt8UDpx.mjs.map → api-tokens-p-iMzvXR.mjs.map} +1 -1
  10. package/dist/{apply-DuXsqll7.mjs → apply-1_6ra7NP.mjs} +19 -19
  11. package/dist/{apply-DuXsqll7.mjs.map → apply-1_6ra7NP.mjs.map} +1 -1
  12. package/dist/astro/index.d.mts +12 -12
  13. package/dist/astro/index.mjs +5 -1
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/middleware/auth.d.mts +10 -10
  16. package/dist/astro/middleware/auth.mjs +5 -5
  17. package/dist/astro/middleware/redirect.mjs +5 -5
  18. package/dist/astro/middleware/request-context.mjs +1 -1
  19. package/dist/astro/middleware.d.mts +10 -10
  20. package/dist/astro/middleware.mjs +87 -59
  21. package/dist/astro/middleware.mjs.map +1 -1
  22. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
  23. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
  24. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
  25. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  26. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
  27. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
  28. package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
  29. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
  30. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +15 -14
  31. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  32. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +15 -14
  33. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
  34. package/dist/astro/routes/api/admin/bylines/index.mjs +15 -15
  35. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
  36. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  37. package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
  38. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  39. package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
  40. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  41. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  42. package/dist/astro/routes/api/admin/media-usage/repair.mjs +12 -10
  43. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  44. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  45. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  46. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +36 -34
  47. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  48. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +36 -34
  49. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  50. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +36 -34
  51. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  52. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
  53. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +36 -34
  54. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +36 -34
  56. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +36 -34
  58. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/plugins/index.mjs +36 -34
  60. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  61. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  62. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +36 -34
  63. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  64. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +36 -34
  65. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  66. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +36 -34
  67. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  68. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +36 -34
  69. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +37 -35
  71. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  72. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +36 -34
  73. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  74. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +37 -35
  75. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  76. package/dist/astro/routes/api/admin/plugins/updates.mjs +36 -34
  77. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  78. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +36 -34
  79. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  81. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +36 -34
  82. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  84. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  85. package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
  86. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  87. package/dist/astro/routes/api/admin/users/index.mjs +6 -6
  88. package/dist/astro/routes/api/auth/dev-bypass.mjs +7 -6
  89. package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -1
  90. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  91. package/dist/astro/routes/api/auth/invite/complete.mjs +7 -7
  92. package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
  93. package/dist/astro/routes/api/auth/invite/register-options.mjs +7 -7
  94. package/dist/astro/routes/api/auth/logout.mjs +3 -3
  95. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  96. package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
  97. package/dist/astro/routes/api/auth/me.mjs +7 -7
  98. package/dist/astro/routes/api/auth/mode.mjs +2 -2
  99. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +2 -2
  100. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +1 -1
  101. package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
  102. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  103. package/dist/astro/routes/api/auth/passkey/options.mjs +8 -8
  104. package/dist/astro/routes/api/auth/passkey/register/options.mjs +7 -7
  105. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +7 -7
  106. package/dist/astro/routes/api/auth/passkey/verify.mjs +7 -7
  107. package/dist/astro/routes/api/auth/signup/complete.mjs +7 -7
  108. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  109. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  110. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  111. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +10 -10
  112. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  113. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  114. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  115. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  116. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +9 -9
  117. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +7 -7
  118. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +16 -14
  119. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  120. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +16 -14
  121. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  122. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  123. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  124. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +7 -7
  125. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +14 -13
  126. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  127. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  128. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  129. package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
  130. package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
  131. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  132. package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
  133. package/dist/astro/routes/api/content/_collection_/index.mjs +7 -7
  134. package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
  135. package/dist/astro/routes/api/dashboard.mjs +7 -7
  136. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  137. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  138. package/dist/astro/routes/api/import/probe.mjs +12 -12
  139. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  140. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  141. package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -11
  142. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  143. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  144. package/dist/astro/routes/api/import/wordpress/media.mjs +7 -7
  145. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  146. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  147. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +11 -9
  148. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  149. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  150. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +12 -12
  151. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  152. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +20 -19
  153. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  154. package/dist/astro/routes/api/manifest.mjs +3 -3
  155. package/dist/astro/routes/api/mcp.mjs +41 -41
  156. package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
  157. package/dist/astro/routes/api/media/_id_/confirm.mjs +105 -36
  158. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  159. package/dist/astro/routes/api/media/_id_/upload.d.mts +8 -0
  160. package/dist/astro/routes/api/media/_id_/upload.d.mts.map +1 -0
  161. package/dist/astro/routes/api/media/_id_/upload.mjs +168 -0
  162. package/dist/astro/routes/api/media/_id_/upload.mjs.map +1 -0
  163. package/dist/astro/routes/api/media/_id_/usage.mjs +14 -12
  164. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  165. package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
  166. package/dist/astro/routes/api/media/_id_.mjs +21 -14
  167. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  168. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  169. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  170. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  171. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  172. package/dist/astro/routes/api/media/upload-url.d.mts.map +1 -1
  173. package/dist/astro/routes/api/media/upload-url.mjs +34 -23
  174. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  175. package/dist/astro/routes/api/media.mjs +13 -12
  176. package/dist/astro/routes/api/media.mjs.map +1 -1
  177. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +9 -8
  178. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs.map +1 -1
  179. package/dist/astro/routes/api/menus/_name_/items.mjs +9 -8
  180. package/dist/astro/routes/api/menus/_name_/items.mjs.map +1 -1
  181. package/dist/astro/routes/api/menus/_name_/reorder.mjs +9 -8
  182. package/dist/astro/routes/api/menus/_name_/reorder.mjs.map +1 -1
  183. package/dist/astro/routes/api/menus/_name_/translations.mjs +9 -8
  184. package/dist/astro/routes/api/menus/_name_/translations.mjs.map +1 -1
  185. package/dist/astro/routes/api/menus/_name_.mjs +9 -8
  186. package/dist/astro/routes/api/menus/_name_.mjs.map +1 -1
  187. package/dist/astro/routes/api/menus/index.mjs +9 -8
  188. package/dist/astro/routes/api/menus/index.mjs.map +1 -1
  189. package/dist/astro/routes/api/oauth/authorize.mjs +5 -5
  190. package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
  191. package/dist/astro/routes/api/oauth/device/code.mjs +7 -7
  192. package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
  193. package/dist/astro/routes/api/oauth/register.mjs +3 -3
  194. package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
  195. package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
  196. package/dist/astro/routes/api/oauth/token.mjs +6 -6
  197. package/dist/astro/routes/api/openapi.json.mjs +30 -8
  198. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  199. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +5 -5
  200. package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
  201. package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
  202. package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
  203. package/dist/astro/routes/api/redirects/index.mjs +11 -11
  204. package/dist/astro/routes/api/relations/_id_/index.mjs +16 -14
  205. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  206. package/dist/astro/routes/api/relations/_id_/translations.mjs +12 -10
  207. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  208. package/dist/astro/routes/api/relations/index.mjs +16 -14
  209. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  210. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  211. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  212. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +36 -34
  213. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  214. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +36 -34
  215. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  216. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +36 -34
  217. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  218. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +36 -34
  219. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  220. package/dist/astro/routes/api/schema/collections/index.mjs +36 -34
  221. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  222. package/dist/astro/routes/api/schema/index.mjs +10 -8
  223. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  224. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +36 -34
  225. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  226. package/dist/astro/routes/api/schema/orphans/index.mjs +36 -34
  227. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/search/enable.mjs +12 -11
  229. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  230. package/dist/astro/routes/api/search/index.mjs +11 -10
  231. package/dist/astro/routes/api/search/index.mjs.map +1 -1
  232. package/dist/astro/routes/api/search/rebuild.mjs +12 -11
  233. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  234. package/dist/astro/routes/api/search/stats.mjs +8 -7
  235. package/dist/astro/routes/api/search/stats.mjs.map +1 -1
  236. package/dist/astro/routes/api/search/suggest.mjs +11 -10
  237. package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
  238. package/dist/astro/routes/api/sections/_slug_.mjs +10 -10
  239. package/dist/astro/routes/api/sections/index.mjs +10 -10
  240. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +4 -4
  241. package/dist/astro/routes/api/settings/backups/archives/index.mjs +4 -4
  242. package/dist/astro/routes/api/settings/backups/export.mjs +4 -4
  243. package/dist/astro/routes/api/settings/backups/index.mjs +5 -5
  244. package/dist/astro/routes/api/settings/email.mjs +4 -4
  245. package/dist/astro/routes/api/settings.mjs +12 -12
  246. package/dist/astro/routes/api/setup/admin-verify.mjs +8 -8
  247. package/dist/astro/routes/api/setup/admin.mjs +8 -8
  248. package/dist/astro/routes/api/setup/dev-bypass.mjs +24 -22
  249. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  250. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  251. package/dist/astro/routes/api/setup/index.mjs +24 -22
  252. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  253. package/dist/astro/routes/api/setup/status.mjs +3 -3
  254. package/dist/astro/routes/api/snapshot.mjs +5 -5
  255. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +14 -13
  256. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  257. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +14 -13
  258. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  259. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  260. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +19 -15
  261. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  262. package/dist/astro/routes/api/taxonomies/index.mjs +14 -13
  263. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  264. package/dist/astro/routes/api/themes/preview.mjs +4 -4
  265. package/dist/astro/routes/api/typegen.mjs +5 -5
  266. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +1 -1
  267. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +1 -1
  268. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
  269. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
  270. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
  271. package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -6
  272. package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
  273. package/dist/astro/routes/api/widget-components.mjs +2 -2
  274. package/dist/astro/routes/robots.txt.mjs +5 -5
  275. package/dist/astro/routes/sitemap-_collection_.xml.mjs +7 -7
  276. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  277. package/dist/astro/types.d.mts +14 -13
  278. package/dist/astro/types.d.mts.map +1 -1
  279. package/dist/auth/providers/github.d.mts +1 -1
  280. package/dist/auth/providers/google.d.mts +1 -1
  281. package/dist/{authorize-6WPZa1xZ.mjs → authorize-R5iYYrpa.mjs} +2 -2
  282. package/dist/{authorize-6WPZa1xZ.mjs.map → authorize-R5iYYrpa.mjs.map} +1 -1
  283. package/dist/{backup-DoW-H0r4.mjs → backup-LpQ1V_sR.mjs} +2 -2
  284. package/dist/{backup-DoW-H0r4.mjs.map → backup-LpQ1V_sR.mjs.map} +1 -1
  285. package/dist/{byline-BTOxUlFF.mjs → byline-C5TAqs8N.mjs} +6 -6
  286. package/dist/{byline-BTOxUlFF.mjs.map → byline-C5TAqs8N.mjs.map} +1 -1
  287. package/dist/{byline-fields-CdU_LTF1.mjs → byline-fields-T9ZLZMxJ.mjs} +1 -1
  288. package/dist/{byline-fields-CdU_LTF1.mjs.map → byline-fields-T9ZLZMxJ.mjs.map} +1 -1
  289. package/dist/{bylines-Bc-3xtpz.mjs → bylines-B8-WGdja.mjs} +6 -6
  290. package/dist/{bylines-Bc-3xtpz.mjs.map → bylines-B8-WGdja.mjs.map} +1 -1
  291. package/dist/{bylines-oVN0iPwX.mjs → bylines-DGuqBLjV.mjs} +5 -5
  292. package/dist/{bylines-oVN0iPwX.mjs.map → bylines-DGuqBLjV.mjs.map} +1 -1
  293. package/dist/{cache-DDwy3JDW.mjs → cache-B26cufFd.mjs} +2 -2
  294. package/dist/{cache-DDwy3JDW.mjs.map → cache-B26cufFd.mjs.map} +1 -1
  295. package/dist/{chunks-Bq-5DPrD.mjs → chunks-D5dlPeRb.mjs} +2 -2
  296. package/dist/{chunks-Bq-5DPrD.mjs.map → chunks-D5dlPeRb.mjs.map} +1 -1
  297. package/dist/cli/index.mjs +20 -19
  298. package/dist/cli/index.mjs.map +1 -1
  299. package/dist/client/cf-access.d.mts +1 -1
  300. package/dist/client/index.d.mts +1 -1
  301. package/dist/client/index.mjs +2 -2
  302. package/dist/{comment-B9cUud8H.mjs → comment-DnTxxVHv.mjs} +2 -2
  303. package/dist/{comment-B9cUud8H.mjs.map → comment-DnTxxVHv.mjs.map} +1 -1
  304. package/dist/{comment-reaction-VUXO981F.mjs → comment-reaction-BUlUz2bh.mjs} +2 -2
  305. package/dist/{comment-reaction-VUXO981F.mjs.map → comment-reaction-BUlUz2bh.mjs.map} +1 -1
  306. package/dist/{comments-PXj1E9Si.mjs → comments-QPKgK3nk.mjs} +3 -3
  307. package/dist/{comments-PXj1E9Si.mjs.map → comments-QPKgK3nk.mjs.map} +1 -1
  308. package/dist/{config-B6b_BobR.mjs → config-DyF3OKg_.mjs} +15 -10
  309. package/dist/config-DyF3OKg_.mjs.map +1 -0
  310. package/dist/{content-BC43pICM.mjs → content-CpfKV9QE.mjs} +9 -9
  311. package/dist/content-CpfKV9QE.mjs.map +1 -0
  312. package/dist/{content-refresh-CPMSauD3.mjs → content-refresh-DgqUBeTv.mjs} +5 -5
  313. package/dist/{content-refresh-CPMSauD3.mjs.map → content-refresh-DgqUBeTv.mjs.map} +1 -1
  314. package/dist/{context-DOurRIrb.mjs → context-kO4YZB03.mjs} +10 -10
  315. package/dist/{context-DOurRIrb.mjs.map → context-kO4YZB03.mjs.map} +1 -1
  316. package/dist/{dashboard-CBm4zhc3.mjs → dashboard-DoBwPnON.mjs} +4 -4
  317. package/dist/{dashboard-CBm4zhc3.mjs.map → dashboard-DoBwPnON.mjs.map} +1 -1
  318. package/dist/db/index.d.mts +2 -2
  319. package/dist/db/index.mjs +3 -2
  320. package/dist/db/index.mjs.map +1 -1
  321. package/dist/{device-flow-BeA6lUS1.mjs → device-flow-DhUsyJU6.mjs} +4 -4
  322. package/dist/{device-flow-BeA6lUS1.mjs.map → device-flow-DhUsyJU6.mjs.map} +1 -1
  323. package/dist/{dialect-helpers-BtOwo2us.mjs → dialect-helpers-DFwyWjTM.mjs} +21 -2
  324. package/dist/dialect-helpers-DFwyWjTM.mjs.map +1 -0
  325. package/dist/{error-DJ98TRbQ.mjs → error-DmmN74gW.mjs} +2 -2
  326. package/dist/{error-DJ98TRbQ.mjs.map → error-DmmN74gW.mjs.map} +1 -1
  327. package/dist/{escape-CFedIB1C.mjs → escape-CdW31WsG.mjs} +1 -1
  328. package/dist/{escape-CFedIB1C.mjs.map → escape-CdW31WsG.mjs.map} +1 -1
  329. package/dist/{field-defs-cache-KfNJ_qLQ.mjs → field-defs-cache-QMVnzTH6.mjs} +2 -2
  330. package/dist/{field-defs-cache-KfNJ_qLQ.mjs.map → field-defs-cache-QMVnzTH6.mjs.map} +1 -1
  331. package/dist/{fts-manager-hyCU9OOp.mjs → fts-manager-vncsZIJm.mjs} +3 -3
  332. package/dist/{fts-manager-hyCU9OOp.mjs.map → fts-manager-vncsZIJm.mjs.map} +1 -1
  333. package/dist/{hash-DFFrkivP.mjs → hash-Cb7U1b5M.mjs} +8 -4
  334. package/dist/hash-Cb7U1b5M.mjs.map +1 -0
  335. package/dist/{hreflang-CbYeCzl9.mjs → hreflang-Bg-zMpkr.mjs} +7 -7
  336. package/dist/{hreflang-CbYeCzl9.mjs.map → hreflang-Bg-zMpkr.mjs.map} +1 -1
  337. package/dist/{hreflang-CIxH_LKz.d.mts → hreflang-CZbSx6Bg.d.mts} +2 -2
  338. package/dist/{hreflang-CIxH_LKz.d.mts.map → hreflang-CZbSx6Bg.d.mts.map} +1 -1
  339. package/dist/{import-oDYCgNYQ.mjs → import-PKhLeXvn.mjs} +2 -2
  340. package/dist/{import-oDYCgNYQ.mjs.map → import-PKhLeXvn.mjs.map} +1 -1
  341. package/dist/{index-BEui-cRd.d.mts → index-BeNto_Vs.d.mts} +3 -3
  342. package/dist/{index-BEui-cRd.d.mts.map → index-BeNto_Vs.d.mts.map} +1 -1
  343. package/dist/{index-BZJ_5FOX.d.mts → index-j9rTh3cp.d.mts} +52 -17
  344. package/dist/index-j9rTh3cp.d.mts.map +1 -0
  345. package/dist/index.d.mts +18 -18
  346. package/dist/index.mjs +49 -49
  347. package/dist/{load-Dg5gtWma.mjs → load-BwTdWE8B.mjs} +2 -2
  348. package/dist/{load-Dg5gtWma.mjs.map → load-BwTdWE8B.mjs.map} +1 -1
  349. package/dist/{loader-D4rigvjR.mjs → loader-C1XOLV5b.mjs} +4 -4
  350. package/dist/{loader-D4rigvjR.mjs.map → loader-C1XOLV5b.mjs.map} +1 -1
  351. package/dist/media/index.d.mts +1 -1
  352. package/dist/media/local-runtime.d.mts +12 -12
  353. package/dist/media/local-runtime.mjs +5 -5
  354. package/dist/{media-BJ6SRY3g.mjs → media-DWIzoVFL.mjs} +8 -4
  355. package/dist/{media-BJ6SRY3g.mjs.map → media-DWIzoVFL.mjs.map} +1 -1
  356. package/dist/{media-allowlist-jUppPWFy.mjs → media-allowlist-B7TPD6aq.mjs} +1 -1
  357. package/dist/{media-allowlist-jUppPWFy.mjs.map → media-allowlist-B7TPD6aq.mjs.map} +1 -1
  358. package/dist/{media-its1WLYv.mjs → media-tQDZEdu7.mjs} +60 -15
  359. package/dist/media-tQDZEdu7.mjs.map +1 -0
  360. package/dist/{media-upload-DNu3QySc.mjs → media-upload-Bc9Nc5Zf.mjs} +6 -6
  361. package/dist/{media-upload-DNu3QySc.mjs.map → media-upload-Bc9Nc5Zf.mjs.map} +1 -1
  362. package/dist/{media-usage-DPQ3K4xB.mjs → media-usage-DPJcNaAq.mjs} +6 -6
  363. package/dist/{media-usage-DPQ3K4xB.mjs.map → media-usage-DPJcNaAq.mjs.map} +1 -1
  364. package/dist/{menus-BMGzWyWH.mjs → menus-BxPLFfIc.mjs} +3 -3
  365. package/dist/{menus-BMGzWyWH.mjs.map → menus-BxPLFfIc.mjs.map} +1 -1
  366. package/dist/{menus-CPf_SW7u.mjs → menus-C6qhqP4X.mjs} +8 -8
  367. package/dist/{menus-CPf_SW7u.mjs.map → menus-C6qhqP4X.mjs.map} +1 -1
  368. package/dist/{oauth-authorization-oqfbBuMH.mjs → oauth-authorization-C9tFinly.mjs} +4 -4
  369. package/dist/{oauth-authorization-oqfbBuMH.mjs.map → oauth-authorization-C9tFinly.mjs.map} +1 -1
  370. package/dist/{oauth-clients-BlrUHAsf.mjs → oauth-clients-BwTxE1aE.mjs} +1 -1
  371. package/dist/{oauth-clients-BlrUHAsf.mjs.map → oauth-clients-BwTxE1aE.mjs.map} +1 -1
  372. package/dist/{oauth-state-store-DnvjRMy6.mjs → oauth-state-store-DaRUwPWO.mjs} +1 -1
  373. package/dist/{oauth-state-store-DnvjRMy6.mjs.map → oauth-state-store-DaRUwPWO.mjs.map} +1 -1
  374. package/dist/{oauth-user-lookup-oUllxvAv.mjs → oauth-user-lookup-CzuzOSlF.mjs} +1 -1
  375. package/dist/{oauth-user-lookup-oUllxvAv.mjs.map → oauth-user-lookup-CzuzOSlF.mjs.map} +1 -1
  376. package/dist/object-cache/memory.d.mts +1 -1
  377. package/dist/{options-BqMkWfYa.d.mts → options-aeIs8WFu.d.mts} +3 -3
  378. package/dist/{options-BqMkWfYa.d.mts.map → options-aeIs8WFu.d.mts.map} +1 -1
  379. package/dist/page/index.d.mts +2 -2
  380. package/dist/{parse-53zyxlpv.mjs → parse-BL49yb9D.mjs} +2 -2
  381. package/dist/{parse-53zyxlpv.mjs.map → parse-BL49yb9D.mjs.map} +1 -1
  382. package/dist/{passkey-config-C-SKpc0-.mjs → passkey-config-KMhgFjdQ.mjs} +1 -1
  383. package/dist/{passkey-config-C-SKpc0-.mjs.map → passkey-config-KMhgFjdQ.mjs.map} +1 -1
  384. package/dist/{placeholder-Dop47JMM.d.mts → placeholder-Dxo5Zt58.d.mts} +1 -1
  385. package/dist/{placeholder-Dop47JMM.d.mts.map → placeholder-Dxo5Zt58.d.mts.map} +1 -1
  386. package/dist/plugin-types.d.mts +1 -1
  387. package/dist/plugin-utils.d.mts +10 -10
  388. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  389. package/dist/{portable-text-sDj92Kxv.mjs → portable-text-CNahFfj6.mjs} +1 -1
  390. package/dist/{portable-text-sDj92Kxv.mjs.map → portable-text-CNahFfj6.mjs.map} +1 -1
  391. package/dist/{query-DdFGHzQF.mjs → query-1xOcEy18.mjs} +3 -3
  392. package/dist/{query-DdFGHzQF.mjs.map → query-1xOcEy18.mjs.map} +1 -1
  393. package/dist/{query-Bz9bWZfm.mjs → query-DCiXI7OZ.mjs} +36 -16
  394. package/dist/query-DCiXI7OZ.mjs.map +1 -0
  395. package/dist/{rate-limit-D3IEiUi2.mjs → rate-limit-Vkp9gQ1Y.mjs} +2 -2
  396. package/dist/{rate-limit-D3IEiUi2.mjs.map → rate-limit-Vkp9gQ1Y.mjs.map} +1 -1
  397. package/dist/{redirect-BoUac4NT.mjs → redirect-BJ6d1yHh.mjs} +3 -3
  398. package/dist/{redirect-BoUac4NT.mjs.map → redirect-BJ6d1yHh.mjs.map} +1 -1
  399. package/dist/{redirect-Botom7X6.mjs → redirect-CDgfv9vh.mjs} +1 -1
  400. package/dist/{redirect-Botom7X6.mjs.map → redirect-CDgfv9vh.mjs.map} +1 -1
  401. package/dist/{redirects-D457qwt0.mjs → redirects-BOn2DIUs.mjs} +8 -4
  402. package/dist/redirects-BOn2DIUs.mjs.map +1 -0
  403. package/dist/{redirects-ChbpqKiS.mjs → redirects-Cwkq2H-G.mjs} +3 -3
  404. package/dist/{redirects-ChbpqKiS.mjs.map → redirects-Cwkq2H-G.mjs.map} +1 -1
  405. package/dist/{registry-y21kjhfG.mjs → registry-BP1JK2xh.mjs} +6 -6
  406. package/dist/{registry-y21kjhfG.mjs.map → registry-BP1JK2xh.mjs.map} +1 -1
  407. package/dist/{relation-Du0r04E0.mjs → relation-DJ5DZh7B.mjs} +3 -3
  408. package/dist/{relation-Du0r04E0.mjs.map → relation-DJ5DZh7B.mjs.map} +1 -1
  409. package/dist/{relations-CZMpBqEd.mjs → relations-C4duJnwI.mjs} +2 -2
  410. package/dist/{relations-CZMpBqEd.mjs.map → relations-C4duJnwI.mjs.map} +1 -1
  411. package/dist/{relations-qKmSj9KM.d.mts → relations-DHFQGX0h.d.mts} +28 -17
  412. package/dist/relations-DHFQGX0h.d.mts.map +1 -0
  413. package/dist/{relations-DtX1UMBH.mjs → relations-DpTjftCl.mjs} +6 -6
  414. package/dist/{relations-DtX1UMBH.mjs.map → relations-DpTjftCl.mjs.map} +1 -1
  415. package/dist/{resolve-DFr4kq72.mjs → resolve-BUvFE0Lr.mjs} +2 -2
  416. package/dist/{resolve-DFr4kq72.mjs.map → resolve-BUvFE0Lr.mjs.map} +1 -1
  417. package/dist/{runner-BdeLEgad.mjs → runner-BsI18UgP.mjs} +230 -212
  418. package/dist/runner-BsI18UgP.mjs.map +1 -0
  419. package/dist/{runner-Dd8m8k9F.d.mts → runner-CHdkERbt.d.mts} +2 -2
  420. package/dist/{runner-Dd8m8k9F.d.mts.map → runner-CHdkERbt.d.mts.map} +1 -1
  421. package/dist/runtime.d.mts +11 -11
  422. package/dist/runtime.mjs +3 -3
  423. package/dist/{schema-BH8P8vaC.mjs → schema-eqZhkR0S.mjs} +3 -3
  424. package/dist/{schema-BH8P8vaC.mjs.map → schema-eqZhkR0S.mjs.map} +1 -1
  425. package/dist/{scopes-DTIkIaGA.mjs → scopes-Be2d8bSb.mjs} +3 -3
  426. package/dist/{scopes-DTIkIaGA.mjs.map → scopes-Be2d8bSb.mjs.map} +1 -1
  427. package/dist/{search-D-Yhpmvs.mjs → search-CeO678cp.mjs} +6 -6
  428. package/dist/{search-D-Yhpmvs.mjs.map → search-CeO678cp.mjs.map} +1 -1
  429. package/dist/{secrets-CQ-69kF9.mjs → secrets-870d-7yA.mjs} +1 -1
  430. package/dist/{secrets-CQ-69kF9.mjs.map → secrets-870d-7yA.mjs.map} +1 -1
  431. package/dist/{sections-CzVBAKjw.mjs → sections-DSuEfirn.mjs} +3 -3
  432. package/dist/{sections-CzVBAKjw.mjs.map → sections-DSuEfirn.mjs.map} +1 -1
  433. package/dist/seed/index.d.mts +2 -2
  434. package/dist/seed/index.mjs +18 -16
  435. package/dist/seo/index.d.mts +3 -3
  436. package/dist/seo/index.mjs +14 -12
  437. package/dist/seo/index.mjs.map +1 -1
  438. package/dist/{seo-kJzVAmmu.mjs → seo-BAPKiED8.mjs} +2 -2
  439. package/dist/{seo-kJzVAmmu.mjs.map → seo-BAPKiED8.mjs.map} +1 -1
  440. package/dist/{service-CQnc276a.mjs → service-DyFT-k-R.mjs} +3 -3
  441. package/dist/{service-CQnc276a.mjs.map → service-DyFT-k-R.mjs.map} +1 -1
  442. package/dist/{session-user-DbHqKDKe.mjs → session-user-BrK2zz7S.mjs} +1 -1
  443. package/dist/{session-user-DbHqKDKe.mjs.map → session-user-BrK2zz7S.mjs.map} +1 -1
  444. package/dist/{settings-Dtc0DU31.mjs → settings-CREKyj8I.mjs} +3 -3
  445. package/dist/{settings-Dtc0DU31.mjs.map → settings-CREKyj8I.mjs.map} +1 -1
  446. package/dist/{settings-C2PaBS49.mjs → settings-DgKouY2S.mjs} +3 -3
  447. package/dist/{settings-C2PaBS49.mjs.map → settings-DgKouY2S.mjs.map} +1 -1
  448. package/dist/{setup-complete-CdIlqloh.mjs → setup-complete-L7bMK2VI.mjs} +1 -1
  449. package/dist/{setup-complete-CdIlqloh.mjs.map → setup-complete-L7bMK2VI.mjs.map} +1 -1
  450. package/dist/{setup-nonce-DqiIPr-J.mjs → setup-nonce-Ct5tjWcq.mjs} +1 -1
  451. package/dist/{setup-nonce-DqiIPr-J.mjs.map → setup-nonce-Ct5tjWcq.mjs.map} +1 -1
  452. package/dist/{site-url-NGJT6NTU.mjs → site-url-8N3kSyVZ.mjs} +1 -1
  453. package/dist/{site-url-NGJT6NTU.mjs.map → site-url-8N3kSyVZ.mjs.map} +1 -1
  454. package/dist/storage/local.d.mts +1 -1
  455. package/dist/storage/s3.d.mts +1 -1
  456. package/dist/storage/s3.mjs +1 -1
  457. package/dist/storage/s3.mjs.map +1 -1
  458. package/dist/{taxonomies-r7pqLBCn.mjs → taxonomies-B61CRSha.mjs} +48 -22
  459. package/dist/taxonomies-B61CRSha.mjs.map +1 -0
  460. package/dist/{taxonomies-BN6M9Nrv.mjs → taxonomies-Dbdki3Un.mjs} +9 -8
  461. package/dist/taxonomies-Dbdki3Un.mjs.map +1 -0
  462. package/dist/{taxonomy-0mCI25HN.mjs → taxonomy-DvwWAPvA.mjs} +3 -3
  463. package/dist/{taxonomy-0mCI25HN.mjs.map → taxonomy-DvwWAPvA.mjs.map} +1 -1
  464. package/dist/{transport-CKrlqSpD.mjs → transport-BWQo0Foy.mjs} +1 -1
  465. package/dist/{transport-CKrlqSpD.mjs.map → transport-BWQo0Foy.mjs.map} +1 -1
  466. package/dist/{transport-Vd7VdqHv.d.mts → transport-DokJOScS.d.mts} +1 -1
  467. package/dist/{transport-Vd7VdqHv.d.mts.map → transport-DokJOScS.d.mts.map} +1 -1
  468. package/dist/{types-DPKhI5ib.d.mts → types-84Bque3-.d.mts} +9 -1
  469. package/dist/types-84Bque3-.d.mts.map +1 -0
  470. package/dist/{types-BvB7gDOD.d.mts → types-BQY7VYw6.d.mts} +2 -2
  471. package/dist/{types-BvB7gDOD.d.mts.map → types-BQY7VYw6.d.mts.map} +1 -1
  472. package/dist/{types-B4vm81VH.d.mts → types-C9P3ysxq.d.mts} +1 -1
  473. package/dist/{types-B4vm81VH.d.mts.map → types-C9P3ysxq.d.mts.map} +1 -1
  474. package/dist/{types-B97jg3aW.d.mts → types-CcSZCcF1.d.mts} +1 -1
  475. package/dist/{types-B97jg3aW.d.mts.map → types-CcSZCcF1.d.mts.map} +1 -1
  476. package/dist/{types-BNfPHfay.d.mts → types-DQLZ58GV.d.mts} +1 -1
  477. package/dist/{types-BNfPHfay.d.mts.map → types-DQLZ58GV.d.mts.map} +1 -1
  478. package/dist/{types-BfYUPJdP.d.mts → types-DqZVPILm.d.mts} +2 -2
  479. package/dist/{types-BfYUPJdP.d.mts.map → types-DqZVPILm.d.mts.map} +1 -1
  480. package/dist/{types-C7U0HlEr.d.mts → types-Dr2GLlwu.d.mts} +1 -1
  481. package/dist/{types-C7U0HlEr.d.mts.map → types-Dr2GLlwu.d.mts.map} +1 -1
  482. package/dist/{types-iVeZ5g6N.d.mts → types-DsAMyD_o.d.mts} +1 -1
  483. package/dist/{types-iVeZ5g6N.d.mts.map → types-DsAMyD_o.d.mts.map} +1 -1
  484. package/dist/{types-CrRUFdej.mjs → types-XrQQ-Aex.mjs} +2 -2
  485. package/dist/{types-CrRUFdej.mjs.map → types-XrQQ-Aex.mjs.map} +1 -1
  486. package/dist/{types-Kmaxdarz.d.mts → types-jISEalTX.d.mts} +1 -1
  487. package/dist/{types-Kmaxdarz.d.mts.map → types-jISEalTX.d.mts.map} +1 -1
  488. package/dist/upload-attempts-C5yd6Gae.mjs +27 -0
  489. package/dist/upload-attempts-C5yd6Gae.mjs.map +1 -0
  490. package/dist/{user-aTXqmXBz.mjs → user-BAumEmpA.mjs} +3 -3
  491. package/dist/{user-aTXqmXBz.mjs.map → user-BAumEmpA.mjs.map} +1 -1
  492. package/dist/{validate-CDT1jl6_.mjs → validate-Bs_wT2ul.mjs} +3 -3
  493. package/dist/{validate-CDT1jl6_.mjs.map → validate-Bs_wT2ul.mjs.map} +1 -1
  494. package/dist/{validate-DULfgpJP.d.mts → validate-CdYcY-Ln.d.mts} +5 -5
  495. package/dist/{validate-DULfgpJP.d.mts.map → validate-CdYcY-Ln.d.mts.map} +1 -1
  496. package/dist/{validation-DlD0E35W.mjs → validation-BngR0nit.mjs} +10 -8
  497. package/dist/{validation-DlD0E35W.mjs.map → validation-BngR0nit.mjs.map} +1 -1
  498. package/dist/version-Bdfb34kk.mjs +7 -0
  499. package/dist/{version-BRMngh9I.mjs.map → version-Bdfb34kk.mjs.map} +1 -1
  500. package/dist/{widgets-5fipNkMT.mjs → widgets-L-VndTKD.mjs} +2 -2
  501. package/dist/{widgets-5fipNkMT.mjs.map → widgets-L-VndTKD.mjs.map} +1 -1
  502. package/dist/{wxr-taxonomies-DrihupaC.mjs → wxr-taxonomies-BexjtNid.mjs} +4 -4
  503. package/dist/{wxr-taxonomies-DrihupaC.mjs.map → wxr-taxonomies-BexjtNid.mjs.map} +1 -1
  504. package/dist/{wxr-taxonomies-B4tMuN5T.d.mts → wxr-taxonomies-DueaLFd9.d.mts} +1 -1
  505. package/dist/{wxr-taxonomies-B4tMuN5T.d.mts.map → wxr-taxonomies-DueaLFd9.d.mts.map} +1 -1
  506. package/dist/{zod-generator-CwkHWz4f.mjs → zod-generator-DMzfga3g.mjs} +3 -3
  507. package/dist/{zod-generator-CwkHWz4f.mjs.map → zod-generator-DMzfga3g.mjs.map} +1 -1
  508. package/package.json +6 -5
  509. package/src/api/handlers/media.ts +4 -4
  510. package/src/api/handlers/taxonomies.ts +10 -9
  511. package/src/api/openapi/document.ts +36 -3
  512. package/src/api/schemas/media.ts +9 -2
  513. package/src/api/schemas/taxonomies.ts +16 -1
  514. package/src/astro/integration/routes.ts +5 -0
  515. package/src/astro/routes/api/content/[collection]/[id].ts +3 -1
  516. package/src/astro/routes/api/media/[id]/confirm.ts +148 -59
  517. package/src/astro/routes/api/media/[id]/upload.ts +262 -0
  518. package/src/astro/routes/api/media/[id].ts +19 -10
  519. package/src/astro/routes/api/media/upload-url.ts +28 -31
  520. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +6 -3
  521. package/src/astro/types.ts +1 -0
  522. package/src/cleanup.ts +24 -1
  523. package/src/components/InlinePortableTextEditor.tsx +4 -0
  524. package/src/components/code-mark.ts +11 -0
  525. package/src/database/dialect-helpers.ts +38 -0
  526. package/src/database/migrations/054_media_upload_attempts.ts +41 -0
  527. package/src/database/migrations/runner.ts +2 -0
  528. package/src/database/repositories/content.ts +2 -5
  529. package/src/database/repositories/media.ts +158 -28
  530. package/src/database/types.ts +10 -0
  531. package/src/emdash-runtime.ts +13 -0
  532. package/src/i18n/config.ts +17 -10
  533. package/src/index.ts +1 -0
  534. package/src/media/upload-attempts.ts +33 -0
  535. package/src/query.ts +29 -2
  536. package/src/storage/s3.ts +1 -1
  537. package/src/taxonomies/term-counts.ts +54 -24
  538. package/src/utils/hash.ts +14 -3
  539. package/dist/api-BkqFzIu4.mjs.map +0 -1
  540. package/dist/config-B6b_BobR.mjs.map +0 -1
  541. package/dist/content-BC43pICM.mjs.map +0 -1
  542. package/dist/dialect-helpers-BtOwo2us.mjs.map +0 -1
  543. package/dist/hash-DFFrkivP.mjs.map +0 -1
  544. package/dist/index-BZJ_5FOX.d.mts.map +0 -1
  545. package/dist/media-its1WLYv.mjs.map +0 -1
  546. package/dist/query-Bz9bWZfm.mjs.map +0 -1
  547. package/dist/redirects-D457qwt0.mjs.map +0 -1
  548. package/dist/relations-qKmSj9KM.d.mts.map +0 -1
  549. package/dist/runner-BdeLEgad.mjs.map +0 -1
  550. package/dist/taxonomies-BN6M9Nrv.mjs.map +0 -1
  551. package/dist/taxonomies-r7pqLBCn.mjs.map +0 -1
  552. package/dist/types-DPKhI5ib.d.mts.map +0 -1
  553. package/dist/version-BRMngh9I.mjs +0 -7
  554. /package/dist/{api-tokens-Cvmixds7.mjs → api-tokens-CEsW_jCg.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { t as Interceptor } from "../transport-Vd7VdqHv.mjs";
1
+ import { t as Interceptor } from "../transport-DokJOScS.mjs";
2
2
 
3
3
  //#region src/client/cf-access.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as tokenInterceptor, i as devBypassInterceptor, n as createTransport, r as csrfInterceptor, t as Interceptor } from "../transport-Vd7VdqHv.mjs";
1
+ import { a as tokenInterceptor, i as devBypassInterceptor, n as createTransport, r as csrfInterceptor, t as Interceptor } from "../transport-DokJOScS.mjs";
2
2
 
3
3
  //#region src/client/portable-text.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { i as portableTextToMarkdown, n as convertDataForWrite, r as markdownToPortableText, t as convertDataForRead } from "../portable-text-sDj92Kxv.mjs";
2
- import { a as tokenInterceptor, i as refreshInterceptor, n as csrfInterceptor, r as devBypassInterceptor, t as createTransport } from "../transport-CKrlqSpD.mjs";
1
+ import { i as portableTextToMarkdown, n as convertDataForWrite, r as markdownToPortableText, t as convertDataForRead } from "../portable-text-CNahFfj6.mjs";
2
+ import { a as tokenInterceptor, i as refreshInterceptor, n as csrfInterceptor, r as devBypassInterceptor, t as createTransport } from "../transport-BWQo0Foy.mjs";
3
3
  import mime from "mime/lite";
4
4
 
5
5
  //#region src/client/index.ts
@@ -1,5 +1,5 @@
1
1
  import { s as invalidateCommentObjectCache } from "./object-cache-Bok5j2ae.mjs";
2
- import { a as encodeCursor, i as decodeCursor } from "./types-CrRUFdej.mjs";
2
+ import { a as encodeCursor, i as decodeCursor } from "./types-XrQQ-Aex.mjs";
3
3
  import { sql } from "kysely";
4
4
  import { ulid } from "ulidx";
5
5
 
@@ -252,4 +252,4 @@ function safeJsonParse(value) {
252
252
 
253
253
  //#endregion
254
254
  export { CommentRepository as t };
255
- //# sourceMappingURL=comment-B9cUud8H.mjs.map
255
+ //# sourceMappingURL=comment-DnTxxVHv.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comment-B9cUud8H.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { invalidateCommentObjectCache } from \"../../object-cache/index.js\";\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\t/** Aggregate reaction counts (`{ like: 12 }`), attached when requested. */\n\treactions?: Record<string, number>;\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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}));\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-DnTxxVHv.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { invalidateCommentObjectCache } from \"../../object-cache/index.js\";\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\t/** Aggregate reaction counts (`{ like: 12 }`), attached when requested. */\n\treactions?: Record<string, number>;\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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\tinvalidateCommentObjectCache();\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}));\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,4 +1,4 @@
1
- import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-Bq-5DPrD.mjs";
1
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-D5dlPeRb.mjs";
2
2
  import { ulid } from "ulidx";
3
3
 
4
4
  //#region src/database/repositories/comment-reaction.ts
@@ -82,4 +82,4 @@ var CommentReactionRepository = class {
82
82
 
83
83
  //#endregion
84
84
  export { CommentReactionRepository as t };
85
- //# sourceMappingURL=comment-reaction-VUXO981F.mjs.map
85
+ //# sourceMappingURL=comment-reaction-BUlUz2bh.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comment-reaction-VUXO981F.mjs","names":[],"sources":["../src/database/repositories/comment-reaction.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { SQL_BATCH_SIZE, chunks } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\n\n/** Per-comment reaction counts: `{ like: 12, love: 3 }`. */\nexport type ReactionCounts = Record<string, number>;\n\nexport interface ToggleReactionInput {\n\tcommentId: string;\n\treaction: string;\n\tvoterHash: string;\n}\n\n/**\n * Repository for comment reactions (likes / emoji).\n *\n * Reactions are deduped per (comment, voter, reaction) by a unique index, so\n * a second toggle of the same reaction by the same voter removes it.\n */\nexport class CommentReactionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Toggle a reaction for a voter on a comment.\n\t *\n\t * @returns `{ reacted: true }` if the reaction was added, `{ reacted: false }`\n\t * if an existing reaction was removed.\n\t */\n\tasync toggle(input: ToggleReactionInput): Promise<{ reacted: boolean }> {\n\t\t// Idempotent add: insert, or no-op if this voter already has this\n\t\t// reaction. Using ON CONFLICT (rather than read-then-write) avoids a\n\t\t// TOCTOU race where two concurrent toggles both see \"no row\" and the\n\t\t// second INSERT violates the unique index — the old path surfaced that\n\t\t// as a 500. The unique index doubles as the conflict target.\n\t\tconst inserted = await this.db\n\t\t\t.insertInto(\"_emdash_comment_reactions\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tcomment_id: input.commentId,\n\t\t\t\treaction: input.reaction,\n\t\t\t\tvoter_hash: input.voterHash,\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.onConflict((oc) => oc.columns([\"comment_id\", \"voter_hash\", \"reaction\"]).doNothing())\n\t\t\t.executeTakeFirst();\n\n\t\tif ((inserted.numInsertedOrUpdatedRows ?? 0n) > 0n) {\n\t\t\treturn { reacted: true };\n\t\t}\n\n\t\t// The reaction already existed → toggle it off.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_comment_reactions\")\n\t\t\t.where(\"comment_id\", \"=\", input.commentId)\n\t\t\t.where(\"voter_hash\", \"=\", input.voterHash)\n\t\t\t.where(\"reaction\", \"=\", input.reaction)\n\t\t\t.execute();\n\t\treturn { reacted: false };\n\t}\n\n\t/**\n\t * Aggregate reaction counts for a set of comments.\n\t *\n\t * @returns a Map keyed by comment id; comments with no reactions are absent.\n\t */\n\tasync countsForComments(commentIds: string[]): Promise<Map<string, ReactionCounts>> {\n\t\tconst result = new Map<string, ReactionCounts>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.groupBy([\"comment_id\", \"reaction\"])\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst counts = result.get(row.comment_id) ?? {};\n\t\t\t\tcounts[row.reaction] = Number(row.count);\n\t\t\t\tresult.set(row.comment_id, counts);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Which reactions a given voter has set, per comment.\n\t *\n\t * @returns a Map keyed by comment id whose values are the reaction names the\n\t * voter has active on that comment.\n\t */\n\tasync viewerReactions(commentIds: string[], voterHash: string): Promise<Map<string, string[]>> {\n\t\tconst result = new Map<string, string[]>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst list = result.get(row.comment_id) ?? [];\n\t\t\t\tlist.push(row.reaction);\n\t\t\t\tresult.set(row.comment_id, list);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Count a voter's reactions within a recent time window (for rate limiting).\n\t */\n\tasync countRecentByVoter(voterHash: string, windowMinutes = 10): Promise<number> {\n\t\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t.where(\"created_at\", \">\", cutoff)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n}\n"],"mappings":";;;;;;;;;;AAqBA,IAAa,4BAAb,MAAuC;CACtC,YAAY,AAAQ,IAAsB;EAAtB;;;;;;;;CAQpB,MAAM,OAAO,OAA2D;AAkBvE,QAZiB,MAAM,KAAK,GAC1B,WAAW,4BAA4B,CACvC,OAAO;GACP,IAAI,MAAM;GACV,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,YAAY,MAAM;GAClB,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,CAAC,CACD,YAAY,OAAO,GAAG,QAAQ;GAAC;GAAc;GAAc;GAAW,CAAC,CAAC,WAAW,CAAC,CACpF,kBAAkB,EAEN,4BAA4B,MAAM,GAC/C,QAAO,EAAE,SAAS,MAAM;AAIzB,QAAM,KAAK,GACT,WAAW,4BAA4B,CACvC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,YAAY,KAAK,MAAM,SAAS,CACtC,SAAS;AACX,SAAO,EAAE,SAAS,OAAO;;;;;;;CAQ1B,MAAM,kBAAkB,YAA4D;EACnF,MAAM,yBAAS,IAAI,KAA6B;AAChD,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,MAAM,MAAM,CAChC,QAAQ,CAAC,cAAc,WAAW,CAAC,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,SAAS,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC/C,WAAO,IAAI,YAAY,OAAO,IAAI,MAAM;AACxC,WAAO,IAAI,IAAI,YAAY,OAAO;;;AAIpC,SAAO;;;;;;;;CASR,MAAM,gBAAgB,YAAsB,WAAmD;EAC9F,MAAM,yBAAS,IAAI,KAAuB;AAC1C,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,MAAM,cAAc,MAAM,MAAM,CAChC,MAAM,cAAc,KAAK,UAAU,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,OAAO,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC7C,SAAK,KAAK,IAAI,SAAS;AACvB,WAAO,IAAI,IAAI,YAAY,KAAK;;;AAIlC,SAAO;;;;;CAMR,MAAM,mBAAmB,WAAmB,gBAAgB,IAAqB;EAChF,MAAM,0BAAS,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,KAAK,IAAK,EAAC,aAAa;EAC7E,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,4BAA4B,CACvC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,KAAK,UAAU,CACnC,MAAM,cAAc,KAAK,OAAO,CAChC,kBAAkB;AACpB,SAAO,OAAO,QAAQ,SAAS,EAAE"}
1
+ {"version":3,"file":"comment-reaction-BUlUz2bh.mjs","names":[],"sources":["../src/database/repositories/comment-reaction.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { SQL_BATCH_SIZE, chunks } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\n\n/** Per-comment reaction counts: `{ like: 12, love: 3 }`. */\nexport type ReactionCounts = Record<string, number>;\n\nexport interface ToggleReactionInput {\n\tcommentId: string;\n\treaction: string;\n\tvoterHash: string;\n}\n\n/**\n * Repository for comment reactions (likes / emoji).\n *\n * Reactions are deduped per (comment, voter, reaction) by a unique index, so\n * a second toggle of the same reaction by the same voter removes it.\n */\nexport class CommentReactionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Toggle a reaction for a voter on a comment.\n\t *\n\t * @returns `{ reacted: true }` if the reaction was added, `{ reacted: false }`\n\t * if an existing reaction was removed.\n\t */\n\tasync toggle(input: ToggleReactionInput): Promise<{ reacted: boolean }> {\n\t\t// Idempotent add: insert, or no-op if this voter already has this\n\t\t// reaction. Using ON CONFLICT (rather than read-then-write) avoids a\n\t\t// TOCTOU race where two concurrent toggles both see \"no row\" and the\n\t\t// second INSERT violates the unique index — the old path surfaced that\n\t\t// as a 500. The unique index doubles as the conflict target.\n\t\tconst inserted = await this.db\n\t\t\t.insertInto(\"_emdash_comment_reactions\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tcomment_id: input.commentId,\n\t\t\t\treaction: input.reaction,\n\t\t\t\tvoter_hash: input.voterHash,\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.onConflict((oc) => oc.columns([\"comment_id\", \"voter_hash\", \"reaction\"]).doNothing())\n\t\t\t.executeTakeFirst();\n\n\t\tif ((inserted.numInsertedOrUpdatedRows ?? 0n) > 0n) {\n\t\t\treturn { reacted: true };\n\t\t}\n\n\t\t// The reaction already existed → toggle it off.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_comment_reactions\")\n\t\t\t.where(\"comment_id\", \"=\", input.commentId)\n\t\t\t.where(\"voter_hash\", \"=\", input.voterHash)\n\t\t\t.where(\"reaction\", \"=\", input.reaction)\n\t\t\t.execute();\n\t\treturn { reacted: false };\n\t}\n\n\t/**\n\t * Aggregate reaction counts for a set of comments.\n\t *\n\t * @returns a Map keyed by comment id; comments with no reactions are absent.\n\t */\n\tasync countsForComments(commentIds: string[]): Promise<Map<string, ReactionCounts>> {\n\t\tconst result = new Map<string, ReactionCounts>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.groupBy([\"comment_id\", \"reaction\"])\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst counts = result.get(row.comment_id) ?? {};\n\t\t\t\tcounts[row.reaction] = Number(row.count);\n\t\t\t\tresult.set(row.comment_id, counts);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Which reactions a given voter has set, per comment.\n\t *\n\t * @returns a Map keyed by comment id whose values are the reaction names the\n\t * voter has active on that comment.\n\t */\n\tasync viewerReactions(commentIds: string[], voterHash: string): Promise<Map<string, string[]>> {\n\t\tconst result = new Map<string, string[]>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst list = result.get(row.comment_id) ?? [];\n\t\t\t\tlist.push(row.reaction);\n\t\t\t\tresult.set(row.comment_id, list);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Count a voter's reactions within a recent time window (for rate limiting).\n\t */\n\tasync countRecentByVoter(voterHash: string, windowMinutes = 10): Promise<number> {\n\t\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t.where(\"created_at\", \">\", cutoff)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n}\n"],"mappings":";;;;;;;;;;AAqBA,IAAa,4BAAb,MAAuC;CACtC,YAAY,AAAQ,IAAsB;EAAtB;;;;;;;;CAQpB,MAAM,OAAO,OAA2D;AAkBvE,QAZiB,MAAM,KAAK,GAC1B,WAAW,4BAA4B,CACvC,OAAO;GACP,IAAI,MAAM;GACV,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,YAAY,MAAM;GAClB,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,CAAC,CACD,YAAY,OAAO,GAAG,QAAQ;GAAC;GAAc;GAAc;GAAW,CAAC,CAAC,WAAW,CAAC,CACpF,kBAAkB,EAEN,4BAA4B,MAAM,GAC/C,QAAO,EAAE,SAAS,MAAM;AAIzB,QAAM,KAAK,GACT,WAAW,4BAA4B,CACvC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,YAAY,KAAK,MAAM,SAAS,CACtC,SAAS;AACX,SAAO,EAAE,SAAS,OAAO;;;;;;;CAQ1B,MAAM,kBAAkB,YAA4D;EACnF,MAAM,yBAAS,IAAI,KAA6B;AAChD,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,MAAM,MAAM,CAChC,QAAQ,CAAC,cAAc,WAAW,CAAC,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,SAAS,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC/C,WAAO,IAAI,YAAY,OAAO,IAAI,MAAM;AACxC,WAAO,IAAI,IAAI,YAAY,OAAO;;;AAIpC,SAAO;;;;;;;;CASR,MAAM,gBAAgB,YAAsB,WAAmD;EAC9F,MAAM,yBAAS,IAAI,KAAuB;AAC1C,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,MAAM,cAAc,MAAM,MAAM,CAChC,MAAM,cAAc,KAAK,UAAU,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,OAAO,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC7C,SAAK,KAAK,IAAI,SAAS;AACvB,WAAO,IAAI,IAAI,YAAY,KAAK;;;AAIlC,SAAO;;;;;CAMR,MAAM,mBAAmB,WAAmB,gBAAgB,IAAqB;EAChF,MAAM,0BAAS,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,KAAK,IAAK,EAAC,aAAa;EAC7E,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,4BAA4B,CACvC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,KAAK,UAAU,CACnC,MAAM,cAAc,KAAK,OAAO,CAChC,kBAAkB;AACpB,SAAO,OAAO,QAAQ,SAAS,EAAE"}
@@ -1,5 +1,5 @@
1
- import { n as InvalidCursorError } from "./types-CrRUFdej.mjs";
2
- import { t as CommentRepository } from "./comment-B9cUud8H.mjs";
1
+ import { n as InvalidCursorError } from "./types-XrQQ-Aex.mjs";
2
+ import { t as CommentRepository } from "./comment-DnTxxVHv.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-PXj1E9Si.mjs.map
204
+ //# sourceMappingURL=comments-QPKgK3nk.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comments-PXj1E9Si.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-QPKgK3nk.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,29 +1,33 @@
1
1
  //#region src/i18n/config.ts
2
- let _config;
2
+ const I18N_CONFIG_KEY = Symbol.for("emdash:i18n-config");
3
+ function configStore() {
4
+ return globalThis;
5
+ }
3
6
  /**
4
7
  * Initialize i18n config from virtual module data.
5
8
  * Called during runtime initialization.
6
9
  */
7
10
  function setI18nConfig(config) {
8
- _config = config;
11
+ configStore()[I18N_CONFIG_KEY] = config;
9
12
  }
10
13
  /**
11
14
  * Get the current i18n config.
12
15
  * Returns null if i18n is not configured.
13
16
  */
14
17
  function getI18nConfig() {
15
- return _config ?? null;
18
+ return configStore()[I18N_CONFIG_KEY] ?? null;
16
19
  }
17
20
  /** Match a locale to the exact casing used by the site configuration. */
18
21
  function resolveConfiguredLocale(locale) {
19
- return _config?.locales.find((configured) => configured.toLowerCase() === locale.toLowerCase()) ?? locale;
22
+ return getI18nConfig()?.locales.find((configured) => configured.toLowerCase() === locale.toLowerCase()) ?? locale;
20
23
  }
21
24
  /**
22
25
  * Check if i18n is enabled.
23
26
  * Returns true when multiple locales are configured.
24
27
  */
25
28
  function isI18nEnabled() {
26
- return _config != null && _config.locales.length > 1;
29
+ const config = getI18nConfig();
30
+ return config != null && config.locales.length > 1;
27
31
  }
28
32
  /**
29
33
  * Resolve fallback locale chain for a given locale.
@@ -31,21 +35,22 @@ function isI18nEnabled() {
31
35
  * Always ends with defaultLocale.
32
36
  */
33
37
  function getFallbackChain(locale) {
34
- if (!_config) return [locale];
38
+ const config = getI18nConfig();
39
+ if (!config) return [locale];
35
40
  const chain = [locale];
36
41
  let current = locale;
37
42
  const visited = new Set([locale]);
38
- while (_config.fallback?.[current]) {
39
- const next = _config.fallback[current];
43
+ while (config.fallback?.[current]) {
44
+ const next = config.fallback[current];
40
45
  if (visited.has(next)) break;
41
46
  chain.push(next);
42
47
  visited.add(next);
43
48
  current = next;
44
49
  }
45
- if (!visited.has(_config.defaultLocale)) chain.push(_config.defaultLocale);
50
+ if (!visited.has(config.defaultLocale)) chain.push(config.defaultLocale);
46
51
  return chain;
47
52
  }
48
53
 
49
54
  //#endregion
50
55
  export { setI18nConfig as a, resolveConfiguredLocale as i, getI18nConfig as n, isI18nEnabled as r, getFallbackChain as t };
51
- //# sourceMappingURL=config-B6b_BobR.mjs.map
56
+ //# sourceMappingURL=config-DyF3OKg_.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-DyF3OKg_.mjs","names":[],"sources":["../src/i18n/config.ts"],"sourcesContent":["/**\n * EmDash i18n Configuration\n *\n * Reads locale configuration from the virtual module (sourced from Astro config).\n * Initialized during runtime startup, then available via getI18nConfig().\n */\n\nexport interface I18nConfig {\n\tdefaultLocale: string;\n\tlocales: string[];\n\tfallback?: Record<string, string>;\n\tprefixDefaultLocale?: boolean;\n}\n\nconst I18N_CONFIG_KEY = Symbol.for(\"emdash:i18n-config\");\n\nfunction configStore(): Record<symbol, I18nConfig | null | undefined> {\n\treturn globalThis;\n}\n\n/**\n * Initialize i18n config from virtual module data.\n * Called during runtime initialization.\n */\nexport function setI18nConfig(config: I18nConfig | null): void {\n\tconfigStore()[I18N_CONFIG_KEY] = config;\n}\n\n/**\n * Get the current i18n config.\n * Returns null if i18n is not configured.\n */\nexport function getI18nConfig(): I18nConfig | null {\n\treturn configStore()[I18N_CONFIG_KEY] ?? null;\n}\n\n/** Match a locale to the exact casing used by the site configuration. */\nexport function resolveConfiguredLocale(locale: string): string {\n\tconst config = getI18nConfig();\n\treturn (\n\t\tconfig?.locales.find((configured) => configured.toLowerCase() === locale.toLowerCase()) ??\n\t\tlocale\n\t);\n}\n\n/**\n * Check if i18n is enabled.\n * Returns true when multiple locales are configured.\n */\nexport function isI18nEnabled(): boolean {\n\tconst config = getI18nConfig();\n\treturn config != null && config.locales.length > 1;\n}\n\n/**\n * Resolve fallback locale chain for a given locale.\n * Returns array of locales to try, from most preferred to least.\n * Always ends with defaultLocale.\n */\nexport function getFallbackChain(locale: string): string[] {\n\tconst config = getI18nConfig();\n\tif (!config) return [locale];\n\n\tconst chain: string[] = [locale];\n\tlet current = locale;\n\tconst visited = new Set<string>([locale]);\n\n\twhile (config.fallback?.[current]) {\n\t\t// eslint-disable-next-line typescript/no-unnecessary-type-assertion -- noUncheckedIndexedAccess\n\t\tconst next = config.fallback[current]!;\n\t\tif (visited.has(next)) break; // prevent cycles\n\t\tchain.push(next);\n\t\tvisited.add(next);\n\t\tcurrent = next;\n\t}\n\n\t// Always end with defaultLocale if not already in chain\n\tif (!visited.has(config.defaultLocale)) {\n\t\tchain.push(config.defaultLocale);\n\t}\n\n\treturn chain;\n}\n"],"mappings":";AAcA,MAAM,kBAAkB,OAAO,IAAI,qBAAqB;AAExD,SAAS,cAA6D;AACrE,QAAO;;;;;;AAOR,SAAgB,cAAc,QAAiC;AAC9D,cAAa,CAAC,mBAAmB;;;;;;AAOlC,SAAgB,gBAAmC;AAClD,QAAO,aAAa,CAAC,oBAAoB;;;AAI1C,SAAgB,wBAAwB,QAAwB;AAE/D,QADe,eAAe,EAErB,QAAQ,MAAM,eAAe,WAAW,aAAa,KAAK,OAAO,aAAa,CAAC,IACvF;;;;;;AAQF,SAAgB,gBAAyB;CACxC,MAAM,SAAS,eAAe;AAC9B,QAAO,UAAU,QAAQ,OAAO,QAAQ,SAAS;;;;;;;AAQlD,SAAgB,iBAAiB,QAA0B;CAC1D,MAAM,SAAS,eAAe;AAC9B,KAAI,CAAC,OAAQ,QAAO,CAAC,OAAO;CAE5B,MAAM,QAAkB,CAAC,OAAO;CAChC,IAAI,UAAU;CACd,MAAM,UAAU,IAAI,IAAY,CAAC,OAAO,CAAC;AAEzC,QAAO,OAAO,WAAW,UAAU;EAElC,MAAM,OAAO,OAAO,SAAS;AAC7B,MAAI,QAAQ,IAAI,KAAK,CAAE;AACvB,QAAM,KAAK,KAAK;AAChB,UAAQ,IAAI,KAAK;AACjB,YAAU;;AAIX,KAAI,CAAC,QAAQ,IAAI,OAAO,cAAc,CACrC,OAAM,KAAK,OAAO,cAAc;AAGjC,QAAO"}
@@ -1,10 +1,10 @@
1
- import { o as __exportAll } from "./runner-BdeLEgad.mjs";
1
+ import { o as __exportAll } from "./runner-BsI18UgP.mjs";
2
2
  import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
3
3
  import { o as invalidateCollectionCache } from "./object-cache-Bok5j2ae.mjs";
4
- import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-Bq-5DPrD.mjs";
4
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-D5dlPeRb.mjs";
5
5
  import { n as isMissingTableError } from "./db-errors-CcWLaRiR.mjs";
6
6
  import { n as slugify } from "./slugify-C_tqlU4G.mjs";
7
- import { a as encodeCursor, i as decodeCursor, r as ScheduledNotDueError, t as EmDashValidationError } from "./types-CrRUFdej.mjs";
7
+ import { a as encodeCursor, i as decodeCursor, r as ScheduledNotDueError, t as EmDashValidationError } from "./types-XrQQ-Aex.mjs";
8
8
  import { sql } from "kysely";
9
9
  import { monotonicFactory, ulid } from "ulidx";
10
10
 
@@ -353,7 +353,8 @@ var ContentRepository = class {
353
353
  slug,
354
354
  data: newData,
355
355
  status: "draft",
356
- authorId: authorId || original.authorId || void 0
356
+ authorId: authorId || original.authorId || void 0,
357
+ locale: original.locale ?? void 0
357
358
  });
358
359
  }
359
360
  /**
@@ -508,11 +509,12 @@ var ContentRepository = class {
508
509
  updates[key] = serializeValue(value);
509
510
  }
510
511
  }
511
- if (Object.keys(updates).length > 0) updates.updated_at = now;
512
+ const hasColumnWrites = Object.keys(updates).length > 0;
513
+ if (hasColumnWrites) updates.updated_at = now;
512
514
  updates.version = sql`version + 1`;
513
515
  await this.db.updateTable(tableName).set(updates).where("id", "=", id).where("deleted_at", "is", null).execute();
514
516
  if (input.status !== void 0 || input.publishedAt !== void 0 || input.scheduledAt !== void 0) await this.restampEntryPivot(type, id);
515
- invalidateCollectionCache(type);
517
+ if (hasColumnWrites) invalidateCollectionCache(type);
516
518
  const updated = await this.findById(type, id);
517
519
  if (!updated) throw new Error("Content not found");
518
520
  return updated;
@@ -1091,7 +1093,6 @@ var ContentRepository = class {
1091
1093
  WHERE id = ${id}
1092
1094
  AND deleted_at IS NULL
1093
1095
  `.execute(this.db);
1094
- invalidateCollectionCache(type);
1095
1096
  }
1096
1097
  /**
1097
1098
  * Discard pending draft changes
@@ -1110,7 +1111,6 @@ var ContentRepository = class {
1110
1111
  WHERE id = ${id}
1111
1112
  AND deleted_at IS NULL
1112
1113
  `.execute(this.db);
1113
- invalidateCollectionCache(type);
1114
1114
  const updated = await this.findById(type, id);
1115
1115
  if (!updated) throw new Error("Content not found");
1116
1116
  return updated;
@@ -1197,4 +1197,4 @@ var ContentRepository = class {
1197
1197
 
1198
1198
  //#endregion
1199
1199
  export { content_exports as n, RevisionRepository as r, ContentRepository as t };
1200
- //# sourceMappingURL=content-BC43pICM.mjs.map
1200
+ //# sourceMappingURL=content-CpfKV9QE.mjs.map