@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,97 +1,97 @@
|
|
|
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 { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
6
|
-
import { Hono } from 'hono'
|
|
7
|
-
import { seedRegistryMiddleware } from './seed-registry.middleware'
|
|
8
|
-
import { __resetSeedRegistryCache } from '../shared/seed-registry-cache'
|
|
9
|
-
import type { ISeedRepository, Seed } from '@beechcms/core'
|
|
10
|
-
|
|
11
|
-
const mockSeed: Seed = {
|
|
12
|
-
slug: 'posts',
|
|
13
|
-
label: 'Posts',
|
|
14
|
-
displayNameAlias: 'title',
|
|
15
|
-
branches: [{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' }],
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function makeRepo(seeds: Seed[] = [mockSeed]): ISeedRepository {
|
|
19
|
-
return {
|
|
20
|
-
listActive: vi.fn().mockResolvedValue(seeds),
|
|
21
|
-
listAll: vi.fn().mockResolvedValue([]),
|
|
22
|
-
get: vi.fn().mockResolvedValue(null),
|
|
23
|
-
upsert: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
softDelete: vi.fn().mockResolvedValue(undefined),
|
|
25
|
-
getRegistryVersion: vi.fn().mockResolvedValue(1),
|
|
26
|
-
bumpRegistryVersion: vi.fn().mockResolvedValue(2),
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('seedRegistryMiddleware', () => {
|
|
31
|
-
beforeEach(() => __resetSeedRegistryCache())
|
|
32
|
-
|
|
33
|
-
it('injects seedRegistry, getSeed, and backrefMap into context', async () => {
|
|
34
|
-
const app = new Hono()
|
|
35
|
-
const repo = makeRepo()
|
|
36
|
-
|
|
37
|
-
app.use('*', async (c, next) => {
|
|
38
|
-
c.set('seedRepository' as never, repo)
|
|
39
|
-
await next()
|
|
40
|
-
})
|
|
41
|
-
app.use('*', seedRegistryMiddleware())
|
|
42
|
-
app.get('/test', (c) => {
|
|
43
|
-
const registry = c.get('seedRegistry' as never) as any
|
|
44
|
-
const getSeed = c.get('getSeed' as never) as any
|
|
45
|
-
const backrefMap = c.get('backrefMap' as never) as any
|
|
46
|
-
return c.json({
|
|
47
|
-
hasRegistry: !!registry,
|
|
48
|
-
hasSeed: !!getSeed('posts'),
|
|
49
|
-
hasBackrefMap: !!backrefMap,
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const res = await app.request('/test')
|
|
54
|
-
const body = await res.json() as any
|
|
55
|
-
expect(body.hasRegistry).toBe(true)
|
|
56
|
-
expect(body.hasSeed).toBe(true)
|
|
57
|
-
expect(body.hasBackrefMap).toBe(true)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('getSeed returns null for unknown slug', async () => {
|
|
61
|
-
const app = new Hono()
|
|
62
|
-
const repo = makeRepo()
|
|
63
|
-
|
|
64
|
-
app.use('*', async (c, next) => {
|
|
65
|
-
c.set('seedRepository' as never, repo)
|
|
66
|
-
await next()
|
|
67
|
-
})
|
|
68
|
-
app.use('*', seedRegistryMiddleware())
|
|
69
|
-
app.get('/test', (c) => {
|
|
70
|
-
const getSeed = c.get('getSeed' as never) as any
|
|
71
|
-
return c.json({ result: getSeed('no-such-slug') })
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const res = await app.request('/test')
|
|
75
|
-
const body = await res.json() as any
|
|
76
|
-
expect(body.result).toBeNull()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('empty seed list yields empty registry without crashing', async () => {
|
|
80
|
-
const app = new Hono()
|
|
81
|
-
const repo = makeRepo([])
|
|
82
|
-
|
|
83
|
-
app.use('*', async (c, next) => {
|
|
84
|
-
c.set('seedRepository' as never, repo)
|
|
85
|
-
await next()
|
|
86
|
-
})
|
|
87
|
-
app.use('*', seedRegistryMiddleware())
|
|
88
|
-
app.get('/test', (c) => {
|
|
89
|
-
const registry = c.get('seedRegistry' as never) as any
|
|
90
|
-
return c.json({ count: registry.all().length })
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const res = await app.request('/test')
|
|
94
|
-
const body = await res.json() as any
|
|
95
|
-
expect(body.count).toBe(0)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
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 { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import { seedRegistryMiddleware } from './seed-registry.middleware'
|
|
8
|
+
import { __resetSeedRegistryCache } from '../shared/seed-registry-cache'
|
|
9
|
+
import type { ISeedRepository, Seed } from '@beechcms/core'
|
|
10
|
+
|
|
11
|
+
const mockSeed: Seed = {
|
|
12
|
+
slug: 'posts',
|
|
13
|
+
label: 'Posts',
|
|
14
|
+
displayNameAlias: 'title',
|
|
15
|
+
branches: [{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' }],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeRepo(seeds: Seed[] = [mockSeed]): ISeedRepository {
|
|
19
|
+
return {
|
|
20
|
+
listActive: vi.fn().mockResolvedValue(seeds),
|
|
21
|
+
listAll: vi.fn().mockResolvedValue([]),
|
|
22
|
+
get: vi.fn().mockResolvedValue(null),
|
|
23
|
+
upsert: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
softDelete: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
getRegistryVersion: vi.fn().mockResolvedValue(1),
|
|
26
|
+
bumpRegistryVersion: vi.fn().mockResolvedValue(2),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('seedRegistryMiddleware', () => {
|
|
31
|
+
beforeEach(() => __resetSeedRegistryCache())
|
|
32
|
+
|
|
33
|
+
it('injects seedRegistry, getSeed, and backrefMap into context', async () => {
|
|
34
|
+
const app = new Hono()
|
|
35
|
+
const repo = makeRepo()
|
|
36
|
+
|
|
37
|
+
app.use('*', async (c, next) => {
|
|
38
|
+
c.set('seedRepository' as never, repo)
|
|
39
|
+
await next()
|
|
40
|
+
})
|
|
41
|
+
app.use('*', seedRegistryMiddleware())
|
|
42
|
+
app.get('/test', (c) => {
|
|
43
|
+
const registry = c.get('seedRegistry' as never) as any
|
|
44
|
+
const getSeed = c.get('getSeed' as never) as any
|
|
45
|
+
const backrefMap = c.get('backrefMap' as never) as any
|
|
46
|
+
return c.json({
|
|
47
|
+
hasRegistry: !!registry,
|
|
48
|
+
hasSeed: !!getSeed('posts'),
|
|
49
|
+
hasBackrefMap: !!backrefMap,
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const res = await app.request('/test')
|
|
54
|
+
const body = await res.json() as any
|
|
55
|
+
expect(body.hasRegistry).toBe(true)
|
|
56
|
+
expect(body.hasSeed).toBe(true)
|
|
57
|
+
expect(body.hasBackrefMap).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('getSeed returns null for unknown slug', async () => {
|
|
61
|
+
const app = new Hono()
|
|
62
|
+
const repo = makeRepo()
|
|
63
|
+
|
|
64
|
+
app.use('*', async (c, next) => {
|
|
65
|
+
c.set('seedRepository' as never, repo)
|
|
66
|
+
await next()
|
|
67
|
+
})
|
|
68
|
+
app.use('*', seedRegistryMiddleware())
|
|
69
|
+
app.get('/test', (c) => {
|
|
70
|
+
const getSeed = c.get('getSeed' as never) as any
|
|
71
|
+
return c.json({ result: getSeed('no-such-slug') })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const res = await app.request('/test')
|
|
75
|
+
const body = await res.json() as any
|
|
76
|
+
expect(body.result).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('empty seed list yields empty registry without crashing', async () => {
|
|
80
|
+
const app = new Hono()
|
|
81
|
+
const repo = makeRepo([])
|
|
82
|
+
|
|
83
|
+
app.use('*', async (c, next) => {
|
|
84
|
+
c.set('seedRepository' as never, repo)
|
|
85
|
+
await next()
|
|
86
|
+
})
|
|
87
|
+
app.use('*', seedRegistryMiddleware())
|
|
88
|
+
app.get('/test', (c) => {
|
|
89
|
+
const registry = c.get('seedRegistry' as never) as any
|
|
90
|
+
return c.json({ count: registry.all().length })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const res = await app.request('/test')
|
|
94
|
+
const body = await res.json() as any
|
|
95
|
+
expect(body.count).toBe(0)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -1,21 +1,21 @@
|
|
|
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 { createMiddleware } from 'hono/factory'
|
|
6
|
-
import type { Env, Variables } from '../types'
|
|
7
|
-
import { getHydratedRegistry } from '../shared/seed-registry-cache'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Hydrates the seed registry from D1 and injects seedRegistry, getSeed, and backrefMap
|
|
11
|
-
* into the context. Must run AFTER repositoryMiddleware (needs seedRepository) and
|
|
12
|
-
* BEFORE any handler that reads the registry.
|
|
13
|
-
*/
|
|
14
|
-
export const seedRegistryMiddleware = () =>
|
|
15
|
-
createMiddleware<{ Bindings: Env; Variables: Variables }>(async (context, next) => {
|
|
16
|
-
const { registry, backrefMap } = await getHydratedRegistry(context.get('seedRepository'))
|
|
17
|
-
context.set('seedRegistry', registry)
|
|
18
|
-
context.set('getSeed', (slug: string) => registry.get(slug))
|
|
19
|
-
context.set('backrefMap', backrefMap)
|
|
20
|
-
await next()
|
|
21
|
-
})
|
|
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 { createMiddleware } from 'hono/factory'
|
|
6
|
+
import type { Env, Variables } from '../types'
|
|
7
|
+
import { getHydratedRegistry } from '../shared/seed-registry-cache'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hydrates the seed registry from D1 and injects seedRegistry, getSeed, and backrefMap
|
|
11
|
+
* into the context. Must run AFTER repositoryMiddleware (needs seedRepository) and
|
|
12
|
+
* BEFORE any handler that reads the registry.
|
|
13
|
+
*/
|
|
14
|
+
export const seedRegistryMiddleware = () =>
|
|
15
|
+
createMiddleware<{ Bindings: Env; Variables: Variables }>(async (context, next) => {
|
|
16
|
+
const { registry, backrefMap } = await getHydratedRegistry(context.get('seedRepository'))
|
|
17
|
+
context.set('seedRegistry', registry)
|
|
18
|
+
context.set('getSeed', (slug: string) => registry.get(slug))
|
|
19
|
+
context.set('backrefMap', backrefMap)
|
|
20
|
+
await next()
|
|
21
|
+
})
|
|
@@ -1,25 +1,25 @@
|
|
|
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 { createMiddleware } from 'hono/factory'
|
|
6
|
-
import { BeechBucket } from '@beechcms/core'
|
|
7
|
-
import { AppEnv } from '../types'
|
|
8
|
-
import { createBucketProvider } from '../shared/storage/factory'
|
|
9
|
-
|
|
10
|
-
interface StorageOverrides {
|
|
11
|
-
bucket?: BeechBucket
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const storageMiddleware = (overrides?: StorageOverrides) => {
|
|
15
|
-
return createMiddleware<AppEnv>(async (context, next) => {
|
|
16
|
-
if (overrides?.bucket) {
|
|
17
|
-
context.set('bucket', overrides.bucket)
|
|
18
|
-
} else {
|
|
19
|
-
// Determine base URL for getUrl() only if we need to create the provider
|
|
20
|
-
const baseUrl = context.env.MEDIA_BASE_URL?.trim().replace(/\/$/, '') || new URL(context.req.url).origin
|
|
21
|
-
context.set('bucket', createBucketProvider(context.env, baseUrl))
|
|
22
|
-
}
|
|
23
|
-
await next()
|
|
24
|
-
})
|
|
25
|
-
}
|
|
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 { createMiddleware } from 'hono/factory'
|
|
6
|
+
import { BeechBucket } from '@beechcms/core'
|
|
7
|
+
import { AppEnv } from '../types'
|
|
8
|
+
import { createBucketProvider } from '../shared/storage/factory'
|
|
9
|
+
|
|
10
|
+
interface StorageOverrides {
|
|
11
|
+
bucket?: BeechBucket
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const storageMiddleware = (overrides?: StorageOverrides) => {
|
|
15
|
+
return createMiddleware<AppEnv>(async (context, next) => {
|
|
16
|
+
if (overrides?.bucket) {
|
|
17
|
+
context.set('bucket', overrides.bucket)
|
|
18
|
+
} else {
|
|
19
|
+
// Determine base URL for getUrl() only if we need to create the provider
|
|
20
|
+
const baseUrl = context.env.MEDIA_BASE_URL?.trim().replace(/\/$/, '') || new URL(context.req.url).origin
|
|
21
|
+
context.set('bucket', createBucketProvider(context.env, baseUrl))
|
|
22
|
+
}
|
|
23
|
+
await next()
|
|
24
|
+
})
|
|
25
|
+
}
|
package/src/middleware.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
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 type { Context, Next } from 'hono'
|
|
7
|
-
import { HTTPException } from 'hono/http-exception'
|
|
8
|
-
import type { Env, Variables } from './types'
|
|
9
|
-
|
|
10
|
-
const UNAUTHORIZED_JSON = JSON.stringify({ error: 'Unauthorized' })
|
|
11
|
-
|
|
12
|
-
function unauthorizedResponse() {
|
|
13
|
-
return new Response(UNAUTHORIZED_JSON, {
|
|
14
|
-
status: 401,
|
|
15
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* JWT authentication middleware. Reads the Bearer token from the Authorization
|
|
21
|
-
* header and delegates verification to the ITokenService injected in context.
|
|
22
|
-
* Returns 401 on any missing or invalid token; never exposes failure details.
|
|
23
|
-
*/
|
|
24
|
-
export function authMiddleware() {
|
|
25
|
-
return async (c: Context<{ Bindings: Env; Variables: Variables }>, next: Next): Promise<Response | void> => {
|
|
26
|
-
const authHeader = c.req.header('Authorization')
|
|
27
|
-
if (!authHeader?.startsWith('Bearer ')) {
|
|
28
|
-
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const token = authHeader.slice(7)
|
|
32
|
-
if (!token) {
|
|
33
|
-
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const claims = await c.get('tokenService').verify(token)
|
|
37
|
-
|
|
38
|
-
if (!claims) {
|
|
39
|
-
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
c.set('jwtPayload', claims)
|
|
43
|
-
await next()
|
|
44
|
-
}
|
|
45
|
-
}
|
|
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 type { Context, Next } from 'hono'
|
|
7
|
+
import { HTTPException } from 'hono/http-exception'
|
|
8
|
+
import type { Env, Variables } from './types'
|
|
9
|
+
|
|
10
|
+
const UNAUTHORIZED_JSON = JSON.stringify({ error: 'Unauthorized' })
|
|
11
|
+
|
|
12
|
+
function unauthorizedResponse() {
|
|
13
|
+
return new Response(UNAUTHORIZED_JSON, {
|
|
14
|
+
status: 401,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JWT authentication middleware. Reads the Bearer token from the Authorization
|
|
21
|
+
* header and delegates verification to the ITokenService injected in context.
|
|
22
|
+
* Returns 401 on any missing or invalid token; never exposes failure details.
|
|
23
|
+
*/
|
|
24
|
+
export function authMiddleware() {
|
|
25
|
+
return async (c: Context<{ Bindings: Env; Variables: Variables }>, next: Next): Promise<Response | void> => {
|
|
26
|
+
const authHeader = c.req.header('Authorization')
|
|
27
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
28
|
+
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const token = authHeader.slice(7)
|
|
32
|
+
if (!token) {
|
|
33
|
+
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const claims = await c.get('tokenService').verify(token)
|
|
37
|
+
|
|
38
|
+
if (!claims) {
|
|
39
|
+
throw new HTTPException(401, { res: unauthorizedResponse() })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
c.set('jwtPayload', claims)
|
|
43
|
+
await next()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
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 type { Seed } from '@beechcms/core'
|
|
6
|
-
|
|
7
|
-
export type PublicOperation = 'read' | 'add' | 'edit'
|
|
8
|
-
|
|
9
|
-
function isAllowed(seed: Seed, operation: PublicOperation): boolean {
|
|
10
|
-
if (operation === 'read') return seed.allowPublicRead === true
|
|
11
|
-
if (operation === 'add') return seed.allowPublicPost === true
|
|
12
|
-
return seed.allowPublicEdit === true
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function checkPublicOperation(seed: Seed, operation: PublicOperation) {
|
|
16
|
-
if (isAllowed(seed, operation)) {
|
|
17
|
-
return { ok: true } as const
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
error: {
|
|
23
|
-
error: 'Forbidden',
|
|
24
|
-
message: `Public ${operation.toUpperCase()} is not allowed for content type '${seed.slug}'.`,
|
|
25
|
-
},
|
|
26
|
-
} as const
|
|
27
|
-
}
|
|
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 type { Seed } from '@beechcms/core'
|
|
6
|
+
|
|
7
|
+
export type PublicOperation = 'read' | 'add' | 'edit'
|
|
8
|
+
|
|
9
|
+
function isAllowed(seed: Seed, operation: PublicOperation): boolean {
|
|
10
|
+
if (operation === 'read') return seed.allowPublicRead === true
|
|
11
|
+
if (operation === 'add') return seed.allowPublicPost === true
|
|
12
|
+
return seed.allowPublicEdit === true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function checkPublicOperation(seed: Seed, operation: PublicOperation) {
|
|
16
|
+
if (isAllowed(seed, operation)) {
|
|
17
|
+
return { ok: true } as const
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: {
|
|
23
|
+
error: 'Forbidden',
|
|
24
|
+
message: `Public ${operation.toUpperCase()} is not allowed for content type '${seed.slug}'.`,
|
|
25
|
+
},
|
|
26
|
+
} as const
|
|
27
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
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 type { Context, Next } from 'hono'
|
|
6
|
-
import { PUBLIC_ERRORS } from './public-errors'
|
|
7
|
-
import { publicProblem } from './problem-details'
|
|
8
|
-
|
|
9
|
-
type PublicBindings = {
|
|
10
|
-
PUBLIC_READ_API_KEY?: string
|
|
11
|
-
PUBLIC_WRITE_API_KEY?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isReadMethod(method: string): boolean {
|
|
15
|
-
return method === 'GET' || method === 'HEAD' || method === 'OPTIONS'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getConfiguredKey(env: PublicBindings, method: string): string | undefined {
|
|
19
|
-
if (isReadMethod(method)) {
|
|
20
|
-
return env.PUBLIC_READ_API_KEY
|
|
21
|
-
}
|
|
22
|
-
return env.PUBLIC_WRITE_API_KEY
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* API key auth middleware for Public API routes.
|
|
27
|
-
* Uses X-API-Key header only.
|
|
28
|
-
*/
|
|
29
|
-
export function apiKeyMiddleware() {
|
|
30
|
-
return async (c: Context, next: Next): Promise<Response | void> => {
|
|
31
|
-
const env = c.env as PublicBindings
|
|
32
|
-
const configuredKey = getConfiguredKey(env, c.req.method)
|
|
33
|
-
|
|
34
|
-
if (!configuredKey) {
|
|
35
|
-
return publicProblem(c, {
|
|
36
|
-
type: 'public-api-not-configured',
|
|
37
|
-
title: PUBLIC_ERRORS.API_KEY_FORBIDDEN.error,
|
|
38
|
-
status: 403,
|
|
39
|
-
detail: PUBLIC_ERRORS.API_KEY_FORBIDDEN.message,
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const providedKey = c.req.header('X-API-Key')
|
|
44
|
-
|
|
45
|
-
if (!providedKey || providedKey !== configuredKey) {
|
|
46
|
-
return publicProblem(c, {
|
|
47
|
-
type: 'public-api-key-unauthorized',
|
|
48
|
-
title: PUBLIC_ERRORS.API_KEY_UNAUTHORIZED.error,
|
|
49
|
-
status: 401,
|
|
50
|
-
detail: PUBLIC_ERRORS.API_KEY_UNAUTHORIZED.message,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
await next()
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
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 type { Context, Next } from 'hono'
|
|
6
|
+
import { PUBLIC_ERRORS } from './public-errors'
|
|
7
|
+
import { publicProblem } from './problem-details'
|
|
8
|
+
|
|
9
|
+
type PublicBindings = {
|
|
10
|
+
PUBLIC_READ_API_KEY?: string
|
|
11
|
+
PUBLIC_WRITE_API_KEY?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isReadMethod(method: string): boolean {
|
|
15
|
+
return method === 'GET' || method === 'HEAD' || method === 'OPTIONS'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getConfiguredKey(env: PublicBindings, method: string): string | undefined {
|
|
19
|
+
if (isReadMethod(method)) {
|
|
20
|
+
return env.PUBLIC_READ_API_KEY
|
|
21
|
+
}
|
|
22
|
+
return env.PUBLIC_WRITE_API_KEY
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* API key auth middleware for Public API routes.
|
|
27
|
+
* Uses X-API-Key header only.
|
|
28
|
+
*/
|
|
29
|
+
export function apiKeyMiddleware() {
|
|
30
|
+
return async (c: Context, next: Next): Promise<Response | void> => {
|
|
31
|
+
const env = c.env as PublicBindings
|
|
32
|
+
const configuredKey = getConfiguredKey(env, c.req.method)
|
|
33
|
+
|
|
34
|
+
if (!configuredKey) {
|
|
35
|
+
return publicProblem(c, {
|
|
36
|
+
type: 'public-api-not-configured',
|
|
37
|
+
title: PUBLIC_ERRORS.API_KEY_FORBIDDEN.error,
|
|
38
|
+
status: 403,
|
|
39
|
+
detail: PUBLIC_ERRORS.API_KEY_FORBIDDEN.message,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const providedKey = c.req.header('X-API-Key')
|
|
44
|
+
|
|
45
|
+
if (!providedKey || providedKey !== configuredKey) {
|
|
46
|
+
return publicProblem(c, {
|
|
47
|
+
type: 'public-api-key-unauthorized',
|
|
48
|
+
title: PUBLIC_ERRORS.API_KEY_UNAUTHORIZED.error,
|
|
49
|
+
status: 401,
|
|
50
|
+
detail: PUBLIC_ERRORS.API_KEY_UNAUTHORIZED.message,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await next()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -1,38 +1,38 @@
|
|
|
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 type { Context } from 'hono'
|
|
6
|
-
|
|
7
|
-
type EdgeCache = {
|
|
8
|
-
cache: Cache
|
|
9
|
-
executionCtx: { waitUntil: (p: Promise<unknown>) => void }
|
|
10
|
-
} | null
|
|
11
|
-
|
|
12
|
-
export function resolveEdgeCache(c: Context): EdgeCache {
|
|
13
|
-
try {
|
|
14
|
-
const cache = caches.default
|
|
15
|
-
let executionCtx: any
|
|
16
|
-
try {
|
|
17
|
-
executionCtx = c.executionCtx
|
|
18
|
-
} catch {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!executionCtx?.waitUntil) return null
|
|
23
|
-
return { cache, executionCtx: executionCtx as { waitUntil: (p: Promise<unknown>) => void } }
|
|
24
|
-
} catch {
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response {
|
|
30
|
-
if (!edgeCache) return response
|
|
31
|
-
const cloned = response.clone()
|
|
32
|
-
const headers = new Headers(cloned.headers)
|
|
33
|
-
headers.set('Cache-Control', 'public, max-age=60')
|
|
34
|
-
edgeCache.executionCtx.waitUntil(
|
|
35
|
-
edgeCache.cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers }))
|
|
36
|
-
)
|
|
37
|
-
return response
|
|
38
|
-
}
|
|
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 type { Context } from 'hono'
|
|
6
|
+
|
|
7
|
+
type EdgeCache = {
|
|
8
|
+
cache: Cache
|
|
9
|
+
executionCtx: { waitUntil: (p: Promise<unknown>) => void }
|
|
10
|
+
} | null
|
|
11
|
+
|
|
12
|
+
export function resolveEdgeCache(c: Context): EdgeCache {
|
|
13
|
+
try {
|
|
14
|
+
const cache = caches.default
|
|
15
|
+
let executionCtx: any
|
|
16
|
+
try {
|
|
17
|
+
executionCtx = c.executionCtx
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!executionCtx?.waitUntil) return null
|
|
23
|
+
return { cache, executionCtx: executionCtx as { waitUntil: (p: Promise<unknown>) => void } }
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response {
|
|
30
|
+
if (!edgeCache) return response
|
|
31
|
+
const cloned = response.clone()
|
|
32
|
+
const headers = new Headers(cloned.headers)
|
|
33
|
+
headers.set('Cache-Control', 'public, max-age=60')
|
|
34
|
+
edgeCache.executionCtx.waitUntil(
|
|
35
|
+
edgeCache.cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers }))
|
|
36
|
+
)
|
|
37
|
+
return response
|
|
38
|
+
}
|