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
@@ -8,22 +8,33 @@
8
8
 
9
9
  import type { APIRoute } from "astro";
10
10
  import { ContentRepository, SchemaRegistry } from "dineway";
11
+ import { z } from "zod";
11
12
 
12
13
  import { requirePerm } from "#api/authorize.js";
13
14
  import { apiError, apiSuccess, handleError } from "#api/error.js";
15
+ import {
16
+ ensureWorkflowHitlRouteRequest,
17
+ hitlRequiredRouteError,
18
+ resolveHitlRouteActor,
19
+ } from "#api/hitl-route-helpers.js";
14
20
  import { isParseError, parseBody } from "#api/parse.js";
15
21
  import { wpPluginExecuteBody } from "#api/schemas.js";
16
22
  import { BylineRepository } from "#db/repositories/byline.js";
17
23
  import { getSource } from "#import/index.js";
18
- import { validateExternalUrl, SsrfError } from "#import/ssrf.js";
24
+ import { resolveAndValidateExternalUrl, SsrfError } from "#import/ssrf.js";
19
25
  import type { ImportConfig, ImportResult, NormalizedItem } from "#import/types.js";
20
26
  import { resolveImportByline } from "#import/utils.js";
21
27
  import type { FieldType } from "#schema/types.js";
28
+ import { RiskPolicyEvaluator, WordPressImportHitlPayloadBuilder } from "#site-context/index.js";
22
29
  import type { DinewayHandlers, DinewayManifest } from "#types";
23
30
  import { slugify } from "#utils/slugify.js";
24
31
 
25
32
  export const prerender = false;
26
33
 
34
+ const wpPluginExecuteRouteBody = wpPluginExecuteBody.extend({
35
+ hitlRequestId: z.string().min(1).optional(),
36
+ });
37
+
27
38
  export interface WpPluginImportConfig extends ImportConfig {
28
39
  /** Author mappings (WP author login -> Dineway user ID) */
29
40
  authorMappings?: Record<string, string | null>;
@@ -46,12 +57,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
46
57
  }
47
58
 
