dineway 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-ByRGV2pq.mjs} +570 -424
  59. package/dist/seed/index.d.mts +2 -2
  60. package/dist/seed/index.mjs +11 -10
  61. package/dist/seo/index.d.mts +1 -1
  62. package/dist/storage/local.d.mts +1 -1
  63. package/dist/storage/local.mjs +1 -1
  64. package/dist/storage/s3.d.mts +11 -3
  65. package/dist/storage/s3.mjs +78 -15
  66. package/dist/taxonomies-1s5PaS_8.mjs +266 -0
  67. package/dist/transaction-Cn2rjY78.mjs +27 -0
  68. package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
  69. package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
  70. package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
  71. package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
  72. package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
  73. package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
  74. package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
  75. package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
  76. package/dist/version-BKXPsfmJ.mjs +6 -0
  77. package/package.json +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,14 +6,35 @@
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 { apiError, apiSuccess, handleError } from "#api/error.js";
12
- import { isParseError, parseBody } from "#api/parse.js";
13
+ import {
14
+ ensureWorkflowHitlRouteRequest,
15
+ hitlRequiredRouteError,
16
+ resolveHitlRouteActor,
17
+ } from "#api/hitl-route-helpers.js";
18
+ import { isParseError, parseBody, parseQuery } from "#api/parse.js";
13
19
  import { updateWidgetBody } from "#api/schemas.js";
20
+ import {
21
+ activityChangedKeys,
22
+ logWidgetActivity,
23
+ RiskPolicyEvaluator,
24
+ widgetApiRouteSource,
25
+ WidgetHitlPayloadBuilder,
26
+ } from "#site-context/index.js";
14
27
 
15
28
  export const prerender = false;
16
29
 
30
+ const updateWidgetHitlBody = updateWidgetBody.extend({
31
+ hitlRequestId: z.string().min(1).optional(),
32
+ });
33
+
34
+ const deleteWidgetQuery = z.object({
35
+ hitlRequestId: z.string().min(1).optional(),
36
+ });
37
+
17
38
  export const PUT: APIRoute = async ({ params, request, locals }) => {
18
39
  const { dineway, user } = locals;
19
40
  const db = dineway.db;
@@ -27,42 +48,50 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
27
48
  }
28
49
 
29
50
  try {
30
- // Get the area
31
- const area = await db
32
- .selectFrom("_dineway_widget_areas")
33
- .select("id")
34
- .where("name", "=", name)
35
- .executeTakeFirst();
51
+ const payloadBuilder = new WidgetHitlPayloadBuilder(db);
52
+ const area = await payloadBuilder.loadWidgetAreaSnapshot(name);
36
53
 
37
54
  if (!area) {
38
55
  return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
39
56
  }
40
57
 
41
- // Check widget exists and belongs to this area
42
- const existingWidget = await db
43
- .selectFrom("_dineway_widgets")
44
- .select("id")
45
- .where("id", "=", id)
46
- .where("area_id", "=", area.id)
47
- .executeTakeFirst();
58
+ const existingWidget = area.widgets.find((widget) => widget.id === id);
48
59
 
49
60
  if (!existingWidget) {
50
61
  return apiError("NOT_FOUND", `Widget "${id}" not found in area "${name}"`, 404);
51
62
  }
52
63
 
53
- const body = await parseBody(request, updateWidgetBody);
64
+ const body = await parseBody(request, updateWidgetHitlBody);
54
65
  if (isParseError(body)) return body;
66
+ const { hitlRequestId, ...widgetInput } = body;
67
+
68
+ const actor = resolveHitlRouteActor(locals);
69
+ const evaluator = new RiskPolicyEvaluator({
70
+ db,
71
+ handlers: dineway,
72
+ });
73
+ let approvedHitlRequestId: string | null = null;
74
+
75
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
76
+ const action = await payloadBuilder.buildUpdateWidgetRequest({
77
+ area,
78
+ currentWidget: existingWidget,
79
+ ...widgetInput,
80
+ });
81
+ const decision = await evaluator.evaluateWorkflowHitl({
82
+ actor: actor.identity,
83
+ hitlRequestId,
84
+ action,
85
+ });
86
+ if (!decision.allowed) {
87
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
88
+ return hitlRequiredRouteError(decision, ensured);
89
+ }
90
+ approvedHitlRequestId = decision.hitlRequest.id;
91
+ }
55
92
 
