@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
@@ -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
+ );
@@ -0,0 +1,125 @@
1
+ -- content_products
2
+ CREATE TABLE IF NOT EXISTS content_products (
3
+ id TEXT NOT NULL PRIMARY KEY,
4
+ slug TEXT NOT NULL UNIQUE,
5
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
6
+ name TEXT NOT NULL,
7
+ description TEXT,
8
+ price REAL,
9
+ inventory REAL,
10
+ rating REAL,
11
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
12
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS idx_products_status ON content_products(status);
16
+ CREATE INDEX IF NOT EXISTS idx_products_created_at ON content_products(created_at);
17
+ CREATE INDEX IF NOT EXISTS idx_products_name ON content_products(name);
18
+ CREATE INDEX IF NOT EXISTS idx_products_price ON content_products(price);
19
+ CREATE INDEX IF NOT EXISTS idx_products_inventory ON content_products(inventory);
20
+ CREATE INDEX IF NOT EXISTS idx_products_rating ON content_products(rating);
21
+
22
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_products USING fts5(
23
+ entry_id UNINDEXED,
24
+ name,
25
+ description,
26
+ tokenize = 'unicode61'
27
+ );
28
+
29
+ CREATE TRIGGER IF NOT EXISTS fts_products_insert
30
+ AFTER INSERT ON content_products BEGIN
31
+ INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
32
+ END;
33
+
34
+ CREATE TRIGGER IF NOT EXISTS fts_products_update
35
+ AFTER UPDATE OF name, description ON content_products BEGIN
36
+ DELETE FROM fts_products WHERE entry_id = old.id;
37
+ INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
38
+ END;
39
+
40
+ CREATE TRIGGER IF NOT EXISTS fts_products_delete
41
+ AFTER DELETE ON content_products BEGIN
42
+ DELETE FROM fts_products WHERE entry_id = old.id;
43
+ END;
44
+
45
+ -- content_reviews
46
+ CREATE TABLE IF NOT EXISTS content_reviews (
47
+ id TEXT NOT NULL PRIMARY KEY,
48
+ slug TEXT NOT NULL UNIQUE,
49
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
50
+ title TEXT NOT NULL,
51
+ body TEXT,
52
+ score REAL,
53
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
54
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_reviews_status ON content_reviews(status);
58
+ CREATE INDEX IF NOT EXISTS idx_reviews_created_at ON content_reviews(created_at);
59
+ CREATE INDEX IF NOT EXISTS idx_reviews_title ON content_reviews(title);
60
+ CREATE INDEX IF NOT EXISTS idx_reviews_body ON content_reviews(body);
61
+ CREATE INDEX IF NOT EXISTS idx_reviews_score ON content_reviews(score);
62
+
63
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_reviews USING fts5(
64
+ entry_id UNINDEXED,
65
+ title,
66
+ body,
67
+ tokenize = 'unicode61'
68
+ );
69
+
70
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_insert
71
+ AFTER INSERT ON content_reviews BEGIN
72
+ INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
73
+ END;
74
+
75
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_update
76
+ AFTER UPDATE OF title, body ON content_reviews BEGIN
77
+ DELETE FROM fts_reviews WHERE entry_id = old.id;
78
+ INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
79
+ END;
80
+
81
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_delete
82
+ AFTER DELETE ON content_reviews BEGIN
83
+ DELETE FROM fts_reviews WHERE entry_id = old.id;
84
+ END;
85
+
86
+ -- content_tasks
87
+ CREATE TABLE IF NOT EXISTS content_tasks (
88
+ id TEXT NOT NULL PRIMARY KEY,
89
+ slug TEXT NOT NULL UNIQUE,
90
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
91
+ title TEXT NOT NULL,
92
+ taskStatus TEXT,
93
+ completion REAL,
94
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
95
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON content_tasks(status);
99
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON content_tasks(created_at);
100
+ CREATE INDEX IF NOT EXISTS idx_tasks_title ON content_tasks(title);
101
+ CREATE INDEX IF NOT EXISTS idx_tasks_taskStatus ON content_tasks(taskStatus);
102
+ CREATE INDEX IF NOT EXISTS idx_tasks_completion ON content_tasks(completion);
103
+
104
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_tasks USING fts5(
105
+ entry_id UNINDEXED,
106
+ title,
107
+ taskStatus,
108
+ tokenize = 'unicode61'
109
+ );
110
+
111
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_insert
112
+ AFTER INSERT ON content_tasks BEGIN
113
+ INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
114
+ END;
115
+
116
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_update
117
+ AFTER UPDATE OF title, taskStatus ON content_tasks BEGIN
118
+ DELETE FROM fts_tasks WHERE entry_id = old.id;
119
+ INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
120
+ END;
121
+
122
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_delete
123
+ AFTER DELETE ON content_tasks BEGIN
124
+ DELETE FROM fts_tasks WHERE entry_id = old.id;
125
+ END;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.4.3",
3
+ "version": "0.6.0-preview.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/factory.ts"
@@ -9,13 +9,14 @@
9
9
  "src/",
10
10
  "migrations/0000_v040_base.sql",
11
11
  "migrations/0029_automations.sql",
12
+ "migrations/0030_test_seeds.sql",
12
13
  "assets/"
13
14
  ],
14
15
  "scripts": {
15
16
  "dev": "wrangler dev --port 8789",
16
17
  "build": "tsc -p tsconfig.build.json --noEmit",
17
- "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",
18
- "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",
19
20
  "deploy": "wrangler deploy --minify",
20
21
  "cf-typegen": "wrangler types --env-interface CloudflareBindings",
21
22
  "test": "vitest run",
@@ -23,16 +24,20 @@
23
24
  },
24
25
  "dependencies": {
25
26
  "@aws-sdk/client-s3": "^3.995.0",
26
- "@beechcms/core": "^0.4.3",
27
+ "@aws-sdk/s3-request-presigner": "^3.995.0",
28
+ "@beechcms/core": "^0.6.0-preview.1",
29
+ "@upstash/qstash": "^2.11.0",
27
30
  "bcryptjs": "^2.4.3",
28
- "hono": "^4.11.9",
31
+ "hono": "^4.12.21",
29
32
  "jose": "^6.1.3"
30
33
  },
31
34
  "devDependencies": {
32
35
  "@cloudflare/workers-types": "^4.20260213.0",
33
36
  "@types/bcryptjs": "^2.4.6",
34
- "@vitest/coverage-v8": "^3.2.4",
35
- "vitest": "^3.2.4",
36
- "wrangler": "^4.4.0"
37
- }
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"
38
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', () => {