48
59
  try {
49
- const body = await parseBody(request, wpPluginExecuteBody);
60
+ const body = await parseBody(request, wpPluginExecuteRouteBody);
50
61
  if (isParseError(body)) return body;
51
62
 
52
- // SSRF: reject internal/private network targets
63
+ // SSRF: reject internal/private network targets before import work starts.
53
64
  try {
54
- validateExternalUrl(body.url);
65
+ await resolveAndValidateExternalUrl(body.url);
55
66
  } catch (e) {
56
67
  const msg = e instanceof SsrfError ? e.message : "Invalid URL";
57
68
  return apiError("SSRF_BLOCKED", msg, 400);
@@ -78,16 +89,39 @@ export const POST: APIRoute = async ({ request, locals }) => {
78
89
  console.log("[WP Plugin Import] Starting import for:", body.url);
79
90
  console.log("[WP Plugin Import] Post types:", postTypes);
80
91
 
92
+ const sourceInput = { type: "url", url: body.url, token: body.token } as const;
93
+ const actor = resolveHitlRouteActor(locals);
94
+ const evaluator = new RiskPolicyEvaluator({
95
+ db: dineway.db,
96
+ handlers: dineway,
97
+ });
98
+ let items: AsyncIterable<NormalizedItem> | Iterable<NormalizedItem>;
99
+
100
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
101
+ const fetchedItems = await collectPluginItems(
102
+ source.fetchContent(sourceInput, { postTypes, includeDrafts: true }),
103
+ );
104
+ const action = await new WordPressImportHitlPayloadBuilder().buildPluginExecuteRequest({
105
+ siteUrl: body.url,
106
+ config,
107
+ items: fetchedItems,
108
+ });
109
+ const decision = await evaluator.evaluateWorkflowHitl({
110
+ actor: actor.identity,
111
+ hitlRequestId: body.hitlRequestId,
112
+ action,
113
+ });
114
+ if (!decision.allowed) {
115
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
116
+ return hitlRequiredRouteError(decision, ensured);
117
+ }
118
+ items = fetchedItems;
119
+ } else {
120
+ items = source.fetchContent(sourceInput, { postTypes, includeDrafts: true });
121
+ }
122
+
81
123
  // Import content (including drafts since we have auth)
82
- const result = await importContent(
83
- source.fetchContent(
84
- { type: "url", url: body.url, token: body.token },
85
- { postTypes, includeDrafts: true },
86
- ),
87
- config,
88
- dineway,
89
- dinewayManifest,
90
- );
124
+ const result = await importContent(items, config, dineway, dinewayManifest);
91
125
 
92
126
  console.log("[WP Plugin Import] Import result:", JSON.stringify(result, null, 2));
93
127
 
@@ -134,7 +168,7 @@ const IMPORT_FIELDS: Array<{
134
168
  ];
135
169
 
136
170
  async function importContent(
137
- items: AsyncGenerator<NormalizedItem>,
171
+ items: AsyncIterable<NormalizedItem> | Iterable<NormalizedItem>,
138
172
  config: WpPluginImportConfig,
139
173
  dineway: DinewayHandlers,
140
174
  manifest: DinewayManifest,
@@ -332,7 +366,7 @@ async function importContent(
332
366
  console.error(`Import error for "${item.title || "Untitled"}":`, error);
333
367
  result.errors.push({
334
368
  title: item.title || "Untitled",
335
- error: "Failed to import item",
369
+ error: error instanceof Error && error.message ? error.message : "Failed to import item",
336
370
  });
337
371
  }
338
372
  }
@@ -355,3 +389,11 @@ function mapStatus(wpStatus: string | undefined): string {
355
389
  return "draft";
356
390
  }
357
391
  }
392
+
393
+ async function collectPluginItems(items: AsyncIterable<NormalizedItem>): Promise<NormalizedItem[]> {
394
+ const collected: NormalizedItem[] = [];
395
+ for await (const item of items) {
396
+ collected.push(item);
397
+ }
398
+ return collected;
399
+ }
@@ -11,6 +11,9 @@ import type { APIRoute } from "astro";
11
11
 
12
12
  import { getAuthMode } from "#auth/mode.js";
13
13
 
14
+ import { experimentalSiteContextWorkflowsEnabled } from "../../../site-context/experimental-workflows.js";
15
+ import { COMMIT, VERSION } from "../../../version.js";
16
+ import { getStoredConfig } from "../../integration/runtime.js";
14
17
  import type { DinewayManifest } from "../../types.js";
15
18
 
16
19
  export const prerender = false;
@@ -20,6 +23,7 @@ export const GET: APIRoute = async ({ locals }) => {
20
23
 
21
24
  // Determine auth mode from config
22
25
  const authMode = getAuthMode(dineway?.config);
26
+ const adminBranding = dinewayManifest?.admin ?? dineway?.config.admin ?? getStoredConfig()?.admin;
23
27
 
24
28
  // Check if self-signup is enabled (any allowed domain with enabled = 1)
25
29
  // Only relevant for passkey auth — external auth providers handle their own signup
@@ -39,17 +43,25 @@ export const GET: APIRoute = async ({ locals }) => {
39
43
  const manifest: DinewayManifest = dinewayManifest
40
44
  ? {
41
45
  ...dinewayManifest,
46
+ features: {
47
+ ...dinewayManifest.features,
48
+ siteContextWorkflows: experimentalSiteContextWorkflowsEnabled(),
49
+ },
42
50
  authMode: authMode.type === "external" ? authMode.providerType : "passkey",
43
51
  signupEnabled,
52
+ admin: adminBranding,
44
53
  }
45
54
  : {
46
- version: "0.1.0",
55
+ version: VERSION,
56
+ commit: COMMIT,
47
57
  hash: "default",
48
58
  collections: {},
49
59
  plugins: {},
60
+ features: { siteContextWorkflows: experimentalSiteContextWorkflowsEnabled() },
50
61
  taxonomies: [],
51
62
  authMode: "passkey",
52
63
  signupEnabled,
64
+ admin: adminBranding,
53
65
  };
54
66
 
55
67
  return Response.json(
@@ -50,6 +50,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
50
50
  userId: user.id,
51
51
  userRole: user.role,
52
52
  tokenScopes: locals.tokenScopes,
53
+ authToken: locals.authToken,
53
54
  },
54
55
  },
55
56
  });
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { APIRoute } from "astro";
9
9
 
