@beechcms/api 0.4.3 → 0.6.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/assets/dashboard/Searching.svg +1 -0
  2. package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
  3. package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
  4. package/assets/dashboard/index.html +2 -2
  5. package/assets/dashboard/noResult.svg +43 -0
  6. package/assets/dashboard/working.svg +1 -0
  7. package/migrations/0000_v040_base.sql +27 -0
  8. package/migrations/0030_test_seeds.sql +125 -0
  9. package/package.json +14 -9
  10. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
  11. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
  12. package/src/auth/bcrypt-hash-provider.ts +6 -2
  13. package/src/auth/constants.ts +4 -0
  14. package/src/auth/generate-refresh-token.test.ts +8 -4
  15. package/src/auth/hash-provider.test.ts +14 -5
  16. package/src/auth/jose-token-service.ts +7 -0
  17. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  18. package/src/auth/login.test.ts +9 -1
  19. package/src/auth/login.ts +5 -1
  20. package/src/auth/refresh.ts +7 -1
  21. package/src/auth/token-service.test.ts +9 -1
  22. package/src/factory.ts +37 -16
  23. package/src/features/automations/__tests__/action-executors.test.ts +59 -72
  24. package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
  25. package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
  26. package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
  27. package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
  28. package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
  29. package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
  30. package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
  31. package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
  32. package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
  33. package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
  34. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  35. package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
  36. package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
  37. package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
  38. package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
  39. package/src/features/automations/action-executors/index.ts +5 -1
  40. package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
  41. package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
  42. package/src/features/automations/action-executors/webhook.executor.ts +55 -13
  43. package/src/features/automations/automation-runner.ts +4 -0
  44. package/src/features/automations/automation-runner.utils.ts +4 -0
  45. package/src/features/automations/automations.handler.ts +4 -0
  46. package/src/features/automations/automations.schema.ts +19 -2
  47. package/src/features/automations/context-resolver.ts +4 -0
  48. package/src/features/automations/cron-runner.ts +4 -0
  49. package/src/features/automations/cron-runner.utils.ts +4 -0
  50. package/src/features/automations/filter-translation.ts +4 -0
  51. package/src/features/automations/index.ts +4 -0
  52. package/src/features/automations/template-grammar.ts +4 -0
  53. package/src/features/automations/var-access-resolver.ts +4 -0
  54. package/src/features/automations/when-evaluator.ts +4 -0
  55. package/src/features/automations/when-pushdown.ts +4 -0
  56. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  57. package/src/features/backrefs/backrefs.handler.ts +168 -0
  58. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  59. package/src/features/backrefs/index.ts +5 -0
  60. package/src/features/content/constants.ts +6 -0
  61. package/src/features/content/handlers/bulk.handler.ts +185 -0
  62. package/src/features/content/handlers/create.ts +19 -55
  63. package/src/features/content/handlers/delete.ts +12 -37
  64. package/src/features/content/handlers/facets.ts +4 -0
  65. package/src/features/content/handlers/get.ts +4 -0
  66. package/src/features/content/handlers/helpers.test.ts +180 -0
  67. package/src/features/content/handlers/helpers.ts +93 -0
  68. package/src/features/content/handlers/list.ts +105 -19
  69. package/src/features/content/handlers/update.ts +19 -64
  70. package/src/features/content/index.ts +6 -0
  71. package/src/features/draft/draft.handler.ts +33 -8
  72. package/src/features/draft/draft.middleware.ts +4 -0
  73. package/src/features/draft/index.ts +4 -0
  74. package/src/features/email/email.provider.ts +4 -0
  75. package/src/features/email/email.service.ts +50 -45
  76. package/src/features/email/email.types.ts +12 -1
  77. package/src/features/email/index.ts +4 -0
  78. package/src/features/email/providers/resend.ts +4 -0
  79. package/src/features/email/providers/smtp.ts +55 -0
  80. package/src/features/email/templates/automation-mail.ts +4 -0
  81. package/src/features/email/templates/password-changed.ts +4 -0
  82. package/src/features/email/templates/password-reset.ts +4 -0
  83. package/src/features/email/templates/shell.ts +4 -0
  84. package/src/features/notifications/index.ts +4 -0
  85. package/src/features/notifications/notifications.handler.ts +4 -0
  86. package/src/features/password-reset/index.ts +5 -1
  87. package/src/features/password-reset/request.ts +12 -2
  88. package/src/features/password-reset/reset.ts +12 -2
  89. package/src/features/rotate-field/index.ts +4 -0
  90. package/src/features/rotate-field/rotate-field.handler.ts +4 -0
  91. package/src/features/rotate-field/rotate-field.schema.ts +4 -0
  92. package/src/features/schema/schema.handler.ts +118 -3
  93. package/src/features/seeds/index.ts +5 -0
  94. package/src/features/seeds/seeds.handler.test.ts +632 -0
  95. package/src/features/seeds/seeds.handler.ts +693 -0
  96. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  97. package/src/features/settings/settings.handler.ts +98 -8
  98. package/src/features/setup/index.ts +124 -11
  99. package/src/features/stats/index.ts +4 -0
  100. package/src/features/stats/stats.handler.ts +11 -21
  101. package/src/features/webhooks/index.ts +63 -0
  102. package/src/index.ts +28 -19
  103. package/src/media-utils.ts +4 -0
  104. package/src/middleware/auth-providers.middleware.ts +13 -0
  105. package/src/middleware/observability.middleware.ts +21 -3
  106. package/src/middleware/rate-limit.middleware.ts +4 -0
  107. package/src/middleware/repository.middleware.ts +25 -2
  108. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  109. package/src/middleware/seed-registry.middleware.ts +21 -0
  110. package/src/middleware/storage.middleware.ts +4 -0
  111. package/src/middleware.ts +5 -7
  112. package/src/public/access-policy.ts +4 -0
  113. package/src/public/api-key-middleware.ts +4 -0
  114. package/src/public/cache-utils.ts +38 -34
  115. package/src/public/entry-projection.ts +46 -42
  116. package/src/public/idempotency.ts +23 -19
  117. package/src/public/index.ts +4 -0
  118. package/src/public/problem-details.ts +71 -0
  119. package/src/public/public-add.ts +4 -0
  120. package/src/public/public-edit.ts +4 -0
  121. package/src/public/public-errors.ts +4 -0
  122. package/src/public/public-read.ts +4 -0
  123. package/src/public/public-routes.ts +4 -0
  124. package/src/public/query-builder.test.ts +4 -0
  125. package/src/public/query-builder.ts +4 -0
  126. package/src/public/rate-limit-middleware.ts +4 -0
  127. package/src/public/read-list.ts +54 -50
  128. package/src/public/read-single.ts +48 -44
  129. package/src/public/response-builder.ts +4 -0
  130. package/src/public/sanitize.ts +4 -0
  131. package/src/public/slug-utils.ts +4 -0
  132. package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
  133. package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
  134. package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
  135. package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
  136. package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
  137. package/src/rate-limit/no-op-rate-limiter.ts +4 -0
  138. package/src/search-utils.test.ts +4 -0
  139. package/src/search-utils.ts +5 -1
  140. package/src/search.ts +5 -1
  141. package/src/shared/apply-policies.test.ts +4 -0
  142. package/src/shared/apply-policies.ts +4 -0
  143. package/src/shared/automations.repository.d1.ts +4 -0
  144. package/src/shared/background-notification-service.test.ts +4 -0
  145. package/src/shared/background-notification-service.ts +4 -0
  146. package/src/shared/base.repository.d1.ts +4 -0
  147. package/src/shared/content-utils.test.ts +4 -0
  148. package/src/shared/content-utils.ts +4 -0
  149. package/src/shared/content.repository.d1.test.ts +147 -1
  150. package/src/shared/content.repository.d1.ts +535 -66
  151. package/src/shared/d1-activity-log.repository.test.ts +4 -0
  152. package/src/shared/d1-activity-log.repository.ts +4 -0
  153. package/src/shared/d1-activity-logger.test.ts +4 -0
  154. package/src/shared/d1-activity-logger.ts +4 -0
  155. package/src/shared/d1-analytics.repository.test.ts +4 -0
  156. package/src/shared/d1-analytics.repository.ts +4 -0
  157. package/src/shared/d1-content-scan.repository.test.ts +9 -5
  158. package/src/shared/d1-content-scan.repository.ts +15 -7
  159. package/src/shared/d1-notification.repository.test.ts +4 -0
  160. package/src/shared/d1-notification.repository.ts +4 -0
  161. package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
  162. package/src/shared/d1-password-reset-token.repository.ts +4 -0
  163. package/src/shared/d1-search.repository.test.ts +4 -0
  164. package/src/shared/d1-search.repository.ts +4 -0
  165. package/src/shared/d1-session.repository.test.ts +4 -0
  166. package/src/shared/d1-session.repository.ts +4 -0
  167. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  168. package/src/shared/d1-user.repository.test.ts +8 -4
  169. package/src/shared/d1-user.repository.ts +15 -5
  170. package/src/shared/d1-widget.repository.test.ts +4 -0
  171. package/src/shared/d1-widget.repository.ts +4 -0
  172. package/src/shared/demo-data-sql.ts +54 -0
  173. package/src/shared/demo-data.repository.d1.ts +20 -0
  174. package/src/shared/execution-context-scheduler.ts +4 -0
  175. package/src/shared/fixed-clock.ts +4 -0
  176. package/src/shared/fts-sync.ts +4 -0
  177. package/src/shared/idempotency.repository.d1.test.ts +4 -0
  178. package/src/shared/idempotency.repository.d1.ts +4 -0
  179. package/src/shared/in-memory-activity-logger.ts +4 -0
  180. package/src/shared/in-memory-notification-service.ts +4 -0
  181. package/src/shared/in-memory-seed.repository.ts +51 -0
  182. package/src/shared/media.repository.d1.test.ts +4 -0
  183. package/src/shared/media.repository.d1.ts +4 -0
  184. package/src/shared/qstash-notification-service.test.ts +67 -0
  185. package/src/shared/qstash-notification-service.ts +55 -0
  186. package/src/shared/query-utils.ts +4 -0
  187. package/src/shared/request-utils.ts +4 -0
  188. package/src/shared/schema-mutator.d1.ts +64 -0
  189. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  190. package/src/shared/seed-layout.repository.d1.ts +62 -0
  191. package/src/shared/seed-registry-cache.test.ts +68 -0
  192. package/src/shared/seed-registry-cache.ts +49 -0
  193. package/src/shared/seed.repository.d1.test.ts +143 -0
  194. package/src/shared/seed.repository.d1.ts +98 -0
  195. package/src/shared/sequential-id-generator.ts +11 -0
  196. package/src/shared/site-settings.repository.d1.ts +53 -0
  197. package/src/shared/storage/factory.ts +16 -15
  198. package/src/shared/storage/s3-bucket.ts +34 -2
  199. package/src/shared/storage-utils.ts +4 -0
  200. package/src/shared/system-stats.repository.d1.test.ts +4 -0
  201. package/src/shared/system-stats.repository.d1.ts +5 -1
  202. package/src/types.ts +23 -3
  203. package/src/upload.ts +134 -123
  204. package/src/widget.ts +6 -2
  205. package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
  206. package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
  207. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { createMiddleware } from 'hono/factory'
