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.
Files changed (193) 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-BApX1xhM.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-hmtC3Cmv.mjs +6 -0
  77. package/package.json +49 -38
  78. package/src/astro/routes/admin.astro +25 -9
  79. package/src/astro/routes/api/admin/api-tokens/[id].ts +4 -0
  80. package/src/astro/routes/api/admin/api-tokens/index.ts +24 -2
  81. package/src/astro/routes/api/admin/briefing.ts +76 -0
  82. package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
  83. package/src/astro/routes/api/admin/bylines/index.ts +2 -0
  84. package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
  85. package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
  86. package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
  87. package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
  88. package/src/astro/routes/api/admin/context/diff.ts +35 -0
  89. package/src/astro/routes/api/admin/context/index.ts +69 -0
  90. package/src/astro/routes/api/admin/context/stale.ts +35 -0
  91. package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
  92. package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
  93. package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
  94. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +58 -17
  95. package/src/astro/routes/api/admin/oauth-clients/[id].ts +28 -1
  96. package/src/astro/routes/api/admin/oauth-clients/index.ts +25 -1
  97. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +54 -2
  98. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +54 -2
  99. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +51 -1
  100. package/src/astro/routes/api/admin/plugins/[id]/update.ts +98 -3
  101. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +72 -1
  102. package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
  103. package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
  104. package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
  105. package/src/astro/routes/api/admin/users/[id]/disable.ts +26 -23
  106. package/src/astro/routes/api/admin/users/[id]/index.ts +41 -21
  107. package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
  108. package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
  109. package/src/astro/routes/api/auth/passkey/options.ts +2 -1
  110. package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
  111. package/src/astro/routes/api/auth/signup/request.ts +20 -8
  112. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +3 -4
  113. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
  114. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +16 -2
  115. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +16 -0
  116. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +9 -0
  117. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
  118. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +45 -1
  119. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +12 -2
  120. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
  121. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +24 -0
  122. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +3 -0
  123. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +20 -0
  124. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +13 -0
  125. package/src/astro/routes/api/content/[collection]/[id].ts +36 -0
  126. package/src/astro/routes/api/content/[collection]/index.ts +48 -4
  127. package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
  128. package/src/astro/routes/api/health.ts +54 -0
  129. package/src/astro/routes/api/import/wordpress/analyze.ts +2 -10
  130. package/src/astro/routes/api/import/wordpress/execute.ts +40 -6
  131. package/src/astro/routes/api/import/wordpress/prepare.ts +36 -5
  132. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +33 -1
  133. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +3 -3
  134. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +57 -15
  135. package/src/astro/routes/api/manifest.ts +13 -1
  136. package/src/astro/routes/api/mcp.ts +1 -0
  137. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
  138. package/src/astro/routes/api/media/upload-url.ts +11 -2
  139. package/src/astro/routes/api/media.ts +9 -7
  140. package/src/astro/routes/api/menus/[name]/items.ts +124 -5
  141. package/src/astro/routes/api/menus/[name]/reorder.ts +47 -1
  142. package/src/astro/routes/api/menus/[name].ts +84 -4
  143. package/src/astro/routes/api/menus/index.ts +46 -2
  144. package/src/astro/routes/api/oauth/authorize.ts +21 -8
  145. package/src/astro/routes/api/oauth/device/code.ts +2 -1
  146. package/src/astro/routes/api/oauth/device/token.ts +2 -1
  147. package/src/astro/routes/api/oauth/register.ts +182 -0
  148. package/src/astro/routes/api/oauth/token.ts +18 -7
  149. package/src/astro/routes/api/openapi.json.ts +3 -2
  150. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +21 -4
  151. package/src/astro/routes/api/redirects/[id].ts +103 -4
  152. package/src/astro/routes/api/redirects/index.ts +50 -2
  153. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +28 -0
  154. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +15 -0
  155. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +13 -0
  156. package/src/astro/routes/api/schema/collections/[slug]/index.ts +27 -0
  157. package/src/astro/routes/api/schema/collections/index.ts +14 -0
  158. package/src/astro/routes/api/search/index.ts +1 -0
  159. package/src/astro/routes/api/search/suggest.ts +1 -0
  160. package/src/astro/routes/api/sections/[slug].ts +123 -4
  161. package/src/astro/routes/api/sections/index.ts +57 -2
  162. package/src/astro/routes/api/settings.ts +51 -2
  163. package/src/astro/routes/api/setup/admin-verify.ts +25 -5
  164. package/src/astro/routes/api/setup/admin.ts +16 -8
  165. package/src/astro/routes/api/setup/index.ts +3 -2
  166. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +141 -4
  167. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +64 -2
  168. package/src/astro/routes/api/taxonomies/index.ts +57 -2
  169. package/src/astro/routes/api/well-known/auth.ts +3 -1
  170. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +8 -5
  171. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
  172. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +58 -16
  173. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +124 -38
  174. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +66 -20
  175. package/src/astro/routes/api/widget-areas/[name].ts +55 -7
  176. package/src/astro/routes/api/widget-areas/index.ts +56 -6
  177. package/src/components/DinewayHead.astro +15 -7
  178. package/src/components/DinewayMedia.astro +1 -1
  179. package/src/components/InlinePortableTextEditor.tsx +1 -1
  180. package/src/components/Table.astro +68 -41
  181. package/src/components/index.ts +2 -12
  182. package/src/components/marks.ts +19 -0
  183. package/LICENSE +0 -9
  184. /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
  185. /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
  186. /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
  187. /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
  188. /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
  189. /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
  190. /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
  191. /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
  192. /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
  193. /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, createMenuBody);
55
+ const body = await parseBody(request, createMenuHitlBody);
40
56
  if (isParseError(body)) return body;
41
57
 
42
- const result = await handleMenuCreate(dineway.db, body);
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 { VALID_SCOPES } from "#auth/api-tokens.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";
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/api/oauth/authorize; HttpOnly; SameSite=Strict${secure}`;
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 validSet = new Set<string>(VALID_SCOPES);
145
- const requestedScopes = (scope ?? "")
146
- .split(" ")
147
- .filter(Boolean)
148
- .filter((s) => validSet.has(s));
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 doc = generateOpenApiDocument();
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 = ["GET", "HEAD", "OPTIONS"].includes(method)
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
- !["GET", "HEAD", "OPTIONS"].includes(method) &&
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 { isParseError, parseBody } from "#api/parse.js";
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, updateRedirectBody);
77
+ const body = await parseBody(request, updateRedirectHitlBody);
57
78
  if (isParseError(body)) return body;
58
79
 
59
- const result = await handleRedirectUpdate(db, id, body);
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, createRedirectBody);
60
+ const body = await parseBody(request, createRedirectHitlBody);
45
61
  if (isParseError(body)) return body;
46
62
 
47
- const result = await handleRedirectCreate(db, body);
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");