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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  4. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  5. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  6. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  7. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  8. package/assets/dashboard/beechLogoDark.svg +48 -48
  9. package/assets/dashboard/index.html +18 -18
  10. package/assets/dashboard/noResult.svg +42 -42
  11. package/assets/dashboard/sol.svg +3 -3
  12. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  13. package/migrations/0000_v040_base.sql +265 -265
  14. package/migrations/0029_automations.sql +14 -14
  15. package/migrations/0030_test_seeds.sql +125 -125
  16. package/package.json +2 -2
  17. package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
  18. package/src/auth/__fixtures__/static-token-service.ts +22 -22
  19. package/src/auth/bcrypt-hash-provider.ts +24 -24
  20. package/src/auth/constants.ts +14 -14
  21. package/src/auth/generate-refresh-token.test.ts +23 -23
  22. package/src/auth/hash-provider.test.ts +55 -55
  23. package/src/auth/jose-token-service.ts +62 -62
  24. package/src/auth/jwt-claims-passthrough.test.ts +87 -87
  25. package/src/auth/login.test.ts +100 -100
  26. package/src/auth/login.ts +78 -78
  27. package/src/auth/refresh.ts +11 -11
  28. package/src/auth/token-service.test.ts +90 -90
  29. package/src/factory.ts +370 -368
  30. package/src/features/automations/__tests__/action-executors.test.ts +255 -255
  31. package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
  32. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
  33. package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
  34. package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
  35. package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
  36. package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
  37. package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
  38. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
  39. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
  40. package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
  41. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
  42. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
  43. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
  44. package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
  45. package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
  46. package/src/features/automations/action-executors/index.ts +37 -37
  47. package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
  48. package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
  49. package/src/features/automations/action-executors/webhook.executor.ts +67 -67
  50. package/src/features/automations/automation-runner.ts +85 -85
  51. package/src/features/automations/automation-runner.utils.ts +47 -47
  52. package/src/features/automations/automations.handler.ts +197 -197
  53. package/src/features/automations/automations.schema.ts +177 -177
  54. package/src/features/automations/context-resolver.ts +152 -152
  55. package/src/features/automations/cron-runner.ts +140 -140
  56. package/src/features/automations/cron-runner.utils.ts +44 -44
  57. package/src/features/automations/filter-translation.ts +46 -46
  58. package/src/features/automations/index.ts +16 -16
  59. package/src/features/automations/template-grammar.ts +245 -245
  60. package/src/features/automations/var-access-resolver.ts +140 -140
  61. package/src/features/automations/when-evaluator.ts +87 -87
  62. package/src/features/automations/when-pushdown.ts +57 -57
  63. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
  64. package/src/features/backrefs/backrefs.handler.ts +168 -168
  65. package/src/features/backrefs/d1-backref.repository.ts +84 -84
  66. package/src/features/backrefs/index.ts +5 -5
  67. package/src/features/content/constants.ts +16 -16
  68. package/src/features/content/handlers/bulk.handler.ts +185 -185
  69. package/src/features/content/handlers/create.ts +129 -129
  70. package/src/features/content/handlers/delete.ts +62 -62
  71. package/src/features/content/handlers/facets.ts +49 -49
  72. package/src/features/content/handlers/get.ts +120 -120
  73. package/src/features/content/handlers/helpers.test.ts +180 -180
  74. package/src/features/content/handlers/helpers.ts +93 -93
  75. package/src/features/content/handlers/list.ts +174 -174
  76. package/src/features/content/handlers/update.ts +174 -174
  77. package/src/features/content/index.ts +26 -26
  78. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  79. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  80. package/src/features/dashboard-layout/index.ts +5 -0
  81. package/src/features/draft/draft.handler.ts +195 -195
  82. package/src/features/draft/draft.middleware.ts +66 -66
  83. package/src/features/draft/index.ts +5 -5
  84. package/src/features/email/email.provider.ts +42 -42
  85. package/src/features/email/email.service.ts +98 -98
  86. package/src/features/email/email.types.ts +119 -119
  87. package/src/features/email/index.ts +33 -33
  88. package/src/features/email/providers/resend.ts +67 -67
  89. package/src/features/email/providers/smtp.ts +55 -55
  90. package/src/features/email/templates/automation-mail.ts +19 -19
  91. package/src/features/email/templates/password-changed.ts +63 -63
  92. package/src/features/email/templates/password-reset.ts +68 -68
  93. package/src/features/email/templates/shell.ts +96 -96
  94. package/src/features/notifications/index.ts +5 -5
  95. package/src/features/notifications/notifications.handler.ts +105 -105
  96. package/src/features/password-reset/index.ts +19 -19
  97. package/src/features/password-reset/request.ts +92 -92
  98. package/src/features/password-reset/reset.ts +102 -102
  99. package/src/features/rotate-field/index.ts +5 -5
  100. package/src/features/rotate-field/rotate-field.handler.ts +132 -132
  101. package/src/features/rotate-field/rotate-field.schema.ts +17 -17
  102. package/src/features/schema/schema.handler.ts +131 -131
  103. package/src/features/seeds/index.ts +5 -5
  104. package/src/features/seeds/seeds.handler.test.ts +1199 -632
  105. package/src/features/seeds/seeds.handler.ts +558 -693
  106. package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
  107. package/src/features/settings/settings.handler.ts +392 -392
  108. package/src/features/setup/index.ts +288 -204
  109. package/src/features/stats/index.ts +5 -5
  110. package/src/features/stats/stats.handler.ts +420 -420
  111. package/src/features/webhooks/index.ts +63 -63
  112. package/src/index.ts +65 -65
  113. package/src/media-utils.ts +82 -82
  114. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  115. package/src/middleware/auth-providers.middleware.ts +45 -45
  116. package/src/middleware/observability.middleware.test.ts +93 -0
  117. package/src/middleware/observability.middleware.ts +70 -70
  118. package/src/middleware/rate-limit.middleware.ts +45 -45
  119. package/src/middleware/repository.middleware.ts +117 -114
  120. package/src/middleware/seed-registry.middleware.test.ts +97 -97
  121. package/src/middleware/seed-registry.middleware.ts +21 -21
  122. package/src/middleware/storage.middleware.ts +25 -25
  123. package/src/middleware.ts +45 -45
  124. package/src/public/access-policy.ts +27 -27
  125. package/src/public/api-key-middleware.ts +57 -57
  126. package/src/public/cache-utils.ts +38 -38
  127. package/src/public/entry-projection.ts +46 -46
  128. package/src/public/idempotency.ts +23 -23
  129. package/src/public/index.ts +16 -16
  130. package/src/public/problem-details.ts +124 -124
  131. package/src/public/public-add.ts +114 -114
  132. package/src/public/public-edit.ts +163 -163
  133. package/src/public/public-errors.ts +19 -19
  134. package/src/public/public-read.ts +63 -63
  135. package/src/public/public-routes.ts +88 -88
  136. package/src/public/query-builder.test.ts +224 -224
  137. package/src/public/query-builder.ts +156 -156
  138. package/src/public/rate-limit-middleware.ts +34 -34
  139. package/src/public/read-list.ts +54 -54
  140. package/src/public/read-single.ts +48 -48
  141. package/src/public/response-builder.ts +30 -30
  142. package/src/public/sanitize.ts +69 -69
  143. package/src/public/slug-utils.ts +18 -18
  144. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
  145. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
  146. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
  147. package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
  148. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
  149. package/src/rate-limit/no-op-rate-limiter.ts +11 -11
  150. package/src/search-utils.test.ts +211 -211
  151. package/src/search-utils.ts +213 -213
  152. package/src/search.ts +65 -65
  153. package/src/shared/apply-policies.test.ts +81 -81
  154. package/src/shared/apply-policies.ts +67 -67
  155. package/src/shared/automations.repository.d1.ts +150 -150
  156. package/src/shared/background-notification-service.test.ts +62 -62
  157. package/src/shared/background-notification-service.ts +52 -52
  158. package/src/shared/base.repository.d1.ts +32 -32
  159. package/src/shared/content-utils.test.ts +165 -165
  160. package/src/shared/content-utils.ts +86 -86
  161. package/src/shared/content.repository.d1.test.ts +507 -458
  162. package/src/shared/content.repository.d1.ts +851 -851
  163. package/src/shared/d1-activity-log.repository.test.ts +140 -140
  164. package/src/shared/d1-activity-log.repository.ts +105 -105
  165. package/src/shared/d1-activity-logger.test.ts +86 -86
  166. package/src/shared/d1-activity-logger.ts +67 -67
  167. package/src/shared/d1-analytics.repository.test.ts +78 -78
  168. package/src/shared/d1-analytics.repository.ts +85 -85
  169. package/src/shared/d1-content-scan.repository.test.ts +80 -80
  170. package/src/shared/d1-content-scan.repository.ts +37 -37
  171. package/src/shared/d1-notification.repository.test.ts +128 -128
  172. package/src/shared/d1-notification.repository.ts +118 -118
  173. package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
  174. package/src/shared/d1-password-reset-token.repository.ts +56 -56
  175. package/src/shared/d1-search.repository.test.ts +87 -87
  176. package/src/shared/d1-search.repository.ts +88 -88
  177. package/src/shared/d1-session.repository.test.ts +125 -125
  178. package/src/shared/d1-session.repository.ts +102 -102
  179. package/src/shared/d1-setup-checklist.repository.ts +36 -36
  180. package/src/shared/d1-user.repository.test.ts +151 -151
  181. package/src/shared/d1-user.repository.ts +119 -119
  182. package/src/shared/d1-widget.repository.test.ts +267 -221
  183. package/src/shared/d1-widget.repository.ts +385 -341
  184. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  185. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  186. package/src/shared/demo-data-sql.ts +216 -54
  187. package/src/shared/demo-data.repository.d1.ts +20 -20
  188. package/src/shared/execution-context-scheduler.ts +13 -13
  189. package/src/shared/fixed-clock.ts +25 -25
  190. package/src/shared/fts-sync.ts +8 -8
  191. package/src/shared/idempotency.repository.d1.test.ts +83 -83
  192. package/src/shared/idempotency.repository.d1.ts +60 -60
  193. package/src/shared/in-memory-activity-logger.ts +19 -19
  194. package/src/shared/in-memory-notification-service.ts +19 -19
  195. package/src/shared/in-memory-seed.repository.ts +58 -51
  196. package/src/shared/media.repository.d1.test.ts +107 -107
  197. package/src/shared/media.repository.d1.ts +68 -68
  198. package/src/shared/qstash-notification-service.test.ts +67 -67
  199. package/src/shared/qstash-notification-service.ts +55 -55
  200. package/src/shared/query-utils.ts +141 -141
  201. package/src/shared/request-utils.ts +26 -26
  202. package/src/shared/schema-mutator.d1.test.ts +142 -0
  203. package/src/shared/schema-mutator.d1.ts +64 -64
  204. package/src/shared/seed-layout.repository.d1.test.ts +114 -114
  205. package/src/shared/seed-layout.repository.d1.ts +62 -62
  206. package/src/shared/seed-registry-cache.test.ts +68 -68
  207. package/src/shared/seed-registry-cache.ts +49 -49
  208. package/src/shared/seed.repository.d1.test.ts +143 -143
  209. package/src/shared/seed.repository.d1.ts +98 -98
  210. package/src/shared/sequential-id-generator.ts +33 -33
  211. package/src/shared/site-settings.repository.d1.ts +53 -53
  212. package/src/shared/storage/factory.ts +41 -41
  213. package/src/shared/storage/s3-bucket.ts +195 -195
  214. package/src/shared/storage-utils.ts +40 -40
  215. package/src/shared/system-stats.repository.d1.test.ts +58 -58
  216. package/src/shared/system-stats.repository.d1.ts +48 -48
  217. package/src/types.ts +90 -87
  218. package/src/upload.ts +198 -198
  219. package/src/widget.test.ts +85 -0
  220. package/src/widget.ts +251 -212
  221. package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
  222. package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