2
6
  import type { Context } from 'hono'
3
7
  import { D1ContentRepository } from '../shared/content.repository.d1'
@@ -13,8 +17,14 @@ import { D1WidgetRepository } from '../shared/d1-widget.repository'
13
17
  import { D1SearchRepository } from '../shared/d1-search.repository'
14
18
  import { D1AnalyticsRepository } from '../shared/d1-analytics.repository'
15
19
  import { D1ContentScanRepository } from '../shared/d1-content-scan.repository'
20
+ import { D1SiteSettingsRepository } from '../shared/site-settings.repository.d1'
21
+ import { D1DemoDataRepository } from '../shared/demo-data.repository.d1'
22
+ import { D1SetupChecklistRepository } from '../shared/d1-setup-checklist.repository'
23
+ import { D1SeedLayoutRepository } from '../shared/seed-layout.repository.d1'
24
+ import { D1SeedRepository } from '../shared/seed.repository.d1'
25
+ import { D1SchemaMutator } from '../shared/schema-mutator.d1'
16
26
  import { SystemClock, SystemIdGenerator } from '@beechcms/core'
17
- import type { ContentRepository, IdempotencyRepository, MediaRepository, SystemStatsRepository, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogRepository, INotificationRepository, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler } from '@beechcms/core'
27
+ import type { ContentRepository, IdempotencyRepository, MediaRepository, SystemStatsRepository, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogRepository, INotificationRepository, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, ISiteSettingsRepository, IDemoDataRepository, ISeedLayoutRepository, ISeedRepository, ISchemaMutator } from '@beechcms/core'
18
28
  import { NoOpScheduler } from '@beechcms/core'