56
93
  // Build update object (only update provided fields)
57
- const updates: Record<string, unknown> = {};
58
- if (body.title !== undefined) updates.title = body.title || null;
59
- if (body.type !== undefined) updates.type = body.type;
60
- if (body.content !== undefined)
61
- updates.content = body.content ? JSON.stringify(body.content) : null;
62
- if (body.menuName !== undefined) updates.menu_name = body.menuName || null;
63
- if (body.componentId !== undefined) updates.component_id = body.componentId || null;
64
- if (body.componentProps !== undefined)
65
- updates.component_props = body.componentProps ? JSON.stringify(body.componentProps) : null;
94
+ const updates = buildWidgetUpdates(widgetInput);
66
95
 
67
96
  if (Object.keys(updates).length === 0) {
68
97
  return apiError("VALIDATION_ERROR", "No fields to update", 400);
@@ -76,13 +105,26 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
76
105
  .where("id", "=", id)
77
106
  .executeTakeFirstOrThrow();
78
107
 
108
+ await logWidgetActivity(db, locals, {
109
+ action: "updated",
110
+ areaId: area.id,
111
+ areaName: area.name,
112
+ widgetId: widget.id,
113
+ ...widgetApiRouteSource("updated"),
114
+ detail: {
115
+ changedKeys: activityChangedKeys(widgetInput as Record<string, unknown>),
116
+ type: widget.type,
117
+ hitlRequestId: approvedHitlRequestId,
118
+ },
119
+ });
120
+
79
121
  return apiSuccess(widget);
80
122
  } catch (error) {
81
123
  return handleError(error, "Failed to update widget", "WIDGET_UPDATE_ERROR");
82
124
  }
83
125
  };
84
126
 
85
- export const DELETE: APIRoute = async ({ params, locals }) => {
127
+ export const DELETE: APIRoute = async ({ params, request, locals }) => {
86
128
  const { dineway, user } = locals;
87
129
  const db = dineway.db;
88
130
  const { name, id } = params;
@@ -94,34 +136,78 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
94
136
  return apiError("VALIDATION_ERROR", "name and id are required", 400);
95
137
  }
96
138
 
139
+ const query = parseQuery(new URL(request.url), deleteWidgetQuery);
140
+ if (isParseError(query)) return query;
141
+
97
142
  try {
98
- // Get the area
99
- const area = await db
100
- .selectFrom("_dineway_widget_areas")
101
- .select("id")
102
- .where("name", "=", name)
103
- .executeTakeFirst();
143
+ const payloadBuilder = new WidgetHitlPayloadBuilder(db);
144
+ const area = await payloadBuilder.loadWidgetAreaSnapshot(name);
104
145
 
105
146
  if (!area) {
106
147
  return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
107
148
  }
108
149
 
109
- // Check widget exists and belongs to this area
110
- const existingWidget = await db
111
- .selectFrom("_dineway_widgets")
112
- .select("id")
113
- .where("id", "=", id)
114
- .where("area_id", "=", area.id)
115
- .executeTakeFirst();
150
+ const existingWidget = area.widgets.find((widget) => widget.id === id);
116
151
 
117
152
  if (!existingWidget) {
118
153
  return apiError("NOT_FOUND", `Widget "${id}" not found in area "${name}"`, 404);
119
154
  }
120
155
 
156
+ const actor = resolveHitlRouteActor(locals);
157
+ const evaluator = new RiskPolicyEvaluator({
158
+ db,
159
+ handlers: dineway,
160
+ });
161
+ let approvedHitlRequestId: string | null = null;
162
+
163
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
164
+ const action = await payloadBuilder.buildDeleteWidgetRequest({
165
+ area,
166
+ currentWidget: existingWidget,
167
+ });
168
+ const decision = await evaluator.evaluateWorkflowHitl({
169
+ actor: actor.identity,
170
+ hitlRequestId: query.hitlRequestId,
171
+ action,
172
+ });
173
+ if (!decision.allowed) {
174
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
175
+ return hitlRequiredRouteError(decision, ensured);
176
+ }
177
+ approvedHitlRequestId = decision.hitlRequest.id;
178
+ }
179
+
121
180
  await db.deleteFrom("_dineway_widgets").where("id", "=", id).execute();
122
181
 
182
+ await logWidgetActivity(db, locals, {
183
+ action: "deleted",
184
+ areaId: area.id,
185
+ areaName: area.name,
186
+ widgetId: existingWidget.id,
187
+ ...widgetApiRouteSource("deleted"),
188
+ detail: {
189
+ type: existingWidget.type,
190
+ sortOrder: existingWidget.sortOrder,
191
+ hitlRequestId: approvedHitlRequestId,
192
+ },
193
+ });
194
+
123
195
  return apiSuccess({ deleted: true });
124
196
  } catch (error) {
125
197
  return handleError(error, "Failed to delete widget", "WIDGET_DELETE_ERROR");
126
198
  }
