@beechcms/api 0.4.3 → 0.6.0-preview.1

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 (207) hide show
  1. package/assets/dashboard/Searching.svg +1 -0
  2. package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
  3. package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
  4. package/assets/dashboard/index.html +2 -2
  5. package/assets/dashboard/noResult.svg +43 -0
  6. package/assets/dashboard/working.svg +1 -0
  7. package/migrations/0000_v040_base.sql +27 -0
  8. package/migrations/0030_test_seeds.sql +125 -0
  9. package/package.json +14 -9
  10. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
  11. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
  12. package/src/auth/bcrypt-hash-provider.ts +6 -2
  13. package/src/auth/constants.ts +4 -0
  14. package/src/auth/generate-refresh-token.test.ts +8 -4
  15. package/src/auth/hash-provider.test.ts +14 -5
  16. package/src/auth/jose-token-service.ts +7 -0
  17. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  18. package/src/auth/login.test.ts +9 -1
  19. package/src/auth/login.ts +5 -1
  20. package/src/auth/refresh.ts +7 -1
  21. package/src/auth/token-service.test.ts +9 -1
  22. package/src/factory.ts +37 -16
  23. package/src/features/automations/__tests__/action-executors.test.ts +59 -72
  24. package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
  25. package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
  26. package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
  27. package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
  28. package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
  29. package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
  30. package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
  31. package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
  32. package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
  33. package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
  34. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  35. package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
  36. package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
  37. package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
  38. package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
  39. package/src/features/automations/action-executors/index.ts +5 -1
  40. package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
  41. package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
  42. package/src/features/automations/action-executors/webhook.executor.ts +55 -13
  43. package/src/features/automations/automation-runner.ts +4 -0
  44. package/src/features/automations/automation-runner.utils.ts +4 -0
  45. package/src/features/automations/automations.handler.ts +4 -0
  46. package/src/features/automations/automations.schema.ts +19 -2
  47. package/src/features/automations/context-resolver.ts +4 -0
  48. package/src/features/automations/cron-runner.ts +4 -0
  49. package/src/features/automations/cron-runner.utils.ts +4 -0
  50. package/src/features/automations/filter-translation.ts +4 -0
  51. package/src/features/automations/index.ts +4 -0
  52. package/src/features/automations/template-grammar.ts +4 -0
  53. package/src/features/automations/var-access-resolver.ts +4 -0
  54. package/src/features/automations/when-evaluator.ts +4 -0
  55. package/src/features/automations/when-pushdown.ts +4 -0
  56. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  57. package/src/features/backrefs/backrefs.handler.ts +168 -0
  58. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  59. package/src/features/backrefs/index.ts +5 -0
  60. package/src/features/content/constants.ts +6 -0
  61. package/src/features/content/handlers/bulk.handler.ts +185 -0
  62. package/src/features/content/handlers/create.ts +19 -55
  63. package/src/features/content/handlers/delete.ts +12 -37
  64. package/src/features/content/handlers/facets.ts +4 -0
  65. package/src/features/content/handlers/get.ts +4 -0
  66. package/src/features/content/handlers/helpers.test.ts +180 -0
  67. package/src/features/content/handlers/helpers.ts +93 -0
  68. package/src/features/content/handlers/list.ts +105 -19
  69. package/src/features/content/handlers/update.ts +19 -64
  70. package/src/features/content/index.ts +6 -0
  71. package/src/features/draft/draft.handler.ts +33 -8
  72. package/src/features/draft/draft.middleware.ts +4 -0
  73. package/src/features/draft/index.ts +4 -0
  74. package/src/features/email/email.provider.ts +4 -0
  75. package/src/features/email/email.service.ts +50 -45
  76. package/src/features/email/email.types.ts +12 -1
  77. package/src/features/email/index.ts +4 -0
  78. package/src/features/email/providers/resend.ts +4 -0
  79. package/src/features/email/providers/smtp.ts +55 -0
  80. package/src/features/email/templates/automation-mail.ts +4 -0
  81. package/src/features/email/templates/password-changed.ts +4 -0
  82. package/src/features/email/templates/password-reset.ts +4 -0
  83. package/src/features/email/templates/shell.ts +4 -0
  84. package/src/features/notifications/index.ts +4 -0
  85. package/src/features/notifications/notifications.handler.ts +4 -0
  86. package/src/features/password-reset/index.ts +5 -1
  87. package/src/features/password-reset/request.ts +12 -2
  88. package/src/features/password-reset/reset.ts +12 -2
  89. package/src/features/rotate-field/index.ts +4 -0
  90. package/src/features/rotate-field/rotate-field.handler.ts +4 -0
  91. package/src/features/rotate-field/rotate-field.schema.ts +4 -0
  92. package/src/features/schema/schema.handler.ts +118 -3
  93. package/src/features/seeds/index.ts +5 -0
  94. package/src/features/seeds/seeds.handler.test.ts +632 -0
  95. package/src/features/seeds/seeds.handler.ts +693 -0
  96. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  97. package/src/features/settings/settings.handler.ts +98 -8
  98. package/src/features/setup/index.ts +124 -11
  99. package/src/features/stats/index.ts +4 -0
  100. package/src/features/stats/stats.handler.ts +11 -21
  101. package/src/features/webhooks/index.ts +63 -0
  102. package/src/index.ts +28 -19
  103. package/src/media-utils.ts +4 -0
  104. package/src/middleware/auth-providers.middleware.ts +13 -0
  105. package/src/middleware/observability.middleware.ts +21 -3
  106. package/src/middleware/rate-limit.middleware.ts +4 -0
  107. package/src/middleware/repository.middleware.ts +25 -2
  108. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  109. package/src/middleware/seed-registry.middleware.ts +21 -0
  110. package/src/middleware/storage.middleware.ts +4 -0
  111. package/src/middleware.ts +5 -7
  112. package/src/public/access-policy.ts +4 -0
  113. package/src/public/api-key-middleware.ts +4 -0
  114. package/src/public/cache-utils.ts +38 -34
  115. package/src/public/entry-projection.ts +46 -42
  116. package/src/public/idempotency.ts +23 -19
  117. package/src/public/index.ts +4 -0
  118. package/src/public/problem-details.ts +71 -0
  119. package/src/public/public-add.ts +4 -0
  120. package/src/public/public-edit.ts +4 -0
  121. package/src/public/public-errors.ts +4 -0
  122. package/src/public/public-read.ts +4 -0
  123. package/src/public/public-routes.ts +4 -0
  124. package/src/public/query-builder.test.ts +4 -0
  125. package/src/public/query-builder.ts +4 -0
  126. package/src/public/rate-limit-middleware.ts +4 -0
  127. package/src/public/read-list.ts +54 -50
  128. package/src/public/read-single.ts +48 -44
  129. package/src/public/response-builder.ts +4 -0
  130. package/src/public/sanitize.ts +4 -0
  131. package/src/public/slug-utils.ts +4 -0
  132. package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
  133. package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
  134. package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
  135. package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
  136. package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
  137. package/src/rate-limit/no-op-rate-limiter.ts +4 -0
  138. package/src/search-utils.test.ts +4 -0
  139. package/src/search-utils.ts +5 -1
  140. package/src/search.ts +5 -1
  141. package/src/shared/apply-policies.test.ts +4 -0
  142. package/src/shared/apply-policies.ts +4 -0
  143. package/src/shared/automations.repository.d1.ts +4 -0
  144. package/src/shared/background-notification-service.test.ts +4 -0
  145. package/src/shared/background-notification-service.ts +4 -0
  146. package/src/shared/base.repository.d1.ts +4 -0
  147. package/src/shared/content-utils.test.ts +4 -0
  148. package/src/shared/content-utils.ts +4 -0
  149. package/src/shared/content.repository.d1.test.ts +147 -1
  150. package/src/shared/content.repository.d1.ts +535 -66
  151. package/src/shared/d1-activity-log.repository.test.ts +4 -0
  152. package/src/shared/d1-activity-log.repository.ts +4 -0
  153. package/src/shared/d1-activity-logger.test.ts +4 -0
  154. package/src/shared/d1-activity-logger.ts +4 -0
  155. package/src/shared/d1-analytics.repository.test.ts +4 -0
  156. package/src/shared/d1-analytics.repository.ts +4 -0
  157. package/src/shared/d1-content-scan.repository.test.ts +9 -5
  158. package/src/shared/d1-content-scan.repository.ts +15 -7
  159. package/src/shared/d1-notification.repository.test.ts +4 -0
  160. package/src/shared/d1-notification.repository.ts +4 -0
  161. package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
  162. package/src/shared/d1-password-reset-token.repository.ts +4 -0
  163. package/src/shared/d1-search.repository.test.ts +4 -0
  164. package/src/shared/d1-search.repository.ts +4 -0
  165. package/src/shared/d1-session.repository.test.ts +4 -0
  166. package/src/shared/d1-session.repository.ts +4 -0
  167. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  168. package/src/shared/d1-user.repository.test.ts +8 -4
  169. package/src/shared/d1-user.repository.ts +15 -5
  170. package/src/shared/d1-widget.repository.test.ts +4 -0
  171. package/src/shared/d1-widget.repository.ts +4 -0
  172. package/src/shared/demo-data-sql.ts +54 -0
  173. package/src/shared/demo-data.repository.d1.ts +20 -0
  174. package/src/shared/execution-context-scheduler.ts +4 -0
  175. package/src/shared/fixed-clock.ts +4 -0
  176. package/src/shared/fts-sync.ts +4 -0
  177. package/src/shared/idempotency.repository.d1.test.ts +4 -0
  178. package/src/shared/idempotency.repository.d1.ts +4 -0
  179. package/src/shared/in-memory-activity-logger.ts +4 -0
  180. package/src/shared/in-memory-notification-service.ts +4 -0
  181. package/src/shared/in-memory-seed.repository.ts +51 -0
  182. package/src/shared/media.repository.d1.test.ts +4 -0
  183. package/src/shared/media.repository.d1.ts +4 -0
  184. package/src/shared/qstash-notification-service.test.ts +67 -0
  185. package/src/shared/qstash-notification-service.ts +55 -0
  186. package/src/shared/query-utils.ts +4 -0
  187. package/src/shared/request-utils.ts +4 -0
  188. package/src/shared/schema-mutator.d1.ts +64 -0
  189. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  190. package/src/shared/seed-layout.repository.d1.ts +62 -0
  191. package/src/shared/seed-registry-cache.test.ts +68 -0
  192. package/src/shared/seed-registry-cache.ts +49 -0
  193. package/src/shared/seed.repository.d1.test.ts +143 -0
  194. package/src/shared/seed.repository.d1.ts +98 -0
  195. package/src/shared/sequential-id-generator.ts +11 -0
  196. package/src/shared/site-settings.repository.d1.ts +53 -0
  197. package/src/shared/storage/factory.ts +16 -15
  198. package/src/shared/storage/s3-bucket.ts +34 -2
  199. package/src/shared/storage-utils.ts +4 -0
  200. package/src/shared/system-stats.repository.d1.test.ts +4 -0
  201. package/src/shared/system-stats.repository.d1.ts +5 -1
  202. package/src/types.ts +23 -3
  203. package/src/upload.ts +134 -123
  204. package/src/widget.ts +6 -2
  205. package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
  206. package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
  207. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1 +1,5 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  export { notificationsApp } from './notifications.handler'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import { Hono } from 'hono'