19
29
  import { AutomationRunner } from '../features/automations'
20
30
  import { D1AutomationRepository } from '../shared/automations.repository.d1'
@@ -40,6 +50,11 @@ interface RepositoryOverrides {
40
50
  automationRepository?: IAutomationRepository
41
51
  automationRunner?: IAutomationRunner
42
52
  scheduler?: IScheduler
53
+ siteSettingsRepository?: ISiteSettingsRepository
54
+ demoDataRepository?: IDemoDataRepository
55
+ seedLayoutRepository?: ISeedLayoutRepository
56
+ seedRepository?: ISeedRepository
57
+ schemaMutator?: ISchemaMutator
43
58
  }
44
59
 
45
60
  function buildScheduler(context: Context): IScheduler {
@@ -75,17 +90,25 @@ export const repositoryMiddleware = (overrides?: RepositoryOverrides) => {
75
90
  ?? new D1AutomationRepository(database)
76
91
 
77
92
  context.set('automationRepository', automationRepository)
93
+ // getSeed is set later by seedRegistryMiddleware — capture context by closure so the
94
+ // runner reads the live value at call time, not at middleware creation time.
78
95
  context.set(
79
96
  'automationRunner',
80
97
  overrides?.automationRunner ?? new AutomationRunner({
81
98
  automationRepository,
82
99
  contentRepository: context.get('repository'),
83
- getSeed: context.get('getSeed'),
100
+ getSeed: (slug: string) => context.get('getSeed')?.(slug) ?? null,
84
101
  idGenerator: resolvedIdGenerator,
85
102
  env: context.env as unknown as Record<string, string | undefined>,
86
103
  }),
87
104
  )
88
105
  context.set('scheduler', overrides?.scheduler ?? buildScheduler(context))
106
+ context.set('siteSettingsRepository', overrides?.siteSettingsRepository ?? new D1SiteSettingsRepository(database))
107
+ context.set('demoDataRepository', overrides?.demoDataRepository ?? new D1DemoDataRepository(database))
108
+ context.set('setupChecklistRepository', new D1SetupChecklistRepository(database))
109
+ context.set('seedLayoutRepository', overrides?.seedLayoutRepository ?? new D1SeedLayoutRepository(database))
110
+ context.set('seedRepository', overrides?.seedRepository ?? new D1SeedRepository(database))
111
+ context.set('schemaMutator', overrides?.schemaMutator ?? new D1SchemaMutator(database))
89
112
  await next()
90
113
  })