127
199
  };
200
+
201
+ function buildWidgetUpdates(body: z.infer<typeof updateWidgetBody>): Record<string, unknown> {
202
+ const updates: Record<string, unknown> = {};
203
+ if (body.title !== undefined) updates.title = body.title || null;
204
+ if (body.type !== undefined) updates.type = body.type;
205
+ if (body.content !== undefined)
206
+ updates.content = body.content ? JSON.stringify(body.content) : null;
207
+ if (body.menuName !== undefined) updates.menu_name = body.menuName || null;
208
+ if (body.componentId !== undefined) updates.component_id = body.componentId || null;
209
+ if (body.componentProps !== undefined) {
210
+ updates.component_props = body.componentProps ? JSON.stringify(body.componentProps) : null;
211
+ }
212
+ return updates;
213
+ }
@@ -6,14 +6,30 @@
6
6
 
7
7
  import type { APIRoute } from "astro";
8
8
  import { ulid } from "ulidx";
9
+ import { z } from "zod";
9
10
 
10
11
  import { requirePerm } from "#api/authorize.js";
11
12
  import { apiError, apiSuccess, handleError } from "#api/error.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 { createWidgetBody } from "#api/schemas.js";
20
+ import {
21
+ logWidgetActivity,
22
+ RiskPolicyEvaluator,
23
+ widgetApiRouteSource,
24
+ WidgetHitlPayloadBuilder,
25
+ } from "#site-context/index.js";
14
26
 
15
27
  export const prerender = false;
16
28
 
29
+ const createWidgetHitlBody = createWidgetBody.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 db = dineway.db;
@@ -27,28 +43,42 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
27
43
  }
28
44
 
29
45
  try {
30
- // Get the area
31
- const area = await db
32
- .selectFrom("_dineway_widget_areas")
33
- .select("id")
34
- .where("name", "=", name)
35
- .executeTakeFirst();
46
+ const payloadBuilder = new WidgetHitlPayloadBuilder(db);
47
+ const area = await payloadBuilder.loadWidgetAreaSnapshot(name);
36
48
 
37
49
  if (!area) {
38
50
  return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
39
51
  }
40
52
 
41
- const body = await parseBody(request, createWidgetBody);
53
+ const body = await parseBody(request, createWidgetHitlBody);
42
54
  if (isParseError(body)) return body;
55
+ const { hitlRequestId, ...widgetInput } = body;
43
56
 
44
- // Get max sort_order
45
- const maxOrder = await db
46
- .selectFrom("_dineway_widgets")
47
- .select(({ fn }) => fn.max("sort_order").as("maxOrder"))
48
- .where("area_id", "=", area.id)
49
- .executeTakeFirst();
57
+ const actor = resolveHitlRouteActor(locals);
58
+ const evaluator = new RiskPolicyEvaluator({
59
+ db,
60
+ handlers: dineway,
61
+ });
62
+ let approvedHitlRequestId: string | null = null;
50
63
 
51
- const sortOrder = (maxOrder?.maxOrder ?? -1) + 1;
64
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
65
+ const action = await payloadBuilder.buildCreateWidgetRequest({
66
+ area,
67
+ ...widgetInput,
68
+ });
69
+ const decision = await evaluator.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
+ approvedHitlRequestId = decision.hitlRequest.id;
79
+ }
80
+
81
+ const sortOrder = (area.widgets.at(-1)?.sortOrder ?? -1) + 1;
52
82
 
