@beechcms/api 0.6.0-preview.2 → 0.6.0-preview.4

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 (388) hide show
  1. package/README.md +4 -4
  2. package/assets/dashboard/BeechLogo.svg +18 -18
  3. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  4. package/assets/dashboard/assets/chart-BwlCt-IA.js +39 -0
  5. package/assets/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  6. package/assets/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  7. package/assets/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  8. package/assets/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  9. package/assets/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  10. package/assets/dashboard/assets/index-BcVN-DNy.js +642 -0
  11. package/assets/dashboard/assets/index-Cf1yRCqG.css +1 -0
  12. package/assets/dashboard/assets/lora-cyrillic-ext-wght-normal-YcVxMP-u.woff2 +0 -0
  13. package/assets/dashboard/assets/lora-cyrillic-wght-normal-UWPuZLjM.woff2 +0 -0
  14. package/assets/dashboard/assets/lora-latin-ext-wght-normal-C2Wlntb9.woff2 +0 -0
  15. package/assets/dashboard/assets/lora-latin-wght-normal-BiLcIKcI.woff2 +0 -0
  16. package/assets/dashboard/assets/lora-math-wght-normal-sOy7rEfW.woff2 +0 -0
  17. package/assets/dashboard/assets/lora-symbols-wght-normal-DQ5VrUkH.woff2 +0 -0
  18. package/assets/dashboard/assets/lora-vietnamese-wght-normal-CfJ7gtf3.woff2 +0 -0
  19. package/assets/dashboard/assets/pie-chart-recharts-BMWpjFzf.js +1 -0
  20. package/assets/dashboard/assets/timeseries-chart-recharts-8_GwAFR0.js +1 -0
  21. package/assets/dashboard/beechLogoDark.svg +48 -48
  22. package/assets/dashboard/index.html +18 -18
  23. package/assets/dashboard/noResult.svg +42 -42
  24. package/assets/dashboard/sol.svg +3 -3
  25. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  26. package/dist/auth/__fixtures__/in-memory-hash-provider.d.ts +5 -0
  27. package/dist/auth/__fixtures__/static-token-service.d.ts +6 -0
  28. package/dist/auth/bcrypt-hash-provider.d.ts +8 -0
  29. package/dist/auth/constants.d.ts +10 -0
  30. package/dist/auth/jose-token-service.d.ts +14 -0
  31. package/dist/auth/login.d.ts +32 -0
  32. package/dist/auth/refresh.d.ts +1 -0
  33. package/dist/factory.d.ts +45 -0
  34. package/dist/features/automations/action-executors/create-entry.executor.d.ts +7 -0
  35. package/dist/features/automations/action-executors/edit-field.executor.d.ts +7 -0
  36. package/dist/features/automations/action-executors/index.d.ts +13 -0
  37. package/dist/features/automations/action-executors/send-mail.executor.d.ts +7 -0
  38. package/dist/features/automations/action-executors/set-variable.executor.d.ts +15 -0
  39. package/dist/features/automations/action-executors/webhook.executor.d.ts +7 -0
  40. package/dist/features/automations/automation-runner.d.ts +13 -0
  41. package/dist/features/automations/automation-runner.utils.d.ts +3 -0
  42. package/dist/features/automations/automations.handler.d.ts +7 -0
  43. package/dist/features/automations/automations.schema.d.ts +191 -0
  44. package/dist/features/automations/context-resolver.d.ts +8 -0
  45. package/dist/features/automations/cron-runner.d.ts +9 -0
  46. package/dist/features/automations/cron-runner.utils.d.ts +1 -0
  47. package/dist/features/automations/filter-translation.d.ts +9 -0
  48. package/dist/features/automations/index.d.ts +7 -0
  49. package/dist/features/automations/template-grammar.d.ts +73 -0
  50. package/dist/features/automations/var-access-resolver.d.ts +6 -0
  51. package/dist/features/automations/when-evaluator.d.ts +3 -0
  52. package/dist/features/automations/when-pushdown.d.ts +2 -0
  53. package/dist/features/backrefs/backrefs.handler.d.ts +3 -0
  54. package/dist/features/backrefs/d1-backref.repository.d.ts +17 -0
  55. package/dist/features/backrefs/index.d.ts +1 -0
  56. package/dist/features/content/constants.d.ts +12 -0
  57. package/dist/features/content/handlers/bulk.handler.d.ts +3 -0
  58. package/dist/features/content/handlers/create.d.ts +3 -0
  59. package/dist/features/content/handlers/delete.d.ts +3 -0
  60. package/dist/features/content/handlers/facets.d.ts +3 -0
  61. package/dist/features/content/handlers/get.d.ts +4 -0
  62. package/dist/features/content/handlers/helpers.d.ts +12 -0
  63. package/dist/features/content/handlers/list.d.ts +3 -0
  64. package/dist/features/content/handlers/update.d.ts +3 -0
  65. package/dist/features/content/index.d.ts +4 -0
  66. package/dist/features/dashboard-layout/dashboard-layout.handler.d.ts +7 -0
  67. package/dist/features/dashboard-layout/index.d.ts +1 -0
  68. package/dist/features/draft/draft.handler.d.ts +4 -0
  69. package/dist/features/draft/draft.middleware.d.ts +9 -0
  70. package/dist/features/draft/index.d.ts +1 -0
  71. package/{src/features/email/email.provider.ts → dist/features/email/email.provider.d.ts} +19 -24
  72. package/dist/features/email/email.service.d.ts +13 -0
  73. package/dist/features/email/email.types.d.ts +94 -0
  74. package/{src/features/email/index.ts → dist/features/email/index.d.ts} +3 -16
  75. package/dist/features/email/providers/resend.d.ts +36 -0
  76. package/dist/features/email/providers/smtp.d.ts +11 -0
  77. package/dist/features/email/templates/automation-mail.d.ts +8 -0
  78. package/dist/features/email/templates/password-changed.d.ts +15 -0
  79. package/dist/features/email/templates/password-reset.d.ts +18 -0
  80. package/dist/features/email/templates/shell.d.ts +58 -0
  81. package/dist/features/notifications/index.d.ts +1 -0
  82. package/dist/features/notifications/notifications.handler.d.ts +15 -0
  83. package/dist/features/password-reset/index.d.ts +6 -0
  84. package/dist/features/password-reset/request.d.ts +10 -0
  85. package/dist/features/password-reset/reset.d.ts +9 -0
  86. package/dist/features/rotate-field/index.d.ts +1 -0
  87. package/dist/features/rotate-field/rotate-field.handler.d.ts +7 -0
  88. package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
  89. package/dist/features/schema/schema.handler.d.ts +7 -0
  90. package/dist/features/seeds/index.d.ts +1 -0
  91. package/dist/features/seeds/seeds.handler.d.ts +6 -0
  92. package/dist/features/settings/settings.handler.d.ts +7 -0
  93. package/dist/features/setup/index.d.ts +7 -0
  94. package/dist/features/stats/index.d.ts +1 -0
  95. package/dist/features/stats/stats.handler.d.ts +7 -0
  96. package/dist/features/webhooks/index.d.ts +3 -0
  97. package/dist/index.d.ts +7 -0
  98. package/dist/index.js +9732 -0
  99. package/dist/media-utils.d.ts +25 -0
  100. package/dist/middleware/auth-providers.middleware.d.ts +8 -0
  101. package/dist/middleware/observability.middleware.d.ts +20 -0
  102. package/dist/middleware/queue.middleware.d.ts +11 -0
  103. package/dist/middleware/rate-limit.middleware.d.ts +9 -0
  104. package/dist/middleware/repository.middleware.d.ts +34 -0
  105. package/dist/middleware/seed-registry.middleware.d.ts +10 -0
  106. package/dist/middleware/storage.middleware.d.ts +7 -0
  107. package/dist/middleware.d.ts +11 -0
  108. package/dist/public/access-policy.d.ts +12 -0
  109. package/dist/public/api-key-middleware.d.ts +6 -0
  110. package/dist/public/cache-utils.d.ts +10 -0
  111. package/dist/public/entry-projection.d.ts +2 -0
  112. package/dist/public/idempotency.d.ts +9 -0
  113. package/dist/public/index.d.ts +11 -0
  114. package/dist/public/problem-details.d.ts +31 -0
  115. package/dist/public/public-add.d.ts +3 -0
  116. package/dist/public/public-edit.d.ts +5 -0
  117. package/dist/public/public-errors.d.ts +13 -0
  118. package/dist/public/public-read.d.ts +3 -0
  119. package/dist/public/public-routes.d.ts +3 -0
  120. package/dist/public/query-builder.d.ts +28 -0
  121. package/dist/public/rate-limit-middleware.d.ts +3 -0
  122. package/dist/public/read-list.d.ts +26 -0
  123. package/dist/public/read-single.d.ts +22 -0
  124. package/dist/public/response-builder.d.ts +22 -0
  125. package/dist/public/sanitize.d.ts +23 -0
  126. package/{src/public/slug-utils.ts → dist/public/slug-utils.d.ts} +3 -10
  127. package/dist/rate-limit/cloudflare-rate-limiter.d.ts +6 -0
  128. package/dist/rate-limit/in-memory-rate-limiter.d.ts +7 -0
  129. package/dist/rate-limit/no-op-rate-limiter.d.ts +4 -0
  130. package/dist/search-utils.d.ts +54 -0
  131. package/dist/search.d.ts +6 -0
  132. package/dist/shared/apply-policies.d.ts +10 -0
  133. package/dist/shared/automations.repository.d1.d.ts +12 -0
  134. package/dist/shared/background-notification-service.d.ts +18 -0
  135. package/dist/shared/base.repository.d1.d.ts +18 -0
  136. package/dist/shared/cloudflare-queue-service.d.ts +11 -0
  137. package/dist/shared/content-utils.d.ts +21 -0
  138. package/dist/shared/content.repository.d1.d.ts +68 -0
  139. package/dist/shared/d1-activity-log.repository.d.ts +13 -0
  140. package/dist/shared/d1-activity-logger.d.ts +20 -0
  141. package/dist/shared/d1-analytics.repository.d.ts +20 -0
  142. package/dist/shared/d1-content-scan.repository.d.ts +7 -0
  143. package/dist/shared/d1-notification.repository.d.ts +17 -0
  144. package/dist/shared/d1-password-reset-token.repository.d.ts +10 -0
  145. package/dist/shared/d1-search.repository.d.ts +14 -0
  146. package/dist/shared/d1-session.repository.d.ts +12 -0
  147. package/dist/shared/d1-setup-checklist.repository.d.ts +12 -0
  148. package/dist/shared/d1-user.repository.d.ts +18 -0
  149. package/dist/shared/d1-widget.repository.d.ts +31 -0
  150. package/dist/shared/dashboard-layout.repository.d1.d.ts +9 -0
  151. package/dist/shared/demo-data-sql.d.ts +1 -0
  152. package/dist/shared/demo-data.repository.d1.d.ts +6 -0
  153. package/dist/shared/execution-context-scheduler.d.ts +6 -0
  154. package/dist/shared/fixed-clock.d.ts +13 -0
  155. package/dist/shared/fts-sync.d.ts +1 -0
  156. package/dist/shared/idempotency.repository.d1.d.ts +16 -0
  157. package/dist/shared/in-memory-activity-logger.d.ts +11 -0
  158. package/dist/shared/in-memory-notification-service.d.ts +11 -0
  159. package/dist/shared/in-memory-queue-service.d.ts +16 -0
  160. package/dist/shared/in-memory-seed.repository.d.ts +18 -0
  161. package/dist/shared/media.repository.d1.d.ts +35 -0
  162. package/dist/shared/qstash-notification-service.d.ts +18 -0
  163. package/dist/shared/query-utils.d.ts +36 -0
  164. package/dist/shared/queue-consumer.d.ts +9 -0
  165. package/dist/shared/request-utils.d.ts +13 -0
  166. package/dist/shared/schema-mutator.d1.d.ts +12 -0
  167. package/dist/shared/seed-layout.repository.d1.d.ts +9 -0
  168. package/dist/shared/seed-registry-cache.d.ts +12 -0
  169. package/dist/shared/seed.repository.d1.d.ts +13 -0
  170. package/dist/shared/sequential-id-generator.d.ts +13 -0
  171. package/dist/shared/site-settings.repository.d1.d.ts +7 -0
  172. package/dist/shared/storage/factory.d.ts +8 -0
  173. package/dist/shared/storage/s3-bucket.d.ts +43 -0
  174. package/dist/shared/storage-utils.d.ts +7 -0
  175. package/dist/shared/system-stats.repository.d1.d.ts +25 -0
  176. package/dist/types.d.ts +87 -0
  177. package/dist/upload.d.ts +16 -0
  178. package/dist/widget.d.ts +7 -0
  179. package/migrations/0000_v040_base.sql +265 -265
  180. package/migrations/0029_automations.sql +14 -14
  181. package/migrations/0030_test_seeds.sql +125 -125
  182. package/package.json +28 -18
  183. package/assets/dashboard/assets/chart-BrJFdsGZ.js +0 -39
  184. package/assets/dashboard/assets/index-CIaoMxuB.css +0 -1
  185. package/assets/dashboard/assets/index-wTKgygsg.js +0 -636
  186. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +0 -1
  187. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +0 -1
  188. package/src/auth/__fixtures__/in-memory-hash-provider.ts +0 -17
  189. package/src/auth/__fixtures__/static-token-service.ts +0 -22
  190. package/src/auth/bcrypt-hash-provider.ts +0 -24
  191. package/src/auth/constants.ts +0 -14
  192. package/src/auth/generate-refresh-token.test.ts +0 -23
  193. package/src/auth/hash-provider.test.ts +0 -55
  194. package/src/auth/jose-token-service.ts +0 -62
  195. package/src/auth/jwt-claims-passthrough.test.ts +0 -87
  196. package/src/auth/login.test.ts +0 -100
  197. package/src/auth/login.ts +0 -78
  198. package/src/auth/refresh.ts +0 -11
  199. package/src/auth/token-service.test.ts +0 -90
  200. package/src/factory.ts +0 -370
  201. package/src/features/automations/__tests__/action-executors.test.ts +0 -255
  202. package/src/features/automations/__tests__/automation-runner.test.ts +0 -196
  203. package/src/features/automations/__tests__/automation-runner.utils.test.ts +0 -60
  204. package/src/features/automations/__tests__/automations.handler.test.ts +0 -264
  205. package/src/features/automations/__tests__/automations.repository.test.ts +0 -163
  206. package/src/features/automations/__tests__/automations.schema.test.ts +0 -224
  207. package/src/features/automations/__tests__/context-resolver.test.ts +0 -126
  208. package/src/features/automations/__tests__/cron-runner.test.ts +0 -267
  209. package/src/features/automations/__tests__/cron-runner.utils.test.ts +0 -144
  210. package/src/features/automations/__tests__/set-variable.executor.test.ts +0 -383
  211. package/src/features/automations/__tests__/template-grammar.test.ts +0 -308
  212. package/src/features/automations/__tests__/webhook.executor.test.ts +0 -142
  213. package/src/features/automations/__tests__/when-evaluator.test.ts +0 -274
  214. package/src/features/automations/__tests__/when-pushdown.test.ts +0 -281
  215. package/src/features/automations/action-executors/create-entry.executor.ts +0 -27
  216. package/src/features/automations/action-executors/edit-field.executor.ts +0 -26
  217. package/src/features/automations/action-executors/index.ts +0 -37
  218. package/src/features/automations/action-executors/send-mail.executor.ts +0 -53
  219. package/src/features/automations/action-executors/set-variable.executor.ts +0 -159
  220. package/src/features/automations/action-executors/webhook.executor.ts +0 -67
  221. package/src/features/automations/automation-runner.ts +0 -85
  222. package/src/features/automations/automation-runner.utils.ts +0 -47
  223. package/src/features/automations/automations.handler.ts +0 -197
  224. package/src/features/automations/automations.schema.ts +0 -177
  225. package/src/features/automations/context-resolver.ts +0 -152
  226. package/src/features/automations/cron-runner.ts +0 -140
  227. package/src/features/automations/cron-runner.utils.ts +0 -44
  228. package/src/features/automations/filter-translation.ts +0 -46
  229. package/src/features/automations/index.ts +0 -16
  230. package/src/features/automations/template-grammar.ts +0 -245
  231. package/src/features/automations/var-access-resolver.ts +0 -140
  232. package/src/features/automations/when-evaluator.ts +0 -87
  233. package/src/features/automations/when-pushdown.ts +0 -57
  234. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +0 -359
  235. package/src/features/backrefs/backrefs.handler.ts +0 -168
  236. package/src/features/backrefs/d1-backref.repository.ts +0 -84
  237. package/src/features/backrefs/index.ts +0 -5
  238. package/src/features/content/constants.ts +0 -16
  239. package/src/features/content/handlers/bulk.handler.ts +0 -185
  240. package/src/features/content/handlers/create.ts +0 -129
  241. package/src/features/content/handlers/delete.ts +0 -62
  242. package/src/features/content/handlers/facets.ts +0 -49
  243. package/src/features/content/handlers/get.ts +0 -120
  244. package/src/features/content/handlers/helpers.test.ts +0 -180
  245. package/src/features/content/handlers/helpers.ts +0 -93
  246. package/src/features/content/handlers/list.ts +0 -174
  247. package/src/features/content/handlers/update.ts +0 -174
  248. package/src/features/content/index.ts +0 -26
  249. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +0 -388
  250. package/src/features/dashboard-layout/dashboard-layout.handler.ts +0 -206
  251. package/src/features/dashboard-layout/index.ts +0 -5
  252. package/src/features/draft/draft.handler.ts +0 -195
  253. package/src/features/draft/draft.middleware.ts +0 -66
  254. package/src/features/draft/index.ts +0 -5
  255. package/src/features/email/email.service.ts +0 -98
  256. package/src/features/email/email.types.ts +0 -119
  257. package/src/features/email/providers/resend.ts +0 -67
  258. package/src/features/email/providers/smtp.ts +0 -55
  259. package/src/features/email/templates/automation-mail.ts +0 -19
  260. package/src/features/email/templates/password-changed.ts +0 -63
  261. package/src/features/email/templates/password-reset.ts +0 -68
  262. package/src/features/email/templates/shell.ts +0 -96
  263. package/src/features/notifications/index.ts +0 -5
  264. package/src/features/notifications/notifications.handler.ts +0 -105
  265. package/src/features/password-reset/index.ts +0 -19
  266. package/src/features/password-reset/request.ts +0 -92
  267. package/src/features/password-reset/reset.ts +0 -102
  268. package/src/features/rotate-field/index.ts +0 -5
  269. package/src/features/rotate-field/rotate-field.handler.ts +0 -132
  270. package/src/features/rotate-field/rotate-field.schema.ts +0 -17
  271. package/src/features/schema/schema.handler.ts +0 -131
  272. package/src/features/seeds/index.ts +0 -5
  273. package/src/features/seeds/seeds.handler.test.ts +0 -1199
  274. package/src/features/seeds/seeds.handler.ts +0 -558
  275. package/src/features/settings/__tests__/settings.handler.test.ts +0 -555
  276. package/src/features/settings/settings.handler.ts +0 -392
  277. package/src/features/setup/index.ts +0 -288
  278. package/src/features/stats/index.ts +0 -5
  279. package/src/features/stats/stats.handler.ts +0 -420
  280. package/src/features/webhooks/index.ts +0 -63
  281. package/src/index.ts +0 -65
  282. package/src/media-utils.ts +0 -82
  283. package/src/middleware/auth-providers.middleware.test.ts +0 -85
  284. package/src/middleware/auth-providers.middleware.ts +0 -45
  285. package/src/middleware/observability.middleware.test.ts +0 -93
  286. package/src/middleware/observability.middleware.ts +0 -70
  287. package/src/middleware/rate-limit.middleware.ts +0 -45
  288. package/src/middleware/repository.middleware.ts +0 -117
  289. package/src/middleware/seed-registry.middleware.test.ts +0 -97
  290. package/src/middleware/seed-registry.middleware.ts +0 -21
  291. package/src/middleware/storage.middleware.ts +0 -25
  292. package/src/middleware.ts +0 -45
  293. package/src/public/access-policy.ts +0 -27
  294. package/src/public/api-key-middleware.ts +0 -57
  295. package/src/public/cache-utils.ts +0 -38
  296. package/src/public/entry-projection.ts +0 -46
  297. package/src/public/idempotency.ts +0 -23
  298. package/src/public/index.ts +0 -16
  299. package/src/public/problem-details.ts +0 -124
  300. package/src/public/public-add.ts +0 -114
  301. package/src/public/public-edit.ts +0 -163
  302. package/src/public/public-errors.ts +0 -19
  303. package/src/public/public-read.ts +0 -63
  304. package/src/public/public-routes.ts +0 -88
  305. package/src/public/query-builder.test.ts +0 -224
  306. package/src/public/query-builder.ts +0 -156
  307. package/src/public/rate-limit-middleware.ts +0 -34
  308. package/src/public/read-list.ts +0 -54
  309. package/src/public/read-single.ts +0 -48
  310. package/src/public/response-builder.ts +0 -30
  311. package/src/public/sanitize.ts +0 -69
  312. package/src/rate-limit/cloudflare-rate-limiter.test.ts +0 -30
  313. package/src/rate-limit/cloudflare-rate-limiter.ts +0 -15
  314. package/src/rate-limit/in-memory-rate-limiter.test.ts +0 -37
  315. package/src/rate-limit/in-memory-rate-limiter.ts +0 -17
  316. package/src/rate-limit/no-op-rate-limiter.test.ts +0 -22
  317. package/src/rate-limit/no-op-rate-limiter.ts +0 -11
  318. package/src/search-utils.test.ts +0 -211
  319. package/src/search-utils.ts +0 -213
  320. package/src/search.ts +0 -65
  321. package/src/shared/apply-policies.test.ts +0 -81
  322. package/src/shared/apply-policies.ts +0 -67
  323. package/src/shared/automations.repository.d1.ts +0 -150
  324. package/src/shared/background-notification-service.test.ts +0 -62
  325. package/src/shared/background-notification-service.ts +0 -52
  326. package/src/shared/base.repository.d1.ts +0 -32
  327. package/src/shared/content-utils.test.ts +0 -165
  328. package/src/shared/content-utils.ts +0 -86
  329. package/src/shared/content.repository.d1.test.ts +0 -507
  330. package/src/shared/content.repository.d1.ts +0 -851
  331. package/src/shared/d1-activity-log.repository.test.ts +0 -140
  332. package/src/shared/d1-activity-log.repository.ts +0 -105
  333. package/src/shared/d1-activity-logger.test.ts +0 -86
  334. package/src/shared/d1-activity-logger.ts +0 -67
  335. package/src/shared/d1-analytics.repository.test.ts +0 -78
  336. package/src/shared/d1-analytics.repository.ts +0 -85
  337. package/src/shared/d1-content-scan.repository.test.ts +0 -80
  338. package/src/shared/d1-content-scan.repository.ts +0 -37
  339. package/src/shared/d1-notification.repository.test.ts +0 -128
  340. package/src/shared/d1-notification.repository.ts +0 -118
  341. package/src/shared/d1-password-reset-token.repository.test.ts +0 -81
  342. package/src/shared/d1-password-reset-token.repository.ts +0 -56
  343. package/src/shared/d1-search.repository.test.ts +0 -87
  344. package/src/shared/d1-search.repository.ts +0 -88
  345. package/src/shared/d1-session.repository.test.ts +0 -125
  346. package/src/shared/d1-session.repository.ts +0 -102
  347. package/src/shared/d1-setup-checklist.repository.ts +0 -36
  348. package/src/shared/d1-user.repository.test.ts +0 -151
  349. package/src/shared/d1-user.repository.ts +0 -119
  350. package/src/shared/d1-widget.repository.test.ts +0 -267
  351. package/src/shared/d1-widget.repository.ts +0 -385
  352. package/src/shared/dashboard-layout.repository.d1.test.ts +0 -135
  353. package/src/shared/dashboard-layout.repository.d1.ts +0 -58
  354. package/src/shared/demo-data-sql.ts +0 -216
  355. package/src/shared/demo-data.repository.d1.ts +0 -20
  356. package/src/shared/execution-context-scheduler.ts +0 -13
  357. package/src/shared/fixed-clock.ts +0 -25
  358. package/src/shared/fts-sync.ts +0 -8
  359. package/src/shared/idempotency.repository.d1.test.ts +0 -83
  360. package/src/shared/idempotency.repository.d1.ts +0 -60
  361. package/src/shared/in-memory-activity-logger.ts +0 -19
  362. package/src/shared/in-memory-notification-service.ts +0 -19
  363. package/src/shared/in-memory-seed.repository.ts +0 -58
  364. package/src/shared/media.repository.d1.test.ts +0 -107
  365. package/src/shared/media.repository.d1.ts +0 -68
  366. package/src/shared/qstash-notification-service.test.ts +0 -67
  367. package/src/shared/qstash-notification-service.ts +0 -55
  368. package/src/shared/query-utils.ts +0 -141
  369. package/src/shared/request-utils.ts +0 -26
  370. package/src/shared/schema-mutator.d1.test.ts +0 -142
  371. package/src/shared/schema-mutator.d1.ts +0 -64
  372. package/src/shared/seed-layout.repository.d1.test.ts +0 -114
  373. package/src/shared/seed-layout.repository.d1.ts +0 -62
  374. package/src/shared/seed-registry-cache.test.ts +0 -68
  375. package/src/shared/seed-registry-cache.ts +0 -49
  376. package/src/shared/seed.repository.d1.test.ts +0 -143
  377. package/src/shared/seed.repository.d1.ts +0 -98
  378. package/src/shared/sequential-id-generator.ts +0 -33
  379. package/src/shared/site-settings.repository.d1.ts +0 -53
  380. package/src/shared/storage/factory.ts +0 -41
  381. package/src/shared/storage/s3-bucket.ts +0 -195
  382. package/src/shared/storage-utils.ts +0 -40
  383. package/src/shared/system-stats.repository.d1.test.ts +0 -58
  384. package/src/shared/system-stats.repository.d1.ts +0 -48
  385. package/src/types.ts +0 -90
  386. package/src/upload.ts +0 -198
  387. package/src/widget.test.ts +0 -85
  388. package/src/widget.ts +0 -251
