@beechcms/api 0.5.0 → 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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,93 +1,98 @@
1
- /**
2
- * Email Service orchestrator of the Beech CMS email module.
3
- *
4
- * Sending pipeline:
5
- * caller service function → template builder → provider → Resend (or other)
6
- *
7
- * This is the only file that imports from both templates and the provider.
8
- * No other layer knows the entire pipeline.
9
- *
10
- * ─── CHANGING PROVIDER ───────────────────────────────────────────────────────
11
- * To replace Resend with another service, ONLY modify the
12
- * `createProvider()` function below: change the import and instantiation.
13
- * No other module file — nor anywhere else in the project — needs to be touched.
14
- * ─────────────────────────────────────────────────────────────────────────────
15
- */
16
- import { ResendEmailProvider } from './providers/resend'
17
- import { buildPasswordResetEmail } from './templates/password-reset'
18
- import { buildPasswordChangedEmail } from './templates/password-changed'
19
- import { buildAutomationEmail } from './templates/automation-mail'
20
- import type { EmailProvider } from './email.provider'
21
- import type {
22
- PasswordResetEmailParams,
23
- PasswordChangedEmailParams,
24
- AutomationMailParams,
25
- } from './email.types'
26
-
27
- /** Default sender address (Resend test sender, works without a verified domain). */
28
- const DEFAULT_FROM = 'Beech CMS <onboarding@resend.dev>'
29
-
30
- /**
31
- * Instantiates the active email provider.
32
- *
33
- * This is the single point for changing the provider: replace the
34
- * `new ResendEmailProvider(...)` line with any class that implements `EmailProvider`.
35
- */
36
- function createProvider(apiKey: string, isDev: boolean): EmailProvider {
37
- return new ResendEmailProvider(apiKey, isDev)
38
- }
39
-
40
- /**
41
- * Sends the password reset link email to the specified recipient.
42
- *
43
- * The email body is built from the localized template in
44
- * `templates/password-reset.ts` and composed with the base layout in
45
- * `templates/shell.ts`.
46
- *
47
- * @throws If the provider rejects the request. The caller decides whether to propagate
48
- * the error (request fail) or handle it silently (fire-and-forget).
49
- */
50
- export async function sendPasswordResetEmail(
51
- params: PasswordResetEmailParams,
52
- ): Promise<void> {
53
- const provider = createProvider(params.apiKey, params.isDev ?? false)
54
- const { subject, html } = buildPasswordResetEmail(params.resetUrl, params.locale)
55
- await provider.send({
56
- from: params.from ?? DEFAULT_FROM,
57
- to: [params.to],
58
- subject,
59
- html,
60
- })
61
- }
62
-
63
- /**
64
- * Sends the "password changed" security notification to the account owner.
65
- *
66
- * Called after a successful password reset to notify the user. It does not have a
67
- * CTA button — it is a pure notification, no action required from the user.
68
- *
69
- * @throws If the provider rejects the request.
70
- */
71
- export async function sendPasswordChangedEmail(
72
- params: PasswordChangedEmailParams,
73
- ): Promise<void> {
74
- const provider = createProvider(params.apiKey, params.isDev ?? false)
75
- const { subject, html } = buildPasswordChangedEmail(params.locale)
76
- await provider.send({
77
- from: params.from ?? DEFAULT_FROM,
78
- to: [params.to],
79
- subject,
80
- html,
81
- })
82
- }
83
-
84
- export async function sendAutomationMail(params: AutomationMailParams): Promise<void> {
85
- const provider = createProvider(params.apiKey ?? params.resendApiKey ?? '', false)
86
- const message = buildAutomationEmail(params)
87
- await provider.send({
88
- from: params.from ?? DEFAULT_FROM,
89
- to: [message.to],
90
- subject: message.subject,
91
- html: message.html,
92
- })
93
- }
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 { ResendEmailProvider } from './providers/resend'
6
+ import { SmtpEmailProvider } from './providers/smtp'
7
+ import { buildPasswordResetEmail } from './templates/password-reset'
8
+ import { buildPasswordChangedEmail } from './templates/password-changed'
9
+ import { buildAutomationEmail } from './templates/automation-mail'
10
+ import type { EmailProvider } from './email.provider'
11
+ import type {
12
+ PasswordResetEmailParams,
13
+ PasswordChangedEmailParams,
14
+ AutomationMailParams,
15
+ } from './email.types'
16
+
17
+ const DEFAULT_FROM = 'Beech CMS <onboarding@resend.dev>'
18
+
19
+ export interface EmailProviderEnv {
20
+ /** "smtp" | "resend"; default "resend" if absent */
21
+ provider?: string
22
+ /** Resend API key, used when provider is "resend" */
23
+ apiKey?: string
24
+ /** Mailpit base URL (e.g. http://localhost:8025), used when provider is "smtp" */
25
+ smtpBaseUrl?: string
26
+ isDev?: boolean
27
+ }
28
+
29
+ // TODO: il sistema di selezione provider è attualmente hardcoded su "smtp" | "resend".
30
+ // Sarebbe meglio aprirlo a provider di terze parti (Postmark, SendGrid, Brevo, SES, ...)
31
+ // senza che l'utente debba toccare il codice. Opzioni da valutare:
32
+ // 1. Strategia plugin: EMAIL_PROVIDER accetta un path a modulo ES (`./my-provider.ts`)
33
+ // che esporta default class implementing EmailProvider zero lock-in.
34
+ // 2. Provider SMTP generico: rimuovere il coupling con Mailpit dalla denominazione
35
+ // e accettare qualsiasi server SMTP via SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS
36
+ // (il SmtpEmailProvider attuale usa già l'HTTP API di Mailpit, non SMTP raw
37
+ // andrebbe riscritto con nodemailer o un client SMTP standard per coprire qualsiasi server).
38
+ // 3. Webhook email: EMAIL_PROVIDER=webhook + EMAIL_WEBHOOK_URL per integrazioni custom.
39
+ // Riferimento: email.provider.ts definisce l'interfaccia — è già sufficientemente astratta.
40
+ function createProvider(env: EmailProviderEnv): EmailProvider {
41
+ if (env.provider === 'smtp') {
42
+ if (!env.smtpBaseUrl) throw new Error('SMTP provider selected but SMTP_HOST is missing')
43
+ return new SmtpEmailProvider({ baseUrl: env.smtpBaseUrl })
44
+ }
45
+ return new ResendEmailProvider(env.apiKey ?? '', env.isDev ?? false)
46
+ }
47
+
48
+ export async function sendPasswordResetEmail(
49
+ params: PasswordResetEmailParams,
50
+ ): Promise<void> {
51
+ const provider = createProvider({
52
+ provider: params.provider,
53
+ apiKey: params.apiKey,
54
+ smtpBaseUrl: params.smtpBaseUrl,
55
+ isDev: params.isDev ?? false,
56
+ })
57
+ const { subject, html } = buildPasswordResetEmail(params.resetUrl, params.locale)
58
+ await provider.send({
59
+ from: params.from ?? DEFAULT_FROM,
60
+ to: [params.to],
61
+ subject,
62
+ html,
63
+ })
64
+ }
65
+
66
+ export async function sendPasswordChangedEmail(
67
+ params: PasswordChangedEmailParams,
68
+ ): Promise<void> {
69
+ const provider = createProvider({
70
+ provider: params.provider,
71
+ apiKey: params.apiKey,
72
+ smtpBaseUrl: params.smtpBaseUrl,
73
+ isDev: params.isDev ?? false,
74
+ })
75
+ const { subject, html } = buildPasswordChangedEmail(params.locale)
76
+ await provider.send({
77
+ from: params.from ?? DEFAULT_FROM,
78
+ to: [params.to],
79
+ subject,
80
+ html,
81
+ })
82
+ }
83
+
84
+ export async function sendAutomationMail(params: AutomationMailParams): Promise<void> {
85
+ const provider = createProvider({
86
+ provider: params.provider,
87
+ apiKey: params.apiKey ?? params.resendApiKey,
88
+ smtpBaseUrl: params.smtpBaseUrl,
89
+ isDev: false,
90
+ })
91
+ const message = buildAutomationEmail(params)
92
+ await provider.send({
93
+ from: params.from ?? DEFAULT_FROM,
94
+ to: [message.to],
95
+ subject: message.subject,
96
+ html: message.html,
97
+ })
98
+ }
@@ -1,108 +1,119 @@
1
- /**
2
- * Shared types for the Beech CMS email module.
3
- *
4
- * All types used across provider, service, and templates are defined here
5
- * so that each layer remains decoupled from the others.
6
- */
7
-
8
- // ── Locale ────────────────────────────────────────────────────────────────────
9
-
10
- /**
11
- * Supported languages for the email template system.
12
- *
13
- * To add a new language:
14
- * 1. Add the ISO code here (e.g., `'fr'`).
15
- * 2. Add the corresponding translation in the `COPY` object of every
16
- * file in `templates/`. TypeScript will flag missing keys.
17
- */
18
- export const SUPPORTED_EMAIL_LOCALES = ['en', 'it'] as const
19
- export type EmailLocale = (typeof SUPPORTED_EMAIL_LOCALES)[number]
20
-
21
- /**
22
- * Resolves an unverified locale string (e.g., from a request body)
23
- * to a supported `EmailLocale` value. Any unknown value
24
- * safely falls back to `'en'`.
25
- *
26
- * @param raw - Raw value from the client (can be anything).
27
- * @returns A valid `EmailLocale`, always.
28
- */
29
- export function resolveEmailLocale(raw: unknown): EmailLocale {
30
- if (
31
- typeof raw === 'string' &&
32
- (SUPPORTED_EMAIL_LOCALES as readonly string[]).includes(raw)
33
- ) {
34
- return raw as EmailLocale
35
- }
36
- return 'en'
37
- }
38
-
39
- // ── Outbound message ──────────────────────────────────────────────────────────
40
-
41
- /**
42
- * The resolved email message that the provider receives and sends.
43
- * It is constructed by the service by combining the call parameters
44
- * with the template builder output.
45
- */
46
- export interface OutboundEmail {
47
- /** Sender address in RFC 5321 format (e.g., "Beech CMS <noreply@beechcms.dev>"). */
48
- from: string
49
- /** List of recipient addresses. Must contain at least one element. */
50
- to: string[]
51
- subject: string
52
- /** Complete HTML body. Must be a valid HTML document (see `templates/shell.ts`). */
53
- html: string
54
- }
55
-
56
- // ── Service function parameters ───────────────────────────────────────────────
57
-
58
- /**
59
- * Shared parameters for every email sending function in `email.service.ts`.
60
- * Specific functions extend this type with the additional fields
61
- * required for their respective templates.
62
- */
63
- export interface BaseEmailParams {
64
- /** Main recipient address. */
65
- to: string
66
- /** Email body language. Use `resolveEmailLocale()` before passing it here. */
67
- locale: EmailLocale
68
- /**
69
- * Resend API key (or the active provider's key). Must be non-empty —
70
- * the caller is responsible for validating it before invoking the service.
71
- */
72
- apiKey: string
73
- /**
74
- * Sender address in RFC 5321 format.
75
- * Default: "Beech CMS <onboarding@resend.dev>" (Resend test sender).
76
- * In production, set a verified address via the
77
- * `EMAIL_FROM` environment variable.
78
- */
79
- from?: string
80
- /**
81
- * When `true`, provider errors are logged to the console.
82
- * Set to `false` in production to avoid exposing internal details.
83
- */
84
- isDev?: boolean
85
- }
86
-
87
- /** Parameters for the password reset email — adds the reset URL. */
88
- export interface PasswordResetEmailParams extends BaseEmailParams {
89
- /**
90
- * Complete URL that the user clicks to set the new password.
91
- * Contains the token in plain text as a query param `?token=<uuid>`.
92
- * Constructed by the caller as `${APP_URL}/reset-password?token=${token}`.
93
- */
94
- resetUrl: string
95
- }
96
-
97
- /** Parameters for the "password changed" notification. No additional fields. */
98
- export type PasswordChangedEmailParams = BaseEmailParams
99
-
100
- export interface AutomationMailParams {
101
- to: string
102
- subject: string
103
- /** Plain text or HTML passed verbatim to provider. */
104
- body: string
105
- apiKey?: string
106
- resendApiKey?: string
107
- from?: string
108
- }
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
+ * Shared types for the Beech CMS email module.
7
+ *
8
+ * All types used across provider, service, and templates are defined here
9
+ * so that each layer remains decoupled from the others.
10
+ */
11
+
12
+ // ── Locale ────────────────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Supported languages for the email template system.
16
+ *
17
+ * To add a new language:
18
+ * 1. Add the ISO code here (e.g., `'fr'`).
19
+ * 2. Add the corresponding translation in the `COPY` object of every
20
+ * file in `templates/`. TypeScript will flag missing keys.
21
+ */
22
+ export const SUPPORTED_EMAIL_LOCALES = ['en', 'it'] as const
23
+ export type EmailLocale = (typeof SUPPORTED_EMAIL_LOCALES)[number]
24
+
25
+ /**
26
+ * Resolves an unverified locale string (e.g., from a request body)
27
+ * to a supported `EmailLocale` value. Any unknown value
28
+ * safely falls back to `'en'`.
29
+ *
30
+ * @param raw - Raw value from the client (can be anything).
31
+ * @returns A valid `EmailLocale`, always.
32
+ */
33
+ export function resolveEmailLocale(raw: unknown): EmailLocale {
34
+ if (
35
+ typeof raw === 'string' &&
36
+ (SUPPORTED_EMAIL_LOCALES as readonly string[]).includes(raw)
37
+ ) {
38
+ return raw as EmailLocale
39
+ }
40
+ return 'en'
41
+ }
42
+
43
+ // ── Outbound message ──────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * The resolved email message that the provider receives and sends.
47
+ * It is constructed by the service by combining the call parameters
48
+ * with the template builder output.
49
+ */
50
+ export interface OutboundEmail {
51
+ /** Sender address in RFC 5321 format (e.g., "Beech CMS <noreply@beechcms.dev>"). */
52
+ from: string
53
+ /** List of recipient addresses. Must contain at least one element. */
54
+ to: string[]
55
+ subject: string
56
+ /** Complete HTML body. Must be a valid HTML document (see `templates/shell.ts`). */
57
+ html: string
58
+ }
59
+
60
+ // ── Service function parameters ───────────────────────────────────────────────
61
+
62
+ /**
63
+ * Shared parameters for every email sending function in `email.service.ts`.
64
+ * Specific functions extend this type with the additional fields
65
+ * required for their respective templates.
66
+ */
67
+ export interface BaseEmailParams {
68
+ /** Main recipient address. */
69
+ to: string
70
+ /** Email body language. Use `resolveEmailLocale()` before passing it here. */
71
+ locale: EmailLocale
72
+ /**
73
+ * Resend API key (or the active provider's key). Must be non-empty —
74
+ * the caller is responsible for validating it before invoking the service.
75
+ */
76
+ apiKey: string
77
+ /**
78
+ * Sender address in RFC 5321 format.
79
+ * Default: "Beech CMS <onboarding@resend.dev>" (Resend test sender).
80
+ * In production, set a verified address via the
81
+ * `EMAIL_FROM` environment variable.
82
+ */
83
+ from?: string
84
+ /**
85
+ * When `true`, provider errors are logged to the console.
86
+ * Set to `false` in production to avoid exposing internal details.
87
+ */
88
+ isDev?: boolean
89
+ }
90
+
91
+ /** Parameters for the password reset email adds the reset URL. */
92
+ export interface PasswordResetEmailParams extends BaseEmailParams {
93
+ /**
94
+ * Complete URL that the user clicks to set the new password.
95
+ * Contains the token in plain text as a query param `?token=<uuid>`.
96
+ * Constructed by the caller as `${APP_URL}/reset-password?token=${token}`.
97
+ */
98
+ resetUrl: string
99
+ provider?: 'smtp' | 'resend'
100
+ smtpBaseUrl?: string
101
+ }
102
+
103
+ /** Parameters for the "password changed" notification. No additional fields. */
104
+ export interface PasswordChangedEmailParams extends BaseEmailParams {
105
+ provider?: 'smtp' | 'resend'
106
+ smtpBaseUrl?: string
107
+ }
108
+
109
+ export interface AutomationMailParams {
110
+ to: string
111
+ subject: string
112
+ /** Plain text or HTML — passed verbatim to provider. */
113
+ body: string
114
+ apiKey?: string
115
+ resendApiKey?: string
116
+ from?: string
117
+ provider?: 'smtp' | 'resend'
118
+ smtpBaseUrl?: string
119
+ }
@@ -1,29 +1,33 @@
1
- /**
2
- * Public API modulo email di Beech CMS
3
- *
4
- * Questo è l'UNICO file da importare da codice esterno a questa feature.
5
- * I dettagli implementativi interni (provider, template, shell) sono privati
6
- * alla slice e non devono mai essere importati direttamente dall'esterno.
7
- *
8
- * ─── FUNZIONI ESPORTATE ───────────────────────────────────────────────────────
9
- * sendPasswordResetEmail — invia l'email con il link di reset
10
- * sendPasswordChangedEmail — invia la notifica "password modificata"
11
- *
12
- * ─── TIPI E UTILITY ESPORTATI ────────────────────────────────────────────────
13
- * EmailLocale — 'en' | 'it' (aggiungere lingue in email.types.ts)
14
- * resolveEmailLocale resolver sicuro per locale da input non verificato
15
- * PasswordResetEmailParams — shape dei parametri per sendPasswordResetEmail
16
- * PasswordChangedEmailParams shape dei parametri per sendPasswordChangedEmail
17
- */
18
-
19
- export { sendPasswordResetEmail, sendPasswordChangedEmail, sendAutomationMail } from './email.service'
20
- export {
21
- resolveEmailLocale,
22
- SUPPORTED_EMAIL_LOCALES,
23
- } from './email.types'
24
- export type {
25
- EmailLocale,
26
- PasswordResetEmailParams,
27
- PasswordChangedEmailParams,
28
- AutomationMailParams,
29
- } from './email.types'
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
+ * Public API modulo email di Beech CMS
7
+ *
8
+ * Questo è l'UNICO file da importare da codice esterno a questa feature.
9
+ * I dettagli implementativi interni (provider, template, shell) sono privati
10
+ * alla slice e non devono mai essere importati direttamente dall'esterno.
11
+ *
12
+ * ─── FUNZIONI ESPORTATE ───────────────────────────────────────────────────────
13
+ * sendPasswordResetEmail invia l'email con il link di reset
14
+ * sendPasswordChangedEmail invia la notifica "password modificata"
15
+ *
16
+ * ─── TIPI E UTILITY ESPORTATI ────────────────────────────────────────────────
17
+ * EmailLocale — 'en' | 'it' (aggiungere lingue in email.types.ts)
18
+ * resolveEmailLocale — resolver sicuro per locale da input non verificato
19
+ * PasswordResetEmailParams — shape dei parametri per sendPasswordResetEmail
20
+ * PasswordChangedEmailParams — shape dei parametri per sendPasswordChangedEmail
21
+ */
22
+
23
+ export { sendPasswordResetEmail, sendPasswordChangedEmail, sendAutomationMail } from './email.service'
24
+ export {
25
+ resolveEmailLocale,
26
+ SUPPORTED_EMAIL_LOCALES,
27
+ } from './email.types'
28
+ export type {
29
+ EmailLocale,
30
+ PasswordResetEmailParams,
31
+ PasswordChangedEmailParams,
32
+ AutomationMailParams,
33
+ } from './email.types'
@@ -1,63 +1,67 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import type { EmailProvider } from '../email.provider'
3
- import type { OutboundEmail } from '../email.types'
4
-
5
- /** Resend REST endpoint for sending emails. */
6
- const RESEND_API_URL = 'https://api.resend.com/emails'
7
-
8
- /**
9
- * Resend implementation of EmailProvider.
10
- *
11
- * This is the ONLY file in the email module that knows about Resend.
12
- * Every other file is completely unaware of which provider is active.
13
- *
14
- * ─── HOW TO REPLACE THIS PROVIDER ──────────────────────────────────────────
15
- * 1. Create a new file in `providers/` (e.g., `providers/sendgrid.ts`).
16
- * 2. Export a class that implements `EmailProvider` (a single method: `send`).
17
- * 3. In `email.service.ts`, replace `new ResendEmailProvider(...)` with
18
- * your new class in the `createProvider()` function.
19
- * 4. Update the environment variables in `types.ts` and `wrangler.jsonc`.
20
- * 5. No other file in the project needs to be modified.
21
- *
22
- * Resend API Documentation: https://resend.com/docs/api-reference/emails/send-email
23
- * ─────────────────────────────────────────────────────────────────────────────
24
- */
25
- export class ResendEmailProvider implements EmailProvider {
26
- private readonly apiKey: string
27
-
28
- /** When `true`, errors are logged to the console (development only). */
29
- private readonly isDev: boolean
30
-
31
- constructor(apiKey: string, isDev = false) {
32
- this.apiKey = apiKey
33
- this.isDev = isDev
34
- }
35
-
36
- /**
37
- * Sends the email via the Resend REST API (`POST /emails`).
38
- *
39
- * Throws an exception if Resend responds with a non-2xx status, so that
40
- * the caller (`email.service.ts`) can decide whether to propagate the error
41
- * or handle it silently (fire-and-forget).
42
- *
43
- * The response body is read for logging only in the development environment,
44
- * to avoid unnecessarily consuming the body stream in production.
45
- */
46
- async send(email: OutboundEmail): Promise<void> {
47
- const response = await fetch(RESEND_API_URL, {
48
- method: 'POST',
49
- headers: {
50
- Authorization: `Bearer ${this.apiKey}`,
51
- 'Content-Type': 'application/json',
52
- },
53
- body: JSON.stringify(email),
54
- })
55
-
56
- if (!response.ok) {
57
- const detail = this.isDev
58
- ? await response.text()
59
- : `HTTP ${response.status}`
60
- throw new Error(`[ResendEmailProvider] send failed — ${detail}`)
61
- }
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
+ /// <reference types="@cloudflare/workers-types" />
6
+ import type { EmailProvider } from '../email.provider'
7
+ import type { OutboundEmail } from '../email.types'
8
+
9
+ /** Resend REST endpoint for sending emails. */
10
+ const RESEND_API_URL = 'https://api.resend.com/emails'
11
+
12
+ /**
13
+ * Resend implementation of EmailProvider.
14
+ *
15
+ * This is the ONLY file in the email module that knows about Resend.
16
+ * Every other file is completely unaware of which provider is active.
17
+ *
18
+ * ─── HOW TO REPLACE THIS PROVIDER ──────────────────────────────────────────
19
+ * 1. Create a new file in `providers/` (e.g., `providers/sendgrid.ts`).
20
+ * 2. Export a class that implements `EmailProvider` (a single method: `send`).
21
+ * 3. In `email.service.ts`, replace `new ResendEmailProvider(...)` with
22
+ * your new class in the `createProvider()` function.
23
+ * 4. Update the environment variables in `types.ts` and `wrangler.jsonc`.
24
+ * 5. No other file in the project needs to be modified.
25
+ *
26
+ * Resend API Documentation: https://resend.com/docs/api-reference/emails/send-email
27
+ * ─────────────────────────────────────────────────────────────────────────────
28
+ */
29
+ export class ResendEmailProvider implements EmailProvider {
30
+ private readonly apiKey: string
31
+
32
+ /** When `true`, errors are logged to the console (development only). */
33
+ private readonly isDev: boolean
34
+
35
+ constructor(apiKey: string, isDev = false) {
36
+ this.apiKey = apiKey
37
+ this.isDev = isDev
38
+ }
39
+
40
+ /**
41
+ * Sends the email via the Resend REST API (`POST /emails`).
42
+ *
43
+ * Throws an exception if Resend responds with a non-2xx status, so that
44
+ * the caller (`email.service.ts`) can decide whether to propagate the error
45
+ * or handle it silently (fire-and-forget).
46
+ *
47
+ * The response body is read for logging only in the development environment,
48
+ * to avoid unnecessarily consuming the body stream in production.
49
+ */
50
+ async send(email: OutboundEmail): Promise<void> {
51
+ const response = await fetch(RESEND_API_URL, {
52
+ method: 'POST',
53
+ headers: {
54
+ Authorization: `Bearer ${this.apiKey}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify(email),
58
+ })
59
+
60
+ if (!response.ok) {
61
+ const detail = this.isDev
62
+ ? await response.text()
63
+ : `HTTP ${response.status}`
64
+ throw new Error(`[ResendEmailProvider] send failed — ${detail}`)
65
+ }
66
+ }
67
+ }