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.
- package/README.md +6 -3
- package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
- package/dist/astro/index.d.mts +18 -9
- package/dist/astro/index.mjs +238 -16
- package/dist/astro/middleware/auth.d.mts +16 -5
- package/dist/astro/middleware/auth.mjs +74 -37
- package/dist/astro/middleware/redirect.mjs +24 -8
- package/dist/astro/middleware/request-context.mjs +18 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +411 -169
- package/dist/astro/types.d.mts +25 -8
- package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
- package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
- package/dist/cache-BdSY-gQN.mjs +42 -0
- package/dist/chunks--4F8ddV4.mjs +18 -0
- package/dist/cli/index.mjs +935 -15
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -3
- package/dist/client/index.mjs +4 -3
- package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
- package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
- package/dist/database/instrumentation.d.mts +34 -0
- package/dist/database/instrumentation.mjs +53 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +11 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +7 -1
- package/dist/db-errors-CEqD7qH9.mjs +23 -0
- package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
- package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
- package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +24 -22
- package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/page/index.d.mts +10 -2
- package/dist/page/index.mjs +22 -1
- package/dist/patterns-CrCYkMBb.mjs +92 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
- package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
- package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
- package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
- package/dist/request-cache-Dk5qPSOx.mjs +66 -0
- package/dist/request-context.d.mts +4 -16
- package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
- package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +11 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.mjs +78 -15
- package/dist/taxonomies-1s5PaS_8.mjs +266 -0
- package/dist/transaction-Cn2rjY78.mjs +27 -0
- package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
- package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
- package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
- package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
- package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
- package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
- package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
- package/dist/version-BKXPsfmJ.mjs +6 -0
- package/package.json +53 -39
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +99 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
- package/src/astro/routes/api/admin/bylines/index.ts +74 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
- package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
- package/src/astro/routes/api/admin/context/diff.ts +35 -0
- package/src/astro/routes/api/admin/context/index.ts +69 -0
- package/src/astro/routes/api/admin/context/stale.ts +35 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
- package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
- package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +86 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +85 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +89 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
- package/src/astro/routes/api/content/[collection]/index.ts +103 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
- package/src/astro/routes/api/manifest.ts +75 -0
- package/src/astro/routes/api/mcp.ts +125 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[...key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +146 -0
- package/src/astro/routes/api/media.ts +204 -0
- package/src/astro/routes/api/menus/[name]/items.ts +206 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
- package/src/astro/routes/api/menus/[name].ts +145 -0
- package/src/astro/routes/api/menus/index.ts +91 -0
- package/src/astro/routes/api/oauth/authorize.ts +430 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +56 -0
- package/src/astro/routes/api/oauth/device/token.ts +70 -0
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +195 -0
- package/src/astro/routes/api/openapi.json.ts +33 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +183 -0
- package/src/astro/routes/api/redirects/index.ts +100 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
- package/src/astro/routes/api/schema/collections/index.ts +61 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +52 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +50 -0
- package/src/astro/routes/api/sections/[slug].ts +203 -0
- package/src/astro/routes/api/sections/index.ts +107 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +116 -0
- package/src/astro/routes/api/setup/admin-verify.ts +122 -0
- package/src/astro/routes/api/setup/admin.ts +104 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +128 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +76 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
- package/src/astro/routes/api/taxonomies/index.ts +114 -0
- package/src/astro/routes/api/themes/preview.ts +78 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +71 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
- package/src/astro/routes/api/widget-areas/[name].ts +135 -0
- package/src/astro/routes/api/widget-areas/index.ts +149 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +81 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +92 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/DinewayBodyEnd.astro +32 -0
- package/src/components/DinewayBodyStart.astro +32 -0
- package/src/components/DinewayHead.astro +61 -0
- package/src/components/DinewayImage.astro +178 -0
- package/src/components/DinewayMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1937 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +135 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +106 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/marks.ts +19 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/ui.ts +75 -0
- package/LICENSE +0 -9
- /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
- /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
- /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
- /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
- /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
- /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
- /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
- /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
- /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
- /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/device/authorize
|
|
3
|
+
*
|
|
4
|
+
* User submits the user code after logging in via the browser.
|
|
5
|
+
* This endpoint requires authentication (the user must be logged in).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/// <reference types="dineway/locals" />
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
14
|
+
import { handleDeviceAuthorize } from "#api/handlers/device-flow.js";
|
|
15
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
const authorizeSchema = z.object({
|
|
20
|
+
user_code: z.string().min(1),
|
|
21
|
+
action: z.enum(["approve", "deny"]).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
25
|
+
const { dineway } = locals;
|
|
26
|
+
const { user } = locals;
|
|
27
|
+
|
|
28
|
+
if (!dineway?.db) {
|
|
29
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!user) {
|
|
33
|
+
return apiError("NOT_AUTHENTICATED", "Authentication required", 401);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const body = await parseBody(request, authorizeSchema);
|
|
38
|
+
if (isParseError(body)) return body;
|
|
39
|
+
|
|
40
|
+
const result = await handleDeviceAuthorize(dineway.db, user.id, user.role, body);
|
|
41
|
+
return unwrapResult(result);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return handleError(error, "Failed to authorize device", "AUTHORIZE_ERROR");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/device/code
|
|
3
|
+
*
|
|
4
|
+
* Issue a device code + user code for the OAuth Device Flow.
|
|
5
|
+
* This is an unauthenticated endpoint (the CLI doesn't have a token yet).
|
|
6
|
+
*
|
|
7
|
+
* Rate limited: 10 requests per minute per IP.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
14
|
+
import { handleDeviceCodeRequest } from "#api/handlers/device-flow.js";
|
|
15
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
|
+
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
18
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
19
|
+
|
|
20
|
+
export const prerender = false;
|
|
21
|
+
|
|
22
|
+
const deviceCodeSchema = z.object({
|
|
23
|
+
client_id: z.string().optional(),
|
|
24
|
+
scope: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const POST: APIRoute = async ({ request, locals, url }) => {
|
|
28
|
+
const { dineway } = locals;
|
|
29
|
+
|
|
30
|
+
if (!dineway?.db) {
|
|
31
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const body = await parseBody(request, deviceCodeSchema);
|
|
36
|
+
if (isParseError(body)) return body;
|
|
37
|
+
|
|
38
|
+
// Rate limit: 10 requests per 60 seconds per IP
|
|
39
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
40
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "device/code", 10, 60);
|
|
41
|
+
if (!rateLimit.allowed) {
|
|
42
|
+
return rateLimitResponse(60);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build the verification URI — device page lives inside the admin SPA
|
|
46
|
+
const verificationUri = new URL(
|
|
47
|
+
"/_dineway/admin/device",
|
|
48
|
+
getPublicOrigin(url, dineway?.config),
|
|
49
|
+
).toString();
|
|
50
|
+
|
|
51
|
+
const result = await handleDeviceCodeRequest(dineway.db, body, verificationUri);
|
|
52
|
+
return unwrapResult(result);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return handleError(error, "Failed to create device code", "DEVICE_CODE_ERROR");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/device/token
|
|
3
|
+
*
|
|
4
|
+
* CLI polls this endpoint to exchange a device code for tokens.
|
|
5
|
+
* Returns RFC 8628 error codes during the polling phase.
|
|
6
|
+
* This is an unauthenticated endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Rate limited: 12 requests per minute per IP.
|
|
9
|
+
* Also enforces RFC 8628 slow_down: if polled faster than the interval,
|
|
10
|
+
* responds with { error: "slow_down", interval: N } and increases the interval by 5s.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { APIRoute } from "astro";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
17
|
+
import { handleDeviceTokenExchange } from "#api/handlers/device-flow.js";
|
|
18
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
+
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
20
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
21
|
+
|
|
22
|
+
export const prerender = false;
|
|
23
|
+
|
|
24
|
+
const deviceTokenSchema = z.object({
|
|
25
|
+
device_code: z.string().min(1),
|
|
26
|
+
grant_type: z.string().min(1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
30
|
+
const { dineway } = locals;
|
|
31
|
+
|
|
32
|
+
if (!dineway?.db) {
|
|
33
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const body = await parseBody(request, deviceTokenSchema);
|
|
38
|
+
if (isParseError(body)) return body;
|
|
39
|
+
|
|
40
|
+
// Rate limit: 12 requests per 60 seconds per IP
|
|
41
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
42
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "device/token", 12, 60);
|
|
43
|
+
if (!rateLimit.allowed) {
|
|
44
|
+
return rateLimitResponse(60);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await handleDeviceTokenExchange(dineway.db, body);
|
|
48
|
+
|
|
49
|
+
// RFC 8628 requires specific error format for device flow errors
|
|
50
|
+
// RFC 6749 §5.1 requires Cache-Control: no-store + Pragma: no-cache on token responses
|
|
51
|
+
if (!result.success && result.deviceFlowError) {
|
|
52
|
+
const errorBody: { error: string; interval?: number } = { error: result.deviceFlowError };
|
|
53
|
+
if (result.deviceFlowInterval !== undefined) {
|
|
54
|
+
errorBody.interval = result.deviceFlowInterval;
|
|
55
|
+
}
|
|
56
|
+
return Response.json(errorBody, {
|
|
57
|
+
status: 400,
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Cache-Control": "no-store",
|
|
61
|
+
Pragma: "no-cache",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return unwrapResult(result);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return handleError(error, "Failed to exchange device code", "TOKEN_EXCHANGE_ERROR");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/register
|
|
3
|
+
*
|
|
4
|
+
* RFC 7591 Dynamic Client Registration. Public, unauthenticated.
|
|
5
|
+
* MCP clients call this to register before starting the OAuth flow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { apiError, handleError } from "#api/error.js";
|
|
11
|
+
import { handleOAuthClientCreate } from "#api/handlers/oauth-clients.js";
|
|
12
|
+
import {
|
|
13
|
+
disabledExperimentalSiteContextWorkflowScopes,
|
|
14
|
+
getExperimentalSiteContextWorkflowScopesDisabledMessage,
|
|
15
|
+
} from "#site-context/experimental-workflows.js";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
const OAUTH_REGISTRATION_HEADERS: HeadersInit = {
|
|
20
|
+
"Cache-Control": "no-store",
|
|
21
|
+
Pragma: "no-cache",
|
|
22
|
+
"Access-Control-Allow-Origin": "*",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const OAUTH_PREFLIGHT_HEADERS: HeadersInit = {
|
|
26
|
+
"Access-Control-Allow-Origin": "*",
|
|
27
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
28
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
29
|
+
"Access-Control-Max-Age": "86400",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SUPPORTED_GRANT_TYPES = new Set([
|
|
33
|
+
"authorization_code",
|
|
34
|
+
"refresh_token",
|
|
35
|
+
"urn:ietf:params:oauth:grant-type:device_code",
|
|
36
|
+
]);
|
|
37
|
+
const SUPPORTED_RESPONSE_TYPES = new Set(["code"]);
|
|
38
|
+
|
|
39
|
+
function registrationError(description: string, status = 400): Response {
|
|
40
|
+
return Response.json(
|
|
41
|
+
{
|
|
42
|
+
error: "invalid_client_metadata",
|
|
43
|
+
error_description: description,
|
|
44
|
+
},
|
|
45
|
+
{ status, headers: OAUTH_REGISTRATION_HEADERS },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isStringArray(value: unknown): value is string[] {
|
|
54
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseScope(value: unknown): string[] | Response | undefined {
|
|
58
|
+
if (value === undefined) return undefined;
|
|
59
|
+
if (typeof value === "string") {
|
|
60
|
+
const scopes = value.split(" ").filter(Boolean);
|
|
61
|
+
return scopes.length > 0 ? scopes : undefined;
|
|
62
|
+
}
|
|
63
|
+
if (isStringArray(value)) {
|
|
64
|
+
const scopes = value.filter(Boolean);
|
|
65
|
+
return scopes.length > 0 ? scopes : undefined;
|
|
66
|
+
}
|
|
67
|
+
return registrationError("scope must be a string or array of strings");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseSupportedStringArray(
|
|
71
|
+
value: unknown,
|
|
72
|
+
field: string,
|
|
73
|
+
supported: ReadonlySet<string>,
|
|
74
|
+
): string[] | Response | undefined {
|
|
75
|
+
if (value === undefined) return undefined;
|
|
76
|
+
if (!isStringArray(value)) {
|
|
77
|
+
return registrationError(`${field} must be an array of strings`);
|
|
78
|
+
}
|
|
79
|
+
const invalidValue = value.find((item) => !supported.has(item));
|
|
80
|
+
if (invalidValue) {
|
|
81
|
+
return registrationError(`${field} contains unsupported value: ${invalidValue}`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const OPTIONS: APIRoute = () => {
|
|
87
|
+
return new Response(null, { status: 204, headers: OAUTH_PREFLIGHT_HEADERS });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
91
|
+
const { dineway } = locals;
|
|
92
|
+
|
|
93
|
+
if (!dineway?.db) {
|
|
94
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
let body: unknown;
|
|
99
|
+
try {
|
|
100
|
+
body = await request.json();
|
|
101
|
+
} catch {
|
|
102
|
+
return registrationError("Request body must be valid JSON");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isRecord(body)) {
|
|
106
|
+
return registrationError("Request body must be a JSON object");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isStringArray(body.redirect_uris) || body.redirect_uris.length === 0) {
|
|
110
|
+
return registrationError("redirect_uris must be a non-empty array of strings");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
body.token_endpoint_auth_method !== undefined &&
|
|
115
|
+
body.token_endpoint_auth_method !== "none"
|
|
116
|
+
) {
|
|
117
|
+
return registrationError("Only token_endpoint_auth_method=none is supported");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const grantTypes = parseSupportedStringArray(
|
|
121
|
+
body.grant_types,
|
|
122
|
+
"grant_types",
|
|
123
|
+
SUPPORTED_GRANT_TYPES,
|
|
124
|
+
);
|
|
125
|
+
if (grantTypes instanceof Response) {
|
|
126
|
+
return grantTypes;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const responseTypes = parseSupportedStringArray(
|
|
130
|
+
body.response_types,
|
|
131
|
+
"response_types",
|
|
132
|
+
SUPPORTED_RESPONSE_TYPES,
|
|
133
|
+
);
|
|
134
|
+
if (responseTypes instanceof Response) {
|
|
135
|
+
return responseTypes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scopes = parseScope(body.scope);
|
|
139
|
+
if (scopes instanceof Response) {
|
|
140
|
+
return scopes;
|
|
141
|
+
}
|
|
142
|
+
if (scopes) {
|
|
143
|
+
const disabledWorkflowScopes = disabledExperimentalSiteContextWorkflowScopes(scopes);
|
|
144
|
+
if (disabledWorkflowScopes.length > 0) {
|
|
145
|
+
return registrationError(getExperimentalSiteContextWorkflowScopesDisabledMessage());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const clientId = crypto.randomUUID();
|
|
150
|
+
const clientName =
|
|
151
|
+
typeof body.client_name === "string" && body.client_name
|
|
152
|
+
? body.client_name
|
|
153
|
+
: `dynamic-${clientId.slice(0, 8)}`;
|
|
154
|
+
|
|
155
|
+
const result = await handleOAuthClientCreate(dineway.db, {
|
|
156
|
+
id: clientId,
|
|
157
|
+
name: clientName,
|
|
158
|
+
redirectUris: body.redirect_uris,
|
|
159
|
+
scopes,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
return registrationError(result.error.message);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Response.json(
|
|
167
|
+
{
|
|
168
|
+
client_id: result.data.id,
|
|
169
|
+
client_id_issued_at: Math.floor(new Date(result.data.createdAt).getTime() / 1000),
|
|
170
|
+
redirect_uris: result.data.redirectUris,
|
|
171
|
+
client_name: result.data.name,
|
|
172
|
+
grant_types: grantTypes ?? ["authorization_code", "refresh_token"],
|
|
173
|
+
response_types: responseTypes ?? ["code"],
|
|
174
|
+
token_endpoint_auth_method: "none",
|
|
175
|
+
scope: result.data.scopes ? result.data.scopes.join(" ") : undefined,
|
|
176
|
+
},
|
|
177
|
+
{ status: 201, headers: OAUTH_REGISTRATION_HEADERS },
|
|
178
|
+
);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return handleError(error, "Failed to register OAuth client", "CLIENT_REGISTER_ERROR");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/token/refresh
|
|
3
|
+
*
|
|
4
|
+
* Exchange a refresh token for a new access token.
|
|
5
|
+
* This is an unauthenticated endpoint (the caller presents the refresh token).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
12
|
+
import { handleTokenRefresh } from "#api/handlers/device-flow.js";
|
|
13
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
+
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
const refreshSchema = z.object({
|
|
18
|
+
refresh_token: z.string().min(1),
|
|
19
|
+
grant_type: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
23
|
+
const { dineway } = locals;
|
|
24
|
+
|
|
25
|
+
if (!dineway?.db) {
|
|
26
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const body = await parseBody(request, refreshSchema);
|
|
31
|
+
if (isParseError(body)) return body;
|
|
32
|
+
|
|
33
|
+
const result = await handleTokenRefresh(dineway.db, body);
|
|
34
|
+
return unwrapResult(result);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return handleError(error, "Failed to refresh token", "TOKEN_REFRESH_ERROR");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/token/revoke
|
|
3
|
+
*
|
|
4
|
+
* Revoke an access or refresh token (RFC 7009).
|
|
5
|
+
* Always returns 200, even for invalid tokens.
|
|
6
|
+
* This is an unauthenticated endpoint (the caller presents the token to revoke).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { APIRoute } from "astro";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
13
|
+
import { handleTokenRevoke } from "#api/handlers/device-flow.js";
|
|
14
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
const revokeSchema = z.object({
|
|
19
|
+
token: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
23
|
+
const { dineway } = locals;
|
|
24
|
+
|
|
25
|
+
if (!dineway?.db) {
|
|
26
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const body = await parseBody(request, revokeSchema);
|
|
31
|
+
if (isParseError(body)) return body;
|
|
32
|
+
|
|
33
|
+
const result = await handleTokenRevoke(dineway.db, body);
|
|
34
|
+
return unwrapResult(result);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return handleError(error, "Failed to revoke token", "TOKEN_REVOKE_ERROR");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/oauth/token
|
|
3
|
+
*
|
|
4
|
+
* Unified token endpoint per OAuth 2.1. Routes by `grant_type`:
|
|
5
|
+
* - authorization_code: Authorization Code + PKCE exchange
|
|
6
|
+
* - urn:ietf:params:oauth:grant-type:device_code: Device Flow
|
|
7
|
+
* - refresh_token: Token refresh
|
|
8
|
+
*
|
|
9
|
+
* Accepts both application/x-www-form-urlencoded (spec-standard) and
|
|
10
|
+
* application/json (for backwards compatibility with existing clients).
|
|
11
|
+
*
|
|
12
|
+
* This is an unauthenticated endpoint — callers present tokens/codes
|
|
13
|
+
* instead of session cookies.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { APIRoute } from "astro";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import { apiError, handleError } from "#api/error.js";
|
|
20
|
+
import { handleDeviceTokenExchange, handleTokenRefresh } from "#api/handlers/device-flow.js";
|
|
21
|
+
import { handleAuthorizationCodeExchange } from "#api/handlers/oauth-authorization.js";
|
|
22
|
+
|
|
23
|
+
export const prerender = false;
|
|
24
|
+
|
|
25
|
+
const OAUTH_TOKEN_HEADERS: HeadersInit = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"Cache-Control": "no-store",
|
|
28
|
+
Pragma: "no-cache",
|
|
29
|
+
"Access-Control-Allow-Origin": "*",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const OAUTH_PREFLIGHT_HEADERS: HeadersInit = {
|
|
33
|
+
"Access-Control-Allow-Origin": "*",
|
|
34
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
35
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
36
|
+
"Access-Control-Max-Age": "86400",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Parse helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse the request body from either form-encoded or JSON.
|
|
45
|
+
* OAuth 2.1 mandates form-encoded, but we accept both.
|
|
46
|
+
*/
|
|
47
|
+
async function parseTokenBody(request: Request): Promise<Record<string, string>> {
|
|
48
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
49
|
+
|
|
50
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
51
|
+
const text = await request.text();
|
|
52
|
+
const params = new URLSearchParams(text);
|
|
53
|
+
const result: Record<string, string> = {};
|
|
54
|
+
for (const [key, value] of params) {
|
|
55
|
+
result[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback: try JSON
|
|
61
|
+
try {
|
|
62
|
+
const json = Object(await request.json()) as Record<string, unknown>;
|
|
63
|
+
const result: Record<string, string> = {};
|
|
64
|
+
for (const [key, value] of Object.entries(json)) {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
result[key] = value;
|
|
67
|
+
} else if (typeof value === "number") {
|
|
68
|
+
result[key] = String(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Schemas
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const authCodeSchema = z.object({
|
|
82
|
+
grant_type: z.literal("authorization_code"),
|
|
83
|
+
code: z.string().min(1),
|
|
84
|
+
redirect_uri: z.string().min(1),
|
|
85
|
+
client_id: z.string().min(1),
|
|
86
|
+
code_verifier: z.string().min(43).max(128),
|
|
87
|
+
resource: z.string().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const deviceCodeSchema = z.object({
|
|
91
|
+
grant_type: z.literal("urn:ietf:params:oauth:grant-type:device_code"),
|
|
92
|
+
device_code: z.string().min(1),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const refreshSchema = z.object({
|
|
96
|
+
grant_type: z.literal("refresh_token"),
|
|
97
|
+
refresh_token: z.string().min(1),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Handler
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export const OPTIONS: APIRoute = () => {
|
|
105
|
+
return new Response(null, { status: 204, headers: OAUTH_PREFLIGHT_HEADERS });
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
109
|
+
const { dineway } = locals;
|
|
110
|
+
|
|
111
|
+
if (!dineway?.db) {
|
|
112
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const body = await parseTokenBody(request);
|
|
117
|
+
const grantType = body.grant_type;
|
|
118
|
+
|
|
119
|
+
if (!grantType) {
|
|
120
|
+
return oauthError("invalid_request", "grant_type is required", 400);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
switch (grantType) {
|
|
124
|
+
case "authorization_code": {
|
|
125
|
+
const parsed = authCodeSchema.safeParse(body);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
return oauthError("invalid_request", formatZodError(parsed.error), 400);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await handleAuthorizationCodeExchange(dineway.db, parsed.data);
|
|
131
|
+
if (!result.success) {
|
|
132
|
+
const err = result.error ?? { code: "unknown", message: "Unknown error" };
|
|
133
|
+
return oauthError(err.code, err.message, 400);
|
|
134
|
+
}
|
|
135
|
+
return oauthSuccess(result.data);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "urn:ietf:params:oauth:grant-type:device_code": {
|
|
139
|
+
const parsed = deviceCodeSchema.safeParse(body);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
return oauthError("invalid_request", formatZodError(parsed.error), 400);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await handleDeviceTokenExchange(dineway.db, parsed.data);
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
const err = result.error ?? { code: "unknown", message: "Unknown error" };
|
|
147
|
+
// RFC 8628 requires specific error format
|
|
148
|
+
if (result.deviceFlowError) {
|
|
149
|
+
return oauthError(result.deviceFlowError, err.message, 400);
|
|
150
|
+
}
|
|
151
|
+
return oauthError(err.code, err.message, 400);
|
|
152
|
+
}
|
|
153
|
+
return oauthSuccess(result.data);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "refresh_token": {
|
|
157
|
+
const parsed = refreshSchema.safeParse(body);
|
|
158
|
+
if (!parsed.success) {
|
|
159
|
+
return oauthError("invalid_request", formatZodError(parsed.error), 400);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const result = await handleTokenRefresh(dineway.db, parsed.data);
|
|
163
|
+
if (!result.success) {
|
|
164
|
+
const err = result.error ?? { code: "unknown", message: "Unknown error" };
|
|
165
|
+
return oauthError(err.code, err.message, 400);
|
|
166
|
+
}
|
|
167
|
+
return oauthSuccess(result.data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
return oauthError("unsupported_grant_type", `Unsupported grant_type: ${grantType}`, 400);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return handleError(error, "Failed to process token request", "TOKEN_ERROR");
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// OAuth response helpers (RFC 6749 §5.1 / §5.2)
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function oauthSuccess(data: unknown): Response {
|
|
183
|
+
return Response.json(data, { headers: OAUTH_TOKEN_HEADERS });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function oauthError(error: string, description: string, status: number): Response {
|
|
187
|
+
return Response.json(
|
|
188
|
+
{ error, error_description: description },
|
|
189
|
+
{ status, headers: OAUTH_TOKEN_HEADERS },
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatZodError(error: z.ZodError): string {
|
|
194
|
+
return error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
195
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI spec endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/openapi.json
|
|
5
|
+
*
|
|
6
|
+
* Returns the generated OpenAPI 3.1 document. The spec is generated once
|
|
7
|
+
* and cached for the lifetime of the process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
import { generateOpenApiDocument } from "../../../api/openapi/index.js";
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
let cachedSpec: string | null = null;
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
19
|
+
if (!cachedSpec) {
|
|
20
|
+
const maxUploadSize = locals.dineway?.config.maxUploadSize;
|
|
21
|
+
const doc = generateOpenApiDocument({ maxUploadSize });
|
|
22
|
+
cachedSpec = JSON.stringify(doc);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new Response(cachedSpec, {
|
|
26
|
+
status: 200,
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
"Cache-Control": "public, max-age=3600",
|
|
30
|
+
"Access-Control-Allow-Origin": "*",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
};
|