@rx-ted/packages-honest-plugins 1.0.1

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 (266) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +45 -0
  3. package/api-doc/.turbo/turbo-build.log +1 -0
  4. package/api-doc/.turbo/turbo-test.log +11 -0
  5. package/api-doc/.turbo/turbo-typecheck.log +1 -0
  6. package/api-doc/CHANGELOG.md +22 -0
  7. package/api-doc/README.md +80 -0
  8. package/api-doc/dist/api-doc.plugin.d.ts +53 -0
  9. package/api-doc/dist/api-doc.plugin.js +452 -0
  10. package/api-doc/dist/index.d.ts +5 -0
  11. package/api-doc/dist/index.js +3 -0
  12. package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
  13. package/api-doc/dist/interfaces/renderer.interface.js +1 -0
  14. package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
  15. package/api-doc/dist/renderers/scalar.renderer.js +76 -0
  16. package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
  17. package/api-doc/dist/renderers/swagger.renderer.js +83 -0
  18. package/api-doc/node_modules/.bin/esbuild +22 -0
  19. package/api-doc/node_modules/.bin/jiti +22 -0
  20. package/api-doc/node_modules/.bin/sass +22 -0
  21. package/api-doc/node_modules/.bin/tsx +22 -0
  22. package/api-doc/node_modules/.bin/vite +22 -0
  23. package/api-doc/node_modules/.bin/vitest +22 -0
  24. package/api-doc/node_modules/.bin/yaml +22 -0
  25. package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  26. package/api-doc/package.json +48 -0
  27. package/api-doc/src/api-doc.plugin.ts +554 -0
  28. package/api-doc/src/index.ts +5 -0
  29. package/api-doc/src/interfaces/renderer.interface.ts +5 -0
  30. package/api-doc/src/plugin.test.ts +740 -0
  31. package/api-doc/src/renderers/scalar.renderer.ts +82 -0
  32. package/api-doc/src/renderers/swagger.renderer.ts +89 -0
  33. package/api-doc/tsconfig.json +21 -0
  34. package/api-doc/vitest.config.ts +13 -0
  35. package/cache/.turbo/turbo-build.log +1 -0
  36. package/cache/.turbo/turbo-test.log +16 -0
  37. package/cache/CHANGELOG.md +23 -0
  38. package/cache/README.md +79 -0
  39. package/cache/dist/cache-service.d.ts +8 -0
  40. package/cache/dist/cache-service.js +21 -0
  41. package/cache/dist/cacheable.d.ts +2 -0
  42. package/cache/dist/cacheable.js +10 -0
  43. package/cache/dist/cloudflare/driver.d.ts +23 -0
  44. package/cache/dist/cloudflare/driver.js +102 -0
  45. package/cache/dist/cloudflare/index.d.ts +3 -0
  46. package/cache/dist/cloudflare/index.js +2 -0
  47. package/cache/dist/cloudflare/plugin.d.ts +19 -0
  48. package/cache/dist/cloudflare/plugin.js +58 -0
  49. package/cache/dist/index.d.ts +11 -0
  50. package/cache/dist/index.js +14 -0
  51. package/cache/dist/local/driver.d.ts +2 -0
  52. package/cache/dist/local/driver.js +98 -0
  53. package/cache/dist/local/index.d.ts +2 -0
  54. package/cache/dist/local/index.js +2 -0
  55. package/cache/dist/local/plugin.d.ts +14 -0
  56. package/cache/dist/local/plugin.js +24 -0
  57. package/cache/dist/redis/driver.d.ts +10 -0
  58. package/cache/dist/redis/driver.js +102 -0
  59. package/cache/dist/redis/index.d.ts +4 -0
  60. package/cache/dist/redis/index.js +2 -0
  61. package/cache/dist/redis/plugin.d.ts +25 -0
  62. package/cache/dist/redis/plugin.js +61 -0
  63. package/cache/dist/resolve.d.ts +24 -0
  64. package/cache/dist/resolve.js +114 -0
  65. package/cache/dist/types.d.ts +18 -0
  66. package/cache/dist/types.js +1 -0
  67. package/cache/node_modules/.bin/esbuild +22 -0
  68. package/cache/node_modules/.bin/jiti +22 -0
  69. package/cache/node_modules/.bin/sass +22 -0
  70. package/cache/node_modules/.bin/tsx +22 -0
  71. package/cache/node_modules/.bin/vite +22 -0
  72. package/cache/node_modules/.bin/vitest +22 -0
  73. package/cache/node_modules/.bin/yaml +22 -0
  74. package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  75. package/cache/package.json +46 -0
  76. package/cache/src/cache-service.test.ts +16 -0
  77. package/cache/src/cache-service.ts +15 -0
  78. package/cache/src/cacheable.test.ts +78 -0
  79. package/cache/src/cacheable.ts +16 -0
  80. package/cache/src/cloudflare/driver.test.ts +188 -0
  81. package/cache/src/cloudflare/driver.ts +118 -0
  82. package/cache/src/cloudflare/index.ts +3 -0
  83. package/cache/src/cloudflare/plugin.ts +71 -0
  84. package/cache/src/index.test.ts +68 -0
  85. package/cache/src/index.ts +20 -0
  86. package/cache/src/local/driver.ts +117 -0
  87. package/cache/src/local/index.ts +2 -0
  88. package/cache/src/local/plugin.ts +34 -0
  89. package/cache/src/redis/driver.test.ts +71 -0
  90. package/cache/src/redis/driver.ts +124 -0
  91. package/cache/src/redis/index.ts +4 -0
  92. package/cache/src/redis/plugin.ts +84 -0
  93. package/cache/src/resolve.test.ts +257 -0
  94. package/cache/src/resolve.ts +140 -0
  95. package/cache/src/types.ts +14 -0
  96. package/cache/tsconfig.json +21 -0
  97. package/cache/vitest.config.ts +13 -0
  98. package/db/.turbo/turbo-build.log +1 -0
  99. package/db/.turbo/turbo-test.log +21 -0
  100. package/db/CHANGELOG.md +26 -0
  101. package/db/README.md +99 -0
  102. package/db/dist/constants.d.ts +1 -0
  103. package/db/dist/constants.js +1 -0
  104. package/db/dist/d1/index.d.ts +3 -0
  105. package/db/dist/d1/index.js +2 -0
  106. package/db/dist/d1/plugin.d.ts +18 -0
  107. package/db/dist/d1/plugin.js +104 -0
  108. package/db/dist/db-service.d.ts +7 -0
  109. package/db/dist/db-service.js +21 -0
  110. package/db/dist/index.d.ts +15 -0
  111. package/db/dist/index.js +20 -0
  112. package/db/dist/mysql/client.d.ts +36 -0
  113. package/db/dist/mysql/client.js +84 -0
  114. package/db/dist/mysql/driver.d.ts +18 -0
  115. package/db/dist/mysql/driver.js +107 -0
  116. package/db/dist/mysql/index.d.ts +11 -0
  117. package/db/dist/mysql/index.js +11 -0
  118. package/db/dist/mysql/plugin.d.ts +51 -0
  119. package/db/dist/mysql/plugin.js +94 -0
  120. package/db/dist/mysql/types.d.ts +38 -0
  121. package/db/dist/mysql/types.js +1 -0
  122. package/db/dist/postgres/index.d.ts +2 -0
  123. package/db/dist/postgres/index.js +3 -0
  124. package/db/dist/repository.d.ts +14 -0
  125. package/db/dist/repository.js +79 -0
  126. package/db/dist/resolve.d.ts +6 -0
  127. package/db/dist/resolve.js +81 -0
  128. package/db/dist/schema-builder/column-builder.d.ts +26 -0
  129. package/db/dist/schema-builder/column-builder.js +53 -0
  130. package/db/dist/schema-builder/compile-d1.d.ts +5 -0
  131. package/db/dist/schema-builder/compile-d1.js +67 -0
  132. package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
  133. package/db/dist/schema-builder/compile-mysql.js +78 -0
  134. package/db/dist/schema-builder/index.d.ts +8 -0
  135. package/db/dist/schema-builder/index.js +22 -0
  136. package/db/dist/schema-builder/table-builder.d.ts +16 -0
  137. package/db/dist/schema-builder/table-builder.js +38 -0
  138. package/db/dist/schema-builder/types.d.ts +26 -0
  139. package/db/dist/schema-builder/types.js +1 -0
  140. package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
  141. package/db/dist/schema-builder/zod-bridge.js +53 -0
  142. package/db/dist/sqlite/index.d.ts +4 -0
  143. package/db/dist/sqlite/index.js +2 -0
  144. package/db/dist/sqlite/plugin.d.ts +17 -0
  145. package/db/dist/sqlite/plugin.js +33 -0
  146. package/db/dist/types.d.ts +42 -0
  147. package/db/dist/types.js +1 -0
  148. package/db/node_modules/.bin/esbuild +22 -0
  149. package/db/node_modules/.bin/jiti +22 -0
  150. package/db/node_modules/.bin/sass +22 -0
  151. package/db/node_modules/.bin/tsx +22 -0
  152. package/db/node_modules/.bin/vite +22 -0
  153. package/db/node_modules/.bin/vitest +22 -0
  154. package/db/node_modules/.bin/yaml +22 -0
  155. package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  156. package/db/package.json +59 -0
  157. package/db/src/constants.test.ts +8 -0
  158. package/db/src/constants.ts +1 -0
  159. package/db/src/d1/index.ts +13 -0
  160. package/db/src/d1/plugin.ts +135 -0
  161. package/db/src/db-service.ts +14 -0
  162. package/db/src/index.test.ts +63 -0
  163. package/db/src/index.ts +57 -0
  164. package/db/src/mysql/client.ts +112 -0
  165. package/db/src/mysql/driver.test.ts +190 -0
  166. package/db/src/mysql/driver.ts +116 -0
  167. package/db/src/mysql/index.ts +51 -0
  168. package/db/src/mysql/plugin.test.ts +228 -0
  169. package/db/src/mysql/plugin.ts +128 -0
  170. package/db/src/mysql/types.ts +32 -0
  171. package/db/src/postgres/index.ts +15 -0
  172. package/db/src/repository.test.ts +195 -0
  173. package/db/src/repository.ts +95 -0
  174. package/db/src/resolve.ts +105 -0
  175. package/db/src/schema-builder/column-builder.test.ts +55 -0
  176. package/db/src/schema-builder/column-builder.ts +87 -0
  177. package/db/src/schema-builder/compile-d1.test.ts +60 -0
  178. package/db/src/schema-builder/compile-d1.ts +70 -0
  179. package/db/src/schema-builder/compile-mysql.test.ts +64 -0
  180. package/db/src/schema-builder/compile-mysql.ts +102 -0
  181. package/db/src/schema-builder/index.test.ts +36 -0
  182. package/db/src/schema-builder/index.ts +41 -0
  183. package/db/src/schema-builder/table-builder.test.ts +55 -0
  184. package/db/src/schema-builder/table-builder.ts +50 -0
  185. package/db/src/schema-builder/types.test.ts +68 -0
  186. package/db/src/schema-builder/types.ts +30 -0
  187. package/db/src/schema-builder/zod-bridge.ts +89 -0
  188. package/db/src/sqlite/index.ts +14 -0
  189. package/db/src/sqlite/plugin.ts +46 -0
  190. package/db/src/types.ts +45 -0
  191. package/db/tsconfig.json +21 -0
  192. package/db/vitest.config.ts +13 -0
  193. package/mail/.turbo/turbo-build.log +1 -0
  194. package/mail/.turbo/turbo-test.log +12 -0
  195. package/mail/.turbo/turbo-typecheck.log +1 -0
  196. package/mail/CHANGELOG.md +25 -0
  197. package/mail/README.md +214 -0
  198. package/mail/dist/index.d.ts +4 -0
  199. package/mail/dist/index.js +2 -0
  200. package/mail/dist/plugin.d.ts +64 -0
  201. package/mail/dist/plugin.js +232 -0
  202. package/mail/dist/providers/brevo.provider.d.ts +15 -0
  203. package/mail/dist/providers/brevo.provider.js +75 -0
  204. package/mail/dist/providers/custom.provider.d.ts +9 -0
  205. package/mail/dist/providers/custom.provider.js +22 -0
  206. package/mail/dist/providers/resend.provider.d.ts +16 -0
  207. package/mail/dist/providers/resend.provider.js +77 -0
  208. package/mail/dist/providers/smtp.provider.d.ts +21 -0
  209. package/mail/dist/providers/smtp.provider.js +86 -0
  210. package/mail/dist/rate-limiter.d.ts +16 -0
  211. package/mail/dist/rate-limiter.js +49 -0
  212. package/mail/dist/types.d.ts +68 -0
  213. package/mail/dist/types.js +6 -0
  214. package/mail/node_modules/.bin/esbuild +22 -0
  215. package/mail/node_modules/.bin/jiti +22 -0
  216. package/mail/node_modules/.bin/sass +22 -0
  217. package/mail/node_modules/.bin/tsx +22 -0
  218. package/mail/node_modules/.bin/vite +22 -0
  219. package/mail/node_modules/.bin/vitest +22 -0
  220. package/mail/node_modules/.bin/yaml +22 -0
  221. package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  222. package/mail/package.json +52 -0
  223. package/mail/src/index.ts +4 -0
  224. package/mail/src/plugin.test.ts +219 -0
  225. package/mail/src/plugin.ts +313 -0
  226. package/mail/src/providers/brevo.provider.ts +78 -0
  227. package/mail/src/providers/custom.provider.ts +29 -0
  228. package/mail/src/providers/resend.provider.ts +74 -0
  229. package/mail/src/providers/smtp.provider.ts +85 -0
  230. package/mail/src/rate-limiter.test.ts +81 -0
  231. package/mail/src/rate-limiter.ts +54 -0
  232. package/mail/src/types.ts +81 -0
  233. package/mail/tsconfig.json +21 -0
  234. package/mail/vitest.config.ts +13 -0
  235. package/package.json +36 -0
  236. package/s3/.turbo/turbo-build.log +1 -0
  237. package/s3/.turbo/turbo-test.log +11 -0
  238. package/s3/CHANGELOG.md +20 -0
  239. package/s3/README.md +90 -0
  240. package/s3/dist/index.d.ts +6 -0
  241. package/s3/dist/index.js +4 -0
  242. package/s3/dist/s3-service.d.ts +7 -0
  243. package/s3/dist/s3-service.js +21 -0
  244. package/s3/dist/s3.driver.d.ts +6 -0
  245. package/s3/dist/s3.driver.js +221 -0
  246. package/s3/dist/s3.plugin.d.ts +24 -0
  247. package/s3/dist/s3.plugin.js +58 -0
  248. package/s3/dist/types.d.ts +30 -0
  249. package/s3/dist/types.js +1 -0
  250. package/s3/node_modules/.bin/esbuild +22 -0
  251. package/s3/node_modules/.bin/jiti +22 -0
  252. package/s3/node_modules/.bin/sass +22 -0
  253. package/s3/node_modules/.bin/tsx +22 -0
  254. package/s3/node_modules/.bin/vite +22 -0
  255. package/s3/node_modules/.bin/vitest +22 -0
  256. package/s3/node_modules/.bin/yaml +22 -0
  257. package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  258. package/s3/package.json +47 -0
  259. package/s3/src/index.test.ts +20 -0
  260. package/s3/src/index.ts +13 -0
  261. package/s3/src/s3-service.ts +14 -0
  262. package/s3/src/s3.driver.ts +259 -0
  263. package/s3/src/s3.plugin.ts +80 -0
  264. package/s3/src/types.ts +37 -0
  265. package/s3/tsconfig.json +21 -0
  266. package/s3/vitest.config.ts +13 -0
