@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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- 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>
|
package/src/media-utils.ts
CHANGED
|
@@ -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
|
+
}
|