91
114
  }
@@ -0,0 +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
+ })
@@ -0,0 +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,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { createMiddleware } from 'hono/factory'
2
6
  import { BeechBucket } from '@beechcms/core'
3
7
  import { AppEnv } from '../types'
package/src/middleware.ts CHANGED
@@ -1,14 +1,12 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /// <reference types="@cloudflare/workers-types" />
2
6
  import type { Context, Next } from 'hono'
3
7
  import { HTTPException } from 'hono/http-exception'
4
8
  import type { Env, Variables } from './types'
5
9
 
6
- export type JwtPayload = {
7
- sub: string
8
- email?: string
9
- name?: string
10
- }
11
-
12
10
  const UNAUTHORIZED_JSON = JSON.stringify({ error: 'Unauthorized' })
13
11
 
14
12
  function unauthorizedResponse() {
@@ -41,7 +39,7 @@ export function authMiddleware() {
41
39
  throw new HTTPException(401, { res: unauthorizedResponse() })
42
40
  }
43
41
 
44
- c.set('jwtPayload', claims as JwtPayload)
42
+ c.set('jwtPayload', claims)
45
43
  await next()
46
44
  }
47
45
  }
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Seed } from '@beechcms/core'
2
6
 
3
7
  export type PublicOperation = 'read' | 'add' | 'edit'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Context, Next } from 'hono'
