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
@@ -18,6 +18,11 @@ import {
18
18
  import { parseBody, isParseError } from "#api/parse.js";
19
19
  import { updateFieldBody } from "#api/schemas.js";
20
20
  import type { UpdateFieldInput } from "#schema/types.js";
21
+ import {
22
+ activityChangedKeys,
23
+ logSchemaActivity,
24
+ schemaApiRouteSource,
25
+ } from "#site-context/activity-events.js";
21
26
 
22
27
  export const prerender = false;
23
28
 
@@ -57,6 +62,19 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
57
62
  fieldSlug,
58
63
  body as UpdateFieldInput,
59
64
  );
65
+ if (result.success) dineway.invalidateManifest();
66
+ if (result.success) {
67
+ await logSchemaActivity(dineway.db, locals, {
68
+ action: "field_updated",
69
+ collection: collectionSlug,
70
+ field: fieldSlug,
71
+ ...schemaApiRouteSource("field_updated"),
72
+ summary: `Updated field ${fieldSlug} in ${collectionSlug}`,
73
+ detail: {
74
+ changedFields: activityChangedKeys(body),
75
+ },
76
+ });
77
+ }
60
78
  return unwrapResult(result);
61
79
  };
62
80
 
@@ -72,5 +90,15 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
72
90
  if (denied) return denied;
73
91
 
74
92
  const result = await handleSchemaFieldDelete(dineway.db, collectionSlug, fieldSlug);
93
+ if (result.success) dineway.invalidateManifest();
94
+ if (result.success) {
95
+ await logSchemaActivity(dineway.db, locals, {
96
+ action: "field_deleted",
97
+ collection: collectionSlug,
98
+ field: fieldSlug,
99
+ ...schemaApiRouteSource("field_deleted"),
100
+ summary: `Deleted field ${fieldSlug} from ${collectionSlug}`,
101
+ });
102
+ }
75
103
  return unwrapResult(result);
76
104
  };
@@ -13,6 +13,7 @@ import { handleSchemaFieldList, handleSchemaFieldCreate } from "#api/index.js";
13
13
  import { parseBody, isParseError } from "#api/parse.js";
14
14
  import { createFieldBody } from "#api/schemas.js";
15
15
  import type { CreateFieldInput } from "#schema/types.js";
16
+ import { logSchemaActivity, schemaApiRouteSource } from "#site-context/activity-events.js";
16
17
 
17
18
  export const prerender = false;
18
19
 
@@ -48,5 +49,19 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
48
49
  collectionSlug,
49
50
  body as CreateFieldInput,
50
51
  );
52
+ if (result.success) dineway.invalidateManifest();
53
+ if (result.success) {
54
+ await logSchemaActivity(dineway.db, locals, {
55
+ action: "field_created",
56
+ collection: collectionSlug,
57
+ field: body.slug,
58
+ ...schemaApiRouteSource("field_created"),
59
+ summary: `Created field ${body.slug} in ${collectionSlug}`,
60
+ detail: {
61
+ fieldType: body.type,
62
+ label: body.label,
63
+ },
64
+ });
65
+ }
51
66
  return unwrapResult(result, 201);
52
67
  };
@@ -11,6 +11,7 @@ import { requireDb, unwrapResult } from "#api/error.js";
11
11
  import { handleSchemaFieldReorder } from "#api/index.js";
12
12
  import { parseBody, isParseError } from "#api/parse.js";
13
13
  import { fieldReorderBody } from "#api/schemas.js";
14
+ import { logSchemaActivity, schemaApiRouteSource } from "#site-context/activity-events.js";
14
15
 
15
16
  export const prerender = false;
16
17
 
@@ -28,5 +29,17 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
28
29
  if (isParseError(body)) return body;
29
30
 
30
31
  const result = await handleSchemaFieldReorder(dineway.db, collectionSlug, body.fieldSlugs);
32
+ if (result.success) dineway.invalidateManifest();
33
+ if (result.success) {
34
+ await logSchemaActivity(dineway.db, locals, {
35
+ action: "fields_reordered",
36
+ collection: collectionSlug,
37
+ ...schemaApiRouteSource("fields_reordered"),
38
+ summary: `Reordered fields in ${collectionSlug}`,
39
+ detail: {
40
+ fieldCount: body.fieldSlugs.length,
41
+ },
42
+ });
43
+ }
31
44
  return unwrapResult(result);