10
+ import { requirePerm } from "#api/authorize.js";
10
11
  import { apiError, apiSuccess, handleError } from "#api/error.js";
11
12
 
12
13
  export const prerender = false;
@@ -15,7 +16,9 @@ export const prerender = false;
15
16
  * Get a single media item from a provider
16
17
  */
17
18
  export const GET: APIRoute = async ({ params, locals }) => {
18
- const { dineway } = locals;
19
+ const { dineway, user } = locals;
20
+ const denied = requirePerm(user, "media:read");
21
+ if (denied) return denied;
19
22
  const { providerId, itemId } = params;
20
23
 
21
24
  if (!providerId || !itemId) {
@@ -56,7 +59,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
56
59
  * Delete a media item from a provider
57
60
  */
58
61
  export const DELETE: APIRoute = async ({ params, locals }) => {
59
- const { dineway } = locals;
62
+ const { dineway, user } = locals;
63
+ const denied = requirePerm(user, "media:delete_any");
64
+ if (denied) return denied;
60
65
  const { providerId, itemId } = params;
61
66
 
62
67
  if (!providerId || !itemId) {
@@ -16,7 +16,7 @@ import { ulid } from "ulidx";
16
16
  import { requirePerm } from "#api/authorize.js";
17
17
  import { apiError, apiSuccess, handleError } from "#api/error.js";
18
18
  import { isParseError, parseBody } from "#api/parse.js";
19
- import { mediaUploadUrlBody } from "#api/schemas.js";
19
+ import { DEFAULT_MAX_UPLOAD_SIZE, mediaUploadUrlBody } from "#api/schemas.js";
20
20
 
21
21
  export const prerender = false;
22
22
 
@@ -59,7 +59,16 @@ export const POST: APIRoute = async ({ request, locals }) => {
59
59
  }
60
60
 
61
61
  try {
62
- const body = await parseBody(request, mediaUploadUrlBody);
62
+ const maxUploadSize = dineway.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
63
+ if (!Number.isFinite(maxUploadSize) || maxUploadSize <= 0) {
64
+ return apiError(
65
+ "CONFIGURATION_ERROR",
66
+ "Invalid maxUploadSize configuration. Expected a positive finite number.",
67
+ 500,
68
+ );
69
+ }
70
+
71
+ const body = await parseBody(request, mediaUploadUrlBody(maxUploadSize));
63
72
  if (isParseError(body)) return body;
64
73
 
65
74
  // Validate content type
@@ -13,7 +13,7 @@ import { ulid } from "ulidx";
13
13
  import { requirePerm } from "#api/authorize.js";
14
14
  import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
15
15
  import { isParseError, parseQuery } from "#api/parse.js";
16
- import { mediaListQuery } from "#api/schemas.js";
16
+ import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
17
17
  import { MediaRepository } from "#db/repositories/media.js";
18
18
  import { generatePlaceholder } from "#media/placeholder.js";
19
19
  import { computeContentHash } from "#utils/hash.js";
@@ -22,9 +22,6 @@ import type { MediaItem } from "../../types.js";
22
22
 
23
23
  export const prerender = false;
24
24
 
25
- /** Maximum allowed file upload size (50 MB). */
26
- const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
27
-
28
25
  /**
29
26
  * Add URL to media items
30
27
  * Uses relative URLs to ensure portability across deployments
@@ -89,9 +86,14 @@ export const POST: APIRoute = async ({ request, locals }) => {
89
86
  }
90
87
 
91
88
  try {
89
+ const maxUploadSize = dineway.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
90
+ if (!Number.isFinite(maxUploadSize) || maxUploadSize <= 0) {
91
+ return apiError("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration", 500);
92
+ }
93
+
92
94
  // Best-effort size check before buffering the full multipart body
93
95
  const contentLength = request.headers.get("Content-Length");
94
- if (contentLength && parseInt(contentLength, 10) > MAX_UPLOAD_SIZE) {
96
+ if (contentLength && parseInt(contentLength, 10) > maxUploadSize) {
95
97
  return apiError("PAYLOAD_TOO_LARGE", "Upload too large", 413);
96
98
  }
97
99
 
@@ -110,10 +112,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
110
112
  }
111
113
 
112
114
  // Check file size before buffering
113
- if (file.size > MAX_UPLOAD_SIZE) {
115
+ if (file.size > maxUploadSize) {
114
116
  return apiError(
115
117
  "PAYLOAD_TOO_LARGE",
116
- `File exceeds maximum size of ${MAX_UPLOAD_SIZE / 1024 / 1024}MB`,
118
+ `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,
117
119
  413,
118
120
  );
119
121
  }
@@ -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 { handleError, unwrapResult } from "#api/error.js";
@@ -15,6 +16,11 @@ import {
15
16
  handleMenuItemDelete,
16
17
  handleMenuItemUpdate,
17
18
  } from "#api/handlers/menus.js";
19
+ import {
20
+ ensureWorkflowHitlRouteRequest,
21
+ hitlRequiredRouteError,
22
+ resolveHitlRouteActor,
23
+ } from "#api/hitl-route-helpers.js";
18
24
  import { isParseError, parseBody, parseQuery } from "#api/parse.js";
19
25
  import {
20
26
  createMenuItemBody,
@@ -22,9 +28,27 @@ import {
22
28
  menuItemUpdateQuery,
23
29
  updateMenuItemBody,
24
30
  } from "#api/schemas.js";
31
+ import {
32
+ logMenuActivity,
33
+ menuApiRouteSource,
34
+ MenuHitlPayloadBuilder,
35
+ RiskPolicyEvaluator,
36
+ } from "#site-context/index.js";
25
37
 
26
38
  export const prerender = false;
27
39
 
40
+ const createMenuItemHitlBody = createMenuItemBody.extend({
41
+ hitlRequestId: z.string().min(1).optional(),
42
+ });
43
+
44
+ const updateMenuItemHitlBody = updateMenuItemBody.extend({
45
+ hitlRequestId: z.string().min(1).optional(),
46
+ });
47
+
48
+ const deleteMenuItemHitlQuery = menuItemDeleteQuery.extend({
49
+ hitlRequestId: z.string().min(1).optional(),
50
+ });
51
+
28
52
  export const POST: APIRoute = async ({ params, request, locals }) => {
29
53
  const { dineway, user } = locals;
30
54
  const name = params.name!;
@@ -33,10 +57,42 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
33
57
  if (denied) return denied;
34
58
 
35
59
  try {
36
- const body = await parseBody(request, createMenuItemBody);
60
+ const body = await parseBody(request, createMenuItemHitlBody);
37
61
  if (isParseError(body)) return body;
38
62
 
39
- const result = await handleMenuItemCreate(dineway.db, name, body);
63
+ const { hitlRequestId, ...menuInput } = body;
64
+ const actor = resolveHitlRouteActor(locals);
65
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildCreateMenuItemRequest({
66
+ menuName: name,
67
+ ...menuInput,
68
+ });
69
+ const decision = await new RiskPolicyEvaluator({
70
+ db: dineway.db,
71
+ handlers: dineway,
72
+ }).evaluateWorkflowHitl({
73
+ actor: actor.identity,
74
+ hitlRequestId,
75
+ action,
76
+ });
77
+ if (!decision.allowed) {
78
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
79
+ return hitlRequiredRouteError(decision, ensured);
80
+ }
81
+
82
+ const result = await handleMenuItemCreate(dineway.db, name, menuInput);
83
+ if (!result.success) return unwrapResult(result, 201);
84
+
85
+ await logMenuActivity(dineway.db, locals, {
86
+ action: "item_created",
87
+ menuName: name,
88
+ itemId: result.data.id,
89
+ ...menuApiRouteSource("item_created"),
90
+ summary: `Created menu item ${result.data.id} in ${name}`,
91
+ detail: {
92
+ label: result.data.label,
93
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
94
+ },
95
+ });
40
96
  return unwrapResult(result, 201);
41
97
  } catch (error) {
42
98
  return handleError(error, "Failed to create menu item", "MENU_ITEM_CREATE_ERROR");
@@ -56,10 +112,43 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
56
112
  const itemId = query.id;
57
113
 
58
114
  try {
59
- const body = await parseBody(request, updateMenuItemBody);
115
+ const body = await parseBody(request, updateMenuItemHitlBody);
60
116
  if (isParseError(body)) return body;
61
117
 
62
- const result = await handleMenuItemUpdate(dineway.db, name, itemId, body);
118
+ const { hitlRequestId, ...menuInput } = body;
119
+ const actor = resolveHitlRouteActor(locals);
120
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildUpdateMenuItemRequest({
121
+ menuName: name,
122
+ itemId,
123
+ ...menuInput,
124
+ });
125
+ const decision = await new RiskPolicyEvaluator({
126
+ db: dineway.db,
127
+ handlers: dineway,
128
+ }).evaluateWorkflowHitl({
129
+ actor: actor.identity,
130
+ hitlRequestId,
131
+ action,
132
+ });
133
+ if (!decision.allowed) {
134
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
135
+ return hitlRequiredRouteError(decision, ensured);
136
+ }
137
+
138
+ const result = await handleMenuItemUpdate(dineway.db, name, itemId, menuInput);
139
+ if (!result.success) return unwrapResult(result);
140
+
141
+ await logMenuActivity(dineway.db, locals, {
142
+ action: "item_updated",
143
+ menuName: name,
144
+ itemId,
145
+ ...menuApiRouteSource("item_updated"),
146
+ summary: `Updated menu item ${itemId} in ${name}`,
147
+ detail: {
148
+ label: result.data.label,
149
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
150
+ },
151
+ });
63
152
  return unwrapResult(result);
64
153
  } catch (error) {
65
154
  return handleError(error, "Failed to update menu item", "MENU_ITEM_UPDATE_ERROR");
@@ -74,12 +163,42 @@ export const DELETE: APIRoute = async ({ params, request, locals }) => {
74
163
  if (denied) return denied;
75
164
 
76
165
  const url = new URL(request.url);
77
- const query = parseQuery(url, menuItemDeleteQuery);
166
+ const query = parseQuery(url, deleteMenuItemHitlQuery);
78
167
  if (isParseError(query)) return query;
79
168
  const itemId = query.id;
80
169
 
81
170
  try {
171
+ const actor = resolveHitlRouteActor(locals);
172
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildDeleteMenuItemRequest({
173
+ menuName: name,
174
+ itemId,
175
+ });
176
+ const decision = await new RiskPolicyEvaluator({
177
+ db: dineway.db,
178
+ handlers: dineway,
179
+ }).evaluateWorkflowHitl({
180
+ actor: actor.identity,
181
+ hitlRequestId: query.hitlRequestId,
182
+ action,
183
+ });
184
+ if (!decision.allowed) {
185
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
186
+ return hitlRequiredRouteError(decision, ensured);
187
+ }
188
+
82
189
  const result = await handleMenuItemDelete(dineway.db, name, itemId);
190
+ if (!result.success) return unwrapResult(result);
191
+
192
+ await logMenuActivity(dineway.db, locals, {
193
+ action: "item_deleted",
194
+ menuName: name,
195
+ itemId,
196
+ ...menuApiRouteSource("item_deleted"),
197
+ summary: `Deleted menu item ${itemId} from ${name}`,
198
+ detail: {
199
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
200
+ },
201
+ });
83
202
  return unwrapResult(result);
84
203
  } catch (error) {
85
204
  return handleError(error, "Failed to delete menu item", "MENU_ITEM_DELETE_ERROR");
@@ -5,15 +5,31 @@
5
5
  */
6
6
 
7
7
  import type { APIRoute } from "astro";
8
+ import { z } from "zod";
8
9
 
9
10
  import { requirePerm } from "#api/authorize.js";
10
11
  import { handleError, unwrapResult } from "#api/error.js";
11
12
  import { handleMenuItemReorder } from "#api/handlers/menus.js";
13
+ import {
14
+ ensureWorkflowHitlRouteRequest,
15
+ hitlRequiredRouteError,
16
+ resolveHitlRouteActor,
17
+ } from "#api/hitl-route-helpers.js";
12
18
  import { isParseError, parseBody } from "#api/parse.js";
13
19
  import { reorderMenuItemsBody } from "#api/schemas.js";
20
+ import {
21
+ logMenuActivity,
22
+ menuApiRouteSource,
23
+ MenuHitlPayloadBuilder,
24
+ RiskPolicyEvaluator,
25
+ } from "#site-context/index.js";
14
26
 
15
27
  export const prerender = false;
16
28
 
29
+ const reorderMenuItemsHitlBody = reorderMenuItemsBody.extend({
30
+ hitlRequestId: z.string().min(1).optional(),
31
+ });
32
+
17
33
  export const POST: APIRoute = async ({ params, request, locals }) => {
18
34
  const { dineway, user } = locals;
19
35
  const name = params.name!;
@@ -22,10 +38,40 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
22
38
  if (denied) return denied;
23
39
 
24
40
  try {
25
- const body = await parseBody(request, reorderMenuItemsBody);
41
+ const body = await parseBody(request, reorderMenuItemsHitlBody);
26
42
  if (isParseError(body)) return body;
27
43
 
44
+ const actor = resolveHitlRouteActor(locals);
45
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildReorderMenuItemsRequest({
46
+ menuName: name,
47
+ items: body.items,
48
+ });
49
+ const decision = await new RiskPolicyEvaluator({
50
+ db: dineway.db,
51
+ handlers: dineway,
52
+ }).evaluateWorkflowHitl({
53
+ actor: actor.identity,
54
+ hitlRequestId: body.hitlRequestId,
55
+ action,
56
+ });
57
+ if (!decision.allowed) {
58
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
59
+ return hitlRequiredRouteError(decision, ensured);
60
+ }
61
+
28
62
  const result = await handleMenuItemReorder(dineway.db, name, body.items);
63
+ if (!result.success) return unwrapResult(result);
64
+
65
+ await logMenuActivity(dineway.db, locals, {
66
+ action: "reordered",
67
+ menuName: name,
68
+ ...menuApiRouteSource("reordered"),
69
+ summary: `Reordered menu ${name}`,
70
+ detail: {
71
+ itemCount: body.items.length,
72
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
73
+ },
74
+ });
29
75
  return unwrapResult(result);
30
76
  } catch (error) {
31
77
  return handleError(error, "Failed to reorder menu items", "MENU_REORDER_ERROR");
@@ -7,15 +7,35 @@
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 { handleError, unwrapResult } from "#api/error.js";
13
14
  import { handleMenuDelete, handleMenuGet, handleMenuUpdate } from "#api/handlers/menus.js";
14
- import { isParseError, parseBody } from "#api/parse.js";
15
+ import {
16
+ ensureWorkflowHitlRouteRequest,
17
+ hitlRequiredRouteError,
18
+ resolveHitlRouteActor,
19
+ } from "#api/hitl-route-helpers.js";
20
+ import { isParseError, parseBody, parseQuery } from "#api/parse.js";
15
21
  import { updateMenuBody } from "#api/schemas.js";
22
+ import {
23
+ logMenuActivity,
24
+ menuApiRouteSource,
25
+ MenuHitlPayloadBuilder,
26
+ RiskPolicyEvaluator,
27
+ } from "#site-context/index.js";
16
28
 
17
29
  export const prerender = false;
18
30
 
31
+ const updateMenuHitlBody = updateMenuBody.extend({
32
+ hitlRequestId: z.string().min(1).optional(),
33
+ });
34
+
35
+ const deleteMenuQuery = z.object({
36
+ hitlRequestId: z.string().min(1).optional(),
37
+ });
38
+
19
39
  export const GET: APIRoute = async ({ params, locals }) => {
20
40
  const { dineway, user } = locals;
21
41
  const name = params.name!;
@@ -39,25 +59,85 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
39
59
  if (denied) return denied;
40
60
 
41
61
  try {
42
- const body = await parseBody(request, updateMenuBody);
62
+ const body = await parseBody(request, updateMenuHitlBody);
43
63
  if (isParseError(body)) return body;
44
64
 
45
- const result = await handleMenuUpdate(dineway.db, name, body);
65
+ const { hitlRequestId, ...menuInput } = body;
66
+ const actor = resolveHitlRouteActor(locals);
67
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildUpdateMenuRequest({
68
+ name,
69
+ ...menuInput,
70
+ });
71
+ const decision = await new RiskPolicyEvaluator({
72
+ db: dineway.db,
73
+ handlers: dineway,
74
+ }).evaluateWorkflowHitl({
75
+ actor: actor.identity,
76
+ hitlRequestId,
77
+ action,
78
+ });
79
+ if (!decision.allowed) {
80
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
81
+ return hitlRequiredRouteError(decision, ensured);
82
+ }
83
+
84
+ const result = await handleMenuUpdate(dineway.db, name, menuInput);
85
+ if (!result.success) return unwrapResult(result);
86
+
87
+ await logMenuActivity(dineway.db, locals, {
88
+ action: "updated",
89
+ menuName: result.data.name,
90
+ ...menuApiRouteSource("updated"),
91
+ summary: `Updated menu ${result.data.name}`,
92
+ detail: {
93
+ label: result.data.label,
94
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
95
+ },
96
+ });
46
97
  return unwrapResult(result);
47
98
  } catch (error) {
48
99
  return handleError(error, "Failed to update menu", "MENU_UPDATE_ERROR");
49
100
  }
50
101
  };
51
102
 
52
- export const DELETE: APIRoute = async ({ params, locals }) => {
103
+ export const DELETE: APIRoute = async ({ params, request, locals }) => {
53
104
  const { dineway, user } = locals;
54
105
  const name = params.name!;
55
106
 
56
107
  const denied = requirePerm(user, "menus:manage");
57
108
  if (denied) return denied;
58
109
 
110
+ const query = parseQuery(new URL(request.url), deleteMenuQuery);
111
+ if (isParseError(query)) return query;
112
+
59
113
  try {
114
+ const actor = resolveHitlRouteActor(locals);
115
+ const action = await new MenuHitlPayloadBuilder(dineway.db).buildDeleteMenuRequest({ name });
116
+ const decision = await new RiskPolicyEvaluator({
117
+ db: dineway.db,
118
+ handlers: dineway,
119
+ }).evaluateWorkflowHitl({
120
+ actor: actor.identity,
121
+ hitlRequestId: query.hitlRequestId,
122
+ action,
123
+ });
124
+ if (!decision.allowed) {
125
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
126
+ return hitlRequiredRouteError(decision, ensured);
127
+ }
128
+
60
129
  const result = await handleMenuDelete(dineway.db, name);
130
+ if (!result.success) return unwrapResult(result);
131
+
132
+ await logMenuActivity(dineway.db, locals, {
133
+ action: "deleted",
134
+ menuName: name,
135
+ ...menuApiRouteSource("deleted"),
136
+ summary: `Deleted menu ${name}`,
137
+ detail: {
138
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
139
+ },
140
+ });
61
141
  return unwrapResult(result);
62
142
  } catch (error) {
63
143
  return handleError(error, "Failed to delete menu", "MENU_DELETE_ERROR");