53
83
  // Prepare values
54
84
  const id = ulid();
@@ -58,12 +88,14 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
58
88
  id,
59
89
  area_id: area.id,
60
90
  sort_order: sortOrder,
61
- type: body.type,
62
- title: body.title ?? null,
63
- content: body.content ? JSON.stringify(body.content) : null,
64
- menu_name: body.menuName ?? null,
65
- component_id: body.componentId ?? null,
66
- component_props: body.componentProps ? JSON.stringify(body.componentProps) : null,
91
+ type: widgetInput.type,
92
+ title: widgetInput.title ?? null,
93
+ content: widgetInput.content ? JSON.stringify(widgetInput.content) : null,
94
+ menu_name: widgetInput.menuName ?? null,
95
+ component_id: widgetInput.componentId ?? null,
96
+ component_props: widgetInput.componentProps
97
+ ? JSON.stringify(widgetInput.componentProps)
98
+ : null,
67
99
  })
68
100
  .execute();
69
101
 
@@ -73,6 +105,20 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
73
105
  .where("id", "=", id)
74
106
  .executeTakeFirstOrThrow();
75
107
 
108
+ await logWidgetActivity(db, locals, {
109
+ action: "created",
110
+ areaId: area.id,
111
+ areaName: area.name,
112
+ widgetId: widget.id,
113
+ ...widgetApiRouteSource("created"),
114
+ detail: {
115
+ type: widget.type,
116
+ sortOrder: widget.sort_order,
117
+ title: widget.title,
118
+ hitlRequestId: approvedHitlRequestId,
119
+ },
120
+ });
121
+
76
122
  return apiSuccess(widget, 201);
77
123
  } catch (error) {
78
124
  return handleError(error, "Failed to create widget", "WIDGET_CREATE_ERROR");
@@ -6,12 +6,29 @@
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 { apiError, apiSuccess, handleError } from "#api/error.js";
13
+ import {
14
+ ensureWorkflowHitlRouteRequest,
15
+ hitlRequiredRouteError,
16
+ resolveHitlRouteActor,
17
+ } from "#api/hitl-route-helpers.js";
18
+ import { isParseError, parseQuery } from "#api/parse.js";
19
+ import {
20
+ logWidgetActivity,
21
+ RiskPolicyEvaluator,
22
+ widgetApiRouteSource,
23
+ WidgetHitlPayloadBuilder,
24
+ } from "#site-context/index.js";
12
25
 
13
26
  export const prerender = false;
14
27
 
28
+ const deleteWidgetAreaQuery = z.object({
29
+ hitlRequestId: z.string().min(1).optional(),
30
+ });
31
+
15
32
  export const GET: APIRoute = async ({ params, locals }) => {
16
33
  const { dineway, user } = locals;
17
34
  const db = dineway.db;
@@ -53,7 +70,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
53
70
  }
54
71
  };
55
72
 