32
45
  };
@@ -18,6 +18,11 @@ import {
18
18
  import { parseBody, parseQuery, isParseError } from "#api/parse.js";
19
19
  import { collectionGetQuery, updateCollectionBody } from "#api/schemas.js";
20
20
  import type { UpdateCollectionInput } from "#schema/types.js";
21
+ import {
22
+ activityChangedKeys,
23
+ logSchemaActivity,
24
+ schemaApiRouteSource,
25
+ } from "#site-context/activity-events.js";
21
26
 
22
27
  export const prerender = false;
23
28
 
@@ -59,6 +64,18 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
59
64
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- parseBody validates via Zod
60
65
  body as UpdateCollectionInput,
61
66
  );
67
+ if (result.success) dineway.invalidateManifest();
68
+ if (result.success) {
69
+ await logSchemaActivity(dineway.db, locals, {
70
+ action: "collection_updated",
71
+ collection: slug,
72
+ ...schemaApiRouteSource("collection_updated"),
73
+ summary: `Updated collection ${slug}`,
74
+ detail: {
75
+ changedFields: activityChangedKeys(body),
76
+ },
77
+ });
78
+ }
62
79
  return unwrapResult(result);
63
80
  };
64
81
 
@@ -76,5 +93,15 @@ export const DELETE: APIRoute = async ({ params, url, locals }) => {
76
93
  const result = await handleSchemaCollectionDelete(dineway.db, slug, {
77
94
  force,
78
95
  });
96
+ if (result.success) dineway.invalidateManifest();
97
+ if (result.success) {
98
+ await logSchemaActivity(dineway.db, locals, {
99
+ action: "collection_deleted",
100
+ collection: slug,
101
+ ...schemaApiRouteSource("collection_deleted"),
102
+ summary: `Deleted collection ${slug}`,
103
+ detail: { force },
104
+ });
105
+ }
79
106
  return unwrapResult(result);
80
107
  };
@@ -13,6 +13,7 @@ import { handleSchemaCollectionList, handleSchemaCollectionCreate } from "#api/i
13
13
  import { parseBody, isParseError } from "#api/parse.js";
14
14
  import { createCollectionBody } from "#api/schemas.js";
15
15
  import type { CreateCollectionInput } from "#schema/types.js";
16
+ import { logSchemaActivity, schemaApiRouteSource } from "#site-context/activity-events.js";
16
17
 
17
18
  export const prerender = false;
18
19
 
@@ -43,5 +44,18 @@ export const POST: APIRoute = async ({ request, locals }) => {
43
44
 
44
45
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to CreateCollectionInput
45
46
  const result = await handleSchemaCollectionCreate(dineway.db, body as CreateCollectionInput);
47
+ if (result.success) dineway.invalidateManifest();
48
+ if (result.success) {
49
+ await logSchemaActivity(dineway.db, locals, {
50
+ action: "collection_created",
51
+ collection: body.slug,
52
+ ...schemaApiRouteSource("collection_created"),
53
+ summary: `Created collection ${body.slug}`,
54
+ detail: {
55
+ label: body.label,
56
+ labelSingular: body.labelSingular ?? null,
57
+ },
58
+ });
59
+ }
46
60
  return unwrapResult(result, 201);
47
61
  };
@@ -37,6 +37,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
37
37
  : undefined;
38
38
 
