@beechcms/api 0.5.0 → 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 (206) 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/package.json +13 -9
  9. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
  10. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
  11. package/src/auth/bcrypt-hash-provider.ts +6 -2
  12. package/src/auth/constants.ts +4 -0
  13. package/src/auth/generate-refresh-token.test.ts +8 -4
  14. package/src/auth/hash-provider.test.ts +14 -5
  15. package/src/auth/jose-token-service.ts +7 -0
  16. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  17. package/src/auth/login.test.ts +9 -1
  18. package/src/auth/login.ts +5 -1
  19. package/src/auth/refresh.ts +7 -1
  20. package/src/auth/token-service.test.ts +9 -1
  21. package/src/factory.ts +37 -16
  22. package/src/features/automations/__tests__/action-executors.test.ts +59 -72
  23. package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
  24. package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
  25. package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
  26. package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
  27. package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
  28. package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
  29. package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
  30. package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
  31. package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
  32. package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
  33. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  34. package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
  35. package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
  36. package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
  37. package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
  38. package/src/features/automations/action-executors/index.ts +5 -1
  39. package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
  40. package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
  41. package/src/features/automations/action-executors/webhook.executor.ts +55 -13
  42. package/src/features/automations/automation-runner.ts +4 -0
  43. package/src/features/automations/automation-runner.utils.ts +4 -0
  44. package/src/features/automations/automations.handler.ts +4 -0
  45. package/src/features/automations/automations.schema.ts +19 -2
  46. package/src/features/automations/context-resolver.ts +4 -0
  47. package/src/features/automations/cron-runner.ts +4 -0
  48. package/src/features/automations/cron-runner.utils.ts +4 -0
  49. package/src/features/automations/filter-translation.ts +4 -0
  50. package/src/features/automations/index.ts +4 -0
  51. package/src/features/automations/template-grammar.ts +4 -0
  52. package/src/features/automations/var-access-resolver.ts +4 -0
  53. package/src/features/automations/when-evaluator.ts +4 -0
  54. package/src/features/automations/when-pushdown.ts +4 -0
  55. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  56. package/src/features/backrefs/backrefs.handler.ts +168 -0
  57. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  58. package/src/features/backrefs/index.ts +5 -0
  59. package/src/features/content/constants.ts +6 -0
  60. package/src/features/content/handlers/bulk.handler.ts +185 -0
  61. package/src/features/content/handlers/create.ts +19 -55
  62. package/src/features/content/handlers/delete.ts +12 -37
  63. package/src/features/content/handlers/facets.ts +4 -0
  64. package/src/features/content/handlers/get.ts +4 -0
  65. package/src/features/content/handlers/helpers.test.ts +180 -0
  66. package/src/features/content/handlers/helpers.ts +93 -0
  67. package/src/features/content/handlers/list.ts +105 -19
  68. package/src/features/content/handlers/update.ts +19 -64
  69. package/src/features/content/index.ts +6 -0
  70. package/src/features/draft/draft.handler.ts +33 -8
  71. package/src/features/draft/draft.middleware.ts +4 -0
  72. package/src/features/draft/index.ts +4 -0
  73. package/src/features/email/email.provider.ts +4 -0
  74. package/src/features/email/email.service.ts +50 -45
  75. package/src/features/email/email.types.ts +12 -1
  76. package/src/features/email/index.ts +4 -0
  77. package/src/features/email/providers/resend.ts +4 -0
  78. package/src/features/email/providers/smtp.ts +55 -0
  79. package/src/features/email/templates/automation-mail.ts +4 -0
  80. package/src/features/email/templates/password-changed.ts +4 -0
  81. package/src/features/email/templates/password-reset.ts +4 -0
  82. package/src/features/email/templates/shell.ts +4 -0
  83. package/src/features/notifications/index.ts +4 -0
  84. package/src/features/notifications/notifications.handler.ts +4 -0
  85. package/src/features/password-reset/index.ts +5 -1
  86. package/src/features/password-reset/request.ts +12 -2
  87. package/src/features/password-reset/reset.ts +12 -2
  88. package/src/features/rotate-field/index.ts +4 -0
  89. package/src/features/rotate-field/rotate-field.handler.ts +4 -0
  90. package/src/features/rotate-field/rotate-field.schema.ts +4 -0
  91. package/src/features/schema/schema.handler.ts +118 -3
  92. package/src/features/seeds/index.ts +5 -0
  93. package/src/features/seeds/seeds.handler.test.ts +632 -0
  94. package/src/features/seeds/seeds.handler.ts +693 -0
  95. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  96. package/src/features/settings/settings.handler.ts +98 -8
  97. package/src/features/setup/index.ts +124 -11
  98. package/src/features/stats/index.ts +4 -0
  99. package/src/features/stats/stats.handler.ts +11 -21
  100. package/src/features/webhooks/index.ts +63 -0
  101. package/src/index.ts +28 -19
  102. package/src/media-utils.ts +4 -0
  103. package/src/middleware/auth-providers.middleware.ts +13 -0
  104. package/src/middleware/observability.middleware.ts +21 -3
  105. package/src/middleware/rate-limit.middleware.ts +4 -0
  106. package/src/middleware/repository.middleware.ts +25 -2
  107. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  108. package/src/middleware/seed-registry.middleware.ts +21 -0
  109. package/src/middleware/storage.middleware.ts +4 -0
  110. package/src/middleware.ts +5 -7
  111. package/src/public/access-policy.ts +4 -0
  112. package/src/public/api-key-middleware.ts +4 -0
  113. package/src/public/cache-utils.ts +4 -0
  114. package/src/public/entry-projection.ts +4 -0
  115. package/src/public/idempotency.ts +4 -0
  116. package/src/public/index.ts +4 -0
  117. package/src/public/problem-details.ts +71 -0
  118. package/src/public/public-add.ts +4 -0
  119. package/src/public/public-edit.ts +4 -0
  120. package/src/public/public-errors.ts +4 -0
  121. package/src/public/public-read.ts +4 -0
  122. package/src/public/public-routes.ts +4 -0
  123. package/src/public/query-builder.test.ts +4 -0
  124. package/src/public/query-builder.ts +4 -0
  125. package/src/public/rate-limit-middleware.ts +4 -0
  126. package/src/public/read-list.ts +4 -0
  127. package/src/public/read-single.ts +4 -0
  128. package/src/public/response-builder.ts +4 -0
  129. package/src/public/sanitize.ts +4 -0
  130. package/src/public/slug-utils.ts +4 -0
  131. package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
  132. package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
  133. package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
  134. package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
  135. package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
  136. package/src/rate-limit/no-op-rate-limiter.ts +4 -0
  137. package/src/search-utils.test.ts +4 -0
  138. package/src/search-utils.ts +4 -0
  139. package/src/search.ts +4 -0
  140. package/src/shared/apply-policies.test.ts +4 -0
  141. package/src/shared/apply-policies.ts +4 -0
  142. package/src/shared/automations.repository.d1.ts +4 -0
  143. package/src/shared/background-notification-service.test.ts +4 -0
  144. package/src/shared/background-notification-service.ts +4 -0
  145. package/src/shared/base.repository.d1.ts +4 -0
  146. package/src/shared/content-utils.test.ts +4 -0
  147. package/src/shared/content-utils.ts +4 -0
  148. package/src/shared/content.repository.d1.test.ts +147 -1
  149. package/src/shared/content.repository.d1.ts +535 -66
  150. package/src/shared/d1-activity-log.repository.test.ts +4 -0
  151. package/src/shared/d1-activity-log.repository.ts +4 -0
  152. package/src/shared/d1-activity-logger.test.ts +4 -0
  153. package/src/shared/d1-activity-logger.ts +4 -0
  154. package/src/shared/d1-analytics.repository.test.ts +4 -0
  155. package/src/shared/d1-analytics.repository.ts +4 -0
  156. package/src/shared/d1-content-scan.repository.test.ts +9 -5
  157. package/src/shared/d1-content-scan.repository.ts +15 -7
  158. package/src/shared/d1-notification.repository.test.ts +4 -0
  159. package/src/shared/d1-notification.repository.ts +4 -0
  160. package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
  161. package/src/shared/d1-password-reset-token.repository.ts +4 -0
  162. package/src/shared/d1-search.repository.test.ts +4 -0
  163. package/src/shared/d1-search.repository.ts +4 -0
  164. package/src/shared/d1-session.repository.test.ts +4 -0
  165. package/src/shared/d1-session.repository.ts +4 -0
  166. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  167. package/src/shared/d1-user.repository.test.ts +8 -4
  168. package/src/shared/d1-user.repository.ts +15 -5
  169. package/src/shared/d1-widget.repository.test.ts +4 -0
  170. package/src/shared/d1-widget.repository.ts +4 -0
  171. package/src/shared/demo-data-sql.ts +54 -0
  172. package/src/shared/demo-data.repository.d1.ts +20 -0
  173. package/src/shared/execution-context-scheduler.ts +4 -0
  174. package/src/shared/fixed-clock.ts +4 -0
  175. package/src/shared/fts-sync.ts +4 -0
  176. package/src/shared/idempotency.repository.d1.test.ts +4 -0
  177. package/src/shared/idempotency.repository.d1.ts +4 -0
  178. package/src/shared/in-memory-activity-logger.ts +4 -0
  179. package/src/shared/in-memory-notification-service.ts +4 -0
  180. package/src/shared/in-memory-seed.repository.ts +51 -0
  181. package/src/shared/media.repository.d1.test.ts +4 -0
  182. package/src/shared/media.repository.d1.ts +4 -0
  183. package/src/shared/qstash-notification-service.test.ts +67 -0
  184. package/src/shared/qstash-notification-service.ts +55 -0
  185. package/src/shared/query-utils.ts +4 -0
  186. package/src/shared/request-utils.ts +4 -0
  187. package/src/shared/schema-mutator.d1.ts +64 -0
  188. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  189. package/src/shared/seed-layout.repository.d1.ts +62 -0
  190. package/src/shared/seed-registry-cache.test.ts +68 -0
  191. package/src/shared/seed-registry-cache.ts +49 -0
  192. package/src/shared/seed.repository.d1.test.ts +143 -0
  193. package/src/shared/seed.repository.d1.ts +98 -0
  194. package/src/shared/sequential-id-generator.ts +11 -0
  195. package/src/shared/site-settings.repository.d1.ts +53 -0
  196. package/src/shared/storage/factory.ts +16 -15
  197. package/src/shared/storage/s3-bucket.ts +34 -2
  198. package/src/shared/storage-utils.ts +4 -0
  199. package/src/shared/system-stats.repository.d1.test.ts +4 -0
  200. package/src/shared/system-stats.repository.d1.ts +5 -1
  201. package/src/types.ts +23 -3
  202. package/src/upload.ts +134 -123
  203. package/src/widget.ts +6 -2
  204. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  205. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  206. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="673.818" viewBox="0 0 800 673.818" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/"><g transform="translate(-826.017 -264.531)"><g transform="matrix(0.946, -0.326, 0.326, 0.946, 826.017, 424.075)"><path d="M114.77,145.533H13.015A13.03,13.03,0,0,1,0,132.514V13.015A13.03,13.03,0,0,1,13.015,0h94.666l20.1,16.024v116.49A13.03,13.03,0,0,1,114.77,145.533Z" transform="translate(0 0)" fill="#e6e6e6"/><path d="M9.269,0A9.284,9.284,0,0,0,0,9.269v119.5a9.284,9.284,0,0,0,9.269,9.269H111.023a9.284,9.284,0,0,0,9.269-9.269V14.082L102.624,0Z" transform="translate(3.746 3.746)" fill="#fff"/><path d="M48.495,4.335H2.168A2.168,2.168,0,1,1,2.168,0H48.495a2.168,2.168,0,0,1,0,4.335Z" transform="translate(46.483 34.16)" fill="#FF6584"/><path d="M57.435,4.335H2.168A2.168,2.168,0,1,1,2.168,0H57.435a2.168,2.168,0,1,1,0,4.335Z" transform="translate(46.483 41.475)" fill="#FF6584"/><path d="M48.493,0H2.167a2.167,2.167,0,0,0,0,4.334H48.493a2.167,2.167,0,1,0,0-4.334Z" transform="translate(46.486 66.94)" fill="#ccc"/><path d="M57.434,0H2.167a2.167,2.167,0,0,0,0,4.334H57.434a2.167,2.167,0,1,0,0-4.334Z" transform="translate(46.486 74.257)" fill="#ccc"/><path d="M48.494,4.334H2.167A2.167,2.167,0,0,1,2.167,0H48.494a2.168,2.168,0,1,1,0,4.335Z" transform="translate(46.483 99.722)" fill="#ccc"/><path d="M57.435,4.335H2.168A2.168,2.168,0,1,1,2.168,0H57.435a2.168,2.168,0,1,1,0,4.335Z" transform="translate(46.483 107.037)" fill="#ccc"/><circle cx="7.661" cy="7.661" r="7.661" transform="translate(21.698 32.325)" fill="#FF6584"/><path d="M15.32,7.661a7.661,7.661,0,0,1-7.66,7.663,1.645,1.645,0,0,1-.2-.007,7.66,7.66,0,1,1,7.856-7.655Z" transform="translate(22.486 65.103)" fill="#FF6584"/><circle cx="7.661" cy="7.661" r="7.661" transform="translate(21.698 97.887)" fill="#FF6584"/><path d="M20.083,16.235H4.334A4.335,4.335,0,0,1,0,11.9V.269A.271.271,0,0,1,.439.058L20.251,15.752a.271.271,0,0,1-.168.483Z" transform="translate(107.31 0.305)" fill="#e6e6e6"/></g><circle cx="1.547" cy="1.547" r="1.547" transform="translate(991.127 861.128)" opacity="0.2"/><g transform="matrix(0.998, 0.07, -0.07, 0.998, 1403.442, 449.522)"><rect width="223.119" height="45.534" rx="16" fill="#e6e6e6"/><path d="M0,22.773V.006H8.061a8.379,8.379,0,0,1,3.931.817A5.449,5.449,0,0,1,14.3,3.008a6.165,6.165,0,0,1,.756,3.033,5.036,5.036,0,0,1-.519,2.422,4.2,4.2,0,0,1-1.366,1.521,6.345,6.345,0,0,1-1.84.84v.214a4.44,4.44,0,0,1,2.13.733,5.359,5.359,0,0,1,1.771,1.91,5.993,5.993,0,0,1,.71,3.026,6.107,6.107,0,0,1-.787,3.086,5.417,5.417,0,0,1-2.466,2.178,10.177,10.177,0,0,1-4.366.8Zm2.9-2.567H8.244a5.564,5.564,0,0,0,3.778-1.025,3.263,3.263,0,0,0,1.122-2.5,4.009,4.009,0,0,0-.581-2.108,4.249,4.249,0,0,0-1.646-1.545,5.188,5.188,0,0,0-2.526-.581H2.9ZM2.9,10H7.909a4.641,4.641,0,0,0,3.1-1.07A3.516,3.516,0,0,0,12.275,6.1a3.4,3.4,0,0,0-1.03-2.5A4.451,4.451,0,0,0,8,2.574H2.9Z" transform="translate(27.321 11.378)" fill="#090814"/><g transform="translate(86.515 11.333)"><path d="M1.29,10.6a1.275,1.275,0,0,1-.911-.388,1.347,1.347,0,0,1,0-1.874L8.144.388a1.273,1.273,0,0,1,1.83,0,1.347,1.347,0,0,1,0,1.874L2.205,10.21a1.275,1.275,0,0,1-.915.388Z" transform="translate(6.475 6.136)"/><path d="M10.353,15.412a1.278,1.278,0,0,1-.978-.455A1.347,1.347,0,0,1,9.5,13.087l.655-.583C12.3,10.278,13.3,5.638,11.422,3.716s-6.407-.9-8.583,1.292l-.569.672a1.274,1.274,0,0,1-1.821.137A1.347,1.347,0,0,1,.315,3.948l.6-.717q.031-.036.064-.071A11.089,11.089,0,0,1,6.907.192,6.98,6.98,0,0,1,13.253,1.84a7.386,7.386,0,0,1,1.61,6.5,11.558,11.558,0,0,1-2.905,6.069q-.032.033-.066.064l-.691.615A1.272,1.272,0,0,1,10.353,15.412Z" transform="translate(7.765 0)"/><path d="M7.015,15.355a6.455,6.455,0,0,1-4.72-1.785A8.057,8.057,0,0,1,0,7.945C0,5.6,2.337,2.262,3.588,1L3.643.943,4.326.33A1.274,1.274,0,0,1,6.147.444a1.347,1.347,0,0,1-.114,1.87l-.65.587c-1.228,1.252-2.8,3.91-2.8,5.042a5.359,5.359,0,0,0,1.527,3.742c1.884,1.907,6.42.911,8.63-1.258L13.2,9.8a1.275,1.275,0,0,1,1.811-.273,1.346,1.346,0,0,1,.263,1.851l-.513.707a1.317,1.317,0,0,1-.129.151,11.217,11.217,0,0,1-5.96,2.968,10.1,10.1,0,0,1-1.656.147Z" transform="translate(0 7.463)"/></g><rect width="2.277" height="45.534" transform="translate(122.943 0)" fill="#fff"/><path d="M0,22.775,2.786,6.235H5.495L2.71,22.775ZM4.608,3.528a1.582,1.582,0,0,1-1.235-.521,1.417,1.417,0,0,1-.342-1.243A1.961,1.961,0,0,1,3.8.512,2.237,2.237,0,0,1,5.215,0a1.611,1.611,0,0,1,1.252.51,1.42,1.42,0,0,1,.342,1.252,1.982,1.982,0,0,1-.771,1.243A2.237,2.237,0,0,1,4.608,3.528Z" transform="translate(61.471 11.376)"/><g transform="translate(138.88 11.384)"><path d="M2.277,0A2.277,2.277,0,1,1,0,2.277,2.277,2.277,0,0,1,2.277,0Z" transform="translate(11.384 4.553)" fill="#090814"/><path d="M4.553,0h13.66a4.553,4.553,0,0,1,4.553,4.553v13.66a4.553,4.553,0,0,1-4.553,4.553H4.553A4.553,4.553,0,0,1,0,18.214V4.553A4.553,4.553,0,0,1,4.553,0Zm13.66,20.491a2.277,2.277,0,0,0,2.277-2.277V4.553a2.277,2.277,0,0,0-2.277-2.277H4.553A2.277,2.277,0,0,0,2.277,4.553v13.66a2.277,2.277,0,0,0,2.277,2.277Z" transform="translate(0 0)"/><path d="M15.937,8.728a1.138,1.138,0,0,1-.8-.333L9.448,2.71a1.165,1.165,0,0,0-1.821,0L1.943,8.394a1.138,1.138,0,1,1-1.61-1.61L6.025,1.093l.016-.015a3.431,3.431,0,0,1,4.994,0l.016.015,5.692,5.692a1.138,1.138,0,0,1-.806,1.943Z" transform="translate(0 8.348)"/><path d="M9.107,6.451a1.138,1.138,0,0,1-.8-.333L4.895,2.71a1.165,1.165,0,0,0-1.821,0l-1.13,1.13a1.138,1.138,0,1,1-1.61-1.61L1.472,1.093l.016-.015a3.431,3.431,0,0,1,4.994,0l.016.015L9.913,4.508a1.138,1.138,0,0,1-.806,1.943Z" transform="translate(12.522 10.624)"/></g><g transform="translate(179.86 11.387)"><path d="M3.131,22.763A3.193,3.193,0,0,1,0,19.511V9.754C0,4.664,2.584,1.309,7.447.045a1.562,1.562,0,0,1,1.9,1.183A1.631,1.631,0,0,1,8.207,3.2c-3.486.911-5.076,2.971-5.076,6.554V11.38h4.7a3.187,3.187,0,0,1,3.123,3.009l.008.244v4.879a3.193,3.193,0,0,1-3.13,3.252Z" transform="translate(14.087 0)"/><path d="M3.131,22.762A3.193,3.193,0,0,1,0,19.51V9.753C0,4.664,2.584,1.309,7.447.045a1.562,1.562,0,0,1,1.9,1.183A1.631,1.631,0,0,1,8.207,3.2c-3.486.911-5.076,2.971-5.076,6.554v1.624h4.7a3.187,3.187,0,0,1,3.123,3.009l.008.244V19.51a3.193,3.193,0,0,1-3.13,3.252Z" transform="translate(0 0.001)"/></g></g><path d="M843.21,705.353H432.577A28.17,28.17,0,0,0,404.44,733.49a27.852,27.852,0,0,0,2.03,10.467,28.139,28.139,0,0,0,26.107,17.679H843.21A28.139,28.139,0,0,0,867.493,747.7a27.17,27.17,0,0,0,1.834-3.742,27.856,27.856,0,0,0,2.03-10.467,28.178,28.178,0,0,0-28.146-28.137Z" transform="translate(576.882 171.013)" fill="#d6d6e3"/><rect width="59.815" height="70.309" transform="translate(1186.541 620.026)" fill="#ed9da0"/><path d="M422.732,358.755h6.253a11.464,11.464,0,0,1,11.461,11.464H411.268a11.464,11.464,0,0,1,11.464-11.464Z" transform="translate(784.75 215.89)" fill="#ccc"/><path d="M64.587,0c27.538,0,60.456,41.809,60.456,41.809s4.131,14.646,4.131,22.779A64.587,64.587,0,1,1,64.587,0Z" transform="translate(1152.034 510.51)" fill="#ed9da0"/><path d="M461.292,766.982H818.543c-2.391-27.251-3.948-131.421-3.948-138.276,0-30.239-24.743-63.238-24.743-63.238l-8.856-4.895L707.032,537.5l-19.141-24.865a21.287,21.287,0,0,0-17.047-8.324l-60.464.455a21.349,21.349,0,0,0-14.343,5.683L558.595,545.18,503.682,571.4l-.093-.093-.569.427-8.15,5.87-5.236,3.788s-13.762,16.5-24.754,65.986C463.226,654.823,462.066,751.175,461.292,766.982Z" transform="translate(579.69 161.084)" fill="#f2f2f2"/><path d="M905.823,742.141c-.86.785-1.73,1.562-2.6,2.338q-4.939,4.392-10.018,8.633-7.675,6.4-15.668,12.408H396.608q-14.685-11.028-28.3-23.385Z" transform="translate(575.098 172.829)" fill="#d6d6e3"/><path d="M523.094,689.248H346.3l3.741-17.772H519.349Z" transform="translate(781.541 231.335)" fill="#090814"/><path d="M867.982,674.4h-46a7.106,7.106,0,0,0-5.454,2.526,10.3,10.3,0,0,0-1.945-.187,10.911,10.911,0,1,0,0,21.822,10.783,10.783,0,0,0,4.749-1.1l.77,9.427,3.05,37.266a6.81,6.81,0,0,0,.579,1.338,7.172,7.172,0,0,0,6.3,3.742H859.94a7.2,7.2,0,0,0,6.3-3.742,7.389,7.389,0,0,0,.569-1.338l8.045-60.447a7.193,7.193,0,0,0-6.876-9.307Zm-53.4,21.047a7.8,7.8,0,0,1,0-15.6c.14,0,.271.038.412.047a7.141,7.141,0,0,0,.114,3.807l3.292,10.7a7.671,7.671,0,0,1-3.817,1.038Z" transform="translate(596.598 169.485)" fill="#FF6584"/><path d="M789.056,485.931H489.161a9.73,9.73,0,0,0-9.72,9.72V698.02a9.73,9.73,0,0,0,9.72,9.72H789.056a9.73,9.73,0,0,0,9.72-9.72V495.651A9.73,9.73,0,0,0,789.056,485.931Z" transform="translate(574.701 153.388)" fill="#3f3d56"/><path d="M661.816,635.978H593.691a3.635,3.635,0,0,0-3.629,3.643V747.239h75.394V639.616a3.637,3.637,0,0,0-3.643-3.643Zm-33.833,61.147a8.082,8.082,0,0,1-8.045-8.045V676.672a8.045,8.045,0,1,1,16.089,0V689.08a8.082,8.082,0,0,1-8.045,8.045Z" transform="translate(586.049 167.586)" fill="#090814"/><path d="M589.839,738.717V749.1a2.034,2.034,0,0,0,2.03,2.03h71.806a2.04,2.04,0,0,0,2.03-2.03V738.717Z" transform="translate(586.038 172.661)" fill="#3f3d56"/><circle cx="4.598" cy="4.598" r="4.598" transform="translate(1349.885 840.806)" fill="#2f2e41"/><path d="M523.543,669.493a1.609,1.609,0,0,0-1.609,1.609v8.278a1.609,1.609,0,0,0,3.218,0V671.1a1.609,1.609,0,0,0-1.609-1.609Z" transform="translate(582.685 169.242)" fill="#2f2e41"/><path d="M531.868,669.493a1.609,1.609,0,0,0-1.609,1.609v8.278a1.609,1.609,0,0,0,3.218,0V671.1a1.608,1.608,0,0,0-1.608-1.609Z" transform="translate(583.095 169.242)" fill="#2f2e41"/><path d="M540.2,669.493a1.609,1.609,0,0,0-1.609,1.609v8.278a1.609,1.609,0,0,0,3.218,0V671.1a1.609,1.609,0,0,0-1.61-1.609Z" transform="translate(583.508 169.242)" fill="#2f2e41"/><path d="M548.519,669.493a1.609,1.609,0,0,0-1.609,1.609v8.278a1.609,1.609,0,0,0,3.218,0V671.1a1.609,1.609,0,0,0-1.609-1.609Z" transform="translate(583.918 169.242)" fill="#2f2e41"/><path d="M24.32,0C37.751,0,53.8,20.726,53.8,34.158S37.751,48.64,24.32,48.64A24.32,24.32,0,0,1,24.32,0Z" transform="translate(1115.087 867.265)" fill="#ed9da0"/><path d="M25.434,0a24.32,24.32,0,1,1,0,48.64C12,48.64-8.048,37.751-8.048,24.32S12,0,25.434,0Z" transform="translate(1283.879 871.942)" fill="#ed9da0"/><path d="M974.16,446.985l1.3-7-17.507-3.249s-24.762-22.707-53.171-6.247S863.917,455.5,863.917,455.5l-13.1,12.056,9.854,5.451-12.454,8.554,10.5,1.95-8.953,9.2,14.752,29.9s2.755-26.654,23.763-22.755S939,480.3,939,480.3l45.373,48.265s.38-21.56,12.644-19.334C997.015,509.232,1012.025,457.635,974.16,446.985Z" transform="translate(284.224 63.235)" fill="#090814"/><g transform="translate(1130.828 265.531)"><path d="M220.083,139.917H3.6a2.608,2.608,0,0,1-2.6-2.6V3.6A2.608,2.608,0,0,1,3.6,1H220.083a2.608,2.608,0,0,1,2.6,2.6V137.312A2.608,2.608,0,0,1,220.083,139.917Z" transform="translate(-1 -1)" fill="#fff"/><path d="M220.083,139.917H3.6a2.608,2.608,0,0,1-2.6-2.6V3.6A2.608,2.608,0,0,1,3.6,1H220.083a2.608,2.608,0,0,1,2.6,2.6V137.312A2.608,2.608,0,0,1,220.083,139.917Z" transform="translate(-1 -1)" fill="none" stroke="#d7d7d7" stroke-miterlimit="10" stroke-width="2"/><path d="M154.417,132.04H20.131a2.442,2.442,0,0,1-2.6-2.231V56.941a2.442,2.442,0,0,1,2.6-2.231H154.417a2.442,2.442,0,0,1,2.6,2.231V129.81a2.442,2.442,0,0,1-2.6,2.231Z" transform="translate(-11.415 -38.09)" fill="#FF6584"/><g transform="translate(4.382 5.21)"><path d="M29.971,25.959h-9.84a2.608,2.608,0,0,1-2.6-2.6V22.2a2.608,2.608,0,0,1,2.6-2.6h9.84a2.608,2.608,0,0,1,2.6,2.6v1.158a2.608,2.608,0,0,1-2.6,2.6Z" transform="translate(-17.278 -19.592)" fill="#e6e6e6"/><path d="M662.185,25.959H656.4a2.608,2.608,0,0,1-2.6-2.6V22.2a2.608,2.608,0,0,1,2.6-2.6h5.788a2.608,2.608,0,0,1,2.6,2.6v1.158a2.608,2.608,0,0,1-2.6,2.6Z" transform="translate(-475.267 -19.592)" fill="#e6e6e6"/><path d="M342.355,412.772H280.422a2.607,2.607,0,0,1-2.6-2.6V389.909a2.607,2.607,0,0,1,2.6-2.6h61.934a2.607,2.607,0,0,1,2.6,2.6v20.259A2.608,2.608,0,0,1,342.355,412.772Z" transform="translate(-204.637 -284.275)" fill="#e6e6e6"/><path d="M81.177,412.266H19.243a2.608,2.608,0,0,1-2.6-2.6V389.4a2.608,2.608,0,0,1,2.6-2.6H81.177a2.608,2.608,0,0,1,2.6,2.6v20.259a2.608,2.608,0,0,1-2.6,2.6Z" transform="translate(-16.639 -283.91)" fill="#e6e6e6"/><path d="M600.58,387.3a2.608,2.608,0,0,1,2.6,2.6v20.259a2.608,2.608,0,0,1-2.6,2.6H538.647a2.607,2.607,0,0,1-2.6-2.6V389.909a2.607,2.607,0,0,1,2.6-2.6" transform="translate(-390.509 -284.275)" fill="#e6e6e6"/><g transform="translate(146.69 39.555)"><path d="M541.62,166.264a1.449,1.449,0,0,0-1.447,1.447v1.158a1.449,1.449,0,0,0,1.447,1.447h33.572a1.449,1.449,0,0,0,1.447-1.447v-1.158a1.449,1.449,0,0,0-1.447-1.447Z" transform="translate(-540.174 -166.264)" fill="#e6e6e6"/><path d="M541.62,199.317a1.447,1.447,0,0,0,0,2.894h51.515a1.447,1.447,0,0,0,0-2.894Z" transform="translate(-540.174 -190.056)" fill="#e6e6e6"/><path d="M541.62,228.238a1.447,1.447,0,0,0,0,2.894h51.515a1.447,1.447,0,0,0,0-2.894Z" transform="translate(-540.174 -210.873)" fill="#e6e6e6"/></g><path d="M703.5,25.959h-5.788a2.608,2.608,0,0,1-2.6-2.6V22.2a2.608,2.608,0,0,1,2.6-2.6H703.5a2.607,2.607,0,0,1,2.6,2.6v1.158A2.607,2.607,0,0,1,703.5,25.959Z" transform="translate(-505.007 -19.592)" fill="#e6e6e6"/><path d="M744.817,25.959h-5.788a2.608,2.608,0,0,1-2.6-2.6V22.2a2.608,2.608,0,0,1,2.6-2.6h5.788a2.608,2.608,0,0,1,2.6,2.6v1.158a2.608,2.608,0,0,1-2.6,2.6Z" transform="translate(-534.746 -19.592)" fill="#e6e6e6"/></g></g></g></svg>
