@beechcms/api 0.5.0 → 0.6.0-preview.2

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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,128 +1,132 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import { Hono } from 'hono'
3
- import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, EntryNotFoundError } from '@beechcms/core'
4
- import { publicProblem } from '../../public/problem-details'
5
- import { rotateFieldRequestSchema } from './rotate-field.schema'
6
- import type { Env, Variables } from '../../types'
7
-
8
- const rotateFieldApp = new Hono<{ Bindings: Env; Variables: Variables }>()
9
-
10
- rotateFieldApp.post('/:slug/:id/rotate-field', async (context) => {
11
- const seedSlug = context.req.param('slug')
12
- const entryId = context.req.param('id')
13
-
14
- const seed = context.get('getSeed')(seedSlug)
15
- if (!seed) {
16
- return publicProblem(context, {
17
- type: 'content-seed-not-found',
18
- title: 'Not Found',
19
- status: 404,
20
- detail: `Seed '${seedSlug}' not found`
21
- })
22
- }
23
-
24
- let requestBody: unknown
25
- try {
26
- requestBody = await context.req.json()
27
- } catch {
28
- return publicProblem(context, {
29
- type: 'rotate-field-invalid-json',
30
- title: 'Bad Request',
31
- status: 400,
32
- detail: 'Invalid JSON body'
33
- })
34
- }
35
-
36
- const parsedRequestBody = rotateFieldRequestSchema.safeParse(requestBody)
37
- if (!parsedRequestBody.success) {
38
- return publicProblem(context, {
39
- type: 'rotate-field-invalid-body',
40
- title: 'Bad Request',
41
- status: 400,
42
- detail: parsedRequestBody.error.issues[0]?.message ?? 'Invalid body'
43
- })
44
- }
45
-
46
- const { fieldAlias, currentValue, nextValue } = parsedRequestBody.data
47
-
48
- const targetFieldBranch = seed.branches.find((branch) => branch.alias === fieldAlias)
49
- if (!targetFieldBranch) {
50
- return publicProblem(context, {
51
- type: 'rotate-field-unknown-field',
52
- title: 'Bad Request',
53
- status: 400,
54
- detail: `Field '${fieldAlias}' does not exist in seed '${seedSlug}'`
55
- })
56
- }
57
-
58
- const { privacy } = resolvePolicies(targetFieldBranch)
59
- if (privacy !== 'hash') {
60
- return publicProblem(context, {
61
- type: 'rotate-field-not-hashable',
62
- title: 'Unprocessable Entity',
63
- status: 422,
64
- detail: `Field '${fieldAlias}' does not use hash privacy and cannot be rotated with this endpoint`
65
- })
66
- }
67
-
68
- let contentRecord: Record<string, unknown>
69
- try {
70
- contentRecord = await context.get('repository').findById(seed, entryId)
71
- } catch (error) {
72
- if (error instanceof EntryNotFoundError) {
73
- return publicProblem(context, {
74
- type: 'content-not-found',
75
- title: 'Not Found',
76
- status: 404,
77
- detail: `Entry '${entryId}' not found`
78
- })
79
- }
80
- throw error
81
- }
82
-
83
- const storedFieldValueHash = contentRecord[targetFieldBranch.alias]
84
-
85
- if (typeof storedFieldValueHash !== 'string' || storedFieldValueHash.length === 0) {
86
- return publicProblem(context, {
87
- type: 'rotate-field-not-set',
88
- title: 'Unprocessable Entity',
89
- status: 422,
90
- detail: `Field '${fieldAlias}' has no stored value to rotate`
91
- })
92
- }
93
-
94
- const isCurrentValueValid = await verifyHashField(storedFieldValueHash, currentValue)
95
- if (!isCurrentValueValid) {
96
- return publicProblem(context, {
97
- type: 'rotate-field-current-mismatch',
98
- title: 'Forbidden',
99
- status: 403,
100
- detail: 'Current value does not match stored value'
101
- })
102
- }
103
-
104
- const fieldValidationResult = validateAndSanitizeSeedPayload(
105
- seed,
106
- { [fieldAlias]: nextValue },
107
- { operation: 'update', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: false }
108
- )
109
-
110
- if (fieldValidationResult.details.length > 0) {
111
- return publicProblem(context, {
112
- type: 'rotate-field-invalid-next',
113
- title: 'Bad Request',
114
- status: 400,
115
- detail: `Invalid value for field '${fieldAlias}': ${fieldValidationResult.details[0]?.message ?? 'validation failed'}`
116
- })
117
- }
118
-
119
- const newFieldValueHash = await sha256hex(nextValue)
120
-
121
- await context.get('repository').update(seed, entryId, { [targetFieldBranch.alias]: newFieldValueHash })
122
-
123
- return context.json({ success: true })
124
- })
125
-
126
-
127
- export { rotateFieldApp }
128
-
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
+ /// <reference types="@cloudflare/workers-types" />
6
+ import { Hono } from 'hono'
7
+ import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, EntryNotFoundError } from '@beechcms/core'
8
+ import { publicProblem } from '../../public/problem-details'
9
+ import { rotateFieldRequestSchema } from './rotate-field.schema'
10
+ import type { Env, Variables } from '../../types'
11
+
12
+ const rotateFieldApp = new Hono<{ Bindings: Env; Variables: Variables }>()
13
+
14
+ rotateFieldApp.post('/:slug/:id/rotate-field', async (context) => {
15
+ const seedSlug = context.req.param('slug')
16
+ const entryId = context.req.param('id')
17
+
18
+ const seed = context.get('getSeed')(seedSlug)
19
+ if (!seed) {
20
+ return publicProblem(context, {
21
+ type: 'content-seed-not-found',
22
+ title: 'Not Found',
23
+ status: 404,
24
+ detail: `Seed '${seedSlug}' not found`
25
+ })
26
+ }
27
+
28
+ let requestBody: unknown
29
+ try {
30
+ requestBody = await context.req.json()
31
+ } catch {
32
+ return publicProblem(context, {
33
+ type: 'rotate-field-invalid-json',
34
+ title: 'Bad Request',
35
+ status: 400,
36
+ detail: 'Invalid JSON body'
37
+ })
38
+ }
39
+
40
+ const parsedRequestBody = rotateFieldRequestSchema.safeParse(requestBody)
41
+ if (!parsedRequestBody.success) {
42
+ return publicProblem(context, {
43
+ type: 'rotate-field-invalid-body',
44
+ title: 'Bad Request',
45
+ status: 400,
46
+ detail: parsedRequestBody.error.issues[0]?.message ?? 'Invalid body'
47
+ })
48
+ }
49
+
50
+ const { fieldAlias, currentValue, nextValue } = parsedRequestBody.data
51
+
52
+ const targetFieldBranch = seed.branches.find((branch) => branch.alias === fieldAlias)
53
+ if (!targetFieldBranch) {
54
+ return publicProblem(context, {
55
+ type: 'rotate-field-unknown-field',
56
+ title: 'Bad Request',
57
+ status: 400,
58
+ detail: `Field '${fieldAlias}' does not exist in seed '${seedSlug}'`
59
+ })
60
+ }
61
+
62
+ const { privacy } = resolvePolicies(targetFieldBranch)
63
+ if (privacy !== 'hash') {
64
+ return publicProblem(context, {
65
+ type: 'rotate-field-not-hashable',
66
+ title: 'Unprocessable Entity',
67
+ status: 422,
68
+ detail: `Field '${fieldAlias}' does not use hash privacy and cannot be rotated with this endpoint`
69
+ })
70
+ }
71
+
72
+ let contentRecord: Record<string, unknown>
73
+ try {
74
+ contentRecord = await context.get('repository').findById(seed, entryId)
75
+ } catch (error) {
76
+ if (error instanceof EntryNotFoundError) {
77
+ return publicProblem(context, {
78
+ type: 'content-not-found',
79
+ title: 'Not Found',
80
+ status: 404,
81
+ detail: `Entry '${entryId}' not found`
82
+ })
83
+ }
84
+ throw error
85
+ }
86
+
87
+ const storedFieldValueHash = contentRecord[targetFieldBranch.alias]
88
+
89
+ if (typeof storedFieldValueHash !== 'string' || storedFieldValueHash.length === 0) {
90
+ return publicProblem(context, {
91
+ type: 'rotate-field-not-set',
92
+ title: 'Unprocessable Entity',
93
+ status: 422,
94
+ detail: `Field '${fieldAlias}' has no stored value to rotate`
95
+ })
96
+ }
97
+
98
+ const isCurrentValueValid = await verifyHashField(storedFieldValueHash, currentValue)
99
+ if (!isCurrentValueValid) {
100
+ return publicProblem(context, {
101
+ type: 'rotate-field-current-mismatch',
102
+ title: 'Forbidden',
103
+ status: 403,
104
+ detail: 'Current value does not match stored value'
105
+ })
106
+ }
107
+
108
+ const fieldValidationResult = validateAndSanitizeSeedPayload(
109
+ seed,
110
+ { [fieldAlias]: nextValue },
111
+ { operation: 'update', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: false }
112
+ )
113
+
114
+ if (fieldValidationResult.details.length > 0) {
115
+ return publicProblem(context, {
116
+ type: 'rotate-field-invalid-next',
117
+ title: 'Bad Request',
118
+ status: 400,
119
+ detail: `Invalid value for field '${fieldAlias}': ${fieldValidationResult.details[0]?.message ?? 'validation failed'}`
120
+ })
121
+ }
122
+
123
+ const newFieldValueHash = await sha256hex(nextValue)
124
+
125
+ await context.get('repository').update(seed, entryId, { [targetFieldBranch.alias]: newFieldValueHash })
126
+
127
+ return context.json({ success: true })
128
+ })
129
+
130
+
131
+ export { rotateFieldApp }
132
+
@@ -1,13 +1,17 @@
1
- import { z } from 'zod'
2
-
3
- /**
4
- * Schema for rotating a hashed field value.
5
- * Used to update fields that have 'hash' privacy (e.g., passwords, PINs).
6
- */
7
- export const rotateFieldRequestSchema = z.object({
8
- fieldAlias: z.string().min(1, "The field alias is required"),
9
- currentValue: z.string().min(1, "The current value is required"),
10
- nextValue: z.string().min(1, "The new value is required"),
11
- })
12
-
13
- export type RotateFieldRequest = z.infer<typeof rotateFieldRequestSchema>
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
+ import { z } from 'zod'
6
+
7
+ /**
8
+ * Schema for rotating a hashed field value.
9
+ * Used to update fields that have 'hash' privacy (e.g., passwords, PINs).
10
+ */
11
+ export const rotateFieldRequestSchema = z.object({
12
+ fieldAlias: z.string().min(1, "The field alias is required"),
13
+ currentValue: z.string().min(1, "The current value is required"),
14
+ nextValue: z.string().min(1, "The new value is required"),
15
+ })
16
+
17
+ export type RotateFieldRequest = z.infer<typeof rotateFieldRequestSchema>
@@ -1,16 +1,131 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import { Hono } from 'hono'
3
- import type { Env, Variables } from '../../types'
4
-
5
- const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
6
-
7
- /**
8
- * Ritorna l'intero schema del CMS (la lista dei Seed configurati).
9
- * Usato dalla Dashboard per generare dinamicamente il menu e le form.
10
- */
11
- schemaApp.get('/', async (context) => {
12
- const registry = context.get('seedRegistry')
13
- return context.json(registry.all())
14
- })
15
-
16
- export { schemaApp }
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
+ /// <reference types="@cloudflare/workers-types" />
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'
10
+ import type { Env, Variables } from '../../types'
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
+
45
+ const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
46
+
47
+ /**
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.
50
+ */
51
+ schemaApp.get('/', async (context) => {
52
+ const registry = context.get('seedRegistry')
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 })
129
+ })
130
+
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'