@@ -1,195 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'
6
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
7
- import { BeechBucket, PutBucketOptions, GetBucketResult, PresignOptions } from '@beechcms/core'
8
-
9
- /**
10
- * Implementation of BeechBucket using S3-compatible API (AWS SDK).
11
- * Used for production when connecting to R2 or any other S3 provider via HTTP.
12
- */
13
- export class S3Bucket implements BeechBucket {
14
- private client: S3Client
15
- private bucketName: string
16
- private baseUrl: string
17
-
18
- private cdnUrl: string | null
19
-
20
- constructor(config: {
21
- endpoint: string,
22
- accessKeyId: string,
23
- secretAccessKey: string,
24
- bucketName: string,
25
- baseUrl: string,
26
- cdnUrl?: string,
27
- }) {
28
- // requestChecksumCalculation/responseChecksumValidation exist at runtime in AWS SDK v3
29
- // but are not yet in the TypeScript types for this version. Casting via unknown to pass them
30
- // through: without them the SDK adds x-amz-checksum-crc32 to presigned URLs, which causes
31
- // R2 to reject PUT requests when the CRC32 of the uploaded file doesn't match.
32
- this.client = new S3Client({
33
- region: 'auto',
34
- endpoint: config.endpoint,
35
- credentials: {
36
- accessKeyId: config.accessKeyId,
37
- secretAccessKey: config.secretAccessKey,
38
- },
39
- forcePathStyle: true,
40
- requestChecksumCalculation: 'when_required',
41
- responseChecksumValidation: 'when_required',
42
- } as unknown as import('@aws-sdk/client-s3').S3ClientConfig)
43
- this.bucketName = config.bucketName
44
- this.baseUrl = config.baseUrl
45
- this.cdnUrl = config.cdnUrl ?? null
46
- }
47
-
48
- async put(key: string, body: ArrayBuffer | Uint8Array | ReadableStream, options?: PutBucketOptions): Promise<void> {
49
- const command = new PutObjectCommand({
50
- Bucket: this.bucketName,
51
- Key: key,
52
- Body: body instanceof ReadableStream ? await this.streamToUint8Array(body) : new Uint8Array(body as ArrayBuffer),
53
- ContentType: options?.contentType,
54
- Metadata: options?.metadata
55
- })
56
- await this.client.send(command)
57
- }
58
-
59
- private async streamToUint8Array(stream: ReadableStream): Promise<Uint8Array> {
60
- const reader = stream.getReader()
61
- const chunks: Uint8Array[] = []
62
- let totalLength = 0
63
- while (true) {
64
- const { done, value } = await reader.read()
65
- if (done) break
66
- chunks.push(value)
67
- totalLength += value.length
68
- }
69
- const result = new Uint8Array(totalLength)
70
- let offset = 0
71
- for (const chunk of chunks) {
72
- result.set(chunk, offset)
73
- offset += chunk.length
74
- }
75
- return result
76
- }
77
-
78
- async get(key: string): Promise<GetBucketResult | null> {
79
- try {
80
- const command = new GetObjectCommand({
81
- Bucket: this.bucketName,
82
- Key: key
83
- })
84
- const response = await this.client.send(command)
85
-
86
- if (!response.Body) return null
87
-
88
- const rawBody = response.Body as any
89
- const body: ReadableStream = typeof rawBody.transformToWebStream === 'function'
90
- ? rawBody.transformToWebStream()
91
- : rawBody as ReadableStream
92
-
93
- return {
94
- body,
95
- contentType: response.ContentType,
96
- size: response.ContentLength ?? 0,
97
- metadata: response.Metadata
98
- }
99
- } catch (err: any) {
100
- if (err.name === 'NoSuchKey' || err.name === 'NotFound' || err.message === 'NoSuchKey') {
101
- return null
102
- }
103
- // Rethrow other errors (AccessDenied, Timeout, etc.)
104
- throw err
105
- }
106
- }
107
-
108
- async delete(key: string): Promise<void> {
109
- const command = new DeleteObjectCommand({
110
- Bucket: this.bucketName,
111
- Key: key
112
- })
113
- await this.client.send(command)
114
- }
115
-
116
- async head(key: string): Promise<{ size: number; contentType?: string; metadata?: Record<string, string> } | null> {
117
- try {
118
- const command = new HeadObjectCommand({
119
- Bucket: this.bucketName,
120
- Key: key
121
- })
122
- const response = await this.client.send(command)
123
- return {
124
- size: response.ContentLength ?? 0,
125
- contentType: response.ContentType,
126
- metadata: response.Metadata
127
- }
128
- } catch (err: any) {
129
- if (err.name === 'NoSuchKey' || err.name === 'NotFound') return null
130
- throw err
131
- }
132
- }
133
-
134
- getUrl(key: string): string {
135
- if (this.cdnUrl) {
136
- return `${this.cdnUrl}/${encodeURIComponent(key)}`
137
- }
138
- return `${this.baseUrl}/api/media/${encodeURIComponent(key)}`
139
- }
140
-
141
- async getTotalSize(): Promise<number> {
142
- let total = 0
143
- let token: string | undefined
144
- do {
145
- const command = new ListObjectsV2Command({
146
- Bucket: this.bucketName,
147
- ContinuationToken: token
148
- })
149
- const response = await this.client.send(command)
150
- for (const obj of response.Contents ?? []) {
151
- total += obj.Size ?? 0
152
- }
153
- token = response.NextContinuationToken
154
- } while (token)
155
- return total
156
- }
157
-
158
- async presignPut(key: string, options: PresignOptions): Promise<string> {
159
- // ContentLength intentionally excluded: including it adds content-length to
160
- // X-Amz-SignedHeaders, requiring the uploading client to send an exact
161
- // Content-Length header. Some fetch implementations omit it, causing MinIO/R2
162
- // to reject the PUT with 400. Size validation already happened server-side.
163
- const command = new PutObjectCommand({
164
- Bucket: this.bucketName,
165
- Key: key,
166
- ContentType: options.contentType,
167
- })
168
- return getSignedUrl(this.client, command, { expiresIn: options.expiresIn })
169
- }
170
-
171
- async presignGet(key: string, options: PresignOptions): Promise<string> {
172
- const command = new GetObjectCommand({
173
- Bucket: this.bucketName,
174
- Key: key,
175
- })
176
- return getSignedUrl(this.client, command, { expiresIn: options.expiresIn })
177
- }
178
-
179
- async list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ objects: Array<{ key: string; size: number }>; cursor?: string }> {
180
- const command = new ListObjectsV2Command({
181
- Bucket: this.bucketName,
182
- Prefix: options?.prefix,
183
- MaxKeys: options?.limit,
184
- ContinuationToken: options?.cursor
185
- })
186
- const response = await this.client.send(command)
187
- return {
188
- objects: (response.Contents ?? []).map(obj => ({
189
- key: obj.Key ?? '',
190
- size: obj.Size ?? 0
191
- })),
192
- cursor: response.NextContinuationToken
193
- }
194
- }
195
- }
@@ -1,40 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { S3Client, ListObjectsV2Command, ListObjectsV2CommandOutput } from '@aws-sdk/client-s3'
6
-
7
- /**
8
- * Calcola la dimensione totale occupata in un bucket R2 (in byte).
9
- * Nota: esegue una scansione completa del bucket. Per bucket molto grandi
10
- * andrebbe implementata una cache o usato Cloudflare Logpush/Analytics.
11
- */
12
- export async function getBucketSize(client: S3Client, bucketName: string): Promise<number> {
13
- let totalSize = 0
14
- let isTruncatedFlag = true
15
- let continuationToken: string | undefined = undefined
16
-
17
- try {
18
- while (isTruncatedFlag) {
19
- const command: ListObjectsV2Command = new ListObjectsV2Command({
20
- Bucket: bucketName,
21
- ContinuationToken: continuationToken,
22
- })
23
-
24
- const response: ListObjectsV2CommandOutput = await client.send(command)
25
-
26
- if (response.Contents) {
27
- for (const obj of response.Contents) {
28
- totalSize += obj.Size ?? 0
29
- }
30
- }
31
-
32
- isTruncatedFlag = response.IsTruncated ?? false
33
- continuationToken = response.NextContinuationToken
34
- }
35
- return totalSize
36
- } catch (err) {
37
- console.error('Error calculating bucket size:', err)
38
- throw new Error('Failed to calculate bucket size. Check your R2/S3 credentials and endpoint.')
39
- }
40
- }
@@ -1,58 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi } from 'vitest'
6
- import { D1SystemStatsRepository } from './system-stats.repository.d1'
7
-
8
- function makeMockDb(firstResult: unknown = null) {
9
- const runMock = vi.fn().mockResolvedValue({ success: true })
10
- const firstMock = vi.fn().mockResolvedValue(firstResult)
11
- // bind() returns the same statement interface for chained calls.
12
- // The statement also exposes first() directly for queries without bind (e.g. getStorageUsage).
13
- const bindMock = vi.fn(() => ({ run: runMock, first: firstMock }))
14
- const stmt = { bind: bindMock, run: runMock, first: firstMock }
15
- const prepareMock = vi.fn(() => stmt)
16
- return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock, firstMock }
17
- }
18
-
19
- describe('D1SystemStatsRepository', () => {
20
- describe('incrementStorage', () => {
21
- it('calls UPDATE system_stats with the byte count', async () => {
22
- const { db, prepareMock, bindMock } = makeMockDb()
23
- await new D1SystemStatsRepository(db).incrementStorage(512)
24
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('UPDATE system_stats'))
25
- expect(bindMock).toHaveBeenCalledWith(512)
26
- })
27
- })
28
-
29
- describe('decrementStorage', () => {
30
- it('calls UPDATE system_stats with MAX(0, ...) and the byte count', async () => {
31
- const { db, prepareMock, bindMock } = makeMockDb()
32
- await new D1SystemStatsRepository(db).decrementStorage(256)
33
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('MAX(0'))
34
- expect(bindMock).toHaveBeenCalledWith(256)
35
- })
36
- })
37
-
38
- describe('setStorage', () => {
39
- it('calls UPDATE system_stats with the stringified byte count', async () => {
40
- const { db, prepareMock, bindMock } = makeMockDb()
41
- await new D1SystemStatsRepository(db).setStorage(1024)
42
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('UPDATE system_stats'))
43
- expect(bindMock).toHaveBeenCalledWith('1024')
44
- })
45
- })
46
-
47
- describe('getStorageUsage', () => {
48
- it('parses and returns the integer value from D1', async () => {
49
- const { db } = makeMockDb({ value: '2048' })
50
- expect(await new D1SystemStatsRepository(db).getStorageUsage()).toBe(2048)
51
- })
52
-
53
- it('returns 0 when D1 returns null (stat not initialised)', async () => {
54
- const { db } = makeMockDb(null)
55
- expect(await new D1SystemStatsRepository(db).getStorageUsage()).toBe(0)
56
- })
57
- })
58
- })
@@ -1,48 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { SystemStatsRepository } from '@beechcms/core'
6
-
7
- /**
8
- * Implementation of SystemStatsRepository using Cloudflare D1.
9
- * Manages global system statistics such as total storage usage.
10
- */
11
- export class D1SystemStatsRepository implements SystemStatsRepository {
12
- constructor(private database: D1Database) {}
13
-
14
- /**
15
- * Increments the total storage usage count.
16
- */
17
- async incrementStorage(byteCount: number): Promise<void> {
18
- await this.database.prepare(
19
- "UPDATE system_stats SET value = CAST(value AS INTEGER) + ? WHERE id = 'total_storage_bytes'"
20
- ).bind(byteCount).run()
21
- }
22
-
23
- /**
24
- * Decrements the total storage usage count, ensuring it doesn't fall below zero.
25
- */
26
- async decrementStorage(byteCount: number): Promise<void> {
27
- await this.database.prepare(
28
- "UPDATE system_stats SET value = MAX(0, CAST(value AS INTEGER) - ?) WHERE id = 'total_storage_bytes'"
29
- ).bind(byteCount).run()
30
- }
31
-
32
- /**
33
- * Sets the total storage usage count to a specific value.
34
- */
35
- async setStorage(byteCount: number): Promise<void> {
36
- await this.database.prepare(
37
- "UPDATE system_stats SET value = ? WHERE id = 'total_storage_bytes'"
38
- ).bind(String(byteCount)).run()
39
- }
40
-
41
- /**
42
- * Retrieves the current total storage usage in bytes.
43
- */
44
- async getStorageUsage(): Promise<number> {
45
- const statsRecordResult = await this.database.prepare("SELECT value FROM system_stats WHERE id = 'total_storage_bytes'").first<{ value: string }>()
46
- return statsRecordResult ? Number.parseInt(statsRecordResult.value, 10) : 0
47
- }
48
- }
package/src/types.ts DELETED
@@ -1,90 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- /// <reference types="@cloudflare/workers-types" />
6
- import type { Seed, ContentRepository, IdempotencyRepository, BeechBucket, MediaRepository, SystemStatsRepository, IHashProvider, ITokenService, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogger, IActivityLogRepository, INotificationRepository, INotificationService, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, ISeedRegistry, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, BackrefMap, ISiteSettingsRepository, IDemoDataRepository, JwtClaims, ISeedLayoutRepository, ISeedRepository, ISchemaMutator, IDashboardLayoutRepository } from '@beechcms/core'
7
- import type { IRateLimiterRegistry } from './middleware/rate-limit.middleware'
8
- import type { ISetupChecklistRepository } from './shared/d1-setup-checklist.repository'
9
-
10
- export interface Env {
11
- DB: D1Database
12
- JWT_SECRET: string
13
- JWT_ISSUER?: string
14
- JWT_AUDIENCE?: string
15
- R2_ACCESS_KEY_ID?: string
16
- R2_SECRET_ACCESS_KEY?: string
17
- R2_ENDPOINT?: string
18
- R2_BUCKET_NAME?: string
19
- LOGIN_RATE_LIMITER?: RateLimit
20
- REFRESH_RATE_LIMITER?: RateLimit
21
- PUBLIC_READ_RATE_LIMITER?: RateLimit
22
- PUBLIC_WRITE_RATE_LIMITER?: RateLimit
23
- CORS_ORIGINS?: string
24
- PUBLIC_READ_API_KEY?: string
25
- PUBLIC_WRITE_API_KEY?: string
26
- PUBLIC_PUBLISHED_ONLY?: string
27
- PUBLIC_IDEMPOTENCY_TTL_SECONDS?: string
28
- MEDIA_BASE_URL?: string
29
- MEDIA_CDN_URL?: string
30
- MAX_UPLOAD_BYTES?: string
31
- RESEND_API_KEY?: string
32
- EMAIL_API_KEY?: string
33
- APP_URL?: string
34
- EMAIL_FROM?: string
35
- EMAIL_PROVIDER?: string
36
- SMTP_HOST?: string
37
- SMTP_PORT?: string
38
- WEBHOOK_TESTER_URL?: string
39
- FORGOT_PASSWORD_RATE_LIMITER?: RateLimit
40
- RESET_PASSWORD_RATE_LIMITER?: RateLimit
41
- ENV?: string
42
- DATE_FORMAT?: string
43
- ASSETS?: Fetcher
44
- QSTASH_TOKEN?: string
45
- QSTASH_URL?: string
46
- /** Public base URL the worker is reachable at, used by QStash to call back the webhook (e.g. an ngrok tunnel in dev). Falls back to APP_URL. */
47
- QSTASH_CALLBACK_URL?: string
48
- QSTASH_CURRENT_SIGNING_KEY?: string
49
- QSTASH_NEXT_SIGNING_KEY?: string
50
- }
51
-
52
- export interface Variables {
53
- jwtPayload: JwtClaims
54
- getSeed: (slug: string) => Seed | null
55
- seedRegistry: ISeedRegistry
56
- repository: ContentRepository
57
- idempotencyRepository: IdempotencyRepository
58
- bucket: BeechBucket
59
- mediaRepository: MediaRepository
60
- systemStatsRepository: SystemStatsRepository
61
- hashProvider: IHashProvider
62
- tokenService: ITokenService
63
- userRepository: IUserRepository
64
- sessionRepository: ISessionRepository
65
- passwordResetTokenRepository: IPasswordResetTokenRepository
66
- rateLimiters: IRateLimiterRegistry
67
- activityLogger: IActivityLogger
68
- activityLogRepository: IActivityLogRepository
69
- notificationRepository: INotificationRepository
70
- notificationService: INotificationService
71
- widgetRepository: IWidgetRepository
72
- searchRepository: ISearchRepository
73
- analyticsRepository: IAnalyticsRepository
74
- contentScanRepository: IContentScanRepository
75
- clock: IClock
76
- idGenerator: IIdGenerator
77
- automationRepository: IAutomationRepository
78
- automationRunner: IAutomationRunner
79
- scheduler: IScheduler
80
- backrefMap: BackrefMap
81
- siteSettingsRepository: ISiteSettingsRepository
82
- demoDataRepository: IDemoDataRepository
83
- setupChecklistRepository: ISetupChecklistRepository
84
- seedLayoutRepository: ISeedLayoutRepository
85
- seedRepository: ISeedRepository
86
- schemaMutator: ISchemaMutator
87
- dashboardLayoutRepository: IDashboardLayoutRepository
88
- }
89
-
90
- export type AppEnv = { Bindings: Env; Variables: Variables }
package/src/upload.ts DELETED
@@ -1,198 +0,0 @@
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
- * Media Engine — Single-path presigned uploads.
7
- * Il Worker non riceve mai i bytes: agisce solo da gatekeeper (auth, validazione, firma).
8
- */
9
- import { Hono } from 'hono'
10
- import { isMimeAccepted } from '@beechcms/core'
11
- import { AppEnv } from './types'
12
-
13
- const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024
14
- const ABSOLUTE_MAX_UPLOAD_BYTES = 500 * 1024 * 1024
15
- const PRESIGN_TTL_SECONDS = 900
16
-
17
- function resolveMaxUploadBytes(env: { MAX_UPLOAD_BYTES?: string }): number {
18
- const raw = env.MAX_UPLOAD_BYTES
19
- if (!raw) return DEFAULT_MAX_UPLOAD_BYTES
20
- const parsed = Number.parseInt(raw, 10)
21
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_UPLOAD_BYTES
22
- return Math.min(parsed, ABSOLUTE_MAX_UPLOAD_BYTES)
23
- }
24
-
25
- function sanitizeFilename(name: string): string {
26
- const base = name.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 100)
27
- return base || 'file'
28
- }
29
-
30
- function generateObjectKey(originalName: string): string {
31
- const timestamp = Math.floor(Date.now() / 1000)
32
- return `${timestamp}-${sanitizeFilename(originalName)}`
33
- }
34
-
35
- export async function deleteR2Objects(
36
- c: { var: { bucket: any, mediaRepository: any, systemStatsRepository: any } },
37
- objectKeys: string[]
38
- ): Promise<void> {
39
- const { bucket, mediaRepository, systemStatsRepository } = c.var
40
- for (const key of objectKeys) {
41
- const media = await mediaRepository.getByKey(key).catch(() => null)
42
- const size = media?.size_bytes ?? 0
43
- try {
44
- await bucket.delete(key)
45
- } catch (err) {
46
- console.warn(`Failed to delete storage object: ${key}`, err)
47
- }
48
- try {
49
- await mediaRepository.untrack(key)
50
- if (size > 0) await systemStatsRepository.decrementStorage(size)
51
- } catch (err) {
52
- console.warn(`Failed to untrack media object: ${key}`, err)
53
- }
54
- }
55
- }
56
-
57
- export const uploadRoutes = new Hono<AppEnv>()
58
-
59
- /** POST /upload/presign — Richiede URL firmata per upload diretto a R2. */
60
- uploadRoutes.post('/upload/presign', async (c) => {
61
- let body: { filename?: unknown, mimeType?: unknown, sizeBytes?: unknown }
62
- try { body = await c.req.json() } catch { return c.json({ error: 'Invalid JSON body' }, 400) }
63
-
64
- const { filename, mimeType, sizeBytes } = body
65
- if (typeof filename !== 'string' || !filename.trim()) return c.json({ error: 'filename is required' }, 400)
66
- if (typeof mimeType !== 'string' || !mimeType.trim()) return c.json({ error: 'mimeType is required' }, 400)
67
- if (typeof sizeBytes !== 'number' || !Number.isFinite(sizeBytes) || sizeBytes <= 0) {
68
- return c.json({ error: 'sizeBytes must be a positive number' }, 400)
69
- }
70
-
71
- const maxBytes = resolveMaxUploadBytes(c.env)
72
- if (sizeBytes > maxBytes) return c.json({ error: `File too large. Max ${maxBytes} bytes` }, 400)
73
- if (!isMimeAccepted(mimeType, 'any')) return c.json({ error: 'File type not allowed' }, 400)
74
-
75
- const key = generateObjectKey(filename)
76
- const uploadUrl = await c.var.bucket.presignPut(key, {
77
- expiresIn: PRESIGN_TTL_SECONDS,
78
- contentType: mimeType,
79
- contentLength: sizeBytes,
80
- })
81
-
82
- return c.json({ uploadUrl, key, expiresIn: PRESIGN_TTL_SECONDS }, 200)
83
- })
84
-
85
- /** POST /upload/confirm — Verifica l'oggetto su R2 e registra metadati. Idempotente sul key. */
86
- uploadRoutes.post('/upload/confirm', async (c) => {
87
- const { bucket, mediaRepository: mediaRepo, systemStatsRepository: statsRepo } = c.var
88
-
89
- let body: { key?: unknown }
90
- try { body = await c.req.json() } catch { return c.json({ error: 'Invalid JSON body' }, 400) }
91
- const { key } = body
92
- if (typeof key !== 'string' || !key.trim()) return c.json({ error: 'key is required' }, 400)
93
-
94
- const existing = await mediaRepo.getByKey(key)
95
- if (existing) return c.json({ url: bucket.getUrl(key) }, 200)
96
-
97
- const head = await bucket.head(key)
98
- if (!head) return c.json({ error: 'Object not found in storage' }, 404)
99
-
100
- const uploadedBy = c.var.jwtPayload?.sub ?? ''
101
- const size = head.size ?? 0
102
- const mime = head.contentType ?? 'application/octet-stream'
103
- const filename = key.replace(/^\d+-/, '') || key
104
-
105
- await statsRepo.incrementStorage(size)
106
- await mediaRepo.trackUpload({
107
- key,
108
- filename,
109
- mime_type: mime,
110
- size_bytes: size,
111
- uploaded_by: uploadedBy,
112
- })
113
-
114
- const jwtPayload = c.get('jwtPayload')
115
- if (jwtPayload) {
116
- c.get('activityLogger').log({
117
- action: 'upload',
118
- entityType: 'media',
119
- entityId: key,
120
- details: { name: filename, size, type: mime },
121
- actor: {
122
- id: jwtPayload.sub,
123
- email: jwtPayload.email ?? 'unknown',
124
- name: [jwtPayload.name, jwtPayload.surname].filter(Boolean).join(' ') || null,
125
- },
126
- })
127
- }
128
-
129
- return c.json({ url: bucket.getUrl(key) }, 200)
130
- })
131
-
132
- /** GET /upload/download-url/:key — URL firmata di lettura per asset privati. */
133
- uploadRoutes.get('/upload/download-url/:key', async (c) => {
134
- const key = decodeURIComponent(c.req.param('key') ?? '')
135
- if (!key) return c.json({ error: 'Missing key' }, 400)
136
-
137
- const head = await c.var.bucket.head(key)
138
- if (!head) return c.json({ error: 'Object not found' }, 404)
139
-
140
- const downloadUrl = await c.var.bucket.presignGet(key, { expiresIn: PRESIGN_TTL_SECONDS })
141
- return c.json({ downloadUrl, expiresIn: PRESIGN_TTL_SECONDS }, 200)
142
- })
143
-
144
- /** DELETE /upload/:key — Invariato. */
145
- uploadRoutes.delete('/upload/:key', async (c) => {
146
- const key = c.req.param('key')
147
- if (!key) return c.json({ error: 'Missing key' }, 400)
148
- await deleteR2Objects(c, [decodeURIComponent(key)])
149
- return c.json({ success: true }, 200)
150
- })
151
-
152
- // MIME types that browsers can render as active content — must be served as
153
- // attachments to prevent stored-XSS via same-origin script execution.
154
- const FORCE_DOWNLOAD_MIME_PREFIXES = [
155
- 'image/svg', // image/svg+xml, image/svg
156
- 'text/', // text/html, text/xml, text/javascript, …
157
- 'application/xml',
158
- 'application/xhtml',
159
- 'application/javascript',
160
- 'application/x-javascript',
161
- ]
162
-
163
- function isActiveMimeType(mime: string): boolean {
164
- const lower = mime.toLowerCase()
165
- return FORCE_DOWNLOAD_MIME_PREFIXES.some((prefix) => lower.startsWith(prefix))
166
- }
167
-
168
- /** Serve un file dallo storage (usato dalla rotta pubblica /api/media/:key). */
169
- export async function serveMediaHandler(c: any): Promise<Response> {
170
- const key = c.req.param('key')
171
- if (!key) return new Response('Missing key', { status: 400 })
172
- try {
173
- const object = await c.var.bucket.get(decodeURIComponent(key))
174
- if (!object) return new Response('Not found', { status: 404 })
175
- const headers = new Headers()
176
- const originalMime = object.contentType ?? 'application/octet-stream'
177
-
178
- // Defense-in-depth: force attachment + opaque MIME for SVG and other
179
- // active content types so browsers cannot render them inline even if a
180
- // legacy object pre-dates the upload-time block in file-types.ts.
181
- if (isActiveMimeType(originalMime)) {
182
- headers.set('Content-Type', 'application/octet-stream')
183
- headers.set('Content-Disposition', 'attachment')
184
- } else {
185
- headers.set('Content-Type', originalMime)
186
- }
187
-
188
- // Restrictive CSP on all media responses — even raster images don't need
189
- // scripts or frames, and this prevents future regressions.
190
- headers.set('Content-Security-Policy', "default-src 'none'; sandbox")
191
- headers.set('X-Content-Type-Options', 'nosniff')
192
- headers.set('Cache-Control', 'public, max-age=31536000, immutable')
193
- return new Response(object.body, { status: 200, headers })
194
- } catch (err) {
195
- console.error(`[serveMediaHandler] Error serving file ${key}:`, err)
196
- return new Response('Internal error', { status: 500 })
197
- }
198
- }