@@ -30,6 +30,7 @@ CREATE TABLE IF NOT EXISTS users (
30
30
  password_hash TEXT NOT NULL,
31
31
  role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('admin', 'editor')),
32
32
  name TEXT,
33
+ surname TEXT,
33
34
  avatar_url TEXT,
34
35
  notification_prefs TEXT NOT NULL DEFAULT '{}',
35
36
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
@@ -236,3 +237,29 @@ CREATE TABLE IF NOT EXISTS automations (
236
237
 
237
238
  CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
238
239
  CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
240
+
241
+
242
+ -- =============================================================================
243
+ -- 12. SITE SETTINGS
244
+ -- Key-value store for site-level configuration (title, locale, timezone…).
245
+ -- =============================================================================
246
+
247
+ CREATE TABLE IF NOT EXISTS site_settings (
248
+ key TEXT NOT NULL PRIMARY KEY,
249
+ value TEXT NOT NULL
250
+ );
251
+
252
+
253
+ -- =============================================================================
254
+ -- 13. SEED LAYOUTS
255
+ -- Persists per-Seed custom editor form layouts (sprint 04a).
256
+ -- layout column stores a FormLayout JSON blob.
257
+ -- Reset = DELETE the row; client recomputes the generated default.
258
+ -- =============================================================================
259
+
260
+ CREATE TABLE IF NOT EXISTS seed_layouts (
261
+ slug TEXT NOT NULL PRIMARY KEY,
262
+ layout TEXT NOT NULL, -- JSON of FormLayout
263
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
264
+ updated_by TEXT NOT NULL -- users.id of the writer
265
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-preview.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/factory.ts"
@@ -15,8 +15,8 @@
15
15
  "scripts": {
16
16
  "dev": "wrangler dev --port 8789",
17
17
  "build": "tsc -p tsconfig.build.json --noEmit",
18
- "db:migrate:local": "wrangler d1 execute beech-db --local --file=./migrations/0000_v040_base.sql && wrangler d1 execute beech-db --local --file=./migrations/0029_automations.sql && wrangler d1 execute beech-db --local --file=./migrations/0030_test_seeds.sql",
19
- "db:reset:local": "node -e \"require('fs').rmSync('.wrangler/state', {recursive:true,force:true})\" && npm run db:migrate:local && cd ../.. && node bin/cli.mjs seed:load --local && cd apps/api && wrangler d1 execute beech-db --local --file=./migrations/0028_v040_seed_data.sql",
18
+ "db:migrate:local": "node ./scripts/bootstrap-d1.mjs",
19
+ "db:reset:local": "node -e \"require('fs').rmSync('.wrangler/state', {recursive:true,force:true})\" && node ./scripts/bootstrap-d1.mjs",
20
20
  "deploy": "wrangler deploy --minify",
21
21
  "cf-typegen": "wrangler types --env-interface CloudflareBindings",
22
22
  "test": "vitest run",
@@ -24,16 +24,20 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@aws-sdk/client-s3": "^3.995.0",
27
- "@beechcms/core": "^0.5.0",
27
+ "@aws-sdk/s3-request-presigner": "^3.995.0",
28
+ "@beechcms/core": "^0.6.0-preview.1",
29
+ "@upstash/qstash": "^2.11.0",
28
30
  "bcryptjs": "^2.4.3",
29
- "hono": "^4.11.9",
31
+ "hono": "^4.12.21",
30
32
  "jose": "^6.1.3"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@cloudflare/workers-types": "^4.20260213.0",
34
36
  "@types/bcryptjs": "^2.4.6",
35
- "@vitest/coverage-v8": "^3.2.4",
36
- "vitest": "^3.2.4",
37
- "wrangler": "^4.4.0"
38
- }
37
+ "@vitest/coverage-v8": "^4.1.0",
38
+ "vitest": "^4.1.0",
39
+ "wrangler": "^4.98.0"
40
+ },
41
+ "license": "BUSL-1.1",
42
+ "licenseFile": "../../LICENSE"
39
43
  }
