@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.
- package/README.md +4 -4
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BwlCt-IA.js +39 -0
- package/assets/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/assets/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/assets/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/assets/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/assets/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/assets/dashboard/assets/index-BcVN-DNy.js +642 -0
- package/assets/dashboard/assets/index-Cf1yRCqG.css +1 -0
- package/assets/dashboard/assets/lora-cyrillic-ext-wght-normal-YcVxMP-u.woff2 +0 -0
- package/assets/dashboard/assets/lora-cyrillic-wght-normal-UWPuZLjM.woff2 +0 -0
- package/assets/dashboard/assets/lora-latin-ext-wght-normal-C2Wlntb9.woff2 +0 -0
- package/assets/dashboard/assets/lora-latin-wght-normal-BiLcIKcI.woff2 +0 -0
- package/assets/dashboard/assets/lora-math-wght-normal-sOy7rEfW.woff2 +0 -0
- package/assets/dashboard/assets/lora-symbols-wght-normal-DQ5VrUkH.woff2 +0 -0
- package/assets/dashboard/assets/lora-vietnamese-wght-normal-CfJ7gtf3.woff2 +0 -0
- package/assets/dashboard/assets/pie-chart-recharts-BMWpjFzf.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-8_GwAFR0.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/dist/auth/__fixtures__/in-memory-hash-provider.d.ts +5 -0
- package/dist/auth/__fixtures__/static-token-service.d.ts +6 -0
- package/dist/auth/bcrypt-hash-provider.d.ts +8 -0
- package/dist/auth/constants.d.ts +10 -0
- package/dist/auth/jose-token-service.d.ts +14 -0
- package/dist/auth/login.d.ts +32 -0
- package/dist/auth/refresh.d.ts +1 -0
- package/dist/factory.d.ts +45 -0
- package/dist/features/automations/action-executors/create-entry.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/edit-field.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/index.d.ts +13 -0
- package/dist/features/automations/action-executors/send-mail.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/set-variable.executor.d.ts +15 -0
- package/dist/features/automations/action-executors/webhook.executor.d.ts +7 -0
- package/dist/features/automations/automation-runner.d.ts +13 -0
- package/dist/features/automations/automation-runner.utils.d.ts +3 -0
- package/dist/features/automations/automations.handler.d.ts +7 -0
- package/dist/features/automations/automations.schema.d.ts +191 -0
- package/dist/features/automations/context-resolver.d.ts +8 -0
- package/dist/features/automations/cron-runner.d.ts +9 -0
- package/dist/features/automations/cron-runner.utils.d.ts +1 -0
- package/dist/features/automations/filter-translation.d.ts +9 -0
- package/dist/features/automations/index.d.ts +7 -0
- package/dist/features/automations/template-grammar.d.ts +73 -0
- package/dist/features/automations/var-access-resolver.d.ts +6 -0
- package/dist/features/automations/when-evaluator.d.ts +3 -0
- package/dist/features/automations/when-pushdown.d.ts +2 -0
- package/dist/features/backrefs/backrefs.handler.d.ts +3 -0
- package/dist/features/backrefs/d1-backref.repository.d.ts +17 -0
- package/dist/features/backrefs/index.d.ts +1 -0
- package/dist/features/content/constants.d.ts +12 -0
- package/dist/features/content/handlers/bulk.handler.d.ts +3 -0
- package/dist/features/content/handlers/create.d.ts +3 -0
- package/dist/features/content/handlers/delete.d.ts +3 -0
- package/dist/features/content/handlers/facets.d.ts +3 -0
- package/dist/features/content/handlers/get.d.ts +4 -0
- package/dist/features/content/handlers/helpers.d.ts +12 -0
- package/dist/features/content/handlers/list.d.ts +3 -0
- package/dist/features/content/handlers/update.d.ts +3 -0
- package/dist/features/content/index.d.ts +4 -0
- package/dist/features/dashboard-layout/dashboard-layout.handler.d.ts +7 -0
- package/dist/features/dashboard-layout/index.d.ts +1 -0
- package/dist/features/draft/draft.handler.d.ts +4 -0
- package/dist/features/draft/draft.middleware.d.ts +9 -0
- package/dist/features/draft/index.d.ts +1 -0
- package/{src/features/email/email.provider.ts → dist/features/email/email.provider.d.ts} +19 -24
- package/dist/features/email/email.service.d.ts +13 -0
- package/dist/features/email/email.types.d.ts +94 -0
- package/{src/features/email/index.ts → dist/features/email/index.d.ts} +3 -16
- package/dist/features/email/providers/resend.d.ts +36 -0
- package/dist/features/email/providers/smtp.d.ts +11 -0
- package/dist/features/email/templates/automation-mail.d.ts +8 -0
- package/dist/features/email/templates/password-changed.d.ts +15 -0
- package/dist/features/email/templates/password-reset.d.ts +18 -0
- package/dist/features/email/templates/shell.d.ts +58 -0
- package/dist/features/notifications/index.d.ts +1 -0
- package/dist/features/notifications/notifications.handler.d.ts +15 -0
- package/dist/features/password-reset/index.d.ts +6 -0
- package/dist/features/password-reset/request.d.ts +10 -0
- package/dist/features/password-reset/reset.d.ts +9 -0
- package/dist/features/rotate-field/index.d.ts +1 -0
- package/dist/features/rotate-field/rotate-field.handler.d.ts +7 -0
- package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
- package/dist/features/schema/schema.handler.d.ts +7 -0
- package/dist/features/seeds/index.d.ts +1 -0
- package/dist/features/seeds/seeds.handler.d.ts +6 -0
- package/dist/features/settings/settings.handler.d.ts +7 -0
- package/dist/features/setup/index.d.ts +7 -0
- package/dist/features/stats/index.d.ts +1 -0
- package/dist/features/stats/stats.handler.d.ts +7 -0
- package/dist/features/webhooks/index.d.ts +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9732 -0
- package/dist/media-utils.d.ts +25 -0
- package/dist/middleware/auth-providers.middleware.d.ts +8 -0
- package/dist/middleware/observability.middleware.d.ts +20 -0
- package/dist/middleware/queue.middleware.d.ts +11 -0
- package/dist/middleware/rate-limit.middleware.d.ts +9 -0
- package/dist/middleware/repository.middleware.d.ts +34 -0
- package/dist/middleware/seed-registry.middleware.d.ts +10 -0
- package/dist/middleware/storage.middleware.d.ts +7 -0
- package/dist/middleware.d.ts +11 -0
- package/dist/public/access-policy.d.ts +12 -0
- package/dist/public/api-key-middleware.d.ts +6 -0
- package/dist/public/cache-utils.d.ts +10 -0
- package/dist/public/entry-projection.d.ts +2 -0
- package/dist/public/idempotency.d.ts +9 -0
- package/dist/public/index.d.ts +11 -0
- package/dist/public/problem-details.d.ts +31 -0
- package/dist/public/public-add.d.ts +3 -0
- package/dist/public/public-edit.d.ts +5 -0
- package/dist/public/public-errors.d.ts +13 -0
- package/dist/public/public-read.d.ts +3 -0
- package/dist/public/public-routes.d.ts +3 -0
- package/dist/public/query-builder.d.ts +28 -0
- package/dist/public/rate-limit-middleware.d.ts +3 -0
- package/dist/public/read-list.d.ts +26 -0
- package/dist/public/read-single.d.ts +22 -0
- package/dist/public/response-builder.d.ts +22 -0
- package/dist/public/sanitize.d.ts +23 -0
- package/{src/public/slug-utils.ts → dist/public/slug-utils.d.ts} +3 -10
- package/dist/rate-limit/cloudflare-rate-limiter.d.ts +6 -0
- package/dist/rate-limit/in-memory-rate-limiter.d.ts +7 -0
- package/dist/rate-limit/no-op-rate-limiter.d.ts +4 -0
- package/dist/search-utils.d.ts +54 -0
- package/dist/search.d.ts +6 -0
- package/dist/shared/apply-policies.d.ts +10 -0
- package/dist/shared/automations.repository.d1.d.ts +12 -0
- package/dist/shared/background-notification-service.d.ts +18 -0
- package/dist/shared/base.repository.d1.d.ts +18 -0
- package/dist/shared/cloudflare-queue-service.d.ts +11 -0
- package/dist/shared/content-utils.d.ts +21 -0
- package/dist/shared/content.repository.d1.d.ts +68 -0
- package/dist/shared/d1-activity-log.repository.d.ts +13 -0
- package/dist/shared/d1-activity-logger.d.ts +20 -0
- package/dist/shared/d1-analytics.repository.d.ts +20 -0
- package/dist/shared/d1-content-scan.repository.d.ts +7 -0
- package/dist/shared/d1-notification.repository.d.ts +17 -0
- package/dist/shared/d1-password-reset-token.repository.d.ts +10 -0
- package/dist/shared/d1-search.repository.d.ts +14 -0
- package/dist/shared/d1-session.repository.d.ts +12 -0
- package/dist/shared/d1-setup-checklist.repository.d.ts +12 -0
- package/dist/shared/d1-user.repository.d.ts +18 -0
- package/dist/shared/d1-widget.repository.d.ts +31 -0
- package/dist/shared/dashboard-layout.repository.d1.d.ts +9 -0
- package/dist/shared/demo-data-sql.d.ts +1 -0
- package/dist/shared/demo-data.repository.d1.d.ts +6 -0
- package/dist/shared/execution-context-scheduler.d.ts +6 -0
- package/dist/shared/fixed-clock.d.ts +13 -0
- package/dist/shared/fts-sync.d.ts +1 -0
- package/dist/shared/idempotency.repository.d1.d.ts +16 -0
- package/dist/shared/in-memory-activity-logger.d.ts +11 -0
- package/dist/shared/in-memory-notification-service.d.ts +11 -0
- package/dist/shared/in-memory-queue-service.d.ts +16 -0
- package/dist/shared/in-memory-seed.repository.d.ts +18 -0
- package/dist/shared/media.repository.d1.d.ts +35 -0
- package/dist/shared/qstash-notification-service.d.ts +18 -0
- package/dist/shared/query-utils.d.ts +36 -0
- package/dist/shared/queue-consumer.d.ts +9 -0
- package/dist/shared/request-utils.d.ts +13 -0
- package/dist/shared/schema-mutator.d1.d.ts +12 -0
- package/dist/shared/seed-layout.repository.d1.d.ts +9 -0
- package/dist/shared/seed-registry-cache.d.ts +12 -0
- package/dist/shared/seed.repository.d1.d.ts +13 -0
- package/dist/shared/sequential-id-generator.d.ts +13 -0
- package/dist/shared/site-settings.repository.d1.d.ts +7 -0
- package/dist/shared/storage/factory.d.ts +8 -0
- package/dist/shared/storage/s3-bucket.d.ts +43 -0
- package/dist/shared/storage-utils.d.ts +7 -0
- package/dist/shared/system-stats.repository.d1.d.ts +25 -0
- package/dist/types.d.ts +87 -0
- package/dist/upload.d.ts +16 -0
- package/dist/widget.d.ts +7 -0
- 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 +28 -18
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +0 -39
- package/assets/dashboard/assets/index-CIaoMxuB.css +0 -1
- package/assets/dashboard/assets/index-wTKgygsg.js +0 -636
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +0 -1
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +0 -1
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +0 -17
- package/src/auth/__fixtures__/static-token-service.ts +0 -22
- package/src/auth/bcrypt-hash-provider.ts +0 -24
- package/src/auth/constants.ts +0 -14
- package/src/auth/generate-refresh-token.test.ts +0 -23
- package/src/auth/hash-provider.test.ts +0 -55
- package/src/auth/jose-token-service.ts +0 -62
- package/src/auth/jwt-claims-passthrough.test.ts +0 -87
- package/src/auth/login.test.ts +0 -100
- package/src/auth/login.ts +0 -78
- package/src/auth/refresh.ts +0 -11
- package/src/auth/token-service.test.ts +0 -90
- package/src/factory.ts +0 -370
- package/src/features/automations/__tests__/action-executors.test.ts +0 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +0 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +0 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +0 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +0 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +0 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +0 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +0 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +0 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +0 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +0 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +0 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +0 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +0 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +0 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +0 -26
- package/src/features/automations/action-executors/index.ts +0 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +0 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +0 -159
- package/src/features/automations/action-executors/webhook.executor.ts +0 -67
- package/src/features/automations/automation-runner.ts +0 -85
- package/src/features/automations/automation-runner.utils.ts +0 -47
- package/src/features/automations/automations.handler.ts +0 -197
- package/src/features/automations/automations.schema.ts +0 -177
- package/src/features/automations/context-resolver.ts +0 -152
- package/src/features/automations/cron-runner.ts +0 -140
- package/src/features/automations/cron-runner.utils.ts +0 -44
- package/src/features/automations/filter-translation.ts +0 -46
- package/src/features/automations/index.ts +0 -16
- package/src/features/automations/template-grammar.ts +0 -245
- package/src/features/automations/var-access-resolver.ts +0 -140
- package/src/features/automations/when-evaluator.ts +0 -87
- package/src/features/automations/when-pushdown.ts +0 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +0 -359
- package/src/features/backrefs/backrefs.handler.ts +0 -168
- package/src/features/backrefs/d1-backref.repository.ts +0 -84
- package/src/features/backrefs/index.ts +0 -5
- package/src/features/content/constants.ts +0 -16
- package/src/features/content/handlers/bulk.handler.ts +0 -185
- package/src/features/content/handlers/create.ts +0 -129
- package/src/features/content/handlers/delete.ts +0 -62
- package/src/features/content/handlers/facets.ts +0 -49
- package/src/features/content/handlers/get.ts +0 -120
- package/src/features/content/handlers/helpers.test.ts +0 -180
- package/src/features/content/handlers/helpers.ts +0 -93
- package/src/features/content/handlers/list.ts +0 -174
- package/src/features/content/handlers/update.ts +0 -174
- package/src/features/content/index.ts +0 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +0 -388
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +0 -206
- package/src/features/dashboard-layout/index.ts +0 -5
- package/src/features/draft/draft.handler.ts +0 -195
- package/src/features/draft/draft.middleware.ts +0 -66
- package/src/features/draft/index.ts +0 -5
- package/src/features/email/email.service.ts +0 -98
- package/src/features/email/email.types.ts +0 -119
- package/src/features/email/providers/resend.ts +0 -67
- package/src/features/email/providers/smtp.ts +0 -55
- package/src/features/email/templates/automation-mail.ts +0 -19
- package/src/features/email/templates/password-changed.ts +0 -63
- package/src/features/email/templates/password-reset.ts +0 -68
- package/src/features/email/templates/shell.ts +0 -96
- package/src/features/notifications/index.ts +0 -5
- package/src/features/notifications/notifications.handler.ts +0 -105
- package/src/features/password-reset/index.ts +0 -19
- package/src/features/password-reset/request.ts +0 -92
- package/src/features/password-reset/reset.ts +0 -102
- package/src/features/rotate-field/index.ts +0 -5
- package/src/features/rotate-field/rotate-field.handler.ts +0 -132
- package/src/features/rotate-field/rotate-field.schema.ts +0 -17
- package/src/features/schema/schema.handler.ts +0 -131
- package/src/features/seeds/index.ts +0 -5
- package/src/features/seeds/seeds.handler.test.ts +0 -1199
- package/src/features/seeds/seeds.handler.ts +0 -558
- package/src/features/settings/__tests__/settings.handler.test.ts +0 -555
- package/src/features/settings/settings.handler.ts +0 -392
- package/src/features/setup/index.ts +0 -288
- package/src/features/stats/index.ts +0 -5
- package/src/features/stats/stats.handler.ts +0 -420
- package/src/features/webhooks/index.ts +0 -63
- package/src/index.ts +0 -65
- package/src/media-utils.ts +0 -82
- package/src/middleware/auth-providers.middleware.test.ts +0 -85
- package/src/middleware/auth-providers.middleware.ts +0 -45
- package/src/middleware/observability.middleware.test.ts +0 -93
- package/src/middleware/observability.middleware.ts +0 -70
- package/src/middleware/rate-limit.middleware.ts +0 -45
- package/src/middleware/repository.middleware.ts +0 -117
- package/src/middleware/seed-registry.middleware.test.ts +0 -97
- package/src/middleware/seed-registry.middleware.ts +0 -21
- package/src/middleware/storage.middleware.ts +0 -25
- package/src/middleware.ts +0 -45
- package/src/public/access-policy.ts +0 -27
- package/src/public/api-key-middleware.ts +0 -57
- package/src/public/cache-utils.ts +0 -38
- package/src/public/entry-projection.ts +0 -46
- package/src/public/idempotency.ts +0 -23
- package/src/public/index.ts +0 -16
- package/src/public/problem-details.ts +0 -124
- package/src/public/public-add.ts +0 -114
- package/src/public/public-edit.ts +0 -163
- package/src/public/public-errors.ts +0 -19
- package/src/public/public-read.ts +0 -63
- package/src/public/public-routes.ts +0 -88
- package/src/public/query-builder.test.ts +0 -224
- package/src/public/query-builder.ts +0 -156
- package/src/public/rate-limit-middleware.ts +0 -34
- package/src/public/read-list.ts +0 -54
- package/src/public/read-single.ts +0 -48
- package/src/public/response-builder.ts +0 -30
- package/src/public/sanitize.ts +0 -69
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +0 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +0 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +0 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +0 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +0 -22
- package/src/rate-limit/no-op-rate-limiter.ts +0 -11
- package/src/search-utils.test.ts +0 -211
- package/src/search-utils.ts +0 -213
- package/src/search.ts +0 -65
- package/src/shared/apply-policies.test.ts +0 -81
- package/src/shared/apply-policies.ts +0 -67
- package/src/shared/automations.repository.d1.ts +0 -150
- package/src/shared/background-notification-service.test.ts +0 -62
- package/src/shared/background-notification-service.ts +0 -52
- package/src/shared/base.repository.d1.ts +0 -32
- package/src/shared/content-utils.test.ts +0 -165
- package/src/shared/content-utils.ts +0 -86
- package/src/shared/content.repository.d1.test.ts +0 -507
- package/src/shared/content.repository.d1.ts +0 -851
- package/src/shared/d1-activity-log.repository.test.ts +0 -140
- package/src/shared/d1-activity-log.repository.ts +0 -105
- package/src/shared/d1-activity-logger.test.ts +0 -86
- package/src/shared/d1-activity-logger.ts +0 -67
- package/src/shared/d1-analytics.repository.test.ts +0 -78
- package/src/shared/d1-analytics.repository.ts +0 -85
- package/src/shared/d1-content-scan.repository.test.ts +0 -80
- package/src/shared/d1-content-scan.repository.ts +0 -37
- package/src/shared/d1-notification.repository.test.ts +0 -128
- package/src/shared/d1-notification.repository.ts +0 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +0 -81
- package/src/shared/d1-password-reset-token.repository.ts +0 -56
- package/src/shared/d1-search.repository.test.ts +0 -87
- package/src/shared/d1-search.repository.ts +0 -88
- package/src/shared/d1-session.repository.test.ts +0 -125
- package/src/shared/d1-session.repository.ts +0 -102
- package/src/shared/d1-setup-checklist.repository.ts +0 -36
- package/src/shared/d1-user.repository.test.ts +0 -151
- package/src/shared/d1-user.repository.ts +0 -119
- package/src/shared/d1-widget.repository.test.ts +0 -267
- package/src/shared/d1-widget.repository.ts +0 -385
- package/src/shared/dashboard-layout.repository.d1.test.ts +0 -135
- package/src/shared/dashboard-layout.repository.d1.ts +0 -58
- package/src/shared/demo-data-sql.ts +0 -216
- package/src/shared/demo-data.repository.d1.ts +0 -20
- package/src/shared/execution-context-scheduler.ts +0 -13
- package/src/shared/fixed-clock.ts +0 -25
- package/src/shared/fts-sync.ts +0 -8
- package/src/shared/idempotency.repository.d1.test.ts +0 -83
- package/src/shared/idempotency.repository.d1.ts +0 -60
- package/src/shared/in-memory-activity-logger.ts +0 -19
- package/src/shared/in-memory-notification-service.ts +0 -19
- package/src/shared/in-memory-seed.repository.ts +0 -58
- package/src/shared/media.repository.d1.test.ts +0 -107
- package/src/shared/media.repository.d1.ts +0 -68
- package/src/shared/qstash-notification-service.test.ts +0 -67
- package/src/shared/qstash-notification-service.ts +0 -55
- package/src/shared/query-utils.ts +0 -141
- package/src/shared/request-utils.ts +0 -26
- package/src/shared/schema-mutator.d1.test.ts +0 -142
- package/src/shared/schema-mutator.d1.ts +0 -64
- package/src/shared/seed-layout.repository.d1.test.ts +0 -114
- package/src/shared/seed-layout.repository.d1.ts +0 -62
- package/src/shared/seed-registry-cache.test.ts +0 -68
- package/src/shared/seed-registry-cache.ts +0 -49
- package/src/shared/seed.repository.d1.test.ts +0 -143
- package/src/shared/seed.repository.d1.ts +0 -98
- package/src/shared/sequential-id-generator.ts +0 -33
- package/src/shared/site-settings.repository.d1.ts +0 -53
- package/src/shared/storage/factory.ts +0 -41
- package/src/shared/storage/s3-bucket.ts +0 -195
- package/src/shared/storage-utils.ts +0 -40
- package/src/shared/system-stats.repository.d1.test.ts +0 -58
- package/src/shared/system-stats.repository.d1.ts +0 -48
- package/src/types.ts +0 -90
- package/src/upload.ts +0 -198
- package/src/widget.test.ts +0 -85
- 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
|
-
}
|