@@ -1,63 +1,63 @@
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 { Hono } from 'hono'
6
- import { Receiver } from '@upstash/qstash'
7
- import type { AppEnv } from '../../types'
8
- import type { CreateNotificationInput } from '@beechcms/core'
9
-
10
- export const webhooksApp = new Hono<AppEnv>()
11
-
12
- webhooksApp.post('/qstash', async (context) => {
13
- const currentSigningKey = context.env.QSTASH_CURRENT_SIGNING_KEY
14
- const nextSigningKey = context.env.QSTASH_NEXT_SIGNING_KEY
15
-
16
- if (!currentSigningKey || !nextSigningKey) {
17
- console.error('QStash webhook called but signing keys are not configured')
18
- return context.text('Configuration error', 500)
19
- }
20
-
21
- const receiver = new Receiver({
22
- currentSigningKey,
23
- nextSigningKey,
24
- })
25
-
26
- const body = await context.req.text()
27
- const signature = context.req.header('Upstash-Signature')
28
-
29
- if (!signature) {
30
- return context.text('Missing signature', 401)
31
- }
32
-
33
- try {
34
- const isValid = await receiver.verify({
35
- signature,
36
- body,
37
- })
38
-
39
- if (!isValid) {
40
- return context.text('Invalid signature', 401)
41
- }
42
- } catch (err) {
43
- console.error('QStash signature verification failed', err)
44
- return context.text('Invalid signature', 401)
45
- }
46
-
47
- try {
48
- const input = JSON.parse(body) as CreateNotificationInput
49
-
50
- const notificationRepository = context.get('notificationRepository')
51
-
52
- await notificationRepository.create({
53
- title: input.title,
54
- message: input.message,
55
- type: input.type ?? 'info',
56
- })
57
-
58
- return context.text('OK', 200)
59
- } catch (error) {
60
- console.error('Failed to process QStash webhook payload', error)
61
- return context.text('Internal server error', 500)
62
- }
63
- })
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 { Hono } from 'hono'
6
+ import { Receiver } from '@upstash/qstash'
7
+ import type { AppEnv } from '../../types'
8
+ import type { CreateNotificationInput } from '@beechcms/core'
9
+
10
+ export const webhooksApp = new Hono<AppEnv>()
11
+
12
+ webhooksApp.post('/qstash', async (context) => {
13
+ const currentSigningKey = context.env.QSTASH_CURRENT_SIGNING_KEY
14
+ const nextSigningKey = context.env.QSTASH_NEXT_SIGNING_KEY
15
+
16
+ if (!currentSigningKey || !nextSigningKey) {
17
+ console.error('QStash webhook called but signing keys are not configured')
18
+ return context.text('Configuration error', 500)
19
+ }
20
+
21
+ const receiver = new Receiver({
22
+ currentSigningKey,
23
+ nextSigningKey,
24
+ })
25
+
26
+ const body = await context.req.text()
27
+ const signature = context.req.header('Upstash-Signature')
28
+
29
+ if (!signature) {
30
+ return context.text('Missing signature', 401)
31
+ }
32
+
33
+ try {
34
+ const isValid = await receiver.verify({
35
+ signature,
36
+ body,
37
+ })
38
+
39
+ if (!isValid) {
40
+ return context.text('Invalid signature', 401)
41
+ }
42
+ } catch (err) {
43
+ console.error('QStash signature verification failed', err)
44
+ return context.text('Invalid signature', 401)
45
+ }
46
+
47
+ try {
48
+ const input = JSON.parse(body) as CreateNotificationInput
49
+
50
+ const notificationRepository = context.get('notificationRepository')
51
+
52
+ await notificationRepository.create({
53
+ title: input.title,
54
+ message: input.message,
55
+ type: input.type ?? 'info',
56
+ })
57
+
58
+ return context.text('OK', 200)
59
+ } catch (error) {
60
+ console.error('Failed to process QStash webhook payload', error)
61
+ return context.text('Internal server error', 500)
62
+ }
63
+ })
package/src/index.ts CHANGED
@@ -1,65 +1,65 @@
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 { createBeechApp } from './factory'
6
- import { SeedRegistry, SystemIdGenerator } from '@beechcms/core'
7
- import { runCronAutomations } from './features/automations'
8
- import { D1AutomationRepository } from './shared/automations.repository.d1'
9
- import { D1ContentRepository } from './shared/content.repository.d1'
10
- import { D1SeedRepository } from './shared/seed.repository.d1'
11
- import type { Env } from './types'
12
-
13
- const app = createBeechApp({ seeds: [] })
14
-
15
- app.get('/', (c) => c.text('Beech API is running'))
16
-
17
- export default {
18
- fetch(request: Request, env: Env, ctx: ExecutionContext) {
19
- if (env.ENV === 'development') {
20
- const checks: Array<{ name: string; url: string }> = [
21
- { name: 'MinIO', url: (env.R2_ENDPOINT ?? 'http://localhost:9000') + '/minio/health/live' },
22
- { name: 'Mailpit', url: `http://${env.SMTP_HOST ?? 'localhost'}:${env.SMTP_PORT ?? '8025'}/livez` },
23
- ]
24
- for (const c of checks) {
25
- fetch(c.url).catch(() => {
26
- console.warn(
27
- `\n⚠️ ${c.name} non raggiungibile su ${c.url}\n` +
28
- ` Beech in dev richiede lo stack Docker completo.\n` +
29
- ` Avvialo con: npm run dev:full\n`
30
- )
31
- })
32
- }
33
- }
34
- return app.fetch(request, env, ctx)
35
- },
36
-
37
- async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
38
- const scheduledTime = controller?.scheduledTime ?? Date.now()
39
-
40
- if (!env.DB) {
41
- console.warn('[cron] D1 binding missing. Skipping cron automations.')
42
- return
43
- }
44
-
45
- const automationRepository = new D1AutomationRepository(env.DB)
46
- const contentRepository = new D1ContentRepository(env.DB)
47
- const seedRepository = new D1SeedRepository(env.DB)
48
- const seeds = await seedRepository.listActive()
49
- const registry = new SeedRegistry(seeds)
50
- const getSeed = (slug: string) => registry.get(slug) ?? null
51
-
52
- ctx.waitUntil(
53
- runCronAutomations(
54
- {
55
- automationRepository,
56
- contentRepository,
57
- getSeed,
58
- env: env as unknown as Record<string, string | undefined>,
59
- idGenerator: SystemIdGenerator,
60
- },
61
- scheduledTime,
62
- ),
63
- )
64
- },
65
- } satisfies ExportedHandler<Env>
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 { createBeechApp } from './factory'
6
+ import { SeedRegistry, SystemIdGenerator } from '@beechcms/core'
7
+ import { runCronAutomations } from './features/automations'
8
+ import { D1AutomationRepository } from './shared/automations.repository.d1'
9
+ import { D1ContentRepository } from './shared/content.repository.d1'
10
+ import { D1SeedRepository } from './shared/seed.repository.d1'
11
+ import type { Env } from './types'
12
+
13
+ const app = createBeechApp({ seeds: [] })
14
+
15
+ app.get('/', (c) => c.text('Beech API is running'))
16
+
17
+ export default {
18
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
19
+ if (env.ENV === 'development') {
20
+ const checks: Array<{ name: string; url: string }> = [
21
+ { name: 'MinIO', url: (env.R2_ENDPOINT ?? 'http://localhost:9000') + '/minio/health/live' },
22
+ { name: 'Mailpit', url: `http://${env.SMTP_HOST ?? 'localhost'}:${env.SMTP_PORT ?? '8025'}/livez` },
23
+ ]
24
+ for (const c of checks) {
25
+ fetch(c.url).catch(() => {
26
+ console.warn(
27
+ `\n⚠️ ${c.name} non raggiungibile su ${c.url}\n` +
28
+ ` Beech in dev richiede lo stack Docker completo.\n` +
29
+ ` Avvialo con: npm run dev:full\n`
30
+ )
31
+ })
32
+ }
33
+ }
34
+ return app.fetch(request, env, ctx)
35
+ },
36
+
37
+ async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
38
+ const scheduledTime = controller?.scheduledTime ?? Date.now()
39
+
40
+ if (!env.DB) {
41
+ console.warn('[cron] D1 binding missing. Skipping cron automations.')
42
+ return
43
+ }
44
+
45
+ const automationRepository = new D1AutomationRepository(env.DB)
46
+ const contentRepository = new D1ContentRepository(env.DB)
47
+ const seedRepository = new D1SeedRepository(env.DB)
48
+ const seeds = await seedRepository.listActive()
49
+ const registry = new SeedRegistry(seeds)
50
+ const getSeed = (slug: string) => registry.get(slug) ?? null
51
+
52
+ ctx.waitUntil(
53
+ runCronAutomations(
54
+ {
55
+ automationRepository,
56
+ contentRepository,
57
+ getSeed,
58
+ env: env as unknown as Record<string, string | undefined>,
59
+ idGenerator: SystemIdGenerator,
60
+ },
61
+ scheduledTime,
62
+ ),
63
+ )
64
+ },
65
+ } satisfies ExportedHandler<Env>
@@ -1,82 +1,82 @@
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 utils: estrazione chiavi R2 dal data di un'entry.
7
- * Usato alla cancellazione entry per eliminare i file associati da R2.
8
- *
9
- * @see docs/media-engine.md
10
- */
11
- import type { Seed } from '@beechcms/core'
12
-
13
- /** Pattern per estrarre la chiave R2 da URL in formato /api/media/KEY */
14
- const MEDIA_URL_PATTERN = /\/api\/media\/([^/?#]+)/
15
-
16
- /**
17
- * Estrae la chiave R2 da un URL di media.
18
- * Es: "https://x.com/api/media/1739123456-avatar.png" → "1739123456-avatar.png"
19
- *
20
- * @param mediaUrl - URL completo o path (es. /api/media/123-foto.png)
21
- * @returns Chiave R2 o null se l'URL non è valido
22
- */
23
- export function extractMediaKey(mediaUrl: string): string | null {
24
- const match = MEDIA_URL_PATTERN.exec(String(mediaUrl))
25
- return match ? decodeURIComponent(match[1]) : null
26
- }
27
-
28
- /**
29
- * Attraversa ricorsivamente un valore (stringa, array, oggetto) e raccoglie
30
- * tutte le chiavi R2 trovate in stringhe che matchano /api/media/KEY.
31
- */
32
- function collectMediaKeysRecursive(value: unknown, collectedKeys: Set<string>): void {
33
- if (typeof value === 'string') {
34
- const r2Key = extractMediaKey(value)
35
- if (r2Key) {
36
- collectedKeys.add(r2Key)
37
- return
38
- }
39
- // Legacy compat: campi json/file possono contenere JSON serializzato.
40
- try {
41
- const parsed = JSON.parse(value) as unknown
42
- if (parsed !== value) {
43
- collectMediaKeysRecursive(parsed, collectedKeys)
44
- }
45
- } catch {
46
- // ignore
47
- }
48
- return
49
- }
50
- if (Array.isArray(value)) {
51
- for (const item of value) collectMediaKeysRecursive(item, collectedKeys)
52
- return
53
- }
54
- if (value != null && typeof value === 'object') {
55
- for (const nestedValue of Object.values(value)) {
56
- collectMediaKeysRecursive(nestedValue, collectedKeys)
57
- }
58
- }
59
- }
60
-
61
- /**
62
- * Estrae tutte le chiavi R2 dal data di un'entry.
63
- * Cerca nei campi `file` (stringa URL) e `json` (array/oggetto con URL).
64
- * Il data è in formato DB: chiavi = branch.id (es. art_03, prd_05).
65
- *
66
- * @param seed - Schema del tipo di contenuto
67
- * @param entryData - Payload in formato DB (chiavi = branch ID)
68
- * @returns Array di chiavi R2 uniche da eliminare
69
- */
70
- export function extractMediaKeysFromData(
71
- seed: Seed,
72
- entryData: Record<string, unknown>
73
- ): string[] {
74
- const r2Keys = new Set<string>()
75
- for (const branch of seed.branches) {
76
- if (branch.type !== 'file' && branch.type !== 'json') continue
77
- const fieldValue = entryData[branch.alias]
78
- if (fieldValue == null) continue
79
- collectMediaKeysRecursive(fieldValue, r2Keys)
80
- }
81
- return [...r2Keys]
82
- }
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 utils: estrazione chiavi R2 dal data di un'entry.
7
+ * Usato alla cancellazione entry per eliminare i file associati da R2.
8
+ *
9
+ * @see docs/media-engine.md
10
+ */
11
+ import type { Seed } from '@beechcms/core'
12
+
13
+ /** Pattern per estrarre la chiave R2 da URL in formato /api/media/KEY */
14
+ const MEDIA_URL_PATTERN = /\/api\/media\/([^/?#]+)/
15
+
16
+ /**
17
+ * Estrae la chiave R2 da un URL di media.
18
+ * Es: "https://x.com/api/media/1739123456-avatar.png" → "1739123456-avatar.png"
19
+ *
20
+ * @param mediaUrl - URL completo o path (es. /api/media/123-foto.png)
21
+ * @returns Chiave R2 o null se l'URL non è valido
22
+ */
23
+ export function extractMediaKey(mediaUrl: string): string | null {
24
+ const match = MEDIA_URL_PATTERN.exec(String(mediaUrl))
25
+ return match ? decodeURIComponent(match[1]) : null
26
+ }
27
+
28
+ /**
29
+ * Attraversa ricorsivamente un valore (stringa, array, oggetto) e raccoglie
30
+ * tutte le chiavi R2 trovate in stringhe che matchano /api/media/KEY.
31
+ */
32
+ function collectMediaKeysRecursive(value: unknown, collectedKeys: Set<string>): void {
33
+ if (typeof value === 'string') {
34
+ const r2Key = extractMediaKey(value)
35
+ if (r2Key) {
36
+ collectedKeys.add(r2Key)
37
+ return
38
+ }
39
+ // Legacy compat: campi json/file possono contenere JSON serializzato.
40
+ try {
41
+ const parsed = JSON.parse(value) as unknown
42
+ if (parsed !== value) {
43
+ collectMediaKeysRecursive(parsed, collectedKeys)
44
+ }
45
+ } catch {
46
+ // ignore
47
+ }
48
+ return
49
+ }
50
+ if (Array.isArray(value)) {
51
+ for (const item of value) collectMediaKeysRecursive(item, collectedKeys)
52
+ return
53
+ }
54
+ if (value != null && typeof value === 'object') {
55
+ for (const nestedValue of Object.values(value)) {
56
+ collectMediaKeysRecursive(nestedValue, collectedKeys)
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Estrae tutte le chiavi R2 dal data di un'entry.
63
+ * Cerca nei campi `file` (stringa URL) e `json` (array/oggetto con URL).
64
+ * Il data è in formato DB: chiavi = branch.id (es. art_03, prd_05).
65
+ *
66
+ * @param seed - Schema del tipo di contenuto
67
+ * @param entryData - Payload in formato DB (chiavi = branch ID)
68
+ * @returns Array di chiavi R2 uniche da eliminare
69
+ */
70
+ export function extractMediaKeysFromData(
71
+ seed: Seed,
72
+ entryData: Record<string, unknown>
73
+ ): string[] {
74
+ const r2Keys = new Set<string>()
75
+ for (const branch of seed.branches) {
76
+ if (branch.type !== 'file' && branch.type !== 'json') continue
77
+ const fieldValue = entryData[branch.alias]
78
+ if (fieldValue == null) continue
79
+ collectMediaKeysRecursive(fieldValue, r2Keys)
80
+ }
81
+ return [...r2Keys]
82
+ }
@@ -0,0 +1,85 @@
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 { Hono } from 'hono'
7
+ import { authProvidersMiddleware } from './auth-providers.middleware'
8
+
9
+ describe('authProvidersMiddleware', () => {
10
+ it('throws an error if ENV is production and JWT_SECRET is the development default', async () => {
11
+ const app = new Hono()
12
+ app.use('*', authProvidersMiddleware())
13
+ app.get('/test', (c) => c.text('ok'))
14
+
15
+ const env = {
16
+ ENV: 'production',
17
+ JWT_SECRET: 'sviluppo-secret-cambiami-almeno-32-byte-per-sicurezza-hono',
18
+ }
19
+
20
+ // Since throwing in Hono middleware returns an unhandled exception or 500 error,
21
+ // let's run app.request and inspect if the error is thrown. Hono's default errorHandler
22
+ // catches it, or we can add a custom error handler to catch it.
23
+ let thrownError: Error | null = null
24
+ app.onError((err) => {
25
+ thrownError = err
26
+ return Hono.prototype.json({ error: err.message }, 500)
27
+ })
28
+
29
+ const res = await app.request('/test', undefined, env)
30
+ expect(res.status).toBe(500)
31
+ expect(thrownError).not.toBeNull()
32
+ expect(thrownError!.message).toContain('JWT_SECRET non configurato')
33
+ })
34
+
35
+ it('injects hashProvider and tokenService when configuration is correct', async () => {
36
+ const app = new Hono()
37
+ app.use('*', authProvidersMiddleware())
38
+ app.get('/test', (c) => {
39
+ const hash = c.get('hashProvider' as never)
40
+ const token = c.get('tokenService' as never)
41
+ return c.json({
42
+ hasHash: !!hash,
43
+ hasToken: !!token,
44
+ })
45
+ })
46
+
47
+ const env = {
48
+ ENV: 'development',
49
+ JWT_SECRET: 'custom-secret-key-that-is-secure-enough',
50
+ JWT_ISSUER: 'issuer',
51
+ JWT_AUDIENCE: 'audience',
52
+ }
53
+
54
+ const res = await app.request('/test', undefined, env)
55
+ expect(res.status).toBe(200)
56
+ const body = await res.json() as any
57
+ expect(body.hasHash).toBe(true)
58
+ expect(body.hasToken).toBe(true)
59
+ })
60
+
61
+ it('respects overrides when provided', async () => {
62
+ const app = new Hono()
63
+ const mockHash = { hash: vi.fn(), verify: vi.fn() }
64
+ const mockToken = { sign: vi.fn(), verify: vi.fn() }
65
+
66
+ app.use('*', authProvidersMiddleware({
67
+ hashProvider: mockHash as any,
68
+ tokenService: mockToken as any,
69
+ }))
70
+
71
+ app.get('/test', (c) => {
72
+ const hash = c.get('hashProvider' as never)
73
+ const token = c.get('tokenService' as never)
74
+ return c.json({
75
+ hashIsOverridden: hash === mockHash,
76
+ tokenIsOverridden: token === mockToken,
77
+ })
78
+ })
79
+
80
+ const res = await app.request('/test')
81
+ const body = await res.json() as any
82
+ expect(body.hashIsOverridden).toBe(true)
83
+ expect(body.tokenIsOverridden).toBe(true)
84
+ })
85
+ })
@@ -1,45 +1,45 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { createMiddleware } from 'hono/factory'
6
- import type { IHashProvider, ITokenService, IClock } from '@beechcms/core'
7
- import { SystemClock } from '@beechcms/core'
8
- import type { AppEnv } from '../types'
9
- import { BcryptHashProvider } from '../auth/bcrypt-hash-provider'
10
- import { JoseTokenService } from '../auth/jose-token-service'
11
-
12
- export interface AuthProviderOverrides {
13
- hashProvider?: IHashProvider
14
- tokenService?: ITokenService
15
- clock?: IClock
16
- }
17
-
18
- // Valore di default presente in .dev.vars.example: mai accettabile in produzione.
19
- const DEV_JWT_SECRET = 'sviluppo-secret-cambiami-almeno-32-byte-per-sicurezza-hono'
20
-
21
- export const authProvidersMiddleware = (overrides?: AuthProviderOverrides) => {
22
- return createMiddleware<AppEnv>(async (context, next) => {
23
- if (context.env.ENV === 'production' && context.env.JWT_SECRET === DEV_JWT_SECRET) {
24
- throw new Error(
25
- 'JWT_SECRET non configurato: in produzione impostare un segreto univoco con `wrangler secret put JWT_SECRET`',
26
- )
27
- }
28
-
29
- const resolvedClock = overrides?.clock ?? SystemClock
30
- const hashProvider = overrides?.hashProvider ?? new BcryptHashProvider()
31
- const tokenService = overrides?.tokenService ?? new JoseTokenService(
32
- context.env.JWT_SECRET,
33
- {
34
- issuer: context.env.JWT_ISSUER,
35
- audience: context.env.JWT_AUDIENCE,
36
- },
37
- resolvedClock,
38
- )
39
-
40
- context.set('hashProvider', hashProvider)
41
- context.set('tokenService', tokenService)
42
-
43
- await next()
44
- })
45
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { createMiddleware } from 'hono/factory'
6
+ import type { IHashProvider, ITokenService, IClock } from '@beechcms/core'
7
+ import { SystemClock } from '@beechcms/core'
8
+ import type { AppEnv } from '../types'
9
+ import { BcryptHashProvider } from '../auth/bcrypt-hash-provider'
10
+ import { JoseTokenService } from '../auth/jose-token-service'
11
+
12
+ export interface AuthProviderOverrides {
13
+ hashProvider?: IHashProvider
14
+ tokenService?: ITokenService
15
+ clock?: IClock
16
+ }
17
+
18
+ // Valore di default presente in .dev.vars.example: mai accettabile in produzione.
19
+ const DEV_JWT_SECRET = 'sviluppo-secret-cambiami-almeno-32-byte-per-sicurezza-hono'
20
+
21
+ export const authProvidersMiddleware = (overrides?: AuthProviderOverrides) => {
22
+ return createMiddleware<AppEnv>(async (context, next) => {
23
+ if (context.env.ENV === 'production' && context.env.JWT_SECRET === DEV_JWT_SECRET) {
24
+ throw new Error(
25
+ 'JWT_SECRET non configurato: in produzione impostare un segreto univoco con `wrangler secret put JWT_SECRET`',
26
+ )
27
+ }
28
+
29
+ const resolvedClock = overrides?.clock ?? SystemClock
30
+ const hashProvider = overrides?.hashProvider ?? new BcryptHashProvider()
31
+ const tokenService = overrides?.tokenService ?? new JoseTokenService(
32
+ context.env.JWT_SECRET,
33
+ {
34
+ issuer: context.env.JWT_ISSUER,
35
+ audience: context.env.JWT_AUDIENCE,
36
+ },
37
+ resolvedClock,
38
+ )
39
+
40
+ context.set('hashProvider', hashProvider)
41
+ context.set('tokenService', tokenService)
42
+
43
+ await next()
44
+ })
45
+ }