2
6
  import { PUBLIC_ERRORS } from './public-errors'
3
7
  import { publicProblem } from './problem-details'
@@ -1,34 +1,38 @@
1
- import type { Context } from 'hono'
2
-
3
- type EdgeCache = {
4
- cache: Cache
5
- executionCtx: { waitUntil: (p: Promise<unknown>) => void }
6
- } | null
7
-
8
- export function resolveEdgeCache(c: Context): EdgeCache {
9
- try {
10
- const cache = caches.default
11
- let executionCtx: any
12
- try {
13
- executionCtx = c.executionCtx
14
- } catch {
15
- return null
16
- }
17
-
18
- if (!executionCtx?.waitUntil) return null
19
- return { cache, executionCtx: executionCtx as { waitUntil: (p: Promise<unknown>) => void } }
20
- } catch {
21
- return null
22
- }
23
- }
24
-
25
- export function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response {
26
- if (!edgeCache) return response
27
- const cloned = response.clone()
28
- const headers = new Headers(cloned.headers)
29
- headers.set('Cache-Control', 'public, max-age=60')
30
- edgeCache.executionCtx.waitUntil(
31
- edgeCache.cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers }))
32
- )
33
- return response
34
- }
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,42 +1,46 @@
1
- import { resolvePolicies } from '@beechcms/core'
2
- import type { Seed } from '@beechcms/core'
3
-
4
- const SYSTEM_FIELDS = ['id', 'slug', 'status', 'created_at', 'updated_at']
5
- const IDENTITY_FIELDS = ['id', 'slug']
6
-
7
- function applyPublicPolicies(data: Record<string, unknown>, seed: Seed): Record<string, unknown> {
8
- const result: Record<string, unknown> = {}
9
-
10
- for (const key of SYSTEM_FIELDS) {
11
- if (key in data) result[key] = data[key]
12
- }
13
-
14
- for (const branch of seed.branches) {
15
- const value = data[branch.alias]
16
- const { public: isPublic, visibility } = resolvePolicies(branch)
17
- if (!isPublic) continue
18
- if (visibility === 'hidden') continue
19
- result[branch.alias] = visibility === 'masked' && typeof value === 'string' && value.length > 0
20
- ? '••••••••'
21
- : value
22
- }
23
- return result
24
- }
25
-
26
- export function toFlatPublicEntry(data: Record<string, unknown>, seed: Seed, fieldsParam?: string): Record<string, unknown> {
27
- const projected = applyPublicPolicies(data, seed)
28
- const requestedFields = (fieldsParam ?? '').split(',').map(f => f.trim()).filter(Boolean)
29
-
30
- if (requestedFields.length === 0) return projected
31
-
32
- const filtered: Record<string, unknown> = {}
33
- for (const field of requestedFields) {
34
- if (field in projected) filtered[field] = projected[field]
35
- }
36
-
37
- for (const key of IDENTITY_FIELDS) {
38
- if (key in projected && !filtered[key]) filtered[key] = projected[key]
39
- }
40
-
41
- return filtered
42
- }
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 { resolvePolicies } from '@beechcms/core'
6
+ import type { Seed } from '@beechcms/core'
7
+
8
+ const SYSTEM_FIELDS = ['id', 'slug', 'status', 'created_at', 'updated_at']
9
+ const IDENTITY_FIELDS = ['id', 'slug']
10
+
11
+ function applyPublicPolicies(data: Record<string, unknown>, seed: Seed): Record<string, unknown> {
12
+ const result: Record<string, unknown> = {}
13
+
14
+ for (const key of SYSTEM_FIELDS) {
15
+ if (key in data) result[key] = data[key]
16
+ }
17
+
18
+ for (const branch of seed.branches) {
19
+ const value = data[branch.alias]
20
+ const { public: isPublic, visibility } = resolvePolicies(branch)
21
+ if (!isPublic) continue
22
+ if (visibility === 'hidden') continue
23
+ result[branch.alias] = visibility === 'masked' && typeof value === 'string' && value.length > 0
24
+ ? '••••••••'
25
+ : value
26
+ }
27
+ return result
28
+ }
29
+
30
+ export function toFlatPublicEntry(data: Record<string, unknown>, seed: Seed, fieldsParam?: string): Record<string, unknown> {
31
+ const projected = applyPublicPolicies(data, seed)
32
+ const requestedFields = (fieldsParam ?? '').split(',').map(f => f.trim()).filter(Boolean)
33
+
34
+ if (requestedFields.length === 0) return projected
35
+
36
+ const filtered: Record<string, unknown> = {}
37
+ for (const field of requestedFields) {
38
+ if (field in projected) filtered[field] = projected[field]
39
+ }
40
+
41
+ for (const key of IDENTITY_FIELDS) {
42
+ if (key in projected && !filtered[key]) filtered[key] = projected[key]
43
+ }
44
+
45
+ return filtered
46
+ }
@@ -1,19 +1,23 @@
1
- import { sha256hex } from '@beechcms/core'
2
-
3
- export function parseIdempotencyKey(rawValue: string | undefined): string | null {
4
- if (!rawValue) return null
5
- const key = rawValue.trim()
6
- if (!key || key.length > 128) return null
7
- return key
8
- }
9
-
10
- type FingerprintInput = {
11
- seedSlug: string
12
- statusValue: unknown
13
- slug: string | null
14
- data: Record<string, unknown>
15
- }
16
-
17
- export function buildRequestFingerprint(input: FingerprintInput): Promise<string> {
18
- return sha256hex(JSON.stringify({ seedSlug: input.seedSlug, statusValue: input.statusValue, slug: input.slug, data: input.data }))
19
- }
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 { sha256hex } from '@beechcms/core'
6
+
7
+ export function parseIdempotencyKey(rawValue: string | undefined): string | null {
8
+ if (!rawValue) return null
9
+ const key = rawValue.trim()
10
+ if (!key || key.length > 128) return null
11
+ return key
12
+ }
13
+
14
+ type FingerprintInput = {
15
+ seedSlug: string
16
+ statusValue: unknown
17
+ slug: string | null
18
+ data: Record<string, unknown>
19
+ }
20
+
21
+ export function buildRequestFingerprint(input: FingerprintInput): Promise<string> {
22
+ return sha256hex(JSON.stringify({ seedSlug: input.seedSlug, statusValue: input.statusValue, slug: input.slug, data: input.data }))
23
+ }
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  export { apiKeyMiddleware } from './api-key-middleware'
2
6
  export { publicRateLimitMiddleware } from './rate-limit-middleware'