@@ -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 { IHashProvider } from '@beechcms/core'
2
6
 
3
7
  const HASH_PREFIX = 'HASH:'
@@ -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 { ITokenService, IssueTokenOptions, JwtClaims } from '@beechcms/core'
2
6
 
3
7
  const TEST_TOKEN_PREFIX = 'test:'
@@ -1,12 +1,16 @@
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 bcrypt from 'bcryptjs'
2
6
  import type { IHashProvider } from '@beechcms/core'
3
7
 
4
- const DEFAULT_BCRYPT_ROUNDS = 10
8
+ export const BCRYPT_SALT_ROUNDS = 10
5
9
 
6
10
  export class BcryptHashProvider implements IHashProvider {
7
11
  private readonly saltRounds: number
8
12
 
9
- constructor(saltRounds: number = DEFAULT_BCRYPT_ROUNDS) {
13
+ constructor(saltRounds: number = BCRYPT_SALT_ROUNDS) {
10
14
  this.saltRounds = saltRounds
11
15
  }
12
16
 
@@ -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
  /** API auth error messages - used by handlers and tests */
2
6
  export const AUTH_ERRORS = {
3
7
  INVALID_REQUEST: 'Invalid request',
@@ -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 { generateRefreshToken } from './refresh'
3
7
 
@@ -6,10 +10,10 @@ describe('generateRefreshToken', () => {
6
10
  expect(generateRefreshToken().length).toBeGreaterThan(0)
7
11
  })
8
12
 
9
- it('returns a UUID-shaped value (8-4-4-4-12 hex groups)', () => {
10
- expect(generateRefreshToken()).toMatch(
11
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
12
- )
13
+ it('returns a 64-character hex string (256-bit / 32 bytes of entropy)', () => {
14
+ const token = generateRefreshToken()
15
+ expect(token).toHaveLength(64)
16
+ expect(token).toMatch(/^[0-9a-f]+$/)
13
17
  })
14
18
 
15
19
  it('returns a different token on every call', () => {
@@ -1,6 +1,11 @@
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
- import { InMemoryHashProvider } from './in-memory-hash-provider'
3
- import { BcryptHashProvider } from './bcrypt-hash-provider'
6
+ import { InMemoryHashProvider } from './__fixtures__/in-memory-hash-provider'
7
+ import { BcryptHashProvider, BCRYPT_SALT_ROUNDS } from './bcrypt-hash-provider'
8
+ import { DUMMY_PASSWORD_HASH } from './login'
4
9
 
5
10
  describe('InMemoryHashProvider', () => {
6
11
  const provider = new InMemoryHashProvider()
@@ -39,8 +44,12 @@ describe('BcryptHashProvider', () => {
39
44
  })
40
45
 
41
46
  it('verify returns false against the DUMMY_PASSWORD_HASH for any plaintext', async () => {
42
- // The dummy hash is used for timing-attack mitigation; it must not match real passwords.
43
- const dummyHash = '$2a$10$SbkRFOafACxVM2ahxerVDu3tSkCXWm29b62WdB.4WGG02Qjsfzni6'
44
- expect(await provider.verify('password123', dummyHash)).toBe(false)
47
+ expect(await provider.verify('password123', DUMMY_PASSWORD_HASH)).toBe(false)
48
+ })
49
+
50
+ it('DUMMY_PASSWORD_HASH bcrypt cost matches BCRYPT_SALT_ROUNDS — timing oracle invariant', () => {
51
+ const match = DUMMY_PASSWORD_HASH.match(/^\$2[aby]\$(\d+)\$/)
52
+ expect(match).not.toBeNull()
53
+ expect(Number(match![1])).toBe(BCRYPT_SALT_ROUNDS)
45
54
  })
46
55
  })
@@ -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 { SignJWT, jwtVerify } from 'jose'
2
6
  import type { ITokenService, IssueTokenOptions, JwtClaims, IClock } from '@beechcms/core'
3
7
 
@@ -17,6 +21,9 @@ export class JoseTokenService implements ITokenService {
17
21
 
18
22
  constructor(secret: string, config: JoseTokenServiceConfig, clock: IClock) {
19
23
  this.secretBytes = new TextEncoder().encode(secret)
24
+ if (this.secretBytes.length < 32) {
25
+ throw new Error('JWT secret must be at least 32 bytes (256-bit)')
26
+ }
20
27
  this.config = config
21
28
  this.clock = clock
22
29
  }
@@ -0,0 +1,87 @@
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
+ /**
6
+ * Sprint 04-pre Part B5 — JWT claims pass-through test.
7
+ *
8
+ * Verifies that `role` and `surname` claims issued in a JWT token are
9
+ * preserved end-to-end through the authMiddleware and available on
10
+ * `context.get('jwtPayload')`. Without the JwtClaims unification these
11
+ * fields would be type-erased and come back undefined.
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest'
15
+ import { Hono } from 'hono'
16
+ import { SystemClock } from '@beechcms/core'
17
+ import type { JwtClaims } from '@beechcms/core'
18
+ import { JoseTokenService } from './jose-token-service'
19
+ import { authMiddleware } from '../middleware'
20
+ import type { Env, Variables } from '../types'
21
+
22
+ const TEST_SECRET = 'super-secret-key-used-only-in-the-vitest-suite-min-length'
23
+
24
+ describe('JWT claims pass-through (sprint 04-pre)', () => {
25
+ const tokenService = new JoseTokenService(TEST_SECRET, {}, SystemClock)
26
+
27
+ async function buildApp() {
28
+ const app = new Hono<{ Bindings: Env; Variables: Variables }>()
29
+
30
+ app.use('*', async (c, next) => {
31
+ c.set('tokenService', tokenService)
32
+ await next()
33
+ })
34
+
35
+ app.use('/protected', authMiddleware())
36
+
37
+ app.get('/protected', (c) => {
38
+ const payload = c.get('jwtPayload')
39
+ return c.json(payload)
40
+ })
41
+
42
+ return app
43
+ }
44
+
45
+ it('role and surname claims survive the authMiddleware round-trip', async () => {
46
+ const app = await buildApp()
47
+
48
+ const claims: JwtClaims = {
49
+ sub: 'user-123',
50
+ email: 'admin@example.com',
51
+ name: 'Admin',
52
+ surname: 'User',
53
+ role: 'admin',
54
+ }
55
+
56
+ const token = await tokenService.issue(claims)
57
+
58
+ const res = await app.request('/protected', {
59
+ method: 'GET',
60
+ headers: { Authorization: `Bearer ${token}` },
61
+ })
62
+
63
+ expect(res.status).toBe(200)
64
+ const body = await res.json<JwtClaims>()
65
+ expect(body.sub).toBe('user-123')
66
+ expect(body.role).toBe('admin')
67
+ expect(body.surname).toBe('User')
68
+ expect(body.email).toBe('admin@example.com')
69
+ })
70
+
71
+ it('a token without role/surname returns undefined for those fields', async () => {
72
+ const app = await buildApp()
73
+
74
+ const token = await tokenService.issue({ sub: 'user-456', email: 'plain@example.com' })
75
+
76
+ const res = await app.request('/protected', {
77
+ method: 'GET',
78
+ headers: { Authorization: `Bearer ${token}` },
79
+ })
80
+
81
+ expect(res.status).toBe(200)
82
+ const body = await res.json<JwtClaims>()
83
+ expect(body.sub).toBe('user-456')
84
+ expect(body.role).toBeUndefined()
85
+ expect(body.surname).toBeUndefined()
86
+ })
87
+ })
@@ -1,6 +1,10 @@
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 { parseLoginBody, validateLoginInput, verifyPassword, DUMMY_PASSWORD_HASH } from './login'
3
- import { InMemoryHashProvider } from './in-memory-hash-provider'
7
+ import { InMemoryHashProvider } from './__fixtures__/in-memory-hash-provider'
4
8
 
5
9
  describe('parseLoginBody', () => {
6
10
  it('returns credentials for a valid object body', () => {
@@ -69,6 +73,10 @@ describe('validateLoginInput', () => {
69
73
  it('accepts passwords at the upper boundary (exactly 128 characters)', () => {
70
74
  expect(validateLoginInput('user@test.com', 'a'.repeat(128))).toBe(true)
71
75
  })
76
+
77
+ it('rejects a password made only of whitespace', () => {
78
+ expect(validateLoginInput('user@test.com', ' '.repeat(8))).toBe(false)
79
+ })
72
80
  })
73
81
 
74
82
  describe('verifyPassword', () => {
package/src/auth/login.ts CHANGED
@@ -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
  /// <reference types="@cloudflare/workers-types" />
2
6
  import type { IHashProvider } from '@beechcms/core'
3
7
 
@@ -53,7 +57,7 @@ export function validateLoginInput(email: string, password: string): boolean {
53
57
  return (
54
58
  email.length <= MAX_EMAIL_LENGTH &&
55
59
  EMAIL_REGEX.test(email) &&
56
- password.length >= MIN_PASSWORD_LENGTH &&
60
+ password.trim().length >= MIN_PASSWORD_LENGTH &&
57
61
  password.length <= MAX_PASSWORD_LENGTH
58
62
  )
59
63
  }
@@ -1,5 +1,11 @@
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
 
3
7
  export function generateRefreshToken(): string {
4
- return crypto.randomUUID()
8
+ const bytes = new Uint8Array(32)
9
+ crypto.getRandomValues(bytes)
10
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
5
11
  }
@@ -1,7 +1,11 @@
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 { SystemClock } from '@beechcms/core'
3
7
  import { JoseTokenService } from './jose-token-service'
4
- import { StaticTokenService } from './static-token-service'
8
+ import { StaticTokenService } from './__fixtures__/static-token-service'
5
9
 
6
10
  const TEST_SECRET = 'super-secret-key-used-only-in-the-vitest-suite-min-length'
7
11
 
@@ -47,6 +51,10 @@ describe('JoseTokenService', () => {
47
51
  const claims = await service.verify(token)
48
52
  expect(claims?.sub).toBe('user-1')
49
53
  })
54
+
55
+ it('throws if the secret is shorter than 32 bytes', () => {
56
+ expect(() => new JoseTokenService('short-secret', {}, SystemClock)).toThrow('JWT secret must be at least 32 bytes')
57
+ })
50
58
  })
51
59
 
52
60
  describe('StaticTokenService', () => {
package/src/factory.ts CHANGED
@@ -1,9 +1,13 @@
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 { Hono } from 'hono'
3
7
  import { cors } from 'hono/cors'
4
8
  import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
5
9
  import type { Seed, ContentRepository, IdempotencyRepository, BeechBucket, MediaRepository, SystemStatsRepository } from '@beechcms/core'
6
- import { sha256hex, SystemClock, SystemIdGenerator, SeedRegistry } from '@beechcms/core'
10
+ import { sha256hex, SystemClock, SystemIdGenerator } from '@beechcms/core'
7
11
  import type { Env, Variables } from './types'
8
12
  import { getClientIp } from './shared/request-utils'
9
13
 
@@ -25,14 +29,20 @@ import { setupApp } from './features/setup'
25
29
  import { draftApp } from './features/draft'
26
30
  import { settingsApp } from './features/settings/settings.handler'
27
31
  import { schemaApp } from './features/schema/schema.handler'
32
+ import { seedsApp } from './features/seeds'
28
33
  import { notificationsApp } from './features/notifications'
29
34
  import { automationsApp } from './features/automations'
30
35
  import { statsApp } from './features/stats'
36
+ import { backrefsApp } from './features/backrefs'
37
+ import { webhooksApp } from './features/webhooks'
31
38
  import { uploadRoutes, serveMediaHandler } from './upload'
32
39
  import { publicRoutes, apiKeyMiddleware, publicRateLimitMiddleware } from './public'
33
40
  import { searchRouter } from "./search"
41
+ import type { ISeedRepository } from '@beechcms/core'
34
42
  import { repositoryMiddleware } from './middleware/repository.middleware'
43
+ import { seedRegistryMiddleware } from './middleware/seed-registry.middleware'
35
44
  import { storageMiddleware } from './middleware/storage.middleware'
45
+ import { InMemorySeedRepository } from './shared/in-memory-seed.repository'
36
46
  import { authProvidersMiddleware } from './middleware/auth-providers.middleware'
37
47
  import { rateLimiterMiddleware } from './middleware/rate-limit.middleware'
38
48
  import { observabilityMiddleware } from './middleware/observability.middleware'
@@ -44,6 +54,10 @@ export interface BeechConfig {
44
54
  bucket?: BeechBucket
45
55
  mediaRepository?: MediaRepository
46
56
  systemStatsRepository?: SystemStatsRepository
57
+ // Optional override: inject a custom ISeedRepository (e.g. InMemorySeedRepository for tests).
58
+ // When omitted and seeds is non-empty, an InMemorySeedRepository is created automatically
59
+ // for backward compatibility.
60
+ seedRepository?: ISeedRepository
47
61
  }
48
62
 
49
63
  // --- Costanti e helper ---
@@ -86,32 +100,34 @@ function extractPublicSeed(path: string): string {
86
100
  }
87
101
 
88
102
  /**
89
- * Builds a fully configured Hono app with the given seeds injected into context.
90
- * This is the main entry point for a BeechCMS project.
103
+ * Builds a fully configured Hono app. Seeds are hydrated from D1 at runtime via
104
+ * seedRegistryMiddleware; config.seeds is retained for back-compat but no longer
105
+ * drives request handling.
91
106
  */
92
107
  export function createBeechApp(config: BeechConfig): Hono<{ Bindings: Env; Variables: Variables }> {
108
+ // Back-compat shim: when callers (e.g. existing tests) pass seeds without a custom
109
+ // seedRepository, wrap them in an InMemorySeedRepository so the middleware can serve
110
+ // them without touching D1. Production index.ts passes seeds:[] so D1SeedRepository
111
+ // is used as the live path.
93
112
  const seedsArray = Array.isArray(config.seeds) ? config.seeds : Object.values(config.seeds)
94
- // Filter out any invalid objects that might have leaked into the registry (e.g. module exports)
95
- const validSeeds = seedsArray.filter(s => s && typeof s === 'object' && 'slug' in s)
96
- const seedRegistry = new SeedRegistry(validSeeds)
113
+ const seedRepository = config.seedRepository
114
+ ?? (seedsArray.length > 0 ? new InMemorySeedRepository(seedsArray) : undefined)
97
115
 
98
116
  const app = new Hono<{ Bindings: Env; Variables: Variables }>()
99
117
 
100
- // 1. Core Middleware (Seeds, CORS, Security)
101
- app.use('*', async (context, next) => {
102
- context.set('getSeed', (slug: string) => seedRegistry.get(slug))
103
- context.set('seedRegistry', seedRegistry)
104
- await next()
105
- })
106
-
107
- // 1.1 Repository Injection
118
+ // 1. Repository Injection (must be first — seedRegistryMiddleware depends on seedRepository)
108
119
  app.use('*', repositoryMiddleware({
109
120
  repository: config.repository,
110
121
  idempotencyRepository: config.idempotencyRepository,
111
122
  mediaRepository: config.mediaRepository,
112
123
  systemStatsRepository: config.systemStatsRepository,
124
+ seedRepository,
113
125
  }))
114
126
 
127
+ // 2. Seed Registry Hydration (D1-backed, version-token-cached per isolate)
128
+ app.use('*', seedRegistryMiddleware())
129
+
130
+ // 3. Storage / Auth / Rate Limiting / Observability
115
131
  app.use('*', storageMiddleware({
116
132
  bucket: config.bucket,
117
133
  }))
@@ -211,7 +227,7 @@ export function createBeechApp(config: BeechConfig): Hono<{ Bindings: Env; Varia
211
227
 
212
228
  if (!user || !isValid) return context.json({ error: AUTH_ERRORS.INVALID_CREDENTIALS }, 401)
213
229
 
214
- const accessToken = await context.get('tokenService').issue({ sub: user.id, email: user.email, name: user.name ?? undefined })
230
+ const accessToken = await context.get('tokenService').issue({ sub: user.id, email: user.email, name: user.name ?? undefined, surname: user.surname ?? undefined, role: user.role })
215
231
  const refreshToken = generateRefreshToken()
216
232
  const refreshTokenHash = await sha256hex(refreshToken)
217
233
  const nowSeconds = SystemClock.nowSeconds()
@@ -251,7 +267,7 @@ export function createBeechApp(config: BeechConfig): Hono<{ Bindings: Env; Varia
251
267
 
252
268
  // Issue new tokens before revoking the old one: if saveRefreshToken fails,
253
269
  // the old token stays valid and the user is not locked out.
254
- const newAccessToken = await context.get('tokenService').issue({ sub: user.id, email: user.email, name: user.name ?? undefined })
270
+ const newAccessToken = await context.get('tokenService').issue({ sub: user.id, email: user.email, name: user.name ?? undefined, surname: user.surname ?? undefined, role: user.role })
255
271
  const newRefreshToken = generateRefreshToken()
256
272
  const newRefreshTokenHash = await sha256hex(newRefreshToken)
257
273
 
@@ -297,10 +313,12 @@ export function createBeechApp(config: BeechConfig): Hono<{ Bindings: Env; Varia
297
313
 
298
314
  apiProtected.route('/settings', settingsApp)
299
315
  apiProtected.route('/schema', schemaApp)
316
+ apiProtected.route('/seeds', seedsApp)
300
317
  apiProtected.route('/content', notificationsApp)
301
318
  apiProtected.route('/content', statsApp)
302
319
  apiProtected.route('/content', rotateFieldApp)
303
320
  apiProtected.route('/content', draftApp)
321
+ apiProtected.route('/content', backrefsApp)
304
322
  apiProtected.route('/content', contentFeature)
305
323
  apiProtected.route('/widget', widgetApp)
306
324
  apiProtected.route('/automations', automationsApp)
@@ -313,6 +331,9 @@ export function createBeechApp(config: BeechConfig): Hono<{ Bindings: Env; Varia
313
331
  apiPublic.route('/', publicRoutes)
314
332
  app.route('/api/v1/public', apiPublic)
315
333
 
334
+ // Webhooks API (public, verified via signature)
335
+ app.route('/api/webhooks', webhooksApp)
336
+
316
337
  app.get('/api/media/:key', (context) => serveMediaHandler(context))
317
338
  app.route('/api', apiProtected)
318
339
  app.route('/api/search', searchRouter)