56
- export const DELETE: APIRoute = async ({ params, locals }) => {
73
+ export const DELETE: APIRoute = async ({ params, request, locals }) => {
57
74
  const { dineway, user } = locals;
58
75
  const db = dineway.db;
59
76
  const { name } = params;
@@ -65,21 +82,52 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
65
82
  return apiError("VALIDATION_ERROR", "name is required", 400);
66
83
  }
67
84
 
85
+ const query = parseQuery(new URL(request.url), deleteWidgetAreaQuery);
86
+ if (isParseError(query)) return query;
87
+
68
88
  try {
69
- // Check if area exists
70
- const area = await db
71
- .selectFrom("_dineway_widget_areas")
72
- .select("id")
73
- .where("name", "=", name)
74
- .executeTakeFirst();
89
+ const payloadBuilder = new WidgetHitlPayloadBuilder(db);
90
+ const area = await payloadBuilder.loadWidgetAreaSnapshot(name);
75
91
 
76
92
  if (!area) {
77
93
  return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
78
94
  }
79
95
 
96
+ const actor = resolveHitlRouteActor(locals);
97
+ const evaluator = new RiskPolicyEvaluator({
98
+ db,
99
+ handlers: dineway,
100
+ });
101
+ let approvedHitlRequestId: string | null = null;
102
+
103
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
104
+ const action = await payloadBuilder.buildDeleteWidgetAreaRequest({ area });
105
+ const decision = await evaluator.evaluateWorkflowHitl({
106
+ actor: actor.identity,
107
+ hitlRequestId: query.hitlRequestId,
108
+ action,
109
+ });
110
+ if (!decision.allowed) {
111
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
112
+ return hitlRequiredRouteError(decision, ensured);
113
+ }
114
+ approvedHitlRequestId = decision.hitlRequest.id;
115
+ }
116
+
80
117
  // Delete area (widgets cascade)
81
118
  await db.deleteFrom("_dineway_widget_areas").where("id", "=", area.id).execute();
82
119
 
120
+ await logWidgetActivity(db, locals, {
121
+ action: "area_deleted",
122
+ areaId: area.id,
123
+ areaName: area.name,
124
+ ...widgetApiRouteSource("area_deleted"),
125
+ detail: {
126
+ widgetCount: area.widgets.length,
127
+ hitlRequestId: approvedHitlRequestId,
128
+ },
129
+ });
130
+
83
131
  return apiSuccess({ deleted: true });
84
132
  } catch (error) {
85
133
  return handleError(error, "Failed to delete widget area", "WIDGET_AREA_DELETE_ERROR");
@@ -7,14 +7,30 @@
7
7
 
8
8
  import type { APIRoute } from "astro";
9
9
  import { ulid } from "ulidx";
10
+ import { z } from "zod";
10
11
 
11
12
  import { requirePerm } from "#api/authorize.js";
12
13
  import { apiError, apiSuccess, handleError } from "#api/error.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 { createWidgetAreaBody } from "#api/schemas.js";
21
+ import {
22
+ logWidgetActivity,
23
+ RiskPolicyEvaluator,
24
+ widgetApiRouteSource,
25
+ WidgetHitlPayloadBuilder,
26
+ } from "#site-context/index.js";
15
27
 
16
28
  export const prerender = false;
17
29
 
30
+ const createWidgetAreaHitlBody = createWidgetAreaBody.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
  const db = dineway.db;
@@ -61,18 +77,40 @@ export const POST: APIRoute = async ({ request, locals }) => {
61
77
  if (denied) return denied;
62
78
 
63
79
  try {
64
- const body = await parseBody(request, createWidgetAreaBody);
80
+ const body = await parseBody(request, createWidgetAreaHitlBody);
65
81
  if (isParseError(body)) return body;
82
+ const { hitlRequestId, ...areaInput } = body;
66
83
 
67
84
  // Check if area name already exists
68
85
  const existing = await db
69
86
  .selectFrom("_dineway_widget_areas")
70
87
  .select("id")
71
- .where("name", "=", body.name)
88
+ .where("name", "=", areaInput.name)
72
89
  .executeTakeFirst();
73
90
 
74
91
  if (existing) {
75
- return apiError("CONFLICT", `Widget area with name "${body.name}" already exists`, 409);
92
+ return apiError("CONFLICT", `Widget area with name "${areaInput.name}" already exists`, 409);
93
+ }
94
+
95
+ const actor = resolveHitlRouteActor(locals);
96
+ const evaluator = new RiskPolicyEvaluator({
97
+ db,
98
+ handlers: dineway,
99
+ });
100
+ let approvedHitlRequestId: string | null = null;
101
+
102
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
103
+ const action = await new WidgetHitlPayloadBuilder(db).buildCreateWidgetAreaRequest(areaInput);
104
+ const decision = await evaluator.evaluateWorkflowHitl({
105
+ actor: actor.identity,
106
+ hitlRequestId,
107
+ action,
108
+ });
109
+ if (!decision.allowed) {
110
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
111
+ return hitlRequiredRouteError(decision, ensured);
112
+ }
113
+ approvedHitlRequestId = decision.hitlRequest.id;
76
114
  }
77
115
 
78
116
  const id = ulid();
@@ -80,9 +118,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
80
118
  .insertInto("_dineway_widget_areas")
81
119
  .values({
82
120
  id,
83
- name: body.name,
84
- label: body.label,
85
- description: body.description ?? null,
121
+ name: areaInput.name,
122
+ label: areaInput.label,
123
+ description: areaInput.description ?? null,
86
124
  })
87
125
  .execute();
88
126
 
@@ -92,6 +130,18 @@ export const POST: APIRoute = async ({ request, locals }) => {
92
130
  .where("id", "=", id)
93
131
  .executeTakeFirstOrThrow();
94
132
 
133
+ await logWidgetActivity(db, locals, {
134
+ action: "area_created",
135
+ areaId: area.id,
136
+ areaName: area.name,
137
+ ...widgetApiRouteSource("area_created"),
138
+ detail: {
139
+ label: area.label,
140
+ description: area.description,
141
+ hitlRequestId: approvedHitlRequestId,
142
+ },
143
+ });
144
+
95
145
  return apiSuccess(area, 201);
96
146
  } catch (error) {
97
147
  return handleError(error, "Failed to create widget area", "WIDGET_AREA_CREATE_ERROR");
@@ -16,8 +16,12 @@
16
16
  import type { PublicPageContext, PageMetadataContribution } from "../plugins/types.js";
17
17
  import { resolvePageMetadata, renderPageMetadata } from "../page/metadata.js";
18
18
  import { renderFragments } from "../page/fragments.js";
19
- import { generateBaseSeoContributions } from "../page/seo-contributions.js";
19
+ import {
20
+ generateBaseSeoContributions,
21
+ generateSiteSeoContributions,
22
+ } from "../page/seo-contributions.js";
20
23
  import { getPageRuntime } from "../page/index.js";
24
+ import { getSiteSetting } from "../settings/index.js";
21
25
 
22
26
  interface Props {
23
27
  page: PublicPageContext;
@@ -33,14 +37,18 @@ let metadataHtml = "";
33
37
  let fragmentsHtml = "";
34
38
 
35
39
  if (runtime) {
36
- // Plugin contributions come BEFORE base, so resolvePageMetadata's
37
- // first-wins dedup lets plugins override base SEO defaults
38
- const pluginContributions = await runtime.collectPageMetadata(page);
39
- const allContributions = [...pluginContributions, ...baseContributions];
40
+ // Plugin contributions come BEFORE site/base, so resolvePageMetadata's
41
+ // first-wins dedup lets plugins override defaults.
42
+ const [seoSettings, pluginContributions, fragments] = await Promise.all([
43
+ getSiteSetting("seo"),
44
+ runtime.collectPageMetadata(page),
45
+ runtime.collectPageFragments(page),
46
+ ]);
47
+
48
+ const siteContributions = generateSiteSeoContributions(seoSettings);
49
+ const allContributions = [...pluginContributions, ...siteContributions, ...baseContributions];
40
50
  const resolved = resolvePageMetadata(allContributions);
41
51
  metadataHtml = renderPageMetadata(resolved);
42
-
43
- const fragments = await runtime.collectPageFragments(page);
44
52
  fragmentsHtml = renderFragments(fragments, "head");
45
53
  } else {
46
54
  // No runtime (Dineway not initialized) — still render base SEO
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Dineway Media component
4
4
  *
5
- * Unified component for rendering media from any provider (local, Cloudflare Images, etc.)
5
+ * Unified component for rendering media from any provider (local, remote image services, etc.)
6
6
  * Calls the provider's getEmbed() method at render time for proper URL generation.
7
7
  *
8
8
  * Usage:
@@ -1117,7 +1117,7 @@ function InlineMediaPicker({
1117
1117
  setUploading(true);
1118
1118
  try {
1119
1119
  // Detect dimensions and generate a thumbnail for large images to
1120
- // avoid OOM in server-side blurhash generation on Workers.
1120
+ // avoid OOM in server-side blurhash generation on constrained runtimes.
1121
1121
  const dims = await new Promise<{
1122
1122
  width?: number;
1123
1123
  height?: number;