3
7
  export { publicRoutes } from './public-routes'
@@ -1,5 +1,76 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Context } from 'hono'
2
6
 
7
+ // ──────────────────────────────────────────────────────────────────────────────
8
+ // FK / SQLite error detection
9
+ // ──────────────────────────────────────────────────────────────────────────────
10
+
11
+ type FkViolationKind = 'relation-in-use' | 'relation-target-not-found' | 'foreign-key-violation'
12
+
13
+ function isFkError(error: unknown): boolean {
14
+ if (!(error instanceof Error)) return false
15
+ // D1 surfaces the error in .message
16
+ if (/FOREIGN KEY constraint failed/i.test(error.message)) return true
17
+ // Some runtimes expose cause.code
18
+ const cause = (error as { cause?: { code?: string } }).cause
19
+ if (cause?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') return true
20
+ return false
21
+ }
22
+
23
+ /**
24
+ * Classify a SQLite FK error by HTTP method.
25
+ * - DELETE → the referenced row is still used → relation-in-use
26
+ * - POST/PUT/PATCH → referenced parent does not exist → relation-target-not-found
27
+ * - anything else → generic foreign-key-violation
28
+ */
29
+ function classifyFkError(method: string): FkViolationKind {
30
+ const m = method.toUpperCase()
31
+ if (m === 'DELETE') return 'relation-in-use'
32
+ if (m === 'POST' || m === 'PUT' || m === 'PATCH') return 'relation-target-not-found'
33
+ return 'foreign-key-violation'
34
+ }
35
+
36
+ const FK_PROBLEM: Record<FkViolationKind, { title: string; status: 409 | 422; detail: string }> = {
37
+ 'relation-in-use': {
38
+ title: 'Relation In Use',
39
+ status: 409,
40
+ detail: 'The record cannot be deleted because it is referenced by one or more related entries.',
41
+ },
42
+ 'relation-target-not-found': {
43
+ title: 'Relation Target Not Found',
44
+ status: 422,
45
+ detail: 'The provided relation id does not match any existing record in the referenced collection.',
46
+ },
47
+ 'foreign-key-violation': {
48
+ title: 'Foreign Key Violation',
49
+ status: 409,
50
+ detail: 'The operation violates a referential integrity constraint.',
51
+ },
52
+ }
53
+
54
+ /**
55
+ * Returns a Problem+JSON response when the error is a SQLite FK violation,
56
+ * or null when the error is unrelated (let the caller handle it).
57
+ *
58
+ * @param c Hono context
59
+ * @param error The caught error
60
+ * @param method HTTP method of the current request (e.g. 'DELETE', 'POST')
61
+ */
62
+ export function fkProblemOrNull(c: Context, error: unknown, method: string): Response | null {
63
+ if (!isFkError(error)) return null
64
+ const kind = classifyFkError(method)
65
+ const meta = FK_PROBLEM[kind]
66
+ return publicProblem(c, {
67
+ type: `https://beechcms.dev/problems/${kind}`,
68
+ title: meta.title,
69
+ status: meta.status,
70
+ detail: meta.detail,
71
+ })
72
+ }
73
+
3
74
  export interface PublicProblemDetailItem {
4
75
  field: string
5
76
  expected: string
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { isValidContentStatus, SlugConflictError } from '@beechcms/core'
2
6
  import type { Context } from 'hono'
3
7
  import { cleanStr } from '../shared/query-utils'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { isValidContentStatus, resolvePolicies, EntryNotFoundError } from '@beechcms/core'
2
6
  import type { Seed } from '@beechcms/core'
3
7
  import type { Context } from 'hono'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  /**
2
6
  * Error constants for Public API responses.
3
7
  */
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Context } from 'hono'
2
6
  import { cleanStr } from '../shared/query-utils'
3
7
  import { checkPublicOperation } from './access-policy'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { Hono } from 'hono'
2
6
  import type { AppEnv } from '../types'
3
7
  import { publicReadHandler } from './public-read'
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import { describe, it, expect } from 'vitest'
2
6
  import {
3
7
  parsePublicFilter,
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Seed, FilterGroup, FilterOperator, FilterType, BranchType } from '@beechcms/core'
2
6
  import { parsePositiveInt } from '../shared/query-utils'
3
7
 
@@ -1,3 +1,7 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
1
5
  import type { Context, Next } from 'hono'
2
6
  import type { AppEnv } from '../types'
3
7
  import { publicProblem } from './problem-details'