dineway 0.1.3 → 0.1.5

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 (296) hide show
  1. package/README.md +6 -3
  2. package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
  3. package/dist/astro/index.d.mts +18 -9
  4. package/dist/astro/index.mjs +238 -16
  5. package/dist/astro/middleware/auth.d.mts +16 -5
  6. package/dist/astro/middleware/auth.mjs +74 -37
  7. package/dist/astro/middleware/redirect.mjs +24 -8
  8. package/dist/astro/middleware/request-context.mjs +18 -5
  9. package/dist/astro/middleware/setup.mjs +1 -1
  10. package/dist/astro/middleware.mjs +411 -169
  11. package/dist/astro/types.d.mts +25 -8
  12. package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
  13. package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
  14. package/dist/cache-BdSY-gQN.mjs +42 -0
  15. package/dist/chunks--4F8ddV4.mjs +18 -0
  16. package/dist/cli/index.mjs +935 -15
  17. package/dist/client/external-auth-headers.d.mts +1 -1
  18. package/dist/client/index.d.mts +11 -3
  19. package/dist/client/index.mjs +4 -3
  20. package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
  21. package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
  22. package/dist/database/instrumentation.d.mts +34 -0
  23. package/dist/database/instrumentation.mjs +53 -0
  24. package/dist/db/index.d.mts +3 -3
  25. package/dist/db/index.mjs +2 -2
  26. package/dist/db/libsql.d.mts +1 -1
  27. package/dist/db/libsql.mjs +11 -5
  28. package/dist/db/postgres.d.mts +1 -1
  29. package/dist/db/sqlite.d.mts +1 -1
  30. package/dist/db/sqlite.mjs +7 -1
  31. package/dist/db-errors-CEqD7qH9.mjs +23 -0
  32. package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
  33. package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
  34. package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
  35. package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
  36. package/dist/index.d.mts +11 -11
  37. package/dist/index.mjs +24 -22
  38. package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
  39. package/dist/media/index.d.mts +1 -1
  40. package/dist/media/index.mjs +1 -1
  41. package/dist/media/local-runtime.d.mts +7 -7
  42. package/dist/page/index.d.mts +10 -2
  43. package/dist/page/index.mjs +22 -1
  44. package/dist/patterns-CrCYkMBb.mjs +92 -0
  45. package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
  46. package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
  47. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  48. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  49. package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
  50. package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
  51. package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
  52. package/dist/request-cache-Dk5qPSOx.mjs +66 -0
  53. package/dist/request-context.d.mts +4 -16
  54. package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
  55. package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
  56. package/dist/runtime.d.mts +6 -6
  57. package/dist/runtime.mjs +2 -2
  58. package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
  59. package/dist/seed/index.d.mts +2 -2
  60. package/dist/seed/index.mjs +11 -10
  61. package/dist/seo/index.d.mts +1 -1
  62. package/dist/storage/local.d.mts +1 -1
  63. package/dist/storage/local.mjs +1 -1
  64. package/dist/storage/s3.d.mts +11 -3
  65. package/dist/storage/s3.mjs +78 -15
  66. package/dist/taxonomies-1s5PaS_8.mjs +266 -0
  67. package/dist/transaction-Cn2rjY78.mjs +27 -0
  68. package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
  69. package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
  70. package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
  71. package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
  72. package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
  73. package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
  74. package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
  75. package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
  76. package/dist/version-BKXPsfmJ.mjs +6 -0
  77. package/package.json +53 -39
  78. package/src/astro/routes/PluginRegistry.tsx +21 -0
  79. package/src/astro/routes/admin.astro +99 -0
  80. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  81. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  82. package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
  83. package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
  84. package/src/astro/routes/api/admin/briefing.ts +76 -0
  85. package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
  86. package/src/astro/routes/api/admin/bylines/index.ts +74 -0
  87. package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
  88. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  89. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  90. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  91. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  92. package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
  93. package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
  94. package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
  95. package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
  96. package/src/astro/routes/api/admin/context/diff.ts +35 -0
  97. package/src/astro/routes/api/admin/context/index.ts +69 -0
  98. package/src/astro/routes/api/admin/context/stale.ts +35 -0
  99. package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
  100. package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
  101. package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
  102. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
  103. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  104. package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
  105. package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
  106. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
  107. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
  108. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  109. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
  110. package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
  111. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  112. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
  113. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  114. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
  115. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  116. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  117. package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
  118. package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
  119. package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
  120. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  121. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
  122. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  123. package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
  124. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  125. package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
  126. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  127. package/src/astro/routes/api/admin/users/index.ts +66 -0
  128. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  129. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  130. package/src/astro/routes/api/auth/invite/complete.ts +86 -0
  131. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  132. package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
  133. package/src/astro/routes/api/auth/logout.ts +40 -0
  134. package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
  135. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  136. package/src/astro/routes/api/auth/me.ts +60 -0
  137. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
  138. package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
  139. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  140. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  141. package/src/astro/routes/api/auth/passkey/options.ts +85 -0
  142. package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
  143. package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
  144. package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
  145. package/src/astro/routes/api/auth/signup/complete.ts +87 -0
  146. package/src/astro/routes/api/auth/signup/request.ts +89 -0
  147. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  148. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
  149. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  150. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
  151. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
  152. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
  153. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  154. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
  155. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
  156. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  157. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
  158. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
  159. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
  160. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
  161. package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
  162. package/src/astro/routes/api/content/[collection]/index.ts +103 -0
  163. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  164. package/src/astro/routes/api/dashboard.ts +32 -0
  165. package/src/astro/routes/api/dev/emails.ts +36 -0
  166. package/src/astro/routes/api/health.ts +54 -0
  167. package/src/astro/routes/api/import/probe.ts +47 -0
  168. package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
  169. package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
  170. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  171. package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
  172. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
  173. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  174. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  175. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
  176. package/src/astro/routes/api/manifest.ts +75 -0
  177. package/src/astro/routes/api/mcp.ts +125 -0
  178. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  179. package/src/astro/routes/api/media/[id].ts +145 -0
  180. package/src/astro/routes/api/media/file/[...key].ts +79 -0
  181. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
  182. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  183. package/src/astro/routes/api/media/providers/index.ts +30 -0
  184. package/src/astro/routes/api/media/upload-url.ts +146 -0
  185. package/src/astro/routes/api/media.ts +204 -0
  186. package/src/astro/routes/api/menus/[name]/items.ts +206 -0
  187. package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
  188. package/src/astro/routes/api/menus/[name].ts +145 -0
  189. package/src/astro/routes/api/menus/index.ts +91 -0
  190. package/src/astro/routes/api/oauth/authorize.ts +430 -0
  191. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  192. package/src/astro/routes/api/oauth/device/code.ts +56 -0
  193. package/src/astro/routes/api/oauth/device/token.ts +70 -0
  194. package/src/astro/routes/api/oauth/register.ts +182 -0
  195. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  196. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  197. package/src/astro/routes/api/oauth/token.ts +195 -0
  198. package/src/astro/routes/api/openapi.json.ts +33 -0
  199. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
  200. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  201. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  202. package/src/astro/routes/api/redirects/[id].ts +183 -0
  203. package/src/astro/routes/api/redirects/index.ts +100 -0
  204. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  205. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
  206. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
  207. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
  208. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
  209. package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
  210. package/src/astro/routes/api/schema/collections/index.ts +61 -0
  211. package/src/astro/routes/api/schema/index.ts +109 -0
  212. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  213. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  214. package/src/astro/routes/api/search/enable.ts +64 -0
  215. package/src/astro/routes/api/search/index.ts +52 -0
  216. package/src/astro/routes/api/search/rebuild.ts +72 -0
  217. package/src/astro/routes/api/search/stats.ts +35 -0
  218. package/src/astro/routes/api/search/suggest.ts +50 -0
  219. package/src/astro/routes/api/sections/[slug].ts +203 -0
  220. package/src/astro/routes/api/sections/index.ts +107 -0
  221. package/src/astro/routes/api/settings/email.ts +150 -0
  222. package/src/astro/routes/api/settings.ts +116 -0
  223. package/src/astro/routes/api/setup/admin-verify.ts +122 -0
  224. package/src/astro/routes/api/setup/admin.ts +104 -0
  225. package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
  226. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  227. package/src/astro/routes/api/setup/index.ts +128 -0
  228. package/src/astro/routes/api/setup/status.ts +122 -0
  229. package/src/astro/routes/api/snapshot.ts +76 -0
  230. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
  231. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
  232. package/src/astro/routes/api/taxonomies/index.ts +114 -0
  233. package/src/astro/routes/api/themes/preview.ts +78 -0
  234. package/src/astro/routes/api/typegen.ts +114 -0
  235. package/src/astro/routes/api/well-known/auth.ts +71 -0
  236. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
  237. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
  238. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
  239. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
  240. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
  241. package/src/astro/routes/api/widget-areas/[name].ts +135 -0
  242. package/src/astro/routes/api/widget-areas/index.ts +149 -0
  243. package/src/astro/routes/api/widget-components.ts +22 -0
  244. package/src/astro/routes/robots.txt.ts +81 -0
  245. package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
  246. package/src/astro/routes/sitemap.xml.ts +92 -0
  247. package/src/components/Break.astro +45 -0
  248. package/src/components/Button.astro +71 -0
  249. package/src/components/Buttons.astro +49 -0
  250. package/src/components/Code.astro +59 -0
  251. package/src/components/Columns.astro +59 -0
  252. package/src/components/CommentForm.astro +315 -0
  253. package/src/components/Comments.astro +232 -0
  254. package/src/components/Cover.astro +128 -0
  255. package/src/components/DinewayBodyEnd.astro +32 -0
  256. package/src/components/DinewayBodyStart.astro +32 -0
  257. package/src/components/DinewayHead.astro +61 -0
  258. package/src/components/DinewayImage.astro +178 -0
  259. package/src/components/DinewayMedia.astro +167 -0
  260. package/src/components/Embed.astro +128 -0
  261. package/src/components/File.astro +122 -0
  262. package/src/components/Gallery.astro +93 -0
  263. package/src/components/HtmlBlock.astro +33 -0
  264. package/src/components/Image.astro +178 -0
  265. package/src/components/InlineEditor.astro +27 -0
  266. package/src/components/InlinePortableTextEditor.tsx +1937 -0
  267. package/src/components/LiveSearch.astro +614 -0
  268. package/src/components/PortableText.astro +51 -0
  269. package/src/components/Pullquote.astro +51 -0
  270. package/src/components/Table.astro +135 -0
  271. package/src/components/WidgetArea.astro +22 -0
  272. package/src/components/WidgetRenderer.astro +72 -0
  273. package/src/components/index.ts +106 -0
  274. package/src/components/marks/Link.astro +31 -0
  275. package/src/components/marks/StrikeThrough.astro +7 -0
  276. package/src/components/marks/Subscript.astro +7 -0
  277. package/src/components/marks/Superscript.astro +7 -0
  278. package/src/components/marks/Underline.astro +7 -0
  279. package/src/components/marks.ts +19 -0
  280. package/src/components/widgets/Archives.astro +65 -0
  281. package/src/components/widgets/Categories.astro +35 -0
  282. package/src/components/widgets/RecentPosts.astro +51 -0
  283. package/src/components/widgets/Search.astro +18 -0
  284. package/src/components/widgets/Tags.astro +38 -0
  285. package/src/ui.ts +75 -0
  286. package/LICENSE +0 -9
  287. /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
  288. /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
  289. /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
  290. /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
  291. /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
  292. /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
  293. /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
  294. /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
  295. /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
  296. /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Public comment endpoints
