@beechcms/api 0.6.0-preview.1 → 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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
|
@@ -1,132 +1,132 @@
|
|
|
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
|
+
// 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,17 +1,17 @@
|
|
|
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
|
+
// 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,131 +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
|
-
|
|
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 }
|
|
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 }
|
|
@@ -1,5 +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'
|
|
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'
|