3
7
  import type { Env, Variables } from '../../types'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import { Hono } from 'hono'
3
7
  import type { Env, Variables } from '../../types'
@@ -8,7 +12,7 @@ export const passwordResetApp = new Hono<{ Bindings: Env; Variables: Variables }
8
12
 
9
13
  // Feature flag: lets the dashboard know whether to show the forgot-password link
10
14
  passwordResetApp.get('/auth/features', (context) => {
11
- return context.json({ passwordReset: Boolean(context.env.RESEND_API_KEY) })
15
+ return context.json({ passwordReset: context.env.EMAIL_PROVIDER === 'smtp' || Boolean(context.env.RESEND_API_KEY) })
12
16
  })
13
17
 
14
18
  passwordResetApp.post('/auth/forgot-password', requestPasswordReset)
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import type { Context } from 'hono'
3
7
  import type { Env, Variables } from '../../types'
@@ -16,7 +20,8 @@ export async function requestPasswordReset(
16
20
  ): Promise<Response> {
17
21
  const { env, req } = context
18
22
 
19
- if (!env.RESEND_API_KEY) {
23
+ const useSmtp = env.EMAIL_PROVIDER === 'smtp'
24
+ if (!useSmtp && !env.RESEND_API_KEY) {
20
25
  return context.json({ error: 'Service not available' }, 503)
21
26
  }
22
27
 
@@ -63,14 +68,19 @@ export async function requestPasswordReset(
63
68
  const baseUrl = (env.APP_URL ?? new URL(req.url).origin).replace(/\/$/, '')
64
69
  const resetUrl = `${baseUrl}/admin/reset-password?token=${resetToken}`
65
70
 
71
+ const smtpBaseUrl = env.SMTP_HOST
72
+ ? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
73
+ : undefined
66
74
  try {
67
75
  await sendPasswordResetEmail({
68
76
  to: normalizedEmail,
69
77
  resetUrl,
70
78
  locale: emailLocale,
71
- apiKey: env.RESEND_API_KEY,
79
+ apiKey: env.RESEND_API_KEY ?? '',
72
80
  from: env.EMAIL_FROM,
73
81
  isDev: env.ENV !== 'production',
82
+ provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
83
+ smtpBaseUrl,
74
84
  })
75
85
  } catch (error) {
76
86
  if (env.ENV !== 'production') {
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import type { Context } from 'hono'
3
7
  import type { Env, Variables } from '../../types'
@@ -16,7 +20,8 @@ export async function resetPassword(
16
20
  ): Promise<Response> {
17
21
  const { env, req, executionCtx } = context
18
22
 
19
- if (!env.RESEND_API_KEY) {
23
+ const useSmtp = env.EMAIL_PROVIDER === 'smtp'
24
+ if (!useSmtp && !env.RESEND_API_KEY) {
20
25
  return context.json({ error: 'Service not available' }, 503)
21
26
  }
22
27
 
@@ -65,14 +70,19 @@ export async function resetPassword(
65
70
  await context.get('userRepository').updatePasswordHash(tokenRecord.userId, hashedNewPassword)
66
71
  await context.get('sessionRepository').revokeAllForUser(tokenRecord.userId, nowTimestamp)
67
72
 
73
+ const smtpBaseUrl = env.SMTP_HOST
74
+ ? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
75
+ : undefined
68
76
  const sendNotification = async () => {
69
77
  try {
70
78
  await sendPasswordChangedEmail({
71
79
  to: tokenRecord.email,
72
80
  locale: emailLocale,
73
- apiKey: env.RESEND_API_KEY!,
81
+ apiKey: env.RESEND_API_KEY ?? '',
74
82
  from: env.EMAIL_FROM,
75
83
  isDev: env.ENV !== 'production',
84
+ provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
85
+ smtpBaseUrl,
76
86
  })
77
87
  } catch (error) {
78
88
  if (env.ENV !== 'production') {
@@ -1 +1,5 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  export { rotateFieldApp } from './rotate-field.handler'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import { Hono } from 'hono'
3
7
  import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, EntryNotFoundError } from '@beechcms/core'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { z } from 'zod'
2
6
 
3
7
  /**
@@ -1,16 +1,131 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import { Hono } from 'hono'
7
+ import { canEditLayout, formLayoutSchema, validateLayoutAgainstSeed } from '@beechcms/core'
8
+ import { publicProblem } from '../../public/problem-details'
9
+ import type { Context } from 'hono'
3
10
  import type { Env, Variables } from '../../types'
4
11
 
12
+ type AppContext = Context<{ Bindings: Env; Variables: Variables }>
13
+
14
+ /** Returns a 403 response if the caller lacks layout-edit permission, otherwise undefined. */
15
+ function requireLayoutEditPermission(context: AppContext) {
16
+ const role = context.get('jwtPayload')?.role
17
+ if (!canEditLayout(role)) {
18
+ return publicProblem(context, {
19
+ type: 'forbidden',
20
+ title: 'Forbidden',
21
+ status: 403,
22
+ detail: 'Layout edit requires admin role.',
23
+ })
24
+ }
25
+ return undefined
26
+ }
27
+
28
+ /** Resolves the seed by slug. Returns a 404 response if not found, otherwise the seed. */
29
+ function requireSeedBySlug(context: AppContext, slug: string) {
30
+ const seed = context.get('seedRegistry').get(slug)
31
+ if (!seed) {
32
+ return {
33
+ seed: null,
34
+ error: publicProblem(context, {
35
+ type: 'seed-not-found',
36
+ title: 'Seed not found',
37
+ status: 404,
38
+ detail: `No seed with slug '${slug}'.`,
39
+ }),
40
+ }
41
+ }
42
+ return { seed, error: null }
43
+ }
44
+
5
45
  const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
6
46
 
7
47
  /**
8
- * Ritorna l'intero schema del CMS (la lista dei Seed configurati).
9
- * Usato dalla Dashboard per generare dinamicamente il menu e le form.
48
+ * Returns the full CMS schema (all configured Seeds), each optionally enriched
49
+ * with a stored custom FormLayout. Used by the Dashboard to drive menus and forms.
10
50
  */
11
51
  schemaApp.get('/', async (context) => {
12
52
  const registry = context.get('seedRegistry')
13
- return context.json(registry.all())
53
+ const layouts = await context.get('seedLayoutRepository').getAllAsMap()
54
+
55
+ const enriched = registry.all().map((seed) => {
56
+ const stored = layouts.get(seed.slug)
57
+ if (!stored) return seed
58
+ const result = validateLayoutAgainstSeed(stored, seed)
59
+ return { ...seed, layout: result.cleaned }
60
+ })
61
+
62
+ return context.json(enriched)
63
+ })
64
+
65
+ /**
66
+ * Upserts a custom FormLayout for a Seed.
67
+ * Admin-only. Validates shape (Zod) then semantic constraints against the Seed.
68
+ */
69
+ schemaApp.put('/:slug/layout', async (context) => {
70
+ const forbidden = requireLayoutEditPermission(context)
71
+ if (forbidden) return forbidden
72
+
73
+ const slug = context.req.param('slug')
74
+ const { seed, error: seedError } = requireSeedBySlug(context, slug)
75
+ if (seedError) return seedError
76
+
77
+ let body: unknown
78
+ try {
79
+ body = await context.req.json()
80
+ } catch {
81
+ return publicProblem(context, {
82
+ type: 'invalid-json',
83
+ title: 'Invalid JSON',
84
+ status: 400,
85
+ detail: 'Body must be valid JSON.',
86
+ })
87
+ }
88
+
89
+ const parsed = formLayoutSchema.safeParse(body)
90
+ if (!parsed.success) {
91
+ return publicProblem(context, {
92
+ type: 'invalid-layout',
93
+ title: 'Invalid layout',
94
+ status: 422,
95
+ detail: parsed.error.message,
96
+ })
97
+ }
98
+
99
+ const semantic = validateLayoutAgainstSeed(parsed.data, seed!)
100
+ if (!semantic.ok) {
101
+ return publicProblem(context, {
102
+ type: 'invalid-layout',
103
+ title: 'Invalid layout',
104
+ status: 422,
105
+ detail: semantic.errors.join('; '),
106
+ })
107
+ }
108
+
109
+ const userId = context.get('jwtPayload')?.sub ?? 'unknown'
110
+ await context.get('seedLayoutRepository').upsert(slug, semantic.cleaned, userId)
111
+
112
+ return context.json({ ok: true, layout: semantic.cleaned })
113
+ })
114
+
115
+ /**
116
+ * Removes the stored layout for a Seed, reverting to the generated default.
117
+ * Admin-only. The "Reset" action in the Layout Builder calls this endpoint.
118
+ */
119
+ schemaApp.delete('/:slug/layout', async (context) => {
120
+ const forbidden = requireLayoutEditPermission(context)
121
+ if (forbidden) return forbidden
122
+
123
+ const slug = context.req.param('slug')
124
+ const { error: seedError } = requireSeedBySlug(context, slug)
125
+ if (seedError) return seedError
126
+
127
+ await context.get('seedLayoutRepository').remove(slug)
128
+ return context.json({ ok: true })
14
129
  })
15
130
 
16
131
  export { schemaApp }
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ export { seedsApp } from './seeds.handler'