3
+ *
4
+ * GET /_dineway/api/comments/:collection/:contentId - List approved comments
5
+ * POST /_dineway/api/comments/:collection/:contentId - Submit a comment
6
+ */
7
+
8
+ import type { APIRoute } from "astro";
9
+
10
+ import { apiError, apiSuccess, handleError, requireDb, unwrapResult } from "#api/error.js";
11
+ import { handleCommentList, checkRateLimit, hashIp } from "#api/handlers/comments.js";
12
+ import { isParseError, parseBody } from "#api/parse.js";
13
+ import { createCommentBody } from "#api/schemas.js";
14
+ import { getSiteBaseUrl } from "#api/site-url.js";
15
+ import { sendCommentNotification } from "#comments/notifications.js";
16
+ import { createComment, type CommentHookRunner } from "#comments/service.js";
17
+ import { CommentRepository } from "#db/repositories/comment.js";
18
+ import { validateIdentifier } from "#db/validate.js";
19
+ import { extractRequestMeta } from "#plugins/request-meta.js";
20
+ import type { CollectionCommentSettings, ModerationDecision } from "#plugins/types.js";
21
+
22
+ export const prerender = false;
23
+
24
+ /**
25
+ * List approved comments for a content item (public, no auth required)
26
+ */
27
+ export const GET: APIRoute = async ({ params, url, locals }) => {
28
+ const { dineway } = locals;
29
+ const { collection, contentId } = params;
30
+
31
+ if (!collection || !contentId) {
32
+ return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
33
+ }
34
+
35
+ const dbErr = requireDb(dineway?.db);
36
+ if (dbErr) return dbErr;
37
+
38
+ try {
39
+ const limit = Math.min(Number(url.searchParams.get("limit") || 50), 100);
40
+ const cursor = url.searchParams.get("cursor") ?? undefined;
41
+ const threaded = url.searchParams.get("threaded") === "true";
42
+
43
+ // Check collection exists and has comments enabled
44
+ const collectionRow = await dineway.db
45
+ .selectFrom("_dineway_collections")
46
+ .select(["comments_enabled"])
47
+ .where("slug", "=", collection)
48
+ .executeTakeFirst();
49
+
50
+ if (!collectionRow) {
51
+ return apiError("NOT_FOUND", `Collection '${collection}' not found`, 404);
52
+ }
53
+
54
+ if (!collectionRow.comments_enabled) {
55
+ return apiError("COMMENTS_DISABLED", "Comments are not enabled for this collection", 403);
56
+ }
57
+
58
+ const result = await handleCommentList(dineway.db, collection, contentId, {
59
+ limit,
60
+ cursor,
61
+ threaded,
62
+ });
63
+
64
+ return unwrapResult(result);
65
+ } catch (error) {
66
+ return handleError(error, "Failed to list comments", "COMMENT_LIST_ERROR");
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Submit a comment (public, gated by anti-spam checks)
72
+ */
73
+ export const POST: APIRoute = async ({ params, request, locals }) => {
74
+ const { dineway, user } = locals;
75
+ const { collection, contentId } = params;
76
+
77
+ if (!collection || !contentId) {
78
+ return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
79
+ }
80
+
81
+ const dbErr = requireDb(dineway?.db);
82
+ if (dbErr) return dbErr;
83
+
84
+ try {
85
+ // Parse and validate input
86
+ const body = await parseBody(request, createCommentBody);
87
+ if (isParseError(body)) return body;
88
+
89
+ // Check collection exists and has comments enabled
90
+ const collectionRow = await dineway.db
91
+ .selectFrom("_dineway_collections")
92
+ .select([
93
+ "comments_enabled",
94
+ "comments_moderation",
95
+ "comments_closed_after_days",
96
+ "comments_auto_approve_users",
97
+ ])
98
+ .where("slug", "=", collection)
99
+ .executeTakeFirst();
100
+
101
+ if (!collectionRow) {
102
+ return apiError("NOT_FOUND", `Collection '${collection}' not found`, 404);
103
+ }
104
+
105
+ if (!collectionRow.comments_enabled) {
106
+ return apiError("COMMENTS_DISABLED", "Comments are not enabled for this collection", 403);
107
+ }
108
+
109
+ // Verify the content item exists, is published, and not soft-deleted
110
+ validateIdentifier(collection, "collection");
111
+ const contentRow = await dineway.db
112
+ .selectFrom(`ec_${collection}` as never)
113
+ .select(["id" as never, "slug" as never, "author_id" as never, "published_at" as never])
114
+ .where("id" as never, "=", contentId as never)
115
+ .where("status" as never, "=", "published" as never)
116
+ .where("deleted_at" as never, "is", null as never)
117
+ .executeTakeFirst();
118
+
119
+ if (!contentRow) {
120
+ return apiError("NOT_FOUND", "Content not found", 404);
121
+ }
122
+
123
+ // Check if comments are closed (published_at + closed_after_days)
124
+ if (collectionRow.comments_closed_after_days > 0) {
125
+ const publishedAt = (contentRow as { published_at: string | null }).published_at;
126
+ if (publishedAt) {
127
+ const closedDate = new Date(publishedAt);
128
+ closedDate.setDate(closedDate.getDate() + collectionRow.comments_closed_after_days);
129
+ if (new Date() > closedDate) {
130
+ return apiError("COMMENTS_CLOSED", "Comments are closed for this content", 403);
131
+ }
132
+ }
133
+ }
134
+
135
+ // Anti-spam: Honeypot — hidden field filled only by bots
136
+ if (body.website_url) {
137
+ // Silently accept — don't reveal the honeypot to bots
138
+ return apiSuccess({ status: "pending", message: "Comment submitted for review" });
139
+ }
140
+
141
+ // Anti-spam: Rate limiting
142
+ const meta = extractRequestMeta(request, dineway.config);
143
+ const ipSalt =
144
+ import.meta.env.DINEWAY_AUTH_SECRET || import.meta.env.AUTH_SECRET || "dineway-ip-salt";
145
+ let ipHash: string;
146
+ if (meta.ip) {
147
+ ipHash = await hashIp(meta.ip, ipSalt);
148
+ } else {
149
+ // Fail closed: all unidentifiable requests share one rate-limit bucket.
150
+ // Use a larger limit since this bucket is shared across all anonymous users.
151
+ ipHash = "unknown";
152
+ }
153
+ const unknownBucketLimit = ipHash === "unknown" ? 20 : undefined;
154
+ const rateLimited = await checkRateLimit(dineway.db, ipHash, unknownBucketLimit);
155
+ if (rateLimited) {
156
+ return apiError("RATE_LIMITED", "Too many comments. Please try again later.", 429);
157
+ }
158
+
159
+ // Build collection settings
160
+ const collectionSettings: CollectionCommentSettings = {
161
+ commentsEnabled: collectionRow.comments_enabled === 1,
162
+ commentsModeration:
163
+ collectionRow.comments_moderation as CollectionCommentSettings["commentsModeration"],
164
+ commentsClosedAfterDays: collectionRow.comments_closed_after_days,
165
+ commentsAutoApproveUsers: collectionRow.comments_auto_approve_users === 1,
166
+ };
167
+
168
+ // Determine author fields — authenticated user overrides form input
169
+ let authorName = body.authorName;
170
+ let authorEmail = body.authorEmail;
171
+ let authorUserId: string | null = null;
172
+
173
+ if (user) {
174
+ authorName = user.name || authorName;
175
+ authorEmail = user.email;
176
+ authorUserId = user.id;
177
+ }
178
+
179
+ // Validate parent exists and belongs to the same content.
180
+ // Enforce 1-level nesting: if the parent is itself a reply, attach to its root.
181
+ let resolvedParentId = body.parentId ?? null;
182
+ if (body.parentId) {
183
+ const repo = new CommentRepository(dineway.db);
184
+ const parent = await repo.findById(body.parentId);
185
+ if (!parent) {
186
+ return apiError("VALIDATION_ERROR", "Parent comment not found", 400);
187
+ }
188
+ if (parent.collection !== collection || parent.contentId !== contentId) {
189
+ return apiError("VALIDATION_ERROR", "Parent comment belongs to different content", 400);
190
+ }
191
+ // Flatten: if parent is a reply, use its parent (the root) instead
192
+ resolvedParentId = parent.parentId ?? parent.id;
193
+ }
194
+
195
+ // Wire the comment service to the real hook pipeline
196
+ const hookRunner: CommentHookRunner = {
197
+ async runBeforeCreate(event) {
198
+ return dineway.hooks.runCommentBeforeCreate(event);
199
+ },
200
+ async runModerate(event) {
201
+ const result = await dineway.hooks.invokeExclusiveHook("comment:moderate", event);
202
+ if (!result) return { status: "pending" as const, reason: "No moderator configured" };
203
+ if (result.error) {
204
+ console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
205
+ return { status: "pending" as const, reason: "Moderation error" };
206
+ }
207
+ return result.result as ModerationDecision;
208
+ },
209
+ fireAfterCreate(event) {
210
+ dineway.hooks
211
+ .runCommentAfterCreate(event)
212
+ .catch((err) =>
213
+ console.error(
214
+ "[comments] afterCreate error:",
215
+ err instanceof Error ? err.message : err,
216
+ ),
217
+ );
218
+ },
219
+ fireAfterModerate(event) {
220
+ dineway.hooks
221
+ .runCommentAfterModerate(event)
222
+ .catch((err) =>
223
+ console.error(
224
+ "[comments] afterModerate error:",
225
+ err instanceof Error ? err.message : err,
226
+ ),
227
+ );
228
+ },
229
+ };
230
+
231
+ // Build content info for afterCreate hooks (e.g. email notifications)
232
+ const typedContent = contentRow as {
233
+ id: string;
234
+ slug: string;
235
+ author_id: string | null;
236
+ };
237
+ let contentAuthor: { id: string; name: string | null; email: string } | undefined;
238
+ if (typedContent.author_id) {
239
+ const authorRow = await dineway.db
240
+ .selectFrom("users")
241
+ .select(["id", "name", "email", "email_verified"])
242
+ .where("id", "=", typedContent.author_id)
243
+ .executeTakeFirst();
244
+ if (authorRow && authorRow.email_verified) {
245
+ contentAuthor = {
246
+ id: authorRow.id,
247
+ name: authorRow.name,
248
+ email: authorRow.email,
249
+ };
250
+ }
251
+ }
252
+
253
+ const result = await createComment(
254
+ dineway.db,
255
+ {
256
+ collection,
257
+ contentId,
258
+ parentId: resolvedParentId,
259
+ authorName,
260
+ authorEmail,
261
+ authorUserId,
262
+ body: body.body,
263
+ ipHash,
264
+ userAgent: meta.userAgent,
265
+ },
266
+ collectionSettings,
267
+ hookRunner,
268
+ {
269
+ id: typedContent.id,
270
+ collection,
271
+ slug: typedContent.slug,
272
+ author: contentAuthor,
273
+ },
274
+ );
275
+
276
+ if (!result) {
277
+ return apiError("COMMENT_REJECTED", "Comment was rejected", 403);
278
+ }
279
+
280
+ // Send notification to content author and await completion before the
281
+ // response is sent so the delivery path does not outlive the request.
282
+ if (result.comment.status === "approved" && dineway.email && contentAuthor) {
283
+ try {
284
+ const adminBaseUrl = await getSiteBaseUrl(dineway.db, request);
285
+ await sendCommentNotification({
286
+ email: dineway.email,
287
+ comment: result.comment,
288
+ contentAuthor,
289
+ adminBaseUrl,
290
+ });
291
+ } catch (err) {
292
+ console.error("[comments] notification error:", err instanceof Error ? err.message : err);
293
+ }
294
+ }
295
+
296
+ return apiSuccess(
297
+ {
298
+ id: result.comment.id,
299
+ status: result.comment.status,
300
+ message:
301
+ result.comment.status === "approved"
302
+ ? "Comment published"
303
+ : "Comment submitted for review",
304
+ },
305
+ 201,
306
+ );
307
+ } catch (error) {
308
+ return handleError(error, "Failed to submit comment", "COMMENT_CREATE_ERROR");
309
+ }
310
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Compare live and draft revisions
3
+ *
4
+ * GET /_dineway/api/content/{collection}/{id}/compare
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requirePerm } from "#api/authorize.js";
10
+ import { apiError, unwrapResult } from "#api/error.js";
11
+
12
+ export const prerender = false;
13
+
14
+ export const GET: APIRoute = async ({ params, locals }) => {
15
+ const { dineway, user } = locals;
16
+ const denied = requirePerm(user, "content:read_drafts");
17
+ if (denied) return denied;
18
+ const collection = params.collection!;
19
+ const id = params.id!;
20
+
21
+ if (!dineway?.handleContentCompare) {
22
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
23
+ }
24
+
25
+ const result = await dineway.handleContentCompare(collection, id);
26
+
27
+ return unwrapResult(result);
28
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Discard draft changes - reverts to live version
3
+ *
4
+ * POST /_dineway/api/content/{collection}/{id}/discard-draft
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requireOwnerPerm } from "#api/authorize.js";
10
+ import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
11
+ import {
12
+ contentApiRouteSource,
13
+ extractActivityItemId,
14
+ logContentActivity,
15
+ } from "#site-context/activity-events.js";
16
+
17
+ export const prerender = false;
18
+
19
+ export const POST: APIRoute = async ({ params, locals, cache }) => {
20
+ const { dineway, user } = locals;
21
+ const collection = params.collection!;
22
+ const id = params.id!;
23
+
24
+ if (!dineway?.handleContentDiscardDraft || !dineway?.handleContentGet) {
25
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
26
+ }
27
+
28
+ // Fetch item to check ownership
29
+ const existing = await dineway.handleContentGet(collection, id);
30
+ if (!existing.success) {
31
+ return apiError(
32
+ existing.error?.code ?? "UNKNOWN_ERROR",
33
+ existing.error?.message ?? "Unknown error",
34
+ mapErrorStatus(existing.error?.code),
35
+ );
36
+ }
37
+ const existingData =
38
+ existing.data && typeof existing.data === "object"
39
+ ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above
40
+ (existing.data as Record<string, unknown>)
41
+ : undefined;
42
+ // Handler returns { item, _rev } — extract the item for ownership check
43
+ const existingItem =
44
+ existingData?.item && typeof existingData.item === "object"
45
+ ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above
46
+ (existingData.item as Record<string, unknown>)
47
+ : existingData;
48
+ const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
49
+ const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
50
+ if (denied) return denied;
51
+ const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
52
+
53
+ const result = await dineway.handleContentDiscardDraft(collection, resolvedId);
54
+
55
+ if (!result.success) return unwrapResult(result);
56
+
57
+ if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
58
+
59
+ await logContentActivity(dineway.db, locals, {
60
+ action: "draft_discarded",
61
+ collection,
62
+ entryId: extractActivityItemId(result.data, resolvedId),
63
+ ...contentApiRouteSource("draft_discarded"),
64
+ summary: `Discarded draft for content in ${collection}`,
65
+ });
66
+
67
+ return unwrapResult(result);
68
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Duplicate content endpoint - injected by Dineway integration
3
+ *
4
+ * POST /_dineway/api/content/{collection}/{id}/duplicate - Create a copy
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
10
+ import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
11
+ import {
12
+ contentApiRouteSource,
13
+ extractActivityItemId,
14
+ logContentActivity,
15
+ } from "#site-context/activity-events.js";
16
+
17
+ export const prerender = false;
18
+
19
+ export const POST: APIRoute = async ({ params, locals, cache }) => {
20
+ const { dineway, user } = locals;
21
+ const collection = params.collection!;
22
+ const id = params.id!;
23
+
24
+ const denied = requirePerm(user, "content:create");
25
+ if (denied) return denied;
26
+
27
+ if (!dineway?.handleContentDuplicate || !dineway?.handleContentGet) {
28
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
29
+ }
30
+
31
+ // Fetch item to check ownership — duplicating requires read access to the source
32
+ const existing = await dineway.handleContentGet(collection, id);
33
+ if (!existing.success) {
34
+ return apiError(
35
+ existing.error?.code ?? "UNKNOWN_ERROR",
36
+ existing.error?.message ?? "Unknown error",
37
+ mapErrorStatus(existing.error?.code),
38
+ );
39
+ }
40
+
41
+ const existingData =
42
+ existing.data && typeof existing.data === "object"
43
+ ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above
44
+ (existing.data as Record<string, unknown>)
45
+ : undefined;
46
+ // Handler returns { item, _rev } — extract the item for ownership check
47
+ const existingItem =
48
+ existingData?.item && typeof existingData.item === "object"
49
+ ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above
50
+ (existingData.item as Record<string, unknown>)
51
+ : existingData;
52
+ const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
53
+ // Duplicating requires read access to the source — check ownership-based edit permissions
54
+ // since content:read is flat (no own/any split). This ensures authors can only duplicate their own.
55
+ const readDenied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
56
+ if (readDenied) return readDenied;
57
+
58
+ const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
59
+ const result = await dineway.handleContentDuplicate(collection, resolvedId, user?.id);
60
+
61
+ if (!result.success) return unwrapResult(result);
62
+
63
+ if (cache.enabled) await cache.invalidate({ tags: [collection] });
64
+
65
+ await logContentActivity(dineway.db, locals, {
66
+ action: "duplicated",
67
+ collection,
68
+ entryId: extractActivityItemId(result.data),
69
+ ...contentApiRouteSource("duplicated"),
70
+ summary: `Duplicated content in ${collection}`,
71
+ detail: {
72
+ sourceEntryId: resolvedId,
73
+ },
74
+ });
75
+
76
+ return unwrapResult(result, 201);
77
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Permanent delete content endpoint - injected by Dineway integration
3
+ *
4
+ * DELETE /_dineway/api/content/{collection}/{id}/permanent - Permanently delete (no undo)
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requirePerm } from "#api/authorize.js";
10
+ import { apiError, unwrapResult } from "#api/error.js";
11
+ import { contentApiRouteSource, logContentActivity } from "#site-context/activity-events.js";
12
+
13
+ export const prerender = false;
14
+
15
+ export const DELETE: APIRoute = async ({ params, locals, cache }) => {
16
+ const { dineway, user } = locals;
17
+ const collection = params.collection!;
18
+ const id = params.id!;
19
+
20
+ const denied = requirePerm(user, "import:execute");
21
+ if (denied) return denied;
22
+
23
+ if (!dineway?.handleContentPermanentDelete) {
24
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
25
+ }
26
+
27
+ const result = await dineway.handleContentPermanentDelete(collection, id);
28
+
29
+ if (!result.success) return unwrapResult(result);
30
+
31
+ if (cache.enabled) await cache.invalidate({ tags: [collection, id] });
32
+
33
+ await logContentActivity(dineway.db, locals, {
34
+ action: "permanently_deleted",
35
+ collection,
36
+ entryId: id,
37
+ ...contentApiRouteSource("permanently_deleted"),
38
+ summary: `Permanently deleted content in ${collection}`,
39
+ });
40
+
41
+ return unwrapResult(result);
42
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Preview URL endpoint - generates a signed preview URL for content
3
+ *
4
+ * POST /_dineway/api/content/{collection}/{id}/preview-url
5
+ *
6
+ * Request body:
7
+ * {
8
+ * expiresIn?: string | number; // Default: "1h"
9
+ * pathPattern?: string; // Default: "/{collection}/{id}"
10
+ * }
11
+ *
12
+ * Response:
13
+ * {
14
+ * url: string; // The preview URL with token
15
+ * expiresAt: number; // Unix timestamp when token expires
16
+ * }
17
+ */
18
+
19
+ import type { APIRoute } from "astro";
20
+
21
+ import { requirePerm } from "#api/authorize.js";
22
+ import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
23
+ import { parseOptionalBody, isParseError } from "#api/parse.js";
24
+ import { contentPreviewUrlBody } from "#api/schemas.js";
25
+ import { getPreviewUrl } from "#preview/index.js";
26
+
27
+ export const prerender = false;
28
+
29
+ const DURATION_PATTERN = /^(\d+)([smhdw])$/;
30
+
31
+ export const POST: APIRoute = async ({ params, request, locals }) => {
32
+ const { dineway, user } = locals;
33
+ const denied = requirePerm(user, "content:read_drafts");
34
+ if (denied) return denied;
35
+ const collection = params.collection!;
36
+ const id = params.id!;
37
+
38
+ // Get the preview secret from environment
39
+ const previewSecret = import.meta.env.DINEWAY_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET;
40
+
41
+ if (!previewSecret) {
42
+ return apiError(
43
+ "NOT_CONFIGURED",
44
+ "Preview not configured. Set DINEWAY_PREVIEW_SECRET environment variable.",
45
+ 500,
46
+ );
47
+ }
48
+
49
+ // Verify the content exists (optional, but good for UX)
50
+ if (dineway?.handleContentGet) {
51
+ const result = await dineway.handleContentGet(collection, id);
52
+ if (!result.success) return unwrapResult(result);
53
+ }
54
+
55
+ // Parse request body
56
+ const body = await parseOptionalBody(request, contentPreviewUrlBody, {});
57
+ if (isParseError(body)) return body;
58
+
59
+ const expiresIn = body.expiresIn || "1h";
60
+ const pathPattern = body.pathPattern;
61
+
62
+ // Calculate expiry timestamp
63
+ const expiresInSeconds = typeof expiresIn === "number" ? expiresIn : parseExpiresIn(expiresIn);
64
+ const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
65
+
66
+ try {
67
+ const url = await getPreviewUrl({
68
+ collection,
69
+ id,
70
+ secret: previewSecret,
71
+ expiresIn,
72
+ pathPattern,
73
+ });
74
+
75
+ return apiSuccess({ url, expiresAt });
76
+ } catch (error) {
77
+ return handleError(error, "Failed to generate preview URL", "TOKEN_ERROR");
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Parse duration string to seconds
83
+ */
84
+ function parseExpiresIn(duration: string): number {
85
+ const match = duration.match(DURATION_PATTERN);
86
+ if (!match) {
87
+ return 3600; // Default 1 hour
88
+ }
89
+
90
+ const value = parseInt(match[1], 10);
91
+ const unit = match[2];
92
+
93
+ switch (unit) {
94
+ case "s":
95
+ return value;
96
+ case "m":
97
+ return value * 60;
98
+ case "h":
99
+ return value * 60 * 60;
100
+ case "d":
101
+ return value * 60 * 60 * 24;
102
+ case "w":
103
+ return value * 60 * 60 * 24 * 7;
104
+ default:
105
+ return 3600;
106
+ }
107
+ }