39
39
  try {
40
+ await dineway.ensureSearchHealthy?.();
40
41
  const result = await searchWithDb(dineway.db, query.q, {
41
42
  collections,
42
43
  status: query.status,
@@ -36,6 +36,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
36
36
  : undefined;
37
37
 
38
38
  try {
39
+ await dineway.ensureSearchHealthy?.();
39
40
  const suggestions = await getSuggestions(dineway.db, query.q, {
40
41
  collections,
41
42
  locale: query.locale,
@@ -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
  handleSectionGet,
16
17
  handleSectionUpdate,
17
18
  } from "#api/handlers/sections.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 { updateSectionBody } from "#api/schemas.js";
26
+ import {
27
+ activityChangedKeys,
28
+ logSectionActivity,
29
+ RiskPolicyEvaluator,
30
+ sectionApiRouteSource,
31
+ SectionHitlPayloadBuilder,
32
+ } from "#site-context/index.js";
20
33
 
21
34
  export const prerender = false;
22
35
 
36
+ const updateSectionHitlBody = updateSectionBody.extend({
37
+ hitlRequestId: z.string().min(1).optional(),
38
+ });
39
+
40
+ const deleteSectionQuery = 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,65 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
53
74
  }
54
75
 
55
76
  try {
56
- const body = await parseBody(request, updateSectionBody);
77
+ const body = await parseBody(request, updateSectionHitlBody);
57
78
  if (isParseError(body)) return body;
58
79
 
59
- const result = await handleSectionUpdate(db, slug, body);
80
+ const current = await new SectionHitlPayloadBuilder(db).loadSectionSnapshot(slug);
81
+ if (!current) {
82
+ return apiError("NOT_FOUND", `Section "${slug}" not found`, 404);
83
+ }
84
+
85
+ const { hitlRequestId, ...sectionInput } = body;
86
+ const actor = resolveHitlRouteActor(locals);
87
+ const evaluator = new RiskPolicyEvaluator({
88
+ db,
89
+ handlers: dineway,
90
+ });
91
+ let approvedHitlRequestId: string | null = null;
92
+
93
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
94
+ const action = await new SectionHitlPayloadBuilder(db).buildUpdateSectionRequest({
95
+ section: current,
96
+ ...sectionInput,
97
+ });
98
+ const decision = await evaluator.evaluateWorkflowHitl({
99
+ actor: actor.identity,
100
+ hitlRequestId,
101
+ action,
102
+ });
103
+ if (!decision.allowed) {
104
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
105
+ return hitlRequiredRouteError(decision, ensured);
106
+ }
107
+ approvedHitlRequestId = decision.hitlRequest.id;
108
+ }
109
+
110
+ const result = await handleSectionUpdate(db, slug, sectionInput);
111
+ if (result.success) {
112
+ await logSectionActivity(db, locals, {
113
+ action: "updated",
114
+ sectionId: result.data.id,
115
+ sectionSlug: result.data.slug,
116
+ ...sectionApiRouteSource("updated"),
117
+ detail: {
118
+ changedKeys: activityChangedKeys(sectionInput),
119
+ source: current.source,
120
+ themeId: current.themeId,
121
+ previewMediaId:
122
+ sectionInput.previewMediaId !== undefined
123
+ ? sectionInput.previewMediaId
124
+ : current.previewMediaId,
125
+ hitlRequestId: approvedHitlRequestId,
126
+ },
127
+ });
128
+ }
60
129
  return unwrapResult(result);
61
130
  } catch (error) {
62
131
  return handleError(error, "Failed to update section", "SECTION_UPDATE_ERROR");
63
132
  }
64
133
  };
65
134
 
66
- export const DELETE: APIRoute = async ({ params, locals }) => {
135
+ export const DELETE: APIRoute = async ({ params, request, locals }) => {
67
136
  const { dineway, user } = locals;
68
137
  const db = dineway.db;
69
138
  const { slug } = params;
@@ -75,8 +144,58 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
75
144
  return apiError("VALIDATION_ERROR", "slug is required", 400);
76
145
  }
77
146
 
147
+ const query = parseQuery(new URL(request.url), deleteSectionQuery);
148
+ if (isParseError(query)) return query;
149
+
78
150
  try {
151
+ const current = await new SectionHitlPayloadBuilder(db).loadSectionSnapshot(slug);
152
+ if (!current) {
153
+ return apiError("NOT_FOUND", `Section "${slug}" not found`, 404);
154
+ }
155
+
156
+ if (current.source === "theme") {
157
+ const result = await handleSectionDelete(db, slug);
158
+ return unwrapResult(result);
159
+ }
160
+
161
+ const actor = resolveHitlRouteActor(locals);
162
+ const evaluator = new RiskPolicyEvaluator({
163
+ db,
164
+ handlers: dineway,
165
+ });
166
+ let approvedHitlRequestId: string | null = null;
167
+
168
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
169
+ const action = await new SectionHitlPayloadBuilder(db).buildDeleteSectionRequest({
170
+ section: current,
171
+ });
172
+ const decision = await evaluator.evaluateWorkflowHitl({
173
+ actor: actor.identity,
174
+ hitlRequestId: query.hitlRequestId,
175
+ action,
176
+ });
177
+ if (!decision.allowed) {
178
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
179
+ return hitlRequiredRouteError(decision, ensured);
180
+ }
181
+ approvedHitlRequestId = decision.hitlRequest.id;
182
+ }
183
+
79
184
  const result = await handleSectionDelete(db, slug);
185
+ if (result.success) {
186
+ await logSectionActivity(db, locals, {
187
+ action: "deleted",
188
+ sectionId: current.id,
189
+ sectionSlug: current.slug,
190
+ ...sectionApiRouteSource("deleted"),
191
+ detail: {
192
+ source: current.source,
193
+ themeId: current.themeId,
194
+ previewMediaId: current.previewMediaId,
195
+ hitlRequestId: approvedHitlRequestId,
196
+ },
197
+ });
198
+ }
80
199
  return unwrapResult(result);
81
200
  } catch (error) {
82
201
  return handleError(error, "Failed to delete section", "SECTION_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 { handleSectionCreate, handleSectionList } from "#api/handlers/sections.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 { createSectionBody, sectionsListQuery } from "#api/schemas.js";
21
+ import {
22
+ logSectionActivity,
23
+ RiskPolicyEvaluator,
24
+ sectionApiRouteSource,
25
+ SectionHitlPayloadBuilder,
26
+ } from "#site-context/index.js";
15
27
 
16
28
  export const prerender = false;
17
29
 
30
+ const createSectionHitlBody = createSectionBody.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,49 @@ export const POST: APIRoute = async ({ request, locals }) => {
41
57
  if (denied) return denied;
42
58
 
43
59
  try {
44
- const body = await parseBody(request, createSectionBody);
60
+ const body = await parseBody(request, createSectionHitlBody);
45
61
  if (isParseError(body)) return body;
46
62
 
47
- const result = await handleSectionCreate(db, body);
63
+ const { hitlRequestId, ...sectionInput } = body;
64
+ const actor = resolveHitlRouteActor(locals);
65
+ const evaluator = new RiskPolicyEvaluator({
66
+ db,
67
+ handlers: dineway,
68
+ });
69
+ let approvedHitlRequestId: string | null = null;
70
+
71
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
72
+ const action = await new SectionHitlPayloadBuilder(db).buildCreateSectionRequest(
73
+ sectionInput,
74
+ );
75
+ const decision = await evaluator.evaluateWorkflowHitl({
76
+ actor: actor.identity,
77
+ hitlRequestId,
78
+ action,
79
+ });
80
+ if (!decision.allowed) {
81
+ const ensured = await ensureWorkflowHitlRouteRequest(db, locals, decision.action);
82
+ return hitlRequiredRouteError(decision, ensured);
83
+ }
84
+ approvedHitlRequestId = decision.hitlRequest.id;
85
+ }
86
+
87
+ const result = await handleSectionCreate(db, sectionInput);
88
+ if (result.success) {
89
+ await logSectionActivity(db, locals, {
90
+ action: "created",
91
+ sectionId: result.data.id,
92
+ sectionSlug: result.data.slug,
93
+ ...sectionApiRouteSource("created"),
94
+ detail: {
95
+ title: result.data.title,
96
+ source: result.data.source,
97
+ themeId: result.data.themeId ?? null,
98
+ previewMediaId: sectionInput.previewMediaId ?? null,
99
+ hitlRequestId: approvedHitlRequestId,
100
+ },
101
+ });
102
+ }
48
103
  return unwrapResult(result, 201);
49
104
  } catch (error) {
50
105
  return handleError(error, "Failed to create section", "SECTION_CREATE_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 { apiError, handleError, unwrapResult } from "#api/error.js";
12
13
  import { handleSettingsGet, handleSettingsUpdate } from "#api/handlers/settings.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 { settingsUpdateBody } from "#api/schemas.js";
21
+ import {
22
+ logSiteActivitySafely,
23
+ RiskPolicyEvaluator,
24
+ settingsApiRouteSource,
25
+ SettingsHitlPayloadBuilder,
26
+ } from "#site-context/index.js";
15
27
 
16
28
  export const prerender = false;
17
29
 
30
+ const settingsHitlBody = settingsUpdateBody.extend({
31
+ hitlRequestId: z.string().min(1).optional(),
32
+ });
33
+
18
34
  /**
19
35
  * GET /_dineway/api/settings
20
36
  *
@@ -56,10 +72,43 @@ export const POST: APIRoute = async ({ request, locals }) => {
56
72
  if (denied) return denied;
57
73
 
58
74
  try {
59
- const body = await parseBody(request, settingsUpdateBody);
75
+ const body = await parseBody(request, settingsHitlBody);
60
76
  if (isParseError(body)) return body;
61
77
 
62
- const result = await handleSettingsUpdate(dineway.db, dineway.storage, body);
78
+ const { hitlRequestId, ...settingsInput } = body;
79
+ const actor = resolveHitlRouteActor(locals);
80
+ const action = await new SettingsHitlPayloadBuilder(dineway.db).buildUpdateSettingsRequest(
81
+ settingsInput,
82
+ );
83
+ const decision = await new RiskPolicyEvaluator({
84
+ db: dineway.db,
85
+ handlers: dineway,
86
+ }).evaluateWorkflowHitl({
87
+ actor: actor.identity,
88
+ hitlRequestId,
89
+ action,
90
+ });
91
+ if (!decision.allowed) {
92
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
93
+ return hitlRequiredRouteError(decision, ensured);
94
+ }
95
+
96
+ const result = await handleSettingsUpdate(dineway.db, dineway.storage, settingsInput);
97
+ if (!result.success) return unwrapResult(result);
98
+
99
+ await logSiteActivitySafely(dineway.db, locals, {
100
+ actionType: "settings.updated",
101
+ subjectType: "site_settings",
102
+ subjectId: "site",
103
+ scope: "site",
104
+ ...settingsApiRouteSource("updated"),
105
+ summary: action.summary ?? "Updated site settings",
106
+ detail: {
107
+ changedKeys: Object.keys(settingsInput).toSorted(),
108
+ touchesSeo: Object.hasOwn(settingsInput, "seo"),
109
+ hitlRequestId: decision.required ? decision.hitlRequest.id : null,
110
+ },
111
+ });
63
112
  return unwrapResult(result);
64
113
  } catch (error) {
65
114
  return handleError(error, "Failed to update settings", "SETTINGS_UPDATE_ERROR");
@@ -8,7 +8,7 @@ import type { APIRoute } from "astro";
8
8
 
9
9
  export const prerender = false;
10
10
 
11
- import { Role } from "@dineway-ai/auth";
11
+ import { Role, secureCompare } from "@dineway-ai/auth";
12
12
  import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
13
13
  import { verifyRegistrationResponse, registerPasskey } from "@dineway-ai/auth/passkey";
14
14
 
@@ -18,9 +18,10 @@ import { getPublicOrigin } from "#api/public-url.js";
18
18
  import { setupAdminVerifyBody } from "#api/schemas.js";
19
19
  import { createChallengeStore } from "#auth/challenge-store.js";
20
20
  import { getPasskeyConfig } from "#auth/passkey-config.js";
21
+ import { SETUP_NONCE_COOKIE } from "#auth/setup-nonce.js";
21
22
  import { OptionsRepository } from "#db/repositories/options.js";
22
23
 
23
- export const POST: APIRoute = async ({ request, locals }) => {
24
+ export const POST: APIRoute = async ({ cookies, request, locals }) => {
24
25
  const { dineway } = locals;
25
26
 
26
27
  if (!dineway?.db) {
@@ -45,12 +46,30 @@ export const POST: APIRoute = async ({ request, locals }) => {
45
46
  }
46
47
 
47
48
  // Get setup state
48
- const setupState = await options.get("dineway:setup_state");
49
+ const setupState = await options.get<{
50
+ step?: string;
51
+ email?: string;
52
+ name?: string | null;
53
+ nonce?: string;
54
+ }>("dineway:setup_state");
49
55
 
50
56
  if (!setupState || setupState.step !== "admin") {
51
57
  return apiError("INVALID_STATE", "Invalid setup state. Please restart setup.", 400);
52
58
  }
53
59
 
60
+ const cookieNonce = cookies.get(SETUP_NONCE_COOKIE)?.value;
61
+ if (!setupState.nonce || !cookieNonce || !secureCompare(cookieNonce, setupState.nonce)) {
62
+ return apiError(
63
+ "INVALID_STATE",
64
+ "Setup session expired or tampered with. Please restart the admin step.",
65
+ 400,
66
+ );
67
+ }
68
+
69
+ if (!setupState.email) {
70
+ return apiError("INVALID_STATE", "Invalid setup state. Please restart setup.", 400);
71
+ }
72
+
54
73
  // Parse request body
55
74
  const body = await parseBody(request, setupAdminVerifyBody);
56
75
  if (isParseError(body)) return body;
@@ -73,7 +92,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
73
92
  // Create the admin user
74
93
  const user = await adapter.createUser({
75
94
  email: setupState.email,
76
- name: setupState.name,
95
+ name: setupState.name ?? null,
77
96
  role: Role.ADMIN,
78
97
  emailVerified: false, // No email verification for first user
79
98
  });
@@ -84,8 +103,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
84
103
  // Mark setup as complete
85
104
  await options.set("dineway:setup_complete", true);
86
105
 
87
- // Clean up setup state
106
+ // Clean up setup state and the session nonce cookie
88
107
  await options.delete("dineway:setup_state");
108
+ cookies.delete(SETUP_NONCE_COOKIE, { path: "/_dineway/" });
89
109
 
90
110
  return apiSuccess({
91
111
  success: true,
@@ -8,6 +8,7 @@ import type { APIRoute } from "astro";
8
8
 
9
9
  export const prerender = false;
10
10
 
11
+ import { generateToken } from "@dineway-ai/auth";
11
12
  import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
13
  import { generateRegistrationOptions } from "@dineway-ai/auth/passkey";
13
14
 
@@ -17,9 +18,10 @@ import { getPublicOrigin } from "#api/public-url.js";
17
18
  import { setupAdminBody } from "#api/schemas.js";
18
19
  import { createChallengeStore } from "#auth/challenge-store.js";
19
20
  import { getPasskeyConfig } from "#auth/passkey-config.js";
21
+ import { SETUP_NONCE_COOKIE, SETUP_NONCE_MAX_AGE_SECONDS } from "#auth/setup-nonce.js";
20
22
  import { OptionsRepository } from "#db/repositories/options.js";
21
23
 
22
- export const POST: APIRoute = async ({ request, locals }) => {
24
+ export const POST: APIRoute = async ({ cookies, request, locals }) => {
23
25
  const { dineway } = locals;
24
26
 
25
27
  if (!dineway?.db) {
@@ -47,12 +49,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
47
49
  const body = await parseBody(request, setupAdminBody);
48
50
  if (isParseError(body)) return body;
49
51
 
50
- // Store admin info in setup state for later
51
- await options.set("dineway:setup_state", {
52
- step: "admin",
53
- email: body.email.toLowerCase(),
54
- name: body.name || null,
55
- });
52
+ const nonce = generateToken();
56
53
 
57
54
  // Get passkey config
58
55
  const url = new URL(request.url);
@@ -78,12 +75,23 @@ export const POST: APIRoute = async ({ request, locals }) => {
78
75
  challengeStore,
79
76
  );
80
77
 
81
- // Store the temp user ID with the setup state
78
+ // Store the nonce with the setup state. The verify endpoint compares
79
+ // it to an HttpOnly cookie so the admin step stays bound to this browser.
82
80
  await options.set("dineway:setup_state", {
83
81
  step: "admin",
84
82
  email: body.email.toLowerCase(),
85
83
  name: body.name || null,
86
84
  tempUserId: tempUser.id,
85
+ nonce,
86
+ });
87
+
88
+ const publicOrigin = new URL(siteUrl);
89
+ cookies.set(SETUP_NONCE_COOKIE, nonce, {
90
+ path: "/_dineway/",
91
+ httpOnly: true,
92
+ sameSite: "strict",
93
+ secure: publicOrigin.protocol === "https:",
94
+ maxAge: SETUP_NONCE_MAX_AGE_SECONDS,
87
95
  });
88
96
 
89
97
  return apiSuccess({
@@ -89,9 +89,10 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
89
89
  const options = new OptionsRepository(dineway.db);
90
90
 
91
91
  // Store the canonical site URL from the setup request.
92
- // This is trusted because setup runs on the real domain.
92
+ // Keep the first value so later unauthenticated setup calls cannot
93
+ // rewrite the public origin during the setup window.
93
94
  const siteUrl = getPublicOrigin(url, dineway.config);
94
- await options.set("dineway:site_url", siteUrl);
95
+ await options.setIfAbsent("dineway:site_url", siteUrl);
95
96
 
96
97
  if (useExternalAuth) {
97
98
  // External auth mode: mark setup complete now