dineway 0.1.4 → 0.1.6
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-BApX1xhM.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-hmtC3Cmv.mjs +6 -0
- package/package.json +49 -38
- package/src/astro/routes/admin.astro +25 -9
- package/src/astro/routes/api/admin/api-tokens/[id].ts +4 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +24 -2
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
- package/src/astro/routes/api/admin/bylines/index.ts +2 -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 +58 -17
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +28 -1
- package/src/astro/routes/api/admin/oauth-clients/index.ts +25 -1
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +54 -2
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +54 -2
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +51 -1
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +98 -3
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +72 -1
- 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/users/[id]/disable.ts +26 -23
- package/src/astro/routes/api/admin/users/[id]/index.ts +41 -21
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +20 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +3 -4
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +16 -2
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +16 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +9 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +45 -1
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +12 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +24 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +3 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +20 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +13 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +36 -0
- package/src/astro/routes/api/content/[collection]/index.ts +48 -4
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +2 -10
- package/src/astro/routes/api/import/wordpress/execute.ts +40 -6
- package/src/astro/routes/api/import/wordpress/prepare.ts +36 -5
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +33 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +3 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +57 -15
- package/src/astro/routes/api/manifest.ts +13 -1
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
- package/src/astro/routes/api/media/upload-url.ts +11 -2
- package/src/astro/routes/api/media.ts +9 -7
- package/src/astro/routes/api/menus/[name]/items.ts +124 -5
- package/src/astro/routes/api/menus/[name]/reorder.ts +47 -1
- package/src/astro/routes/api/menus/[name].ts +84 -4
- package/src/astro/routes/api/menus/index.ts +46 -2
- package/src/astro/routes/api/oauth/authorize.ts +21 -8
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token.ts +18 -7
- package/src/astro/routes/api/openapi.json.ts +3 -2
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +21 -4
- package/src/astro/routes/api/redirects/[id].ts +103 -4
- package/src/astro/routes/api/redirects/index.ts +50 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +28 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +15 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +13 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +27 -0
- package/src/astro/routes/api/schema/collections/index.ts +14 -0
- package/src/astro/routes/api/search/index.ts +1 -0
- package/src/astro/routes/api/search/suggest.ts +1 -0
- package/src/astro/routes/api/sections/[slug].ts +123 -4
- package/src/astro/routes/api/sections/index.ts +57 -2
- package/src/astro/routes/api/settings.ts +51 -2
- package/src/astro/routes/api/setup/admin-verify.ts +25 -5
- package/src/astro/routes/api/setup/admin.ts +16 -8
- package/src/astro/routes/api/setup/index.ts +3 -2
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +141 -4
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +64 -2
- package/src/astro/routes/api/taxonomies/index.ts +57 -2
- package/src/astro/routes/api/well-known/auth.ts +3 -1
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +8 -5
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +58 -16
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +124 -38
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +66 -20
- package/src/astro/routes/api/widget-areas/[name].ts +55 -7
- package/src/astro/routes/api/widget-areas/index.ts +56 -6
- package/src/components/DinewayHead.astro +15 -7
- package/src/components/DinewayMedia.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +1 -1
- package/src/components/Table.astro +68 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +19 -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
|
@@ -6,15 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
|
|
10
11
|
import { requirePerm } from "#api/authorize.js";
|
|
11
12
|
import { handleError, unwrapResult } from "#api/error.js";
|
|
12
13
|
import { handleMenuCreate, handleMenuList } from "#api/handlers/menus.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureWorkflowHitlRouteRequest,
|
|
16
|
+
hitlRequiredRouteError,
|
|
17
|
+
resolveHitlRouteActor,
|
|
18
|
+
} from "#api/hitl-route-helpers.js";
|
|
13
19
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
20
|
import { createMenuBody } from "#api/schemas.js";
|
|
21
|
+
import {
|
|
22
|
+
logMenuActivity,
|
|
23
|
+
menuApiRouteSource,
|
|
24
|
+
MenuHitlPayloadBuilder,
|
|
25
|
+
RiskPolicyEvaluator,
|
|
26
|
+
} from "#site-context/index.js";
|
|
15
27
|
|
|
16
28
|
export const prerender = false;
|
|
17
29
|
|
|
30
|
+
const createMenuHitlBody = createMenuBody.extend({
|
|
31
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
18
34
|
export const GET: APIRoute = async ({ locals }) => {
|
|
19
35
|
const { dineway, user } = locals;
|
|
20
36
|
|
|
@@ -36,10 +52,38 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
36
52
|
if (denied) return denied;
|
|
37
53
|
|
|
38
54
|
try {
|
|
39
|
-
const body = await parseBody(request,
|
|
55
|
+
const body = await parseBody(request, createMenuHitlBody);
|
|
40
56
|
if (isParseError(body)) return body;
|
|
41
57
|
|
|
42
|
-
const
|
|
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
|
+
});
|
|
43
87
|
return unwrapResult(result, 201);
|
|
44
88
|
} catch (error) {
|
|
45
89
|
return handleError(error, "Failed to create menu", "MENU_CREATE_ERROR");
|
|
@@ -19,11 +19,16 @@ import { escapeHtml } from "#api/escape.js";
|
|
|
19
19
|
import {
|
|
20
20
|
buildDeniedRedirect,
|
|
21
21
|
handleAuthorizationApproval,
|
|
22
|
-
validateRedirectUri,
|
|
23
22
|
} from "#api/handlers/oauth-authorization.js";
|
|
24
23
|
import { lookupOAuthClient, validateClientRedirectUri } from "#api/handlers/oauth-clients.js";
|
|
24
|
+
import { validateRedirectUri } from "#api/oauth/redirect-uri.js";
|
|
25
25
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
26
|
-
import {
|
|
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";
|
|
27
32
|
|
|
28
33
|
export const prerender = false;
|
|
29
34
|
|
|
@@ -52,7 +57,7 @@ function csrfCookieHeader(token: string, request: Request, siteUrl?: string): st
|
|
|
52
57
|
? siteUrl.startsWith("https:")
|
|
53
58
|
: new URL(request.url).protocol === "https:";
|
|
54
59
|
const secure = isSecure ? "; Secure" : "";
|
|
55
|
-
return `${CSRF_COOKIE_NAME}=${token}; Path=/_dineway/
|
|
60
|
+
return `${CSRF_COOKIE_NAME}=${token}; Path=/_dineway/oauth/authorize; HttpOnly; SameSite=Strict${secure}`;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/** Extract the CSRF token from the request's cookies. */
|
|
@@ -141,11 +146,19 @@ export const GET: APIRoute = async ({ url, request, locals }) => {
|
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
// Parse and validate scopes
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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));
|
|
149
162
|
|
|
150
163
|
if (requestedScopes.length === 0) {
|
|
151
164
|
return new Response(renderErrorPage("No valid scopes requested."), {
|
|
@@ -15,6 +15,7 @@ import { handleDeviceCodeRequest } from "#api/handlers/device-flow.js";
|
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
16
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
17
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
18
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
18
19
|
|
|
19
20
|
export const prerender = false;
|
|
20
21
|
|
|
@@ -35,7 +36,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|
|
35
36
|
if (isParseError(body)) return body;
|
|
36
37
|
|
|
37
38
|
// Rate limit: 10 requests per 60 seconds per IP
|
|
38
|
-
const ip = getClientIp(request);
|
|
39
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
39
40
|
const rateLimit = await checkRateLimit(dineway.db, ip, "device/code", 10, 60);
|
|
40
41
|
if (!rateLimit.allowed) {
|
|
41
42
|
return rateLimitResponse(60);
|
|
@@ -17,6 +17,7 @@ import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
|
17
17
|
import { handleDeviceTokenExchange } from "#api/handlers/device-flow.js";
|
|
18
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
19
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
20
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
20
21
|
|
|
21
22
|
export const prerender = false;
|
|
22
23
|
|
|
@@ -37,7 +38,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
37
38
|
if (isParseError(body)) return body;
|
|
38
39
|
|
|
39
40
|
// Rate limit: 12 requests per 60 seconds per IP
|
|
40
|
-
const ip = getClientIp(request);
|
|
41
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
41
42
|
const rateLimit = await checkRateLimit(dineway.db, ip, "device/token", 12, 60);
|
|
42
43
|
if (!rateLimit.allowed) {
|
|
43
44
|
return rateLimitResponse(60);
|
|
@@ -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
|
+
};
|
|
@@ -22,6 +22,20 @@ import { handleAuthorizationCodeExchange } from "#api/handlers/oauth-authorizati
|
|
|
22
22
|
|
|
23
23
|
export const prerender = false;
|
|
24
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
|
+
|
|
25
39
|
// ---------------------------------------------------------------------------
|
|
26
40
|
// Parse helpers
|
|
27
41
|
// ---------------------------------------------------------------------------
|
|
@@ -87,6 +101,10 @@ const refreshSchema = z.object({
|
|
|
87
101
|
// Handler
|
|
88
102
|
// ---------------------------------------------------------------------------
|
|
89
103
|
|
|
104
|
+
export const OPTIONS: APIRoute = () => {
|
|
105
|
+
return new Response(null, { status: 204, headers: OAUTH_PREFLIGHT_HEADERS });
|
|
106
|
+
};
|
|
107
|
+
|
|
90
108
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
91
109
|
const { dineway } = locals;
|
|
92
110
|
|
|
@@ -161,13 +179,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
161
179
|
// OAuth response helpers (RFC 6749 §5.1 / §5.2)
|
|
162
180
|
// ---------------------------------------------------------------------------
|
|
163
181
|
|
|
164
|
-
/** RFC 6749 §5.1 requires Cache-Control: no-store and Pragma: no-cache on token responses */
|
|
165
|
-
const OAUTH_TOKEN_HEADERS: HeadersInit = {
|
|
166
|
-
"Content-Type": "application/json",
|
|
167
|
-
"Cache-Control": "no-store",
|
|
168
|
-
Pragma: "no-cache",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
182
|
function oauthSuccess(data: unknown): Response {
|
|
172
183
|
return Response.json(data, { headers: OAUTH_TOKEN_HEADERS });
|
|
173
184
|
}
|
|
@@ -15,9 +15,10 @@ export const prerender = false;
|
|
|
15
15
|
|
|
16
16
|
let cachedSpec: string | null = null;
|
|
17
17
|
|
|
18
|
-
export const GET: APIRoute = async () => {
|
|
18
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
19
19
|
if (!cachedSpec) {
|
|
20
|
-
const
|
|
20
|
+
const maxUploadSize = locals.dineway?.config.maxUploadSize;
|
|
21
|
+
const doc = generateOpenApiDocument({ maxUploadSize });
|
|
21
22
|
cachedSpec = JSON.stringify(doc);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -39,10 +39,10 @@ const handleRequest: APIRoute = async ({ params, request, locals }) => {
|
|
|
39
39
|
|
|
40
40
|
// Public routes skip auth, CSRF, and scope checks entirely
|
|
41
41
|
if (!routeMeta.public) {
|
|
42
|
+
const isStateChangingMethod = !["GET", "HEAD", "OPTIONS"].includes(method);
|
|
43
|
+
|
|
42
44
|
// Private routes require authentication and permission checks
|
|
43
|
-
const permission =
|
|
44
|
-
? "plugins:read"
|
|
45
|
-
: "plugins:manage";
|
|
45
|
+
const permission = isStateChangingMethod ? "plugins:manage" : "plugins:read";
|
|
46
46
|
const denied = requirePerm(user, permission);
|
|
47
47
|
if (denied) return denied;
|
|
48
48
|
|
|
@@ -51,13 +51,30 @@ const handleRequest: APIRoute = async ({ params, request, locals }) => {
|
|
|
51
51
|
const scopeError = requireScope(locals, "admin");
|
|
52
52
|
if (scopeError) return scopeError;
|
|
53
53
|
|
|
54
|
+
// Generic private plugin routes can carry arbitrary plugin-defined payloads,
|
|
55
|
+
// including secret-bearing config. Until route metadata can describe a
|
|
56
|
+
// redacted review payload plus immutable reviewed target, API-token writes
|
|
57
|
+
// stay blocked instead of being routed through generic HITL.
|
|
58
|
+
if (isStateChangingMethod && locals.authToken?.type === "api_token") {
|
|
59
|
+
return apiError(
|
|
60
|
+
"HITL_UNSUPPORTED",
|
|
61
|
+
"Private plugin routes do not yet support API-token writes without a reviewed redaction contract",
|
|
62
|
+
409,
|
|
63
|
+
{
|
|
64
|
+
reason: "review_contract_missing",
|
|
65
|
+
pluginId,
|
|
66
|
+
path: `/${path}`,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// CSRF protection for state-changing requests on private routes.
|
|
55
72
|
// Plugin routes use soft auth in the middleware (user resolved but not required),
|
|
56
73
|
// so the middleware's CSRF check doesn't run. We enforce it here for private routes.
|
|
57
74
|
// Token-authed requests (which set tokenScopes) are exempt — tokens aren't
|
|
58
75
|
// ambient credentials like cookies.
|
|
59
76
|
if (
|
|
60
|
-
|
|
77
|
+
isStateChangingMethod &&
|
|
61
78
|
!locals.tokenScopes &&
|
|
62
79
|
request.headers.get("X-Dineway-Request") !== "1"
|
|
63
80
|
) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { APIRoute } from "astro";
|
|
10
|
+
import { z } from "zod";
|
|
10
11
|
|
|
11
12
|
import { requirePerm } from "#api/authorize.js";
|
|
12
13
|
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
@@ -15,11 +16,31 @@ import {
|
|
|
15
16
|
handleRedirectGet,
|
|
16
17
|
handleRedirectUpdate,
|
|
17
18
|
} from "#api/handlers/redirects.js";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ensureWorkflowHitlRouteRequest,
|
|
21
|
+
hitlRequiredRouteError,
|
|
22
|
+
resolveHitlRouteActor,
|
|
23
|
+
} from "#api/hitl-route-helpers.js";
|
|
24
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
19
25
|
import { updateRedirectBody } from "#api/schemas.js";
|
|
26
|
+
import {
|
|
27
|
+
activityChangedKeys,
|
|
28
|
+
logRedirectActivity,
|
|
29
|
+
redirectApiRouteSource,
|
|
30
|
+
RedirectHitlPayloadBuilder,
|
|
31
|
+
RiskPolicyEvaluator,
|
|
32
|
+
} from "#site-context/index.js";
|
|
20
33
|
|
|
21
34
|
export const prerender = false;
|
|
22
35
|
|
|
36
|
+
const updateRedirectHitlBody = updateRedirectBody.extend({
|
|
37
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const deleteRedirectQuery = z.object({
|
|
41
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
23
44
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
24
45
|
const { dineway, user } = locals;
|
|
25
46
|
const db = dineway.db;
|
|
@@ -53,17 +74,56 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
try {
|
|
56
|
-
const body = await parseBody(request,
|
|
77
|
+
const body = await parseBody(request, updateRedirectHitlBody);
|
|
57
78
|
if (isParseError(body)) return body;
|
|
58
79
|
|
|
59
|
-
const
|
|
80
|
+
const current = await handleRedirectGet(db, id);
|
|
81
|
+
if (!current.success) return unwrapResult(current);
|
|
82
|
+
|
|
83
|
+
const { hitlRequestId, ...redirectInput } = body;
|
|
84
|
+
const actor = resolveHitlRouteActor(locals);
|
|
85
|
+
const action = await new RedirectHitlPayloadBuilder().buildUpdateRedirectRequest({
|
|
86
|
+
redirect: current.data,
|
|
87
|
+
...redirectInput,
|
|
88
|
+
});
|
|
89
|
+
const decision = await new RiskPolicyEvaluator({
|
|
90
|
+
db,
|
|
91
|
+
handlers: dineway,
|
|
92
|
+
}).evaluateWorkflowHitl({
|
|
93
|
+
actor: actor.identity,
|
|
94
|
+
hitlRequestId,
|
|
95
|
+
action,
|
|
96
|
+
});
|
|
97
|
+
if (!decision.allowed) {
|
|
98
|
+
const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
|
|
99
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await handleRedirectUpdate(db, id, redirectInput);
|
|
103
|
+
if (!result.success) return unwrapResult(result);
|
|
104
|
+
|
|
105
|
+
await logRedirectActivity(db, locals, {
|
|
106
|
+
action: "updated",
|
|
107
|
+
redirectId: result.data.id,
|
|
108
|
+
source: result.data.source,
|
|
109
|
+
destination: result.data.destination,
|
|
110
|
+
...redirectApiRouteSource("updated"),
|
|
111
|
+
summary: `Updated redirect ${result.data.source} -> ${result.data.destination}`,
|
|
112
|
+
detail: {
|
|
113
|
+
changedKeys: activityChangedKeys(redirectInput),
|
|
114
|
+
type: result.data.type,
|
|
115
|
+
enabled: result.data.enabled,
|
|
116
|
+
groupName: result.data.groupName,
|
|
117
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
60
120
|
return unwrapResult(result);
|
|
61
121
|
} catch (error) {
|
|
62
122
|
return handleError(error, "Failed to update redirect", "REDIRECT_UPDATE_ERROR");
|
|
63
123
|
}
|
|
64
124
|
};
|
|
65
125
|
|
|
66
|
-
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
126
|
+
export const DELETE: APIRoute = async ({ params, request, locals }) => {
|
|
67
127
|
const { dineway, user } = locals;
|
|
68
128
|
const db = dineway.db;
|
|
69
129
|
const { id } = params;
|
|
@@ -75,8 +135,47 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
|
75
135
|
return apiError("VALIDATION_ERROR", "id is required", 400);
|
|
76
136
|
}
|
|
77
137
|
|
|
138
|
+
const query = parseQuery(new URL(request.url), deleteRedirectQuery);
|
|
139
|
+
if (isParseError(query)) return query;
|
|
140
|
+
|
|
78
141
|
try {
|
|
142
|
+
const current = await handleRedirectGet(db, id);
|
|
143
|
+
if (!current.success) return unwrapResult(current);
|
|
144
|
+
|
|
145
|
+
const actor = resolveHitlRouteActor(locals);
|
|
146
|
+
const action = await new RedirectHitlPayloadBuilder().buildDeleteRedirectRequest({
|
|
147
|
+
redirect: current.data,
|
|
148
|
+
});
|
|
149
|
+
const decision = await new RiskPolicyEvaluator({
|
|
150
|
+
db,
|
|
151
|
+
handlers: dineway,
|
|
152
|
+
}).evaluateWorkflowHitl({
|
|
153
|
+
actor: actor.identity,
|
|
154
|
+
hitlRequestId: query.hitlRequestId,
|
|
155
|
+
action,
|
|
156
|
+
});
|
|
157
|
+
if (!decision.allowed) {
|
|
158
|
+
const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
|
|
159
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
160
|
+
}
|
|
161
|
+
|
|
79
162
|
const result = await handleRedirectDelete(db, id);
|
|
163
|
+
if (!result.success) return unwrapResult(result);
|
|
164
|
+
|
|
165
|
+
await logRedirectActivity(db, locals, {
|
|
166
|
+
action: "deleted",
|
|
167
|
+
redirectId: current.data.id,
|
|
168
|
+
source: current.data.source,
|
|
169
|
+
destination: current.data.destination,
|
|
170
|
+
...redirectApiRouteSource("deleted"),
|
|
171
|
+
summary: `Deleted redirect ${current.data.source} -> ${current.data.destination}`,
|
|
172
|
+
detail: {
|
|
173
|
+
type: current.data.type,
|
|
174
|
+
enabled: current.data.enabled,
|
|
175
|
+
groupName: current.data.groupName,
|
|
176
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
80
179
|
return unwrapResult(result);
|
|
81
180
|
} catch (error) {
|
|
82
181
|
return handleError(error, "Failed to delete redirect", "REDIRECT_DELETE_ERROR");
|
|
@@ -6,15 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
|
|
10
11
|
import { requirePerm } from "#api/authorize.js";
|
|
11
12
|
import { handleError, unwrapResult } from "#api/error.js";
|
|
12
13
|
import { handleRedirectCreate, handleRedirectList } from "#api/handlers/redirects.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureWorkflowHitlRouteRequest,
|
|
16
|
+
hitlRequiredRouteError,
|
|
17
|
+
resolveHitlRouteActor,
|
|
18
|
+
} from "#api/hitl-route-helpers.js";
|
|
13
19
|
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
20
|
import { createRedirectBody, redirectsListQuery } from "#api/schemas.js";
|
|
21
|
+
import {
|
|
22
|
+
logRedirectActivity,
|
|
23
|
+
redirectApiRouteSource,
|
|
24
|
+
RedirectHitlPayloadBuilder,
|
|
25
|
+
RiskPolicyEvaluator,
|
|
26
|
+
} from "#site-context/index.js";
|
|
15
27
|
|
|
16
28
|
export const prerender = false;
|
|
17
29
|
|
|
30
|
+
const createRedirectHitlBody = createRedirectBody.extend({
|
|
31
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
18
34
|
export const GET: APIRoute = async ({ url, locals }) => {
|
|
19
35
|
const { dineway, user } = locals;
|
|
20
36
|
const db = dineway.db;
|
|
@@ -41,10 +57,42 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
41
57
|
if (denied) return denied;
|
|
42
58
|
|
|
43
59
|
try {
|
|
44
|
-
const body = await parseBody(request,
|
|
60
|
+
const body = await parseBody(request, createRedirectHitlBody);
|
|
45
61
|
if (isParseError(body)) return body;
|
|
46
62
|
|
|
47
|
-
const
|
|
63
|
+
const { hitlRequestId, ...redirectInput } = body;
|
|
64
|
+
const actor = resolveHitlRouteActor(locals);
|
|
65
|
+
const action = await new RedirectHitlPayloadBuilder().buildCreateRedirectRequest(redirectInput);
|
|
66
|
+
const decision = await new RiskPolicyEvaluator({
|
|
67
|
+
db,
|
|
68
|
+
handlers: dineway,
|
|
69
|
+
}).evaluateWorkflowHitl({
|
|
70
|
+
actor: actor.identity,
|
|
71
|
+
hitlRequestId,
|
|
72
|
+
action,
|
|
73
|
+
});
|
|
74
|
+
if (!decision.allowed) {
|
|
75
|
+
const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
|
|
76
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await handleRedirectCreate(db, redirectInput);
|
|
80
|
+
if (!result.success) return unwrapResult(result, 201);
|
|
81
|
+
|
|
82
|
+
await logRedirectActivity(db, locals, {
|
|
83
|
+
action: "created",
|
|
84
|
+
redirectId: result.data.id,
|
|
85
|
+
source: result.data.source,
|
|
86
|
+
destination: result.data.destination,
|
|
87
|
+
...redirectApiRouteSource("created"),
|
|
88
|
+
summary: `Created redirect ${result.data.source} -> ${result.data.destination}`,
|
|
89
|
+
detail: {
|
|
90
|
+
type: result.data.type,
|
|
91
|
+
enabled: result.data.enabled,
|
|
92
|
+
groupName: result.data.groupName,
|
|
93
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
48
96
|
return unwrapResult(result, 201);
|
|
49
97
|
} catch (error) {
|
|
50
98
|
return handleError(error, "Failed to create redirect", "REDIRECT_CREATE_ERROR");
|