@@ -0,0 +1,12 @@
1
+ $ vitest run
2
+
3
+  RUN  v4.1.8 /Users/ben/projects/app/packages/honest-plugins/mail
4
+
5
+ ✓ src/rate-limiter.test.ts (8 tests) 7ms
6
+ ✓ src/plugin.test.ts (16 tests) 5ms
7
+
8
+  Test Files  2 passed (2)
9
+  Tests  24 passed (24)
10
+  Start at  14:17:13
11
+  Duration  137ms (transform 69ms, setup 0ms, import 91ms, tests 12ms, environment 0ms)
12
+
@@ -0,0 +1 @@
1
+ $ tsc -p tsconfig.json --noEmit
@@ -0,0 +1,25 @@
1
+ # @rx-ted/packages-honest-plugins-mail
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 11bbae7: Remove defunct packages (web-admin, auth, http-client, search) and introduce barrel packages for consolidated exports. Migrate all internal imports from scoped plugin names (`@rx-ted/packages-honest-plugins-db`) to path-based barrel imports (`@rx-ted/packages-honest-plugins/db`). Reset all package versions to 1.0.0 with consolidated changelogs. Clean up obsolete documentation across the monorepo.
8
+ - Updated dependencies [11bbae7]
9
+ - @rx-ted/packages-core@1.0.1
10
+ - @rx-ted/packages-honest@1.0.1
11
+
12
+ ## 1.0.0
13
+
14
+ Initial consolidated release.
15
+
16
+ ### Features
17
+
18
+ - Mail plugin for @rx-ted/packages-honest
19
+ - Multiple mail providers (Resend, Brevo, SMTP, Custom)
20
+ - Rate limiting, health check, and quota management
21
+ - `warmUp(provider)` — eagerly establishes SMTP connection before first send
22
+ - `failedProviders` Set — tracks providers that failed warmUp/health check
23
+ - `runHealthChecks()` — periodic background health check for all providers
24
+ - `warmUpTimeout` / `sendTimeout` config options
25
+ - Pull-based configuration from ComponentManager caches
package/mail/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @rx-ted/packages-honest-plugins-mail
2
+
3
+ Mail plugin for [@rx-ted/packages-honest](https://github.com/rx-ted/honest). Supports multiple email providers with automatic failover, per-provider daily rate limiting, and health checks.
4
+
5
+ ## Features
6
+
7
+ - **Multi-provider**: Resend, Brevo, SMTP, or custom — up to 2 providers
8
+ - **Auto-failover**: tries the next provider if the primary fails
9
+ - **Rate limiting**: per-provider daily quota via cache (`incr`/`expire`)
10
+ - **Health checks**: verifies each provider once per day with configurable retries
11
+ - **Runtime-agnostic**: works on Node.js, Bun, Deno, and Cloudflare Workers
12
+ - **Lazy imports**: optional SDKs (`resend`, `@getbrevo/brevo`, `sently`) are loaded only when needed
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @rx-ted/packages-honest-plugins-mail
18
+ ```
19
+
20
+ Peer dependencies: `@rx-ted/packages-honest`, `hono`
21
+
22
+ Optional SDKs (install only the providers you need):
23
+ ```bash
24
+ pnpm add resend # for Resend provider
25
+ pnpm add sently # for SMTP provider
26
+ pnpm add @getbrevo/brevo # for Brevo provider
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Register as a plugin
32
+
33
+ ```ts
34
+ import { MailPlugin } from '@rx-ted/packages-honest-plugins-mail';
35
+ import type { MailPluginOptions } from '@rx-ted/packages-honest-plugins-mail';
36
+
37
+ const plugin = new MailPlugin({
38
+ providers: [
39
+ {
40
+ name: 'primary',
41
+ provider: 'resend',
42
+ fromEmail: 'noreply@example.com',
43
+ resend: { apiKey: 're_xxx' },
44
+ limits: { dailyQuota: 100 },
45
+ },
46
+ {
47
+ name: 'fallback',
48
+ provider: 'smtp',
49
+ fromEmail: 'noreply@example.com',
50
+ smtp: {
51
+ host: 'smtp.example.com',
52
+ port: 465,
53
+ user: 'user',
54
+ pass: 'pass',
55
+ secure: true,
56
+ },
57
+ },
58
+ ],
59
+ healthCheck: {
60
+ intervalMs: 300_000, // check every 5 minutes
61
+ maxRetries: 3,
62
+ retryDelayMs: 1000,
63
+ },
64
+ });
65
+ ```
66
+
67
+ Then pass the plugin to `Application.create`:
68
+
69
+ ```ts
70
+ const { hono } = await Application.create(AppModule, {
71
+ plugins: [plugin],
72
+ // ...
73
+ });
74
+ ```
75
+
76
+ ### Send an email
77
+
78
+ The primary provider is registered under `'mail'` in `ComponentManager`. Individual providers are registered under `'mail:<name>'`.
79
+
80
+ ```ts
81
+ import { ComponentManager } from '@rx-ted/packages-honest';
82
+ import type { MailProvider } from '@rx-ted/packages-honest-plugins-mail';
83
+
84
+ const provider = ComponentManager.getPlugin<MailProvider>('mail');
85
+ const result = await provider.send({
86
+ from: 'noreply@example.com',
87
+ to: 'user@example.com',
88
+ subject: 'Hello',
89
+ html: '<h1>Hello</h1>',
90
+ text: 'Hello',
91
+ });
92
+ // { id: '...', provider: 'primary' }
93
+ ```
94
+
95
+ ### Get a specific provider
96
+
97
+ ```ts
98
+ const provider = mailPlugin.getClient('brevo-provider');
99
+ ```
100
+
101
+ ## Provider Configuration
102
+
103
+ ### Resend
104
+
105
+ ```ts
106
+ {
107
+ name: 'resend',
108
+ provider: 'resend',
109
+ fromEmail: 'noreply@example.com',
110
+ fromName: 'My App', // optional
111
+ resend: { apiKey: 're_xxx' },
112
+ }
113
+ ```
114
+
115
+ ### Brevo
116
+
117
+ ```ts
118
+ {
119
+ name: 'brevo',
120
+ provider: 'brevo',
121
+ fromEmail: 'noreply@example.com',
122
+ brevo: { apiKey: 'xxx' },
123
+ }
124
+ ```
125
+
126
+ ### SMTP
127
+
128
+ ```ts
129
+ {
130
+ name: 'smtp',
131
+ provider: 'smtp',
132
+ fromEmail: 'noreply@example.com',
133
+ smtp: {
134
+ host: 'smtp.example.com',
135
+ port: 465,
136
+ user: 'username',
137
+ pass: 'password',
138
+ secure: true, // false for port 587 (STARTTLS)
139
+ },
140
+ }
141
+ ```
142
+
143
+ > **Note on Cloudflare Workers:** SMTP uses `sently` which relies on `cloudflare:sockets` TCP API. Port 25 is blocked; use 465 or 587.
144
+
145
+ ### Custom
146
+
147
+ ```ts
148
+ import { CustomMailProvider } from '@rx-ted/packages-honest-plugins-mail';
149
+
150
+ {
151
+ name: 'custom',
152
+ provider: 'custom',
153
+ custom: myCustomMailProvider, // any MailProvider instance
154
+ }
155
+ ```
156
+
157
+ ## Rate Limiting
158
+
159
+ When a cache driver is available (e.g., `RedisPlugin`), the `RateLimiter` enforces daily quotas via cache `incr`/`expire`. If no cache is registered, rate limiting is disabled.
160
+
161
+ ```ts
162
+ {
163
+ name: 'resend',
164
+ provider: 'resend',
165
+ fromEmail: '...',
166
+ resend: { apiKey: '...' },
167
+ limits: { dailyQuota: 1000 },
168
+ }
169
+ ```
170
+
171
+ ## Health Checks
172
+
173
+ The plugin runs health checks in `afterModulesRegistered` with configurable retries. Each provider is checked once per day (tracked via cache).
174
+
175
+ ## API
176
+
177
+ ### `MailPlugin`
178
+
179
+ | Method | Description |
180
+ |--------|-------------|
181
+ | `getClient(name?)` | Returns the primary provider or a named provider |
182
+ | `beforeModulesRegistered(app, hono)` | Registers providers in ComponentManager |
183
+ | `afterModulesRegistered(app, hono)` | Runs initial health checks |
184
+ | `close()` | Closes all provider connections |
185
+
186
+ ### Types
187
+
188
+ ```ts
189
+ interface MailProvider {
190
+ readonly name: string;
191
+ send(options: SendMailOptions): Promise<SendMailResult>;
192
+ healthCheck(): Promise<boolean>;
193
+ close(): Promise<void>;
194
+ }
195
+
196
+ interface SendMailOptions {
197
+ from: string;
198
+ to: string | string[];
199
+ cc?: string | string[];
200
+ bcc?: string | string[];
201
+ replyTo?: string;
202
+ subject: string;
203
+ text?: string;
204
+ html?: string;
205
+ attachments?: Attachment[];
206
+ headers?: Record<string, string>;
207
+ tags?: Record<string, string>;
208
+ }
209
+
210
+ interface SendMailResult {
211
+ id: string;
212
+ provider: string;
213
+ }
214
+ ```
@@ -0,0 +1,4 @@
1
+ export { MailPlugin, MAIL_GLOBAL_KEY, MAIL_DISABLED_MSG } from './plugin';
2
+ export type { MailPluginOptions } from './plugin';
3
+ export type { MailProvider, SendMailOptions, SendMailResult, Attachment } from './types';
4
+ export { MailQuotaExceededError } from './types';
@@ -0,0 +1,2 @@
1
+ export { MailPlugin, MAIL_GLOBAL_KEY, MAIL_DISABLED_MSG } from './plugin';
2
+ export { MailQuotaExceededError } from './types';
@@ -0,0 +1,64 @@
1
+ import { type ILogger } from '@rx-ted/packages-core';
2
+ import type { Application, IPlugin } from '@rx-ted/packages-honest';
3
+ import type { SendMailOptions, SendMailResult } from './types';
4
+ import type { MailProvider } from './types';
5
+ export declare const MAIL_GLOBAL_KEY = "app:mail";
6
+ export interface MailPluginOptions {
7
+ resend?: {
8
+ apiKey?: string;
9
+ fromEmail?: string;
10
+ fromName?: string;
11
+ dailyQuota?: number;
12
+ };
13
+ brevo?: {
14
+ apiKey?: string;
15
+ fromEmail?: string;
16
+ fromName?: string;
17
+ dailyQuota?: number;
18
+ };
19
+ smtp?: {
20
+ host?: string;
21
+ port?: number;
22
+ user?: string;
23
+ pass?: string;
24
+ fromEmail?: string;
25
+ fromName?: string;
26
+ secure?: boolean;
27
+ dailyQuota?: number;
28
+ };
29
+ fromEmail?: string;
30
+ fromName?: string;
31
+ healthCheck?: {
32
+ enabled?: boolean;
33
+ intervalMs?: number;
34
+ maxRetries?: number;
35
+ retryDelayMs?: number;
36
+ };
37
+ warmUpTimeout?: number;
38
+ sendTimeout?: number;
39
+ }
40
+ export declare const MAIL_DISABLED_MSG = "Mail service is not configured. Configure resend, brevo, or smtp provider.";
41
+ export declare class MailPlugin implements IPlugin, MailProvider {
42
+ readonly name = "app:mail";
43
+ readonly version = "0.0.1";
44
+ logger?: ILogger;
45
+ private providers;
46
+ private rateLimiters;
47
+ private readonly options;
48
+ private failedProviders;
49
+ private isWarming;
50
+ constructor(options?: MailPluginOptions);
51
+ getClient(name?: string): MailProvider | undefined;
52
+ warmUp(): Promise<void>;
53
+ send(options: SendMailOptions, providerName?: string): Promise<SendMailResult>;
54
+ healthCheck(): Promise<boolean>;
55
+ private tryInitResend;
56
+ private tryInitBrevo;
57
+ private tryInitSmtp;
58
+ beforeModulesRegistered(app: Application, _hono: unknown): Promise<void>;
59
+ afterModulesRegistered(_app: Application, _hono: unknown): Promise<void>;
60
+ runHealthChecks(): Promise<void>;
61
+ close(): Promise<void>;
62
+ private getCacheDriver;
63
+ private getEnv;
64
+ }
@@ -0,0 +1,232 @@
1
+ import { ENV_SYMBOL } from '@rx-ted/packages-core';
2
+ import { ComponentManager, resolvePluginLogger } from '@rx-ted/packages-honest';
3
+ import { RateLimiter } from './rate-limiter';
4
+ import { ResendMailProvider } from './providers/resend.provider';
5
+ import { BrevoMailProvider } from './providers/brevo.provider';
6
+ import { SmtpMailProvider } from './providers/smtp.provider';
7
+ export const MAIL_GLOBAL_KEY = 'app:mail';
8
+ export const MAIL_DISABLED_MSG = 'Mail service is not configured. Configure resend, brevo, or smtp provider.';
9
+ export class MailPlugin {
10
+ name = MAIL_GLOBAL_KEY;
11
+ version = '0.0.1';
12
+ logger;
13
+ providers = [];
14
+ rateLimiters = new Map();
15
+ options;
16
+ failedProviders = new Set();
17
+ isWarming = false;
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ }
21
+ getClient(name) {
22
+ if (name)
23
+ return this.providers.find((p) => p.name === name);
24
+ return this.providers[0];
25
+ }
26
+ async warmUp() {
27
+ if (this.isWarming)
28
+ return;
29
+ this.isWarming = true;
30
+ const timeout = this.options.warmUpTimeout ?? 10_000;
31
+ const relevant = this.providers.filter((p) => typeof p.warmUp === 'function');
32
+ if (relevant.length === 0) {
33
+ this.isWarming = false;
34
+ return;
35
+ }
36
+ const results = await Promise.allSettled(relevant.map((p) => (async () => {
37
+ try {
38
+ await Promise.race([
39
+ p.warmUp(),
40
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Warm-up timeout')), timeout)),
41
+ ]);
42
+ this.logger?.info('[mail] Provider warmed up', { provider: p.name });
43
+ }
44
+ catch (err) {
45
+ this.failedProviders.add(p.name);
46
+ this.logger?.warn('[mail] Provider warm-up failed or timed out', {
47
+ provider: p.name,
48
+ error: err instanceof Error ? err.message : String(err),
49
+ });
50
+ }
51
+ })()));
52
+ const succeeded = results.filter((r) => r.status === 'fulfilled').length;
53
+ this.logger?.info('[mail] Warm-up completed', { total: relevant.length, succeeded });
54
+ this.isWarming = false;
55
+ }
56
+ async send(options, providerName) {
57
+ if (this.providers.length === 0) {
58
+ throw new Error(MAIL_DISABLED_MSG);
59
+ }
60
+ if (providerName) {
61
+ const provider = this.providers.find((p) => p.name === providerName);
62
+ if (!provider)
63
+ throw new Error(`Unknown provider "${providerName}"`);
64
+ if (this.failedProviders.has(providerName)) {
65
+ throw new Error(`Provider "${providerName}" unavailable (warm-up failed)`);
66
+ }
67
+ const limiter = this.rateLimiters.get(providerName);
68
+ if (limiter)
69
+ await limiter.checkAndIncrement();
70
+ return provider.send(options);
71
+ }
72
+ const errors = [];
73
+ for (const provider of this.providers) {
74
+ if (this.failedProviders.has(provider.name))
75
+ continue;
76
+ try {
77
+ const limiter = this.rateLimiters.get(provider.name);
78
+ if (limiter)
79
+ await limiter.checkAndIncrement();
80
+ return await provider.send(options);
81
+ }
82
+ catch (err) {
83
+ errors.push(err);
84
+ this.logger?.warn?.('Provider failed, trying next', {
85
+ provider: provider.name,
86
+ error: err,
87
+ });
88
+ }
89
+ }
90
+ throw new Error(`All providers failed: ${errors.map((e) => e.message).join('; ')}`);
91
+ }
92
+ healthCheck() {
93
+ if (this.providers.length === 0)
94
+ return Promise.resolve(false);
95
+ return this.providers[0].healthCheck();
96
+ }
97
+ tryInitResend(cfg, cacheDrv, appEnv) {
98
+ const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_RESEND_API_KEY');
99
+ if (!apiKey)
100
+ return;
101
+ const fromEmail = cfg.fromEmail ??
102
+ appEnv?.var('MAIL_RESEND_FROM_EMAIL', 'noreply@19981204.xyz') ??
103
+ 'noreply@19981204.xyz';
104
+ const fromName = cfg.fromName ?? appEnv?.var('MAIL_RESEND_FROM_NAME', 'noreply') ?? 'noreply';
105
+ this.providers.push(new ResendMailProvider('resend', { apiKey }, fromEmail, fromName));
106
+ this.rateLimiters.set('resend', new RateLimiter(cfg.dailyQuota ?? Infinity, 'resend', cacheDrv, this.logger));
107
+ this.logger?.info('Mail provider registered', { name: 'resend' });
108
+ }
109
+ tryInitBrevo(cfg, cacheDrv, appEnv) {
110
+ const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_BREVO_API_KEY');
111
+ if (!apiKey)
112
+ return;
113
+ const fromEmail = cfg.fromEmail ??
114
+ appEnv?.var('MAIL_BREVO_FROM_EMAIL', 'noreply@19981204.xyz') ??
115
+ 'noreply@19981204.xyz';
116
+ const fromName = cfg.fromName ?? appEnv?.var('MAIL_BREVO_FROM_NAME', 'noreply') ?? 'noreply';
117
+ this.providers.push(new BrevoMailProvider('brevo', { apiKey }, fromEmail, fromName));
118
+ this.rateLimiters.set('brevo', new RateLimiter(cfg.dailyQuota ?? Infinity, 'brevo', cacheDrv, this.logger));
119
+ this.logger?.info('Mail provider registered', { name: 'brevo' });
120
+ }
121
+ tryInitSmtp(cfg, cacheDrv, appEnv) {
122
+ const host = cfg.host ?? appEnv?.get('MAIL_SMTP_HOST');
123
+ const user = cfg.user ?? appEnv?.get('MAIL_SMTP_USER');
124
+ const pass = cfg.pass ?? appEnv?.get('MAIL_SMTP_PASS');
125
+ const port = cfg.port ?? appEnv?.get('MAIL_SMTP_PORT', 'number') ?? 587;
126
+ if (!host || !user || !pass)
127
+ return;
128
+ const fromEmail = (cfg.fromEmail ?? appEnv?.var('MAIL_SMTP_FROM_EMAIL')) || user;
129
+ const fromName = cfg.fromName ?? appEnv?.var('MAIL_SMTP_FROM_NAME', 'rx-ted') ?? 'rx-ted';
130
+ this.providers.push(new SmtpMailProvider('smtp', { host, port, user, pass, secure: cfg.secure }, fromEmail, fromName));
131
+ this.rateLimiters.set('smtp', new RateLimiter(cfg.dailyQuota ?? Infinity, 'smtp', cacheDrv, this.logger));
132
+ this.logger?.info('Mail provider registered', { name: 'smtp' });
133
+ }
134
+ async beforeModulesRegistered(app, _hono) {
135
+ this.logger ??= resolvePluginLogger(this.name);
136
+ const cacheDrv = this.getCacheDriver(app);
137
+ const appEnv = this.getEnv();
138
+ this.tryInitResend(this.options.resend ?? {}, cacheDrv, appEnv);
139
+ this.tryInitBrevo(this.options.brevo ?? {}, cacheDrv, appEnv);
140
+ this.tryInitSmtp(this.options.smtp ?? {}, cacheDrv, appEnv);
141
+ if (this.providers.length === 0) {
142
+ this.logger.warn(MAIL_DISABLED_MSG);
143
+ }
144
+ else {
145
+ for (const p of this.providers) {
146
+ ComponentManager.registerPlugin(`mail:${p.name}`, p);
147
+ }
148
+ }
149
+ ComponentManager.registerPlugin(MAIL_GLOBAL_KEY, this);
150
+ }
151
+ async afterModulesRegistered(_app, _hono) {
152
+ if (this.providers.length === 0 || !this.logger)
153
+ return;
154
+ if (this.options.healthCheck?.enabled === false) {
155
+ this.logger.info('Health check disabled by config');
156
+ return;
157
+ }
158
+ this.logger.info('Health check deferred to background (will run via waitUntil)');
159
+ }
160
+ async runHealthChecks() {
161
+ if (this.providers.length === 0 || !this.logger)
162
+ return;
163
+ if (this.options.healthCheck?.enabled === false)
164
+ return;
165
+ const maxRetries = this.options.healthCheck?.maxRetries ?? 3;
166
+ const retryDelay = this.options.healthCheck?.retryDelayMs ?? 1000;
167
+ for (const provider of this.providers) {
168
+ const limiter = this.rateLimiters.get(provider.name);
169
+ if (!limiter)
170
+ continue;
171
+ const done = await limiter.wasHealthCheckDoneToday();
172
+ if (done) {
173
+ this.logger.info('Health check already done today', { provider: provider.name });
174
+ continue;
175
+ }
176
+ let success = false;
177
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
178
+ this.logger.info('Running health check', { provider: provider.name, attempt });
179
+ success = await provider.healthCheck();
180
+ if (success)
181
+ break;
182
+ if (attempt < maxRetries)
183
+ await sleep(retryDelay);
184
+ }
185
+ if (success) {
186
+ await limiter.markHealthCheckDone();
187
+ this.logger.info('Health check passed', { provider: provider.name });
188
+ }
189
+ else {
190
+ this.logger.warn('Health check failed after retries', {
191
+ provider: provider.name,
192
+ maxRetries,
193
+ });
194
+ }
195
+ }
196
+ }
197
+ async close() {
198
+ for (const provider of this.providers) {
199
+ try {
200
+ await provider.close();
201
+ }
202
+ catch (e) {
203
+ this.logger?.error?.('Error closing provider', { provider: provider.name, error: e });
204
+ }
205
+ }
206
+ }
207
+ getCacheDriver(_app) {
208
+ try {
209
+ if (ComponentManager.hasPlugin('cache')) {
210
+ return ComponentManager.getPlugin('cache');
211
+ }
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ return null;
217
+ }
218
+ getEnv() {
219
+ try {
220
+ if (ComponentManager.hasPlugin(ENV_SYMBOL)) {
221
+ return ComponentManager.getPlugin(ENV_SYMBOL);
222
+ }
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ return undefined;
228
+ }
229
+ }
230
+ function sleep(ms) {
231
+ return new Promise((resolve) => setTimeout(resolve, ms));
232
+ }
@@ -0,0 +1,15 @@
1
+ import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
2
+ export declare class BrevoMailProvider implements MailProvider {
3
+ private config;
4
+ private fromEmail;
5
+ private fromName?;
6
+ readonly name: string;
7
+ private client;
8
+ constructor(name: string, config: {
9
+ apiKey: string;
10
+ }, fromEmail: string, fromName?: string | undefined);
11
+ private ensureInit;
12
+ send(options: SendMailOptions, _providerName?: string): Promise<SendMailResult>;
13
+ healthCheck(): Promise<boolean>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,75 @@
1
+ import { BrevoClient } from '@getbrevo/brevo';
2
+ export class BrevoMailProvider {
3
+ config;
4
+ fromEmail;
5
+ fromName;
6
+ name;
7
+ client = null;
8
+ constructor(name, config, fromEmail, fromName) {
9
+ this.config = config;
10
+ this.fromEmail = fromEmail;
11
+ this.fromName = fromName;
12
+ this.name = name;
13
+ }
14
+ async ensureInit() {
15
+ if (this.client)
16
+ return;
17
+ this.client = new BrevoClient({ apiKey: this.config.apiKey });
18
+ }
19
+ async send(options, _providerName) {
20
+ await this.ensureInit();
21
+ const to = (Array.isArray(options.to) ? options.to : [options.to]).map((e) => ({
22
+ email: e,
23
+ }));
24
+ const payload = {
25
+ sender: { name: this.fromName ?? '', email: options.from || this.fromEmail },
26
+ to,
27
+ subject: options.subject,
28
+ };
29
+ if (options.html)
30
+ payload.htmlContent = options.html;
31
+ if (options.text)
32
+ payload.textContent = options.text;
33
+ if (options.cc)
34
+ payload.cc = (Array.isArray(options.cc) ? options.cc : [options.cc]).map((e) => ({
35
+ email: e,
36
+ }));
37
+ if (options.bcc)
38
+ payload.bcc = (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map((e) => ({
39
+ email: e,
40
+ }));
41
+ if (options.attachments?.length) {
42
+ payload.attachment = options.attachments.map((a) => ({
43
+ name: a.filename,
44
+ content: a.content instanceof Uint8Array ? uint8ArrayToBase64(a.content) : a.content,
45
+ }));
46
+ }
47
+ const result = await this.client.transactionalEmails.sendTransacEmail(payload);
48
+ return { id: result?.messageId ?? 'unknown', provider: this.name };
49
+ }
50
+ async healthCheck() {
51
+ try {
52
+ await this.ensureInit();
53
+ await this.client.transactionalEmails.sendTransacEmail({
54
+ sender: { name: this.fromName ?? '', email: this.fromEmail },
55
+ to: [{ email: this.fromEmail }],
56
+ subject: 'Health check',
57
+ textContent: 'This is a health check email from Brevo provider.',
58
+ });
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ async close() {
66
+ this.client = null;
67
+ }
68
+ }
69
+ function uint8ArrayToBase64(u8) {
70
+ let binary = '';
71
+ for (let i = 0; i < u8.length; i++) {
72
+ binary += String.fromCharCode(u8[i]);
73
+ }
74
+ return btoa(binary);
75
+ }
@@ -0,0 +1,9 @@
1
+ import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
2
+ export declare class CustomMailProvider implements MailProvider {
3
+ private impl;
4
+ readonly name: string;
5
+ constructor(name: string, impl: MailProvider);
6
+ send(options: SendMailOptions, providerName?: string): Promise<SendMailResult>;
7
+ healthCheck(): Promise<boolean>;
8
+ close(): Promise<void>;
9
+ }
@@ -0,0 +1,22 @@
1
+ export class CustomMailProvider {
2
+ impl;
3
+ name;
4
+ constructor(name, impl) {
5
+ this.impl = impl;
6
+ this.name = name;
7
+ }
8
+ async send(options, providerName) {
9
+ return this.impl.send(options, providerName);
10
+ }
11
+ async healthCheck() {
12
+ if (typeof this.impl.healthCheck === 'function') {
13
+ return this.impl.healthCheck();
14
+ }
15
+ return true;
16
+ }
17
+ async close() {
18
+ if (typeof this.impl.close === 'function') {
19
+ await this.impl.close();
20
+ }
21
+ }
22
+ }