@beechcms/api 0.5.0 → 0.6.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,52 +1,70 @@
1
- import { createMiddleware } from 'hono/factory'
2
- import type { IActivityLogger, INotificationService, IClock, IIdGenerator } from '@beechcms/core'
3
- import { SystemClock, SystemIdGenerator } from '@beechcms/core'
4
- import type { AppEnv } from '../types'
5
- import { D1ActivityLogger } from '../shared/d1-activity-logger'
6
- import { BackgroundNotificationService } from '../shared/background-notification-service'
7
-
8
- export interface ObservabilityOverrides {
9
- activityLogger?: IActivityLogger
10
- notificationService?: INotificationService
11
- clock?: IClock
12
- idGenerator?: IIdGenerator
13
- }
14
-
15
- /**
16
- * Observability middleware.
17
- *
18
- * Injects the activity logger and the notification service into the Hono
19
- * context. Both depend on `executionCtx.waitUntil` to fire-and-forget their
20
- * persistence work in production. The notification service additionally
21
- * depends on the notification repository — `repositoryMiddleware` MUST run
22
- * before this middleware in the factory pipeline.
23
- *
24
- * Tests can pass their own implementations via `overrides` to bypass D1.
25
- */
26
- export const observabilityMiddleware = (overrides?: ObservabilityOverrides) => {
27
- return createMiddleware<AppEnv>(async (context, next) => {
28
- let scheduleBackgroundTask: ((task: Promise<unknown>) => void) | undefined
29
- try {
30
- const executionContext = context.executionCtx
31
- scheduleBackgroundTask = executionContext.waitUntil.bind(executionContext)
32
- } catch {
33
- scheduleBackgroundTask = undefined
34
- }
35
-
36
- const resolvedClock = overrides?.clock ?? SystemClock
37
- const resolvedIdGenerator = overrides?.idGenerator ?? SystemIdGenerator
38
-
39
- const activityLogger =
40
- overrides?.activityLogger ??
41
- new D1ActivityLogger(context.env.DB, resolvedClock, resolvedIdGenerator, scheduleBackgroundTask)
42
-
43
- const notificationService =
44
- overrides?.notificationService ??
45
- new BackgroundNotificationService(context.get('notificationRepository'), scheduleBackgroundTask)
46
-
47
- context.set('activityLogger', activityLogger)
48
- context.set('notificationService', notificationService)
49
-
50
- await next()
51
- })
52
- }
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 { IActivityLogger, INotificationService, IClock, IIdGenerator } from '@beechcms/core'
7
+ import { SystemClock, SystemIdGenerator } from '@beechcms/core'
8
+ import type { AppEnv } from '../types'
9
+ import { D1ActivityLogger } from '../shared/d1-activity-logger'
10
+ import { BackgroundNotificationService } from '../shared/background-notification-service'
11
+ import { QStashNotificationService } from '../shared/qstash-notification-service'
12
+
13
+ export interface ObservabilityOverrides {
14
+ activityLogger?: IActivityLogger
15
+ notificationService?: INotificationService
16
+ clock?: IClock
17
+ idGenerator?: IIdGenerator
18
+ }
19
+
20
+ /**
21
+ * Observability middleware.
22
+ *
23
+ * Injects the activity logger and the notification service into the Hono
24
+ * context. Both depend on `executionCtx.waitUntil` to fire-and-forget their
25
+ * persistence work in production. The notification service additionally
26
+ * depends on the notification repository — `repositoryMiddleware` MUST run
27
+ * before this middleware in the factory pipeline.
28
+ *
29
+ * Tests can pass their own implementations via `overrides` to bypass D1.
30
+ */
31
+ export const observabilityMiddleware = (overrides?: ObservabilityOverrides) => {
32
+ return createMiddleware<AppEnv>(async (context, next) => {
33
+ let scheduleBackgroundTask: ((task: Promise<unknown>) => void) | undefined
34
+ try {
35
+ const executionContext = context.executionCtx
36
+ scheduleBackgroundTask = executionContext.waitUntil.bind(executionContext)
37
+ } catch {
38
+ scheduleBackgroundTask = undefined
39
+ }
40
+
41
+ const resolvedClock = overrides?.clock ?? SystemClock
42
+ const resolvedIdGenerator = overrides?.idGenerator ?? SystemIdGenerator
43
+
44
+ const activityLogger =
45
+ overrides?.activityLogger ??
46
+ new D1ActivityLogger(context.env.DB, resolvedClock, resolvedIdGenerator, scheduleBackgroundTask)
47
+
48
+ let notificationService: INotificationService
49
+ if (overrides?.notificationService) {
50
+ notificationService = overrides.notificationService
51
+ } else if (context.env.QSTASH_TOKEN && (context.env.QSTASH_CALLBACK_URL || context.env.APP_URL)) {
52
+ notificationService = new QStashNotificationService(
53
+ context.env.QSTASH_TOKEN,
54
+ (context.env.QSTASH_CALLBACK_URL ?? context.env.APP_URL) as string,
55
+ scheduleBackgroundTask,
56
+ context.env.QSTASH_URL
57
+ )
58
+ } else {
59
+ notificationService = new BackgroundNotificationService(
60
+ context.get('notificationRepository'),
61
+ scheduleBackgroundTask
62
+ )
63
+ }
64
+
65
+ context.set('activityLogger', activityLogger)
66
+ context.set('notificationService', notificationService)
67
+
68
+ await next()
69
+ })
70
+ }
@@ -1,41 +1,45 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import { createMiddleware } from 'hono/factory'
3
- import type { IRateLimiter } from '@beechcms/core'
4
- import type { AppEnv, Env } from '../types'
5
- import { CloudflareRateLimiter } from '../rate-limit/cloudflare-rate-limiter'
6
- import { NoOpRateLimiter } from '../rate-limit/no-op-rate-limiter'
7
-
8
- export type RateLimiterName =
9
- | 'login'
10
- | 'tokenRefresh'
11
- | 'forgotPassword'
12
- | 'resetPassword'
13
- | 'publicApiRead'
14
- | 'publicApiWrite'
15
-
16
- export interface IRateLimiterRegistry {
17
- getLimiter(name: RateLimiterName): IRateLimiter
18
- }
19
-
20
- const NO_OP = new NoOpRateLimiter()
21
-
22
- function buildDefaultRegistry(env: Env): IRateLimiterRegistry {
23
- const limiters: Record<RateLimiterName, IRateLimiter> = {
24
- login: env.LOGIN_RATE_LIMITER ? new CloudflareRateLimiter(env.LOGIN_RATE_LIMITER) : NO_OP,
25
- tokenRefresh: env.REFRESH_RATE_LIMITER ? new CloudflareRateLimiter(env.REFRESH_RATE_LIMITER) : NO_OP,
26
- forgotPassword: env.FORGOT_PASSWORD_RATE_LIMITER ? new CloudflareRateLimiter(env.FORGOT_PASSWORD_RATE_LIMITER) : NO_OP,
27
- resetPassword: env.RESET_PASSWORD_RATE_LIMITER ? new CloudflareRateLimiter(env.RESET_PASSWORD_RATE_LIMITER) : NO_OP,
28
- publicApiRead: env.PUBLIC_READ_RATE_LIMITER ? new CloudflareRateLimiter(env.PUBLIC_READ_RATE_LIMITER) : NO_OP,
29
- publicApiWrite: env.PUBLIC_WRITE_RATE_LIMITER ? new CloudflareRateLimiter(env.PUBLIC_WRITE_RATE_LIMITER) : NO_OP,
30
- }
31
-
32
- return { getLimiter: (name) => limiters[name] }
33
- }
34
-
35
- export const rateLimiterMiddleware = (overrides?: { registry?: IRateLimiterRegistry }) => {
36
- return createMiddleware<AppEnv>(async (context, next) => {
37
- const registry = overrides?.registry ?? buildDefaultRegistry(context.env)
38
- context.set('rateLimiters', registry)
39
- await next()
40
- })
41
- }
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 { createMiddleware } from 'hono/factory'
7
+ import type { IRateLimiter } from '@beechcms/core'
8
+ import type { AppEnv, Env } from '../types'
9
+ import { CloudflareRateLimiter } from '../rate-limit/cloudflare-rate-limiter'
10
+ import { NoOpRateLimiter } from '../rate-limit/no-op-rate-limiter'
11
+
12
+ export type RateLimiterName =
13
+ | 'login'
14
+ | 'tokenRefresh'
15
+ | 'forgotPassword'
16
+ | 'resetPassword'
17
+ | 'publicApiRead'
18
+ | 'publicApiWrite'
19
+
20
+ export interface IRateLimiterRegistry {
21
+ getLimiter(name: RateLimiterName): IRateLimiter
22
+ }
23
+
24
+ const NO_OP = new NoOpRateLimiter()
25
+
26
+ function buildDefaultRegistry(env: Env): IRateLimiterRegistry {
27
+ const limiters: Record<RateLimiterName, IRateLimiter> = {
28
+ login: env.LOGIN_RATE_LIMITER ? new CloudflareRateLimiter(env.LOGIN_RATE_LIMITER) : NO_OP,
29
+ tokenRefresh: env.REFRESH_RATE_LIMITER ? new CloudflareRateLimiter(env.REFRESH_RATE_LIMITER) : NO_OP,
30
+ forgotPassword: env.FORGOT_PASSWORD_RATE_LIMITER ? new CloudflareRateLimiter(env.FORGOT_PASSWORD_RATE_LIMITER) : NO_OP,
31
+ resetPassword: env.RESET_PASSWORD_RATE_LIMITER ? new CloudflareRateLimiter(env.RESET_PASSWORD_RATE_LIMITER) : NO_OP,
32
+ publicApiRead: env.PUBLIC_READ_RATE_LIMITER ? new CloudflareRateLimiter(env.PUBLIC_READ_RATE_LIMITER) : NO_OP,
33
+ publicApiWrite: env.PUBLIC_WRITE_RATE_LIMITER ? new CloudflareRateLimiter(env.PUBLIC_WRITE_RATE_LIMITER) : NO_OP,
34
+ }
35
+
36
+ return { getLimiter: (name) => limiters[name] }
37
+ }
38
+
39
+ export const rateLimiterMiddleware = (overrides?: { registry?: IRateLimiterRegistry }) => {
40
+ return createMiddleware<AppEnv>(async (context, next) => {
41
+ const registry = overrides?.registry ?? buildDefaultRegistry(context.env)
42
+ context.set('rateLimiters', registry)
43
+ await next()
44
+ })
45
+ }
@@ -1,91 +1,117 @@
1
- import { createMiddleware } from 'hono/factory'
2
- import type { Context } from 'hono'
3
- import { D1ContentRepository } from '../shared/content.repository.d1'
4
- import { D1IdempotencyRepository } from '../shared/idempotency.repository.d1'
5
- import { D1MediaRepository } from '../shared/media.repository.d1'
6
- import { D1SystemStatsRepository } from '../shared/system-stats.repository.d1'
7
- import { D1UserRepository } from '../shared/d1-user.repository'
8
- import { D1SessionRepository } from '../shared/d1-session.repository'
9
- import { D1PasswordResetTokenRepository } from '../shared/d1-password-reset-token.repository'
10
- import { D1ActivityLogRepository } from '../shared/d1-activity-log.repository'
11
- import { D1NotificationRepository } from '../shared/d1-notification.repository'
12
- import { D1WidgetRepository } from '../shared/d1-widget.repository'
13
- import { D1SearchRepository } from '../shared/d1-search.repository'
14
- import { D1AnalyticsRepository } from '../shared/d1-analytics.repository'
15
- import { D1ContentScanRepository } from '../shared/d1-content-scan.repository'
16
- 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'
18
- import { NoOpScheduler } from '@beechcms/core'
19
- import { AutomationRunner } from '../features/automations'
20
- import { D1AutomationRepository } from '../shared/automations.repository.d1'
21
- import { ExecutionContextScheduler } from '../shared/execution-context-scheduler'
22
- import type { Env, Variables } from '../types'
23
-
24
- interface RepositoryOverrides {
25
- repository?: ContentRepository
26
- idempotencyRepository?: IdempotencyRepository
27
- mediaRepository?: MediaRepository
28
- systemStatsRepository?: SystemStatsRepository
29
- userRepository?: IUserRepository
30
- sessionRepository?: ISessionRepository
31
- passwordResetTokenRepository?: IPasswordResetTokenRepository
32
- activityLogRepository?: IActivityLogRepository
33
- notificationRepository?: INotificationRepository
34
- widgetRepository?: IWidgetRepository
35
- searchRepository?: ISearchRepository
36
- analyticsRepository?: IAnalyticsRepository
37
- contentScanRepository?: IContentScanRepository
38
- clock?: IClock
39
- idGenerator?: IIdGenerator
40
- automationRepository?: IAutomationRepository
41
- automationRunner?: IAutomationRunner
42
- scheduler?: IScheduler
43
- }
44
-
45
- function buildScheduler(context: Context): IScheduler {
46
- try {
47
- return new ExecutionContextScheduler(context.executionCtx)
48
- } catch {
49
- return new NoOpScheduler()
50
- }
51
- }
52
-
53
- export const repositoryMiddleware = (overrides?: RepositoryOverrides) => {
54
- return createMiddleware<{ Bindings: Env; Variables: Variables }>(async (context, next) => {
55
- const resolvedClock = overrides?.clock ?? SystemClock
56
- const resolvedIdGenerator = overrides?.idGenerator ?? SystemIdGenerator
57
- const database = context.env.DB
58
-
59
- context.set('repository', overrides?.repository ?? new D1ContentRepository(database))
60
- context.set('idempotencyRepository', overrides?.idempotencyRepository ?? new D1IdempotencyRepository(database))
61
- context.set('mediaRepository', overrides?.mediaRepository ?? new D1MediaRepository(database))
62
- context.set('systemStatsRepository', overrides?.systemStatsRepository ?? new D1SystemStatsRepository(database))
63
- context.set('userRepository', overrides?.userRepository ?? new D1UserRepository(database))
64
- context.set('sessionRepository', overrides?.sessionRepository ?? new D1SessionRepository(database, resolvedClock))
65
- context.set('passwordResetTokenRepository', overrides?.passwordResetTokenRepository ?? new D1PasswordResetTokenRepository(database, resolvedIdGenerator))
66
- context.set('activityLogRepository', overrides?.activityLogRepository ?? new D1ActivityLogRepository(database))
67
- context.set('notificationRepository', overrides?.notificationRepository ?? new D1NotificationRepository(database, resolvedClock, resolvedIdGenerator))
68
- context.set('widgetRepository', overrides?.widgetRepository ?? new D1WidgetRepository(database))
69
- context.set('searchRepository', overrides?.searchRepository ?? new D1SearchRepository(database))
70
- context.set('analyticsRepository', overrides?.analyticsRepository ?? new D1AnalyticsRepository(database, resolvedClock))
71
- context.set('contentScanRepository', overrides?.contentScanRepository ?? new D1ContentScanRepository(database))
72
- context.set('clock', resolvedClock)
73
- context.set('idGenerator', resolvedIdGenerator)
74
- const automationRepository = overrides?.automationRepository
75
- ?? new D1AutomationRepository(database)
76
-
77
- context.set('automationRepository', automationRepository)
78
- context.set(
79
- 'automationRunner',
80
- overrides?.automationRunner ?? new AutomationRunner({
81
- automationRepository,
82
- contentRepository: context.get('repository'),
83
- getSeed: context.get('getSeed'),
84
- idGenerator: resolvedIdGenerator,
85
- env: context.env as unknown as Record<string, string | undefined>,
86
- }),
87
- )
88
- context.set('scheduler', overrides?.scheduler ?? buildScheduler(context))
89
- await next()
90
- })
91
- }
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 { Context } from 'hono'
7
+ import { D1ContentRepository } from '../shared/content.repository.d1'
8
+ import { D1IdempotencyRepository } from '../shared/idempotency.repository.d1'
9
+ import { D1MediaRepository } from '../shared/media.repository.d1'
10
+ import { D1SystemStatsRepository } from '../shared/system-stats.repository.d1'
11
+ import { D1UserRepository } from '../shared/d1-user.repository'
12
+ import { D1SessionRepository } from '../shared/d1-session.repository'
13
+ import { D1PasswordResetTokenRepository } from '../shared/d1-password-reset-token.repository'
14
+ import { D1ActivityLogRepository } from '../shared/d1-activity-log.repository'
15
+ import { D1NotificationRepository } from '../shared/d1-notification.repository'
16
+ import { D1WidgetRepository } from '../shared/d1-widget.repository'
17
+ import { D1SearchRepository } from '../shared/d1-search.repository'
18
+ import { D1AnalyticsRepository } from '../shared/d1-analytics.repository'
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 { D1DashboardLayoutRepository } from '../shared/dashboard-layout.repository.d1'
25
+ import { D1SeedRepository } from '../shared/seed.repository.d1'
26
+ import { D1SchemaMutator } from '../shared/schema-mutator.d1'
27
+ import { SystemClock, SystemIdGenerator } from '@beechcms/core'
28
+ 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, IDashboardLayoutRepository } from '@beechcms/core'
29
+ import { NoOpScheduler } from '@beechcms/core'
30
+ import { AutomationRunner } from '../features/automations'
31
+ import { D1AutomationRepository } from '../shared/automations.repository.d1'
32
+ import { ExecutionContextScheduler } from '../shared/execution-context-scheduler'
33
+ import type { Env, Variables } from '../types'
34
+
35
+ interface RepositoryOverrides {
36
+ repository?: ContentRepository
37
+ idempotencyRepository?: IdempotencyRepository
38
+ mediaRepository?: MediaRepository
39
+ systemStatsRepository?: SystemStatsRepository
40
+ userRepository?: IUserRepository
41
+ sessionRepository?: ISessionRepository
42
+ passwordResetTokenRepository?: IPasswordResetTokenRepository
43
+ activityLogRepository?: IActivityLogRepository
44
+ notificationRepository?: INotificationRepository
45
+ widgetRepository?: IWidgetRepository
46
+ searchRepository?: ISearchRepository
47
+ analyticsRepository?: IAnalyticsRepository
48
+ contentScanRepository?: IContentScanRepository
49
+ clock?: IClock
50
+ idGenerator?: IIdGenerator
51
+ automationRepository?: IAutomationRepository
52
+ automationRunner?: IAutomationRunner
53
+ scheduler?: IScheduler
54
+ siteSettingsRepository?: ISiteSettingsRepository
55
+ demoDataRepository?: IDemoDataRepository
56
+ seedLayoutRepository?: ISeedLayoutRepository
57
+ seedRepository?: ISeedRepository
58
+ schemaMutator?: ISchemaMutator
59
+ dashboardLayoutRepository?: IDashboardLayoutRepository
60
+ }
61
+
62
+ function buildScheduler(context: Context): IScheduler {
63
+ try {
64
+ return new ExecutionContextScheduler(context.executionCtx)
65
+ } catch {
66
+ return new NoOpScheduler()
67
+ }
68
+ }
69
+
70
+ export const repositoryMiddleware = (overrides?: RepositoryOverrides) => {
71
+ return createMiddleware<{ Bindings: Env; Variables: Variables }>(async (context, next) => {
72
+ const resolvedClock = overrides?.clock ?? SystemClock
73
+ const resolvedIdGenerator = overrides?.idGenerator ?? SystemIdGenerator
74
+ const database = context.env.DB
75
+
76
+ context.set('repository', overrides?.repository ?? new D1ContentRepository(database))
77
+ context.set('idempotencyRepository', overrides?.idempotencyRepository ?? new D1IdempotencyRepository(database))
78
+ context.set('mediaRepository', overrides?.mediaRepository ?? new D1MediaRepository(database))
79
+ context.set('systemStatsRepository', overrides?.systemStatsRepository ?? new D1SystemStatsRepository(database))
80
+ context.set('userRepository', overrides?.userRepository ?? new D1UserRepository(database))
81
+ context.set('sessionRepository', overrides?.sessionRepository ?? new D1SessionRepository(database, resolvedClock))
82
+ context.set('passwordResetTokenRepository', overrides?.passwordResetTokenRepository ?? new D1PasswordResetTokenRepository(database, resolvedIdGenerator))
83
+ context.set('activityLogRepository', overrides?.activityLogRepository ?? new D1ActivityLogRepository(database))
84
+ context.set('notificationRepository', overrides?.notificationRepository ?? new D1NotificationRepository(database, resolvedClock, resolvedIdGenerator))
85
+ context.set('widgetRepository', overrides?.widgetRepository ?? new D1WidgetRepository(database))
86
+ context.set('searchRepository', overrides?.searchRepository ?? new D1SearchRepository(database))
87
+ context.set('analyticsRepository', overrides?.analyticsRepository ?? new D1AnalyticsRepository(database, resolvedClock))
88
+ context.set('contentScanRepository', overrides?.contentScanRepository ?? new D1ContentScanRepository(database))
89
+ context.set('clock', resolvedClock)
90
+ context.set('idGenerator', resolvedIdGenerator)
91
+ const automationRepository = overrides?.automationRepository
92
+ ?? new D1AutomationRepository(database)
93
+
94
+ context.set('automationRepository', automationRepository)
95
+ // getSeed is set later by seedRegistryMiddleware — capture context by closure so the
96
+ // runner reads the live value at call time, not at middleware creation time.
97
+ context.set(
98
+ 'automationRunner',
99
+ overrides?.automationRunner ?? new AutomationRunner({
100
+ automationRepository,
101
+ contentRepository: context.get('repository'),
102
+ getSeed: (slug: string) => context.get('getSeed')?.(slug) ?? null,
103
+ idGenerator: resolvedIdGenerator,
104
+ env: context.env as unknown as Record<string, string | undefined>,
105
+ }),
106
+ )
107
+ context.set('scheduler', overrides?.scheduler ?? buildScheduler(context))
108
+ context.set('siteSettingsRepository', overrides?.siteSettingsRepository ?? new D1SiteSettingsRepository(database))
109
+ context.set('demoDataRepository', overrides?.demoDataRepository ?? new D1DemoDataRepository(database))
110
+ context.set('setupChecklistRepository', new D1SetupChecklistRepository(database))
111
+ context.set('seedLayoutRepository', overrides?.seedLayoutRepository ?? new D1SeedLayoutRepository(database))
112
+ context.set('seedRepository', overrides?.seedRepository ?? new D1SeedRepository(database))
113
+ context.set('schemaMutator', overrides?.schemaMutator ?? new D1SchemaMutator(database))
114
+ context.set('dashboardLayoutRepository', overrides?.dashboardLayoutRepository ?? new D1DashboardLayoutRepository(database))
115
+ await next()
116
+ })
117
+ }
@@ -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,21 +1,25 @@
1
- import { createMiddleware } from 'hono/factory'
2
- import { BeechBucket } from '@beechcms/core'
3
- import { AppEnv } from '../types'
4
- import { createBucketProvider } from '../shared/storage/factory'
5
-
6
- interface StorageOverrides {
7
- bucket?: BeechBucket
8
- }
9
-
10
- export const storageMiddleware = (overrides?: StorageOverrides) => {
11
- return createMiddleware<AppEnv>(async (context, next) => {
12
- if (overrides?.bucket) {
13
- context.set('bucket', overrides.bucket)
14
- } else {
15
- // Determine base URL for getUrl() only if we need to create the provider
16
- const baseUrl = context.env.MEDIA_BASE_URL?.trim().replace(/\/$/, '') || new URL(context.req.url).origin
17
- context.set('bucket', createBucketProvider(context.env, baseUrl))
18
- }
19
- await next()
20
- })
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 { 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
+ }