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,145 @@
1
+ /**
2
+ * Single menu endpoint
3
+ *
4
+ * GET /_dineway/api/menus/:name - Get menu with items
5
+ * PUT /_dineway/api/menus/:name - Update menu metadata
6
+ * DELETE /_dineway/api/menus/:name - Delete menu
7
+ */
8
+
9
+ import type { APIRoute } from "astro";
10
+ import { z } from "zod";
11
+
12
+ import { requirePerm } from "#api/authorize.js";
13
+ import { handleError, unwrapResult } from "#api/error.js";
14
+ import { handleMenuDelete, handleMenuGet, handleMenuUpdate } from "#api/handlers/menus.js";
15
+ import {
16
+ ensureWorkflowHitlRouteRequest,
17
+ hitlRequiredRouteError,
18
+ resolveHitlRouteActor,
19
+ } from "#api/hitl-route-helpers.js";
20
+ import { isParseError, parseBody, parseQuery } from "#api/parse.js";
21
+ import { updateMenuBody } from "#api/schemas.js";
22
+ import {
23
+ logMenuActivity,
24
+ menuApiRouteSource,
25
+ MenuHitlPayloadBuilder,
26
+ RiskPolicyEvaluator,
27
+ } from "#site-context/index.js";
28
+
29
+ export const prerender = false;
30
+
31
+ const updateMenuHitlBody = updateMenuBody.extend({
32
+ hitlRequestId: z.string().min(1).optional(),
33
+ });
34
+
35
+ const deleteMenuQuery = z.object({
36
+ hitlRequestId: z.string().min(1).optional(),
37
+ });
38
+
39
+ export const GET: APIRoute = async ({ params, locals }) => {
40
+ const { dineway, user } = locals;
41
+ const name = params.name!;
42
+
43
+ const denied = requirePerm(user, "menus:read");
44
+ if (denied) return denied;
45
+
46
+ try {
47
+ const result = await handleMenuGet(dineway.db, name);
48
+ return unwrapResult(result);
49
+ } catch (error) {
50
+ return handleError(error, "Failed to fetch menu", "MENU_GET_ERROR");
51
+ }
52
+ };
53
+
54
+ export const PUT: APIRoute = async ({ params, request, locals }) => {
55
+ const { dineway, user } = locals;
56
+ const name = params.name!;
57
+
58
+ const denied = requirePerm(user, "menus:manage");
59
+ if (denied) return denied;
60
+
61
+ try {
62
+ const body = await parseBody(request, updateMenuHitlBody);
63
+ if (isParseError(body)) return body;
64
+
65
+ const { hitlRequestId, ...menuInput } = body;
66
+ const actor = resolveHitlRouteActor(locals);
67
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildUpdateMenuRequest({
68
+ name,
69
+ ...menuInput,
70
+ });
71
+ const decision = await new RiskPolicyEvaluator({
72
+ db: dineway.db,
73
+ handlers: dineway,
74
+ }).evaluateWorkflowHitl({
75
+ actor: actor.identity,
76
+ hitlRequestId,
77
+ action,
78
+ });
79
+ if (!decision.allowed) {
80
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
81
+ return hitlRequiredRouteError(decision, ensured);
82
+ }
83
+
84
+ const result = await handleMenuUpdate(dineway.db, name, menuInput);
85
+ if (!result.success) return unwrapResult(result);
86
+
87
+ await logMenuActivity(dineway.db, locals, {
88
+ action: "updated",
89
+ menuName: result.data.name,
90
+ ...menuApiRouteSource("updated"),
91
+ summary: `Updated menu ${result.data.name}`,
92
+ detail: {
93
+ label: result.data.label,
94
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
95
+ },
96
+ });
97
+ return unwrapResult(result);
98
+ } catch (error) {
99
+ return handleError(error, "Failed to update menu", "MENU_UPDATE_ERROR");
100
+ }
101
+ };
102
+
103
+ export const DELETE: APIRoute = async ({ params, request, locals }) => {
104
+ const { dineway, user } = locals;
105
+ const name = params.name!;
106
+
107
+ const denied = requirePerm(user, "menus:manage");
108
+ if (denied) return denied;
109
+
110
+ const query = parseQuery(new URL(request.url), deleteMenuQuery);
111
+ if (isParseError(query)) return query;
112
+
113
+ try {
114
+ const actor = resolveHitlRouteActor(locals);
115
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildDeleteMenuRequest({ name });
116
+ const decision = await new RiskPolicyEvaluator({
117
+ db: dineway.db,
118
+ handlers: dineway,
119
+ }).evaluateWorkflowHitl({
120
+ actor: actor.identity,
121
+ hitlRequestId: query.hitlRequestId,
122
+ action,
123
+ });
124
+ if (!decision.allowed) {
125
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
126
+ return hitlRequiredRouteError(decision, ensured);
127
+ }
128
+
129
+ const result = await handleMenuDelete(dineway.db, name);
130
+ if (!result.success) return unwrapResult(result);
131
+
132
+ await logMenuActivity(dineway.db, locals, {
133
+ action: "deleted",
134
+ menuName: name,
135
+ ...menuApiRouteSource("deleted"),
136
+ summary: `Deleted menu ${name}`,
137
+ detail: {
138
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
139
+ },
140
+ });
141
+ return unwrapResult(result);
142
+ } catch (error) {
143
+ return handleError(error, "Failed to delete menu", "MENU_DELETE_ERROR");
144
+ }
145
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Menus list and create endpoints
3
+ *
4
+ * GET /_dineway/api/menus - List all menus
5
+ * POST /_dineway/api/menus - Create menu
6
+ */
7
+
8
+ import type { APIRoute } from "astro";
9
+ import { z } from "zod";
10
+
11
+ import { requirePerm } from "#api/authorize.js";
12
+ import { handleError, unwrapResult } from "#api/error.js";
13
+ import { handleMenuCreate, handleMenuList } from "#api/handlers/menus.js";
14
+ import {
15
+ ensureWorkflowHitlRouteRequest,
16
+ hitlRequiredRouteError,
17
+ resolveHitlRouteActor,
18
+ } from "#api/hitl-route-helpers.js";
19
+ import { isParseError, parseBody } from "#api/parse.js";
20
+ import { createMenuBody } from "#api/schemas.js";
21
+ import {
22
+ logMenuActivity,
23
+ menuApiRouteSource,
24
+ MenuHitlPayloadBuilder,
25
+ RiskPolicyEvaluator,
26
+ } from "#site-context/index.js";
27
+
28
+ export const prerender = false;
29
+
30
+ const createMenuHitlBody = createMenuBody.extend({
31
+ hitlRequestId: z.string().min(1).optional(),
32
+ });
33
+
34
+ export const GET: APIRoute = async ({ locals }) => {
35
+ const { dineway, user } = locals;
36
+
37
+ const denied = requirePerm(user, "menus:read");
38
+ if (denied) return denied;
39
+
40
+ try {
41
+ const result = await handleMenuList(dineway.db);
42
+ return unwrapResult(result);
43
+ } catch (error) {
44
+ return handleError(error, "Failed to fetch menus", "MENU_LIST_ERROR");
45
+ }
46
+ };
47
+
48
+ export const POST: APIRoute = async ({ request, locals }) => {
49
+ const { dineway, user } = locals;
50
+
51
+ const denied = requirePerm(user, "menus:manage");
52
+ if (denied) return denied;
53
+
54
+ try {
55
+ const body = await parseBody(request, createMenuHitlBody);
56
+ if (isParseError(body)) return body;
57
+
58
+ const { hitlRequestId, ...menuInput } = body;
59
+ const actor = resolveHitlRouteActor(locals);
60
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildCreateMenuRequest(menuInput);
61
+ const decision = await new RiskPolicyEvaluator({
62
+ db: dineway.db,
63
+ handlers: dineway,
64
+ }).evaluateWorkflowHitl({
65
+ actor: actor.identity,
66
+ hitlRequestId,
67
+ action,
68
+ });
69
+ if (!decision.allowed) {
70
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
71
+ return hitlRequiredRouteError(decision, ensured);
72
+ }
73
+
74
+ const result = await handleMenuCreate(dineway.db, menuInput);
75
+ if (!result.success) return unwrapResult(result, 201);
76
+
77
+ await logMenuActivity(dineway.db, locals, {
78
+ action: "created",
79
+ menuName: result.data.name,
80
+ ...menuApiRouteSource("created"),
81
+ summary: `Created menu ${result.data.name}`,
82
+ detail: {
83
+ label: result.data.label,
84
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
85
+ },
86
+ });
87
+ return unwrapResult(result, 201);
88
+ } catch (error) {
89
+ return handleError(error, "Failed to create menu", "MENU_CREATE_ERROR");
90
+ }
91
+ };
@@ -0,0 +1,430 @@
1
+ /**
2
+ * GET/POST /_dineway/oauth/authorize
3
+ *
4
+ * OAuth 2.1 Authorization Endpoint. Handles both the consent page (GET)
5
+ * and consent submission (POST).
6
+ *
7
+ * GET: Renders an HTML consent page showing which client is requesting
8
+ * access and which scopes are being requested.
9
+ * POST: Processes the user's decision (approve/deny) and redirects
10
+ * to the client's redirect_uri with an authorization code or error.
11
+ *
12
+ * Requires an authenticated session (not token auth). If unauthenticated,
13
+ * redirects to login with a return URL.
14
+ */
15
+
16
+ import type { APIRoute } from "astro";
17
+
18
+ import { escapeHtml } from "#api/escape.js";
19
+ import {
20
+ buildDeniedRedirect,
21
+ handleAuthorizationApproval,
22
+ } from "#api/handlers/oauth-authorization.js";
23
+ import { lookupOAuthClient, validateClientRedirectUri } from "#api/handlers/oauth-clients.js";
24
+ import { validateRedirectUri } from "#api/oauth/redirect-uri.js";
25
+ import { getPublicOrigin } from "#api/public-url.js";
26
+ import { ALL_VALID_SCOPES } from "#auth/api-tokens.js";
27
+ import {
28
+ disabledExperimentalSiteContextWorkflowScopes,
29
+ filterExperimentalSiteContextWorkflowScopes,
30
+ getExperimentalSiteContextWorkflowScopesDisabledMessage,
31
+ } from "#site-context/experimental-workflows.js";
32
+
33
+ export const prerender = false;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // CSRF (SEC-18): Double-submit cookie pattern
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const CSRF_COOKIE_NAME = "dineway_oauth_csrf";
40
+
41
+ /** Generate a 32-byte random token as hex. */
42
+ function generateCsrfToken(): string {
43
+ const bytes = new Uint8Array(32);
44
+ crypto.getRandomValues(bytes);
45
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
46
+ }
47
+
48
+ /** Build the Set-Cookie header value for the CSRF token. */
49
+ function csrfCookieHeader(token: string, request: Request, siteUrl?: string): string {
50
+ // SameSite=Strict prevents cross-site form submission.
51
+ // HttpOnly: the token value is embedded in the form hidden field server-side,
52
+ // so JS never needs to read the cookie. HttpOnly adds defense-in-depth.
53
+ // Secure is set when:
54
+ // - siteUrl is configured and uses https (proxy case — request may be http internally), OR
55
+ // - the actual request is over https (non-proxy case, preserve existing behavior — H-2)
56
+ const isSecure = siteUrl
57
+ ? siteUrl.startsWith("https:")
58
+ : new URL(request.url).protocol === "https:";
59
+ const secure = isSecure ? "; Secure" : "";
60
+ return `${CSRF_COOKIE_NAME}=${token}; Path=/_dineway/oauth/authorize; HttpOnly; SameSite=Strict${secure}`;
61
+ }
62
+
63
+ /** Extract the CSRF token from the request's cookies. */
64
+ function getCsrfCookie(request: Request): string | null {
65
+ const cookieHeader = request.headers.get("Cookie");
66
+ if (!cookieHeader) return null;
67
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${CSRF_COOKIE_NAME}=([^;]+)`));
68
+ return match?.[1] ?? null;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Human-readable scope labels
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const SCOPE_LABELS: Record<string, string> = {
76
+ "content:read": "Read content (posts, pages, etc.)",
77
+ "content:write": "Create, edit, and delete content",
78
+ "media:read": "View media files",
79
+ "media:write": "Upload and manage media files",
80
+ "schema:read": "View collection schemas",
81
+ "schema:write": "Create and modify collection schemas",
82
+ admin: "Full administrative access",
83
+ };
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // GET: Render consent page
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export const GET: APIRoute = async ({ url, request, locals }) => {
90
+ const { dineway, user } = locals;
91
+
92
+ // Validate required OAuth params before rendering
93
+ const clientId = url.searchParams.get("client_id");
94
+ const redirectUri = url.searchParams.get("redirect_uri");
95
+ const responseType = url.searchParams.get("response_type");
96
+ const codeChallenge = url.searchParams.get("code_challenge");
97
+ const codeChallengeMethod = url.searchParams.get("code_challenge_method");
98
+ const scope = url.searchParams.get("scope");
99
+ const state = url.searchParams.get("state");
100
+
101
+ // Basic validation — detailed validation happens on POST
102
+ if (!clientId || !redirectUri || responseType !== "code" || !codeChallenge) {
103
+ return new Response(
104
+ renderErrorPage("Invalid authorization request. Missing required parameters."),
105
+ {
106
+ status: 400,
107
+ headers: { "Content-Type": "text/html; charset=utf-8" },
108
+ },
109
+ );
110
+ }
111
+
112
+ if (codeChallengeMethod && codeChallengeMethod !== "S256") {
113
+ return new Response(renderErrorPage("Only S256 code challenge method is supported."), {
114
+ status: 400,
115
+ headers: { "Content-Type": "text/html; charset=utf-8" },
116
+ });
117
+ }
118
+
119
+ // Validate client_id is registered and redirect_uri is in the allowlist.
120
+ // This check happens BEFORE authentication so we never redirect to an
121
+ // unregistered URI (even for the login redirect, we only redirect to our
122
+ // own login page, not to the client's redirect_uri).
123
+ if (dineway?.db) {
124
+ const client = await lookupOAuthClient(dineway.db, clientId);
125
+ if (!client) {
126
+ return new Response(renderErrorPage("Unknown client application."), {
127
+ status: 400,
128
+ headers: { "Content-Type": "text/html; charset=utf-8" },
129
+ });
130
+ }
131
+
132
+ const clientUriError = validateClientRedirectUri(redirectUri, client.redirectUris);
133
+ if (clientUriError) {
134
+ return new Response(renderErrorPage("The redirect URI is not registered for this client."), {
135
+ status: 400,
136
+ headers: { "Content-Type": "text/html; charset=utf-8" },
137
+ });
138
+ }
139
+ }
140
+
141
+ // If not authenticated, redirect to login with return URL
142
+ if (!user) {
143
+ const loginUrl = new URL("/_dineway/admin/login", getPublicOrigin(url, dineway?.config));
144
+ loginUrl.searchParams.set("redirect", url.pathname + url.search);
145
+ return Response.redirect(loginUrl.toString(), 302);
146
+ }
147
+
148
+ // Parse and validate scopes
149
+ const rawRequestedScopes = (scope ?? "").split(" ").filter(Boolean);
150
+ const disabledWorkflowScopes = disabledExperimentalSiteContextWorkflowScopes(rawRequestedScopes);
151
+ if (disabledWorkflowScopes.length > 0) {
152
+ return new Response(
153
+ renderErrorPage(getExperimentalSiteContextWorkflowScopesDisabledMessage()),
154
+ {
155
+ status: 400,
156
+ headers: { "Content-Type": "text/html; charset=utf-8" },
157
+ },
158
+ );
159
+ }
160
+ const validSet = new Set<string>(filterExperimentalSiteContextWorkflowScopes(ALL_VALID_SCOPES));
161
+ const requestedScopes = rawRequestedScopes.filter((s) => validSet.has(s));
162
+
163
+ if (requestedScopes.length === 0) {
164
+ return new Response(renderErrorPage("No valid scopes requested."), {
165
+ status: 400,
166
+ headers: { "Content-Type": "text/html; charset=utf-8" },
167
+ });
168
+ }
169
+
170
+ // SEC-18: Generate CSRF token for the consent form (double-submit cookie pattern)
171
+ const csrfToken = generateCsrfToken();
172
+
173
+ // Render the consent page
174
+ const html = renderConsentPage({
175
+ clientId,
176
+ scopes: requestedScopes,
177
+ redirectUri,
178
+ responseType,
179
+ codeChallenge,
180
+ codeChallengeMethod: codeChallengeMethod ?? "S256",
181
+ state: state ?? "",
182
+ resource: url.searchParams.get("resource") ?? "",
183
+ userName: user.name ?? user.email,
184
+ csrfToken,
185
+ });
186
+
187
+ return new Response(html, {
188
+ headers: {
189
+ "Content-Type": "text/html; charset=utf-8",
190
+ "Set-Cookie": csrfCookieHeader(csrfToken, request, getPublicOrigin(url, dineway?.config)),
191
+ },
192
+ });
193
+ };
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // POST: Process consent
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export const POST: APIRoute = async ({ request, locals }) => {
200
+ const { dineway, user } = locals;
201
+
202
+ if (!dineway?.db) {
203
+ return new Response(renderErrorPage("Dineway is not initialized."), {
204
+ status: 500,
205
+ headers: { "Content-Type": "text/html; charset=utf-8" },
206
+ });
207
+ }
208
+
209
+ if (!user) {
210
+ return new Response(renderErrorPage("Authentication required."), {
211
+ status: 401,
212
+ headers: { "Content-Type": "text/html; charset=utf-8" },
213
+ });
214
+ }
215
+
216
+ const formData = await request.formData();
217
+ const field = (name: string, fallback = ""): string => {
218
+ const v = formData.get(name);
219
+ return typeof v === "string" ? v : fallback;
220
+ };
221
+
222
+ // SEC-18: Validate CSRF token (double-submit cookie pattern).
223
+ // The form includes a hidden csrf_token field; the cookie has the same value.
224
+ // An attacker cannot read the cookie to forge the form field (HttpOnly + SameSite=Strict).
225
+ const formCsrf = field("csrf_token");
226
+ const cookieCsrf = getCsrfCookie(request);
227
+ const csrfError = new Response(
228
+ renderErrorPage("Invalid or missing CSRF token. Please try again."),
229
+ { status: 403, headers: { "Content-Type": "text/html; charset=utf-8" } },
230
+ );
231
+ if (!formCsrf || !cookieCsrf) return csrfError;
232
+
233
+ // Constant-time comparison: hash both values to fixed-length 32-byte digests,
234
+ // then XOR every byte pair. This avoids non-standard timing-safe helpers and
235
+ // works across the supported Node/Web Crypto path.
236
+ // The SHA-256 pre-hash ensures fixed length, eliminating length-leaking.
237
+ const csrfEncoder = new TextEncoder();
238
+ const [csrfHashA, csrfHashB] = await Promise.all([
239
+ crypto.subtle.digest("SHA-256", csrfEncoder.encode(formCsrf)),
240
+ crypto.subtle.digest("SHA-256", csrfEncoder.encode(cookieCsrf)),
241
+ ]);
242
+ const a = new Uint8Array(csrfHashA);
243
+ const b = new Uint8Array(csrfHashB);
244
+ let diff = 0;
245
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- tsgo needs these
246
+ for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;
247
+ if (diff !== 0) return csrfError;
248
+
249
+ const action = field("action");
250
+ const redirectUri = field("redirect_uri");
251
+ const state = field("state") || undefined;
252
+
253
+ if (!redirectUri) {
254
+ return new Response(renderErrorPage("Missing redirect_uri."), {
255
+ status: 400,
256
+ headers: { "Content-Type": "text/html; charset=utf-8" },
257
+ });
258
+ }
259
+
260
+ // Validate redirect_uri scheme/host before using it for any redirect
261
+ const uriError = validateRedirectUri(redirectUri);
262
+ if (uriError) {
263
+ return new Response(renderErrorPage(escapeHtml(uriError)), {
264
+ status: 400,
265
+ headers: { "Content-Type": "text/html; charset=utf-8" },
266
+ });
267
+ }
268
+
269
+ // User denied — SEC-44: validate redirect_uri against client's registered URIs
270
+ // before redirecting, to prevent open redirect on the deny path.
271
+ if (action === "deny") {
272
+ const clientId = field("client_id");
273
+ if (!clientId) {
274
+ return new Response(renderErrorPage("Missing client_id."), {
275
+ status: 400,
276
+ headers: { "Content-Type": "text/html; charset=utf-8" },
277
+ });
278
+ }
279
+
280
+ const client = await lookupOAuthClient(dineway.db, clientId);
281
+ if (!client) {
282
+ return new Response(renderErrorPage("Unknown client application."), {
283
+ status: 400,
284
+ headers: { "Content-Type": "text/html; charset=utf-8" },
285
+ });
286
+ }
287
+
288
+ const clientUriError = validateClientRedirectUri(redirectUri, client.redirectUris);
289
+ if (clientUriError) {
290
+ return new Response(renderErrorPage("The redirect URI is not registered for this client."), {
291
+ status: 400,
292
+ headers: { "Content-Type": "text/html; charset=utf-8" },
293
+ });
294
+ }
295
+
296
+ const denyUrl = buildDeniedRedirect(redirectUri, state);
297
+ return Response.redirect(denyUrl, 302);
298
+ }
299
+
300
+ // User approved — process the authorization
301
+ const result = await handleAuthorizationApproval(dineway.db, user.id, user.role, {
302
+ response_type: field("response_type", "code"),
303
+ client_id: field("client_id"),
304
+ redirect_uri: redirectUri,
305
+ scope: field("scope"),
306
+ state,
307
+ code_challenge: field("code_challenge"),
308
+ code_challenge_method: field("code_challenge_method", "S256"),
309
+ resource: field("resource") || undefined,
310
+ });
311
+
312
+ if (!result.success) {
313
+ const errMsg = result.error?.message ?? "Authorization failed";
314
+ // On error, redirect back with error params — use generic description to avoid
315
+ // leaking internal error details to the (already-validated) redirect target
316
+ try {
317
+ const errorUrl = new URL(redirectUri);
318
+ errorUrl.searchParams.set("error", "server_error");
319
+ errorUrl.searchParams.set("error_description", "Authorization failed");
320
+ if (state) errorUrl.searchParams.set("state", state);
321
+ return Response.redirect(errorUrl.toString(), 302);
322
+ } catch {
323
+ return new Response(renderErrorPage(escapeHtml(errMsg)), {
324
+ status: 400,
325
+ headers: { "Content-Type": "text/html; charset=utf-8" },
326
+ });
327
+ }
328
+ }
329
+
330
+ return Response.redirect(result.data.redirect_url, 302);
331
+ };
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // HTML rendering
335
+ // ---------------------------------------------------------------------------
336
+
337
+ function renderConsentPage(params: {
338
+ clientId: string;
339
+ scopes: string[];
340
+ redirectUri: string;
341
+ responseType: string;
342
+ codeChallenge: string;
343
+ codeChallengeMethod: string;
344
+ state: string;
345
+ resource: string;
346
+ userName: string;
347
+ csrfToken: string;
348
+ }): string {
349
+ const scopeList = params.scopes
350
+ .map((s) => {
351
+ const label = SCOPE_LABELS[s] ?? s;
352
+ return `<li>${escapeHtml(label)}</li>`;
353
+ })
354
+ .join("\n");
355
+
356
+ return `<!DOCTYPE html>
357
+ <html lang="en">
358
+ <head>
359
+ <meta charset="utf-8">
360
+ <meta name="viewport" content="width=device-width, initial-scale=1">
361
+ <title>Authorize Application — Dineway</title>
362
+ <style>
363
+ * { margin: 0; padding: 0; box-sizing: border-box; }
364
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem; }
365
+ .card { background: #171717; border: 1px solid #262626; border-radius: 12px; max-width: 420px; width: 100%; padding: 2rem; }
366
+ h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
367
+ .client-id { color: #a3a3a3; font-size: 0.875rem; word-break: break-all; margin-bottom: 1.5rem; }
368
+ .user { color: #a3a3a3; font-size: 0.875rem; margin-bottom: 1rem; }
369
+ h2 { font-size: 0.875rem; font-weight: 500; color: #a3a3a3; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.75rem; }
370
+ ul { list-style: none; margin-bottom: 1.5rem; }
371
+ li { padding: 0.5rem 0; border-bottom: 1px solid #262626; font-size: 0.875rem; }
372
+ li:last-child { border-bottom: none; }
373
+ .actions { display: flex; gap: 0.75rem; }
374
+ button { flex: 1; padding: 0.625rem 1rem; border-radius: 8px; border: none; font-size: 0.875rem; font-weight: 500; cursor: pointer; }
375
+ .approve { background: #2563eb; color: white; }
376
+ .approve:hover { background: #1d4ed8; }
377
+ .deny { background: #262626; color: #e5e5e5; }
378
+ .deny:hover { background: #333; }
379
+ </style>
380
+ </head>
381
+ <body>
382
+ <div class="card">
383
+ <h1>Authorize Application</h1>
384
+ <p class="client-id">${escapeHtml(params.clientId)}</p>
385
+ <p class="user">Signed in as <strong>${escapeHtml(params.userName)}</strong></p>
386
+ <h2>Permissions requested</h2>
387
+ <ul>${scopeList}</ul>
388
+ <form method="POST">
389
+ <input type="hidden" name="csrf_token" value="${escapeHtml(params.csrfToken)}">
390
+ <input type="hidden" name="response_type" value="${escapeHtml(params.responseType)}">
391
+ <input type="hidden" name="client_id" value="${escapeHtml(params.clientId)}">
392
+ <input type="hidden" name="redirect_uri" value="${escapeHtml(params.redirectUri)}">
393
+ <input type="hidden" name="scope" value="${escapeHtml(params.scopes.join(" "))}">
394
+ <input type="hidden" name="state" value="${escapeHtml(params.state)}">
395
+ <input type="hidden" name="code_challenge" value="${escapeHtml(params.codeChallenge)}">
396
+ <input type="hidden" name="code_challenge_method" value="${escapeHtml(params.codeChallengeMethod)}">
397
+ <input type="hidden" name="resource" value="${escapeHtml(params.resource)}">
398
+ <div class="actions">
399
+ <button type="submit" name="action" value="deny" class="deny">Deny</button>
400
+ <button type="submit" name="action" value="approve" class="approve">Approve</button>
401
+ </div>
402
+ </form>
403
+ </div>
404
+ </body>
405
+ </html>`;
406
+ }
407
+
408
+ function renderErrorPage(message: string): string {
409
+ return `<!DOCTYPE html>
410
+ <html lang="en">
411
+ <head>
412
+ <meta charset="utf-8">
413
+ <meta name="viewport" content="width=device-width, initial-scale=1">
414
+ <title>Authorization Error — Dineway</title>
415
+ <style>
416
+ * { margin: 0; padding: 0; box-sizing: border-box; }
417
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem; }
418
+ .card { background: #171717; border: 1px solid #262626; border-radius: 12px; max-width: 420px; width: 100%; padding: 2rem; }
419
+ h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: #ef4444; }
420
+ p { font-size: 0.875rem; color: #a3a3a3; }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <div class="card">
425
+ <h1>Authorization Error</h1>
426
+ <p>${escapeHtml(message)}</p>
427
+ </div>
428
+ </body>
429
+ </html>`;
430
+ }