alepha 0.14.3 → 0.15.0

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 (317) hide show
  1. package/README.md +2 -5
  2. package/dist/api/audits/index.d.ts +620 -811
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/files/index.d.ts +185 -377
  5. package/dist/api/files/index.d.ts.map +1 -1
  6. package/dist/api/files/index.js +0 -1
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +245 -435
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/notifications/index.d.ts +238 -429
  11. package/dist/api/notifications/index.d.ts.map +1 -1
  12. package/dist/api/parameters/index.d.ts +236 -427
  13. package/dist/api/parameters/index.d.ts.map +1 -1
  14. package/dist/api/users/index.browser.js +1 -2
  15. package/dist/api/users/index.browser.js.map +1 -1
  16. package/dist/api/users/index.d.ts +1010 -1196
  17. package/dist/api/users/index.d.ts.map +1 -1
  18. package/dist/api/users/index.js +178 -151
  19. package/dist/api/users/index.js.map +1 -1
  20. package/dist/api/verifications/index.d.ts +17 -17
  21. package/dist/api/verifications/index.d.ts.map +1 -1
  22. package/dist/batch/index.d.ts +122 -122
  23. package/dist/batch/index.d.ts.map +1 -1
  24. package/dist/batch/index.js +1 -2
  25. package/dist/batch/index.js.map +1 -1
  26. package/dist/bucket/index.d.ts +163 -163
  27. package/dist/bucket/index.d.ts.map +1 -1
  28. package/dist/cache/core/index.d.ts +46 -46
  29. package/dist/cache/core/index.d.ts.map +1 -1
  30. package/dist/cache/redis/index.d.ts.map +1 -1
  31. package/dist/cli/index.d.ts +384 -285
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/index.js +1113 -623
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/command/index.d.ts +299 -300
  36. package/dist/command/index.d.ts.map +1 -1
  37. package/dist/command/index.js +13 -9
  38. package/dist/command/index.js.map +1 -1
  39. package/dist/core/index.browser.js +445 -103
  40. package/dist/core/index.browser.js.map +1 -1
  41. package/dist/core/index.d.ts +733 -625
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +446 -103
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/index.native.js +445 -103
  46. package/dist/core/index.native.js.map +1 -1
  47. package/dist/datetime/index.d.ts +44 -44
  48. package/dist/datetime/index.d.ts.map +1 -1
  49. package/dist/datetime/index.js +4 -4
  50. package/dist/datetime/index.js.map +1 -1
  51. package/dist/email/index.d.ts +97 -50
  52. package/dist/email/index.d.ts.map +1 -1
  53. package/dist/email/index.js +129 -33
  54. package/dist/email/index.js.map +1 -1
  55. package/dist/fake/index.d.ts +7981 -14
  56. package/dist/fake/index.d.ts.map +1 -1
  57. package/dist/file/index.d.ts +523 -390
  58. package/dist/file/index.d.ts.map +1 -1
  59. package/dist/file/index.js +253 -1
  60. package/dist/file/index.js.map +1 -1
  61. package/dist/lock/core/index.d.ts +208 -208
  62. package/dist/lock/core/index.d.ts.map +1 -1
  63. package/dist/lock/redis/index.d.ts.map +1 -1
  64. package/dist/logger/index.d.ts +25 -26
  65. package/dist/logger/index.d.ts.map +1 -1
  66. package/dist/logger/index.js +12 -2
  67. package/dist/logger/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts +197 -197
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +1 -1
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/orm/chunk-DtkW-qnP.js +38 -0
  73. package/dist/orm/index.browser.js.map +1 -1
  74. package/dist/orm/index.bun.js +2814 -0
  75. package/dist/orm/index.bun.js.map +1 -0
  76. package/dist/orm/index.d.ts +1228 -1216
  77. package/dist/orm/index.d.ts.map +1 -1
  78. package/dist/orm/index.js +2041 -1967
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +248 -248
  81. package/dist/queue/core/index.d.ts.map +1 -1
  82. package/dist/queue/redis/index.d.ts.map +1 -1
  83. package/dist/redis/index.bun.js +285 -0
  84. package/dist/redis/index.bun.js.map +1 -0
  85. package/dist/redis/index.d.ts +118 -136
  86. package/dist/redis/index.d.ts.map +1 -1
  87. package/dist/redis/index.js +18 -38
  88. package/dist/redis/index.js.map +1 -1
  89. package/dist/retry/index.d.ts +69 -69
  90. package/dist/retry/index.d.ts.map +1 -1
  91. package/dist/router/index.d.ts +6 -6
  92. package/dist/router/index.d.ts.map +1 -1
  93. package/dist/scheduler/index.d.ts +25 -25
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/security/index.browser.js +5 -1
  96. package/dist/security/index.browser.js.map +1 -1
  97. package/dist/security/index.d.ts +417 -254
  98. package/dist/security/index.d.ts.map +1 -1
  99. package/dist/security/index.js +386 -86
  100. package/dist/security/index.js.map +1 -1
  101. package/dist/server/auth/index.d.ts +110 -110
  102. package/dist/server/auth/index.d.ts.map +1 -1
  103. package/dist/server/auth/index.js +20 -20
  104. package/dist/server/auth/index.js.map +1 -1
  105. package/dist/server/cache/index.d.ts +62 -47
  106. package/dist/server/cache/index.d.ts.map +1 -1
  107. package/dist/server/cache/index.js +56 -3
  108. package/dist/server/cache/index.js.map +1 -1
  109. package/dist/server/compress/index.d.ts +6 -0
  110. package/dist/server/compress/index.d.ts.map +1 -1
  111. package/dist/server/compress/index.js +36 -1
  112. package/dist/server/compress/index.js.map +1 -1
  113. package/dist/server/cookies/index.d.ts +6 -6
  114. package/dist/server/cookies/index.d.ts.map +1 -1
  115. package/dist/server/cookies/index.js +3 -3
  116. package/dist/server/cookies/index.js.map +1 -1
  117. package/dist/server/core/index.browser.js +2 -2
  118. package/dist/server/core/index.browser.js.map +1 -1
  119. package/dist/server/core/index.d.ts +242 -150
  120. package/dist/server/core/index.d.ts.map +1 -1
  121. package/dist/server/core/index.js +294 -125
  122. package/dist/server/core/index.js.map +1 -1
  123. package/dist/server/cors/index.d.ts +11 -12
  124. package/dist/server/cors/index.d.ts.map +1 -1
  125. package/dist/server/health/index.d.ts +0 -1
  126. package/dist/server/health/index.d.ts.map +1 -1
  127. package/dist/server/helmet/index.d.ts +2 -2
  128. package/dist/server/helmet/index.d.ts.map +1 -1
  129. package/dist/server/links/index.browser.js.map +1 -1
  130. package/dist/server/links/index.d.ts +123 -124
  131. package/dist/server/links/index.d.ts.map +1 -1
  132. package/dist/server/links/index.js +1 -2
  133. package/dist/server/links/index.js.map +1 -1
  134. package/dist/server/metrics/index.d.ts.map +1 -1
  135. package/dist/server/multipart/index.d.ts +6 -6
  136. package/dist/server/multipart/index.d.ts.map +1 -1
  137. package/dist/server/proxy/index.d.ts +102 -103
  138. package/dist/server/proxy/index.d.ts.map +1 -1
  139. package/dist/server/rate-limit/index.d.ts +16 -16
  140. package/dist/server/rate-limit/index.d.ts.map +1 -1
  141. package/dist/server/static/index.d.ts +44 -44
  142. package/dist/server/static/index.d.ts.map +1 -1
  143. package/dist/server/static/index.js +4 -0
  144. package/dist/server/static/index.js.map +1 -1
  145. package/dist/server/swagger/index.d.ts +48 -49
  146. package/dist/server/swagger/index.d.ts.map +1 -1
  147. package/dist/server/swagger/index.js +3 -5
  148. package/dist/server/swagger/index.js.map +1 -1
  149. package/dist/sms/index.d.ts +13 -11
  150. package/dist/sms/index.d.ts.map +1 -1
  151. package/dist/sms/index.js +7 -7
  152. package/dist/sms/index.js.map +1 -1
  153. package/dist/thread/index.d.ts +71 -72
  154. package/dist/thread/index.d.ts.map +1 -1
  155. package/dist/topic/core/index.d.ts +318 -318
  156. package/dist/topic/core/index.d.ts.map +1 -1
  157. package/dist/topic/redis/index.d.ts +6 -6
  158. package/dist/topic/redis/index.d.ts.map +1 -1
  159. package/dist/vite/index.d.ts +5805 -249
  160. package/dist/vite/index.d.ts.map +1 -1
  161. package/dist/vite/index.js +599 -513
  162. package/dist/vite/index.js.map +1 -1
  163. package/dist/websocket/index.browser.js +6 -6
  164. package/dist/websocket/index.browser.js.map +1 -1
  165. package/dist/websocket/index.d.ts +247 -247
  166. package/dist/websocket/index.d.ts.map +1 -1
  167. package/dist/websocket/index.js +6 -6
  168. package/dist/websocket/index.js.map +1 -1
  169. package/package.json +9 -14
  170. package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
  171. package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
  172. package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
  173. package/src/api/users/entities/users.ts +1 -1
  174. package/src/api/users/index.ts +8 -8
  175. package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
  176. package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
  177. package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
  178. package/src/api/users/services/CredentialService.ts +7 -7
  179. package/src/api/users/services/IdentityService.ts +4 -4
  180. package/src/api/users/services/RegistrationService.spec.ts +25 -27
  181. package/src/api/users/services/RegistrationService.ts +38 -27
  182. package/src/api/users/services/SessionCrudService.ts +3 -3
  183. package/src/api/users/services/SessionService.spec.ts +3 -3
  184. package/src/api/users/services/SessionService.ts +28 -9
  185. package/src/api/users/services/UserService.ts +7 -7
  186. package/src/batch/providers/BatchProvider.ts +1 -2
  187. package/src/cli/apps/AlephaCli.ts +0 -2
  188. package/src/cli/apps/AlephaPackageBuilderCli.ts +38 -19
  189. package/src/cli/assets/apiHelloControllerTs.ts +18 -0
  190. package/src/cli/assets/apiIndexTs.ts +16 -0
  191. package/src/cli/assets/claudeMd.ts +303 -0
  192. package/src/cli/assets/mainBrowserTs.ts +2 -2
  193. package/src/cli/assets/mainServerTs.ts +24 -0
  194. package/src/cli/assets/webAppRouterTs.ts +15 -0
  195. package/src/cli/assets/webHelloComponentTsx.ts +16 -0
  196. package/src/cli/assets/webIndexTs.ts +16 -0
  197. package/src/cli/atoms/buildOptions.ts +88 -0
  198. package/src/cli/commands/build.ts +70 -87
  199. package/src/cli/commands/db.ts +21 -22
  200. package/src/cli/commands/deploy.ts +17 -5
  201. package/src/cli/commands/dev.ts +22 -14
  202. package/src/cli/commands/format.ts +8 -2
  203. package/src/cli/commands/gen/env.ts +53 -0
  204. package/src/cli/commands/gen/openapi.ts +1 -1
  205. package/src/cli/commands/gen/resource.ts +15 -0
  206. package/src/cli/commands/gen.ts +7 -1
  207. package/src/cli/commands/init.ts +74 -30
  208. package/src/cli/commands/lint.ts +8 -2
  209. package/src/cli/commands/test.ts +8 -3
  210. package/src/cli/commands/typecheck.ts +5 -1
  211. package/src/cli/commands/verify.ts +5 -3
  212. package/src/cli/defineConfig.ts +49 -7
  213. package/src/cli/index.ts +0 -1
  214. package/src/cli/services/AlephaCliUtils.ts +39 -589
  215. package/src/cli/services/PackageManagerUtils.ts +301 -0
  216. package/src/cli/services/ProjectScaffolder.ts +306 -0
  217. package/src/command/helpers/Runner.spec.ts +2 -2
  218. package/src/command/helpers/Runner.ts +16 -4
  219. package/src/command/primitives/$command.ts +0 -6
  220. package/src/command/providers/CliProvider.ts +1 -3
  221. package/src/core/Alepha.ts +42 -0
  222. package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
  223. package/src/core/index.shared.ts +1 -0
  224. package/src/core/index.ts +2 -0
  225. package/src/core/primitives/$hook.ts +6 -2
  226. package/src/core/primitives/$module.spec.ts +4 -0
  227. package/src/core/providers/AlsProvider.ts +1 -1
  228. package/src/core/providers/CodecManager.spec.ts +12 -6
  229. package/src/core/providers/CodecManager.ts +26 -6
  230. package/src/core/providers/EventManager.ts +169 -13
  231. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +621 -0
  232. package/src/core/providers/KeylessJsonSchemaCodec.ts +407 -0
  233. package/src/core/providers/StateManager.spec.ts +27 -16
  234. package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
  235. package/src/email/providers/LocalEmailProvider.ts +52 -15
  236. package/src/email/providers/NodemailerEmailProvider.ts +167 -56
  237. package/src/file/errors/FileError.ts +7 -0
  238. package/src/file/index.ts +9 -1
  239. package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
  240. package/src/logger/index.ts +15 -3
  241. package/src/mcp/transports/StdioMcpTransport.ts +1 -1
  242. package/src/orm/index.browser.ts +1 -19
  243. package/src/orm/index.bun.ts +77 -0
  244. package/src/orm/index.shared-server.ts +22 -0
  245. package/src/orm/index.shared.ts +15 -0
  246. package/src/orm/index.ts +13 -39
  247. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
  248. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  249. package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
  250. package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
  251. package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
  252. package/src/orm/services/Repository.ts +8 -0
  253. package/src/queue/core/providers/WorkerProvider.spec.ts +48 -32
  254. package/src/redis/index.bun.ts +35 -0
  255. package/src/redis/providers/BunRedisProvider.ts +12 -43
  256. package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
  257. package/src/redis/providers/NodeRedisProvider.ts +16 -34
  258. package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
  259. package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
  260. package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
  261. package/src/security/index.browser.ts +5 -0
  262. package/src/security/index.ts +90 -7
  263. package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
  264. package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
  265. package/src/security/primitives/$role.ts +5 -5
  266. package/src/security/primitives/$serviceAccount.spec.ts +5 -5
  267. package/src/security/primitives/$serviceAccount.ts +3 -3
  268. package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
  269. package/src/server/auth/primitives/$auth.ts +10 -10
  270. package/src/server/auth/primitives/$authCredentials.ts +3 -3
  271. package/src/server/auth/primitives/$authGithub.ts +3 -3
  272. package/src/server/auth/primitives/$authGoogle.ts +3 -3
  273. package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
  274. package/src/server/cache/providers/ServerCacheProvider.spec.ts +183 -0
  275. package/src/server/cache/providers/ServerCacheProvider.ts +95 -10
  276. package/src/server/compress/providers/ServerCompressProvider.ts +61 -2
  277. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
  278. package/src/server/core/helpers/ServerReply.ts +2 -2
  279. package/src/server/core/providers/NodeHttpServerProvider.ts +25 -6
  280. package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
  281. package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
  282. package/src/server/core/providers/ServerProvider.ts +155 -22
  283. package/src/server/core/providers/ServerRouterProvider.ts +259 -115
  284. package/src/server/core/providers/ServerTimingProvider.ts +2 -2
  285. package/src/server/links/index.ts +1 -1
  286. package/src/server/links/providers/LinkProvider.ts +1 -1
  287. package/src/server/static/providers/ServerStaticProvider.ts +10 -0
  288. package/src/server/swagger/index.ts +1 -1
  289. package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -8
  290. package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
  291. package/src/sms/providers/LocalSmsProvider.ts +8 -7
  292. package/src/vite/helpers/boot.ts +28 -17
  293. package/src/vite/helpers/importViteReact.ts +13 -0
  294. package/src/vite/index.ts +1 -21
  295. package/src/vite/plugins/viteAlephaDev.ts +16 -1
  296. package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
  297. package/src/vite/tasks/buildClient.ts +11 -0
  298. package/src/vite/tasks/buildServer.ts +59 -4
  299. package/src/vite/tasks/devServer.ts +71 -0
  300. package/src/vite/tasks/generateCloudflare.ts +7 -0
  301. package/src/vite/tasks/index.ts +2 -1
  302. package/dist/server/security/index.browser.js +0 -13
  303. package/dist/server/security/index.browser.js.map +0 -1
  304. package/dist/server/security/index.d.ts +0 -173
  305. package/dist/server/security/index.d.ts.map +0 -1
  306. package/dist/server/security/index.js +0 -311
  307. package/dist/server/security/index.js.map +0 -1
  308. package/src/cli/assets/appRouterTs.ts +0 -9
  309. package/src/cli/assets/mainTs.ts +0 -13
  310. package/src/cli/assets/viteConfigTs.ts +0 -14
  311. package/src/cli/commands/run.ts +0 -24
  312. package/src/server/security/index.browser.ts +0 -10
  313. package/src/server/security/index.ts +0 -94
  314. package/src/vite/plugins/viteAlepha.ts +0 -37
  315. package/src/vite/plugins/viteAlephaBuild.ts +0 -281
  316. /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
  317. /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
@@ -1,42 +1,27 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
1
+ import { Alepha } from "alepha";
2
+ import { FileSystemProvider, MemoryFileSystemProvider } from "alepha/file";
3
3
  import { beforeEach, describe, expect, test, vi } from "vitest";
4
4
  import { EmailError } from "../errors/EmailError.ts";
5
- import { LocalEmailProvider } from "../providers/LocalEmailProvider.ts";
5
+ import {
6
+ LocalEmailProvider,
7
+ localEmailOptions,
8
+ } from "../providers/LocalEmailProvider.ts";
6
9
 
7
- // Mock fs and path modules
8
- vi.mock("node:fs/promises");
9
- vi.mock("node:path");
10
+ // ---------------------------------------------------------------------------------------------------------------------
10
11
 
11
- // Mock logger
12
- vi.mock("alepha/logger", () => ({
13
- $logger: () => ({
14
- debug: vi.fn(),
15
- info: vi.fn(),
16
- error: vi.fn(),
17
- }),
18
- }));
19
-
20
- const mockedFs = vi.mocked(fs);
21
- const mockedPath = vi.mocked(path);
12
+ const DEFAULT_DIRECTORY = localEmailOptions.options.default.directory;
22
13
 
23
14
  describe("LocalEmailProvider", () => {
24
- let provider: LocalEmailProvider;
25
-
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- // Setup default path.join mock
29
- mockedPath.join.mockImplementation((...args) => args.join("/"));
30
- });
31
-
32
15
  describe("send", () => {
33
- beforeEach(() => {
34
- provider = new LocalEmailProvider({ directory: "test-emails" });
35
- });
36
-
37
16
  test("should successfully send email to local file", async () => {
38
- mockedFs.mkdir.mockResolvedValue(undefined);
39
- mockedFs.writeFile.mockResolvedValue();
17
+ const alepha = Alepha.create().with({
18
+ provide: FileSystemProvider,
19
+ use: MemoryFileSystemProvider,
20
+ });
21
+
22
+ const provider = alepha.inject(LocalEmailProvider);
23
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
24
+ await alepha.start();
40
25
 
41
26
  const to = "test@example.com";
42
27
  const subject = "Test Subject";
@@ -48,19 +33,20 @@ describe("LocalEmailProvider", () => {
48
33
  body,
49
34
  });
50
35
 
51
- expect(mockedFs.mkdir).toHaveBeenCalledWith("test-emails", {
52
- recursive: true,
53
- });
54
- expect(mockedFs.writeFile).toHaveBeenCalledWith(
55
- expect.stringContaining("test@example.com"),
56
- expect.stringContaining(subject),
57
- "utf8",
58
- );
36
+ expect(memoryFs.writeFileCalls).toHaveLength(1);
37
+ expect(memoryFs.writeFileCalls[0].path).toContain("test@example.com");
38
+ expect(memoryFs.writeFileCalls[0].data).toContain(subject);
59
39
  });
60
40
 
61
41
  test("should create proper filename with sanitized email and timestamp", async () => {
62
- mockedFs.mkdir.mockResolvedValue(undefined);
63
- mockedFs.writeFile.mockResolvedValue();
42
+ const alepha = Alepha.create().with({
43
+ provide: FileSystemProvider,
44
+ use: MemoryFileSystemProvider,
45
+ });
46
+
47
+ const provider = alepha.inject(LocalEmailProvider);
48
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
49
+ await alepha.start();
64
50
 
65
51
  const to = "user+test@example.com";
66
52
  const subject = "Test Subject";
@@ -76,17 +62,24 @@ describe("LocalEmailProvider", () => {
76
62
  body,
77
63
  });
78
64
 
79
- expect(mockedPath.join).toHaveBeenCalledWith(
80
- "test-emails",
65
+ expect(memoryFs.joinCalls).toHaveLength(1);
66
+ expect(memoryFs.joinCalls[0]).toEqual([
67
+ DEFAULT_DIRECTORY,
81
68
  "user_test@example.com+2023-01-01T12-00-00-000Z.html",
82
- );
69
+ ]);
83
70
 
84
71
  vi.useRealTimers();
85
72
  });
86
73
 
87
74
  test("should sanitize special characters in email address", async () => {
88
- mockedFs.mkdir.mockResolvedValue(undefined);
89
- mockedFs.writeFile.mockResolvedValue();
75
+ const alepha = Alepha.create().with({
76
+ provide: FileSystemProvider,
77
+ use: MemoryFileSystemProvider,
78
+ });
79
+
80
+ const provider = alepha.inject(LocalEmailProvider);
81
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
82
+ await alepha.start();
90
83
 
91
84
  const to = "user<script>@example.com";
92
85
  const subject = "Test Subject";
@@ -98,15 +91,21 @@ describe("LocalEmailProvider", () => {
98
91
  body,
99
92
  });
100
93
 
101
- expect(mockedPath.join).toHaveBeenCalledWith(
102
- "test-emails",
103
- expect.stringMatching(/user_script_@example\.com\+.+\.html/),
94
+ expect(memoryFs.joinCalls).toHaveLength(1);
95
+ expect(memoryFs.joinCalls[0][1]).toMatch(
96
+ /user_script_@example\.com\+.+\.html/,
104
97
  );
105
98
  });
106
99
 
107
100
  test("should create proper HTML content", async () => {
108
- mockedFs.mkdir.mockResolvedValue(undefined);
109
- mockedFs.writeFile.mockResolvedValue();
101
+ const alepha = Alepha.create().with({
102
+ provide: FileSystemProvider,
103
+ use: MemoryFileSystemProvider,
104
+ });
105
+
106
+ const provider = alepha.inject(LocalEmailProvider);
107
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
108
+ await alepha.start();
110
109
 
111
110
  const to = "test@example.com";
112
111
  const subject = "Test <Subject>";
@@ -118,8 +117,7 @@ describe("LocalEmailProvider", () => {
118
117
  body,
119
118
  });
120
119
 
121
- const writeCall = mockedFs.writeFile.mock.calls[0];
122
- const htmlContent = writeCall[1] as string;
120
+ const htmlContent = memoryFs.writeFileCalls[0].data;
123
121
 
124
122
  expect(htmlContent).toContain("<!DOCTYPE html>");
125
123
  expect(htmlContent).toContain("Test &lt;Subject&gt;"); // escaped subject
@@ -130,36 +128,17 @@ describe("LocalEmailProvider", () => {
130
128
  expect(htmlContent).toContain("Sent:");
131
129
  });
132
130
 
133
- test("should throw EmailError when mkdir fails", async () => {
134
- const mkdirError = new Error("Permission denied");
135
- mockedFs.mkdir.mockRejectedValue(mkdirError);
136
-
137
- const to = "test@example.com";
138
- const subject = "Test Subject";
139
- const body = "<p>Test body</p>";
131
+ test("should throw EmailError when writeFile fails", async () => {
132
+ const alepha = Alepha.create().with({
133
+ provide: FileSystemProvider,
134
+ use: MemoryFileSystemProvider,
135
+ });
140
136
 
141
- await expect(
142
- provider.send({
143
- to,
144
- subject,
145
- body,
146
- }),
147
- ).rejects.toThrow(EmailError);
148
- await expect(
149
- provider.send({
150
- to,
151
- subject,
152
- body,
153
- }),
154
- ).rejects.toThrow(
155
- "Failed to save email to local file: Permission denied",
156
- );
157
- });
137
+ const provider = alepha.inject(LocalEmailProvider);
138
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
139
+ await alepha.start();
158
140
 
159
- test("should throw EmailError when writeFile fails", async () => {
160
- mockedFs.mkdir.mockResolvedValue(undefined);
161
- const writeError = new Error("Disk full");
162
- mockedFs.writeFile.mockRejectedValue(writeError);
141
+ memoryFs.writeFileError = new Error("Disk full");
163
142
 
164
143
  const to = "test@example.com";
165
144
  const subject = "Test Subject";
@@ -172,6 +151,7 @@ describe("LocalEmailProvider", () => {
172
151
  body,
173
152
  }),
174
153
  ).rejects.toThrow(EmailError);
154
+
175
155
  await expect(
176
156
  provider.send({
177
157
  to,
@@ -182,8 +162,16 @@ describe("LocalEmailProvider", () => {
182
162
  });
183
163
 
184
164
  test("should handle non-Error exceptions", async () => {
185
- mockedFs.mkdir.mockResolvedValue(undefined);
186
- mockedFs.writeFile.mockRejectedValue("String error");
165
+ const alepha = Alepha.create().with({
166
+ provide: FileSystemProvider,
167
+ use: MemoryFileSystemProvider,
168
+ });
169
+
170
+ const provider = alepha.inject(LocalEmailProvider);
171
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
172
+ await alepha.start();
173
+
174
+ memoryFs.writeFileError = "String error" as unknown as Error;
187
175
 
188
176
  const to = "test@example.com";
189
177
  const subject = "Test Subject";
@@ -196,6 +184,7 @@ describe("LocalEmailProvider", () => {
196
184
  body,
197
185
  }),
198
186
  ).rejects.toThrow(EmailError);
187
+
199
188
  await expect(
200
189
  provider.send({
201
190
  to,
@@ -204,11 +193,39 @@ describe("LocalEmailProvider", () => {
204
193
  }),
205
194
  ).rejects.toThrow("Failed to save email to local file: String error");
206
195
  });
196
+
197
+ test("should handle multiple recipients", async () => {
198
+ const alepha = Alepha.create().with({
199
+ provide: FileSystemProvider,
200
+ use: MemoryFileSystemProvider,
201
+ });
202
+
203
+ const provider = alepha.inject(LocalEmailProvider);
204
+ const memoryFs = alepha.inject(MemoryFileSystemProvider);
205
+ await alepha.start();
206
+
207
+ await provider.send({
208
+ to: ["user1@example.com", "user2@example.com"],
209
+ subject: "Broadcast",
210
+ body: "<p>Hello all</p>",
211
+ });
212
+
213
+ expect(memoryFs.writeFileCalls).toHaveLength(2);
214
+ expect(memoryFs.writeFileCalls[0].path).toContain("user1@example.com");
215
+ expect(memoryFs.writeFileCalls[1].path).toContain("user2@example.com");
216
+ });
207
217
  });
208
218
 
209
219
  describe("createEmailHtml", () => {
210
- beforeEach(() => {
211
- provider = new LocalEmailProvider();
220
+ let provider: LocalEmailProvider;
221
+
222
+ beforeEach(async () => {
223
+ const alepha = Alepha.create().with({
224
+ provide: FileSystemProvider,
225
+ use: MemoryFileSystemProvider,
226
+ });
227
+ provider = alepha.inject(LocalEmailProvider);
228
+ await alepha.start();
212
229
  });
213
230
 
214
231
  test("should create proper HTML structure", () => {
@@ -290,8 +307,15 @@ describe("LocalEmailProvider", () => {
290
307
  });
291
308
 
292
309
  describe("escapeHtml", () => {
293
- beforeEach(() => {
294
- provider = new LocalEmailProvider();
310
+ let provider: LocalEmailProvider;
311
+
312
+ beforeEach(async () => {
313
+ const alepha = Alepha.create().with({
314
+ provide: FileSystemProvider,
315
+ use: MemoryFileSystemProvider,
316
+ });
317
+ provider = alepha.inject(LocalEmailProvider);
318
+ await alepha.start();
295
319
  });
296
320
 
297
321
  test("should escape ampersands", () => {
@@ -1,24 +1,64 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
1
+ import { $atom, $hook, $inject, $use, type Static, t } from "alepha";
2
+ import { FileSystemProvider } from "alepha/file";
3
3
  import { $logger } from "alepha/logger";
4
4
  import { EmailError } from "../errors/EmailError.ts";
5
5
  import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
6
6
 
7
- export interface LocalEmailProviderOptions {
8
- /**
9
- * Directory to save email files.
10
- */
11
- directory?: string;
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Local email provider configuration atom
11
+ */
12
+ export const localEmailOptions = $atom({
13
+ name: "alepha.email.local.options",
14
+ schema: t.object({
15
+ directory: t.string({
16
+ description: "Directory path where email files will be stored",
17
+ }),
18
+ }),
19
+ default: {
20
+ directory: "node_modules/.alepha/emails",
21
+ },
22
+ });
23
+
24
+ export type LocalEmailProviderOptions = Static<typeof localEmailOptions.schema>;
25
+
26
+ declare module "alepha" {
27
+ interface State {
28
+ [localEmailOptions.key]: LocalEmailProviderOptions;
29
+ }
12
30
  }
13
31
 
32
+ // ---------------------------------------------------------------------------------------------------------------------
33
+
14
34
  export class LocalEmailProvider implements EmailProvider {
15
35
  protected readonly log = $logger();
16
- protected readonly directory: string;
36
+ protected readonly fs = $inject(FileSystemProvider);
37
+ protected readonly options = $use(localEmailOptions);
17
38
 
18
- constructor(options: LocalEmailProviderOptions = {}) {
19
- this.directory = options.directory ?? "node_modules/.alepha/emails";
39
+ protected get directory(): string {
40
+ return this.options.directory;
20
41
  }
21
42
 
43
+ protected onStart = $hook({
44
+ on: "start",
45
+ handler: async () => {
46
+ try {
47
+ await this.fs.mkdir(this.directory, { recursive: true });
48
+ this.log.info("Email directory OK", {
49
+ directory: this.directory,
50
+ });
51
+ } catch (error) {
52
+ const message = `Failed to create email directory: ${error instanceof Error ? error.message : String(error)}`;
53
+ this.log.error(message, { directory: this.directory });
54
+ throw new EmailError(
55
+ message,
56
+ error instanceof Error ? error : undefined,
57
+ );
58
+ }
59
+ },
60
+ });
61
+
22
62
  public async send(options: EmailSendOptions): Promise<void> {
23
63
  const { to, subject, body } = options;
24
64
 
@@ -29,15 +69,12 @@ export class LocalEmailProvider implements EmailProvider {
29
69
  });
30
70
 
31
71
  try {
32
- // Ensure directory exists
33
- await fs.mkdir(this.directory, { recursive: true });
34
-
35
72
  // Create filename: emailcontact+date
36
73
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
37
74
  for (const recipient of Array.isArray(to) ? to : [to]) {
38
75
  const sanitizedEmail = recipient.replace(/[^a-zA-Z0-9@.-]/g, "_");
39
76
  const filename = `${sanitizedEmail}+${timestamp}.html`;
40
- const filepath = path.join(this.directory, filename);
77
+ const filepath = this.fs.join(this.directory, filename);
41
78
 
42
79
  // Create HTML content
43
80
  const htmlContent = this.createEmailHtml({
@@ -47,7 +84,7 @@ export class LocalEmailProvider implements EmailProvider {
47
84
  });
48
85
 
49
86
  // Write to file
50
- await fs.writeFile(filepath, htmlContent, "utf8");
87
+ await this.fs.writeFile(filepath, htmlContent);
51
88
 
52
89
  this.log.info("Email saved to local file", { filepath, to, subject });
53
90
  }
@@ -1,69 +1,173 @@
1
- import { $env, $hook, t } from "alepha";
1
+ import { $atom, $env, $hook, $use, type Static, t } from "alepha";
2
2
  import { $logger } from "alepha/logger";
3
3
  import type { Transporter } from "nodemailer";
4
4
  import nodemailer from "nodemailer";
5
5
  import { EmailError } from "../errors/EmailError.ts";
6
6
  import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
7
7
 
8
+ // ---------------------------------------------------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Environment variables for nodemailer configuration
12
+ */
8
13
  const envSchema = t.object({
9
- EMAIL_HOST: t.text({
10
- description: "SMTP server host",
11
- }),
14
+ EMAIL_HOST: t.optional(
15
+ t.text({
16
+ description: "SMTP server host",
17
+ }),
18
+ ),
12
19
  EMAIL_PORT: t.number({
13
20
  default: 587,
14
21
  description: "SMTP server port",
15
22
  }),
16
- EMAIL_USER: t.text({
17
- description: "SMTP authentication username",
18
- }),
19
- EMAIL_PASS: t.text({
20
- description: "SMTP authentication password",
21
- }),
22
- EMAIL_FROM: t.text({
23
- description: "Default from email address",
24
- }),
23
+ EMAIL_USER: t.optional(
24
+ t.text({
25
+ description: "SMTP authentication username",
26
+ }),
27
+ ),
28
+ EMAIL_PASS: t.optional(
29
+ t.text({
30
+ description: "SMTP authentication password",
31
+ }),
32
+ ),
33
+ EMAIL_FROM: t.optional(
34
+ t.text({
35
+ description: "Default from email address",
36
+ }),
37
+ ),
25
38
  EMAIL_SECURE: t.boolean({
26
39
  default: false,
27
40
  description: "Use secure connection (TLS)",
28
41
  }),
29
42
  });
30
43
 
31
- export interface NodemailerEmailProviderOptions {
32
- /**
33
- * Custom transporter configuration.
34
- * If provided, will override environment variables.
35
- */
36
- transporter?: Transporter;
44
+ // ---------------------------------------------------------------------------------------------------------------------
37
45
 
38
- /**
39
- * Custom from email address.
40
- * If not provided, will use EMAIL_FROM from environment.
41
- */
42
- from?: string;
46
+ /**
47
+ * Nodemailer connection pooling and rate limiting options
48
+ */
49
+ export const nodemailerEmailOptions = $atom({
50
+ name: "alepha.email.nodemailer.options",
51
+ schema: t.object({
52
+ pool: t.optional(
53
+ t.boolean({
54
+ description: "Enable connection pooling",
55
+ }),
56
+ ),
57
+ maxConnections: t.optional(
58
+ t.number({
59
+ description: "Maximum number of connections in pool",
60
+ }),
61
+ ),
62
+ maxMessages: t.optional(
63
+ t.number({
64
+ description: "Maximum messages per connection",
65
+ }),
66
+ ),
67
+ rateDelta: t.optional(
68
+ t.number({
69
+ description: "Time in milliseconds between message sends",
70
+ }),
71
+ ),
72
+ rateLimit: t.optional(
73
+ t.number({
74
+ description: "Maximum number of messages per rateDelta",
75
+ }),
76
+ ),
77
+ }),
78
+ default: {},
79
+ });
43
80
 
44
- /**
45
- * Additional nodemailer options.
46
- */
47
- options?: {
48
- pool?: boolean;
49
- maxConnections?: number;
50
- maxMessages?: number;
51
- rateDelta?: number;
52
- rateLimit?: number;
53
- };
81
+ export type NodemailerEmailProviderOptions = Static<
82
+ typeof nodemailerEmailOptions.schema
83
+ >;
84
+
85
+ declare module "alepha" {
86
+ interface State {
87
+ [nodemailerEmailOptions.key]: NodemailerEmailProviderOptions;
88
+ }
54
89
  }
55
90
 
91
+ // ---------------------------------------------------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Email provider using Nodemailer for SMTP transport.
95
+ *
96
+ * Configuration is provided via environment variables:
97
+ * - EMAIL_HOST: SMTP server host
98
+ * - EMAIL_PORT: SMTP server port (default: 587)
99
+ * - EMAIL_USER: SMTP authentication username
100
+ * - EMAIL_PASS: SMTP authentication password
101
+ * - EMAIL_FROM: Default from email address
102
+ * - EMAIL_SECURE: Use secure connection (default: false)
103
+ *
104
+ * Advanced pooling/rate limiting options can be configured via atom:
105
+ * @see {@link nodemailerEmailOptions}
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * // Configure via environment variables
110
+ * // EMAIL_HOST=smtp.example.com
111
+ * // EMAIL_PORT=587
112
+ * // EMAIL_USER=user@example.com
113
+ * // EMAIL_PASS=secret
114
+ * // EMAIL_FROM=noreply@example.com
115
+ *
116
+ * // Optionally configure pooling via atom
117
+ * alepha.state.set(nodemailerEmailOptions.key, {
118
+ * pool: true,
119
+ * maxConnections: 5,
120
+ * rateLimit: 10,
121
+ * });
122
+ * ```
123
+ */
56
124
  export class NodemailerEmailProvider implements EmailProvider {
57
125
  protected readonly env = $env(envSchema);
58
126
  protected readonly log = $logger();
59
- protected transporter: Transporter;
60
- protected fromAddress: string;
127
+ protected readonly options = $use(nodemailerEmailOptions);
128
+ protected transporter: Transporter | null = null;
61
129
 
62
- public readonly options: NodemailerEmailProviderOptions = {};
130
+ protected get host(): string {
131
+ const host = this.env.EMAIL_HOST;
132
+ if (!host) {
133
+ throw new EmailError(
134
+ "Email host not configured. Set EMAIL_HOST env var.",
135
+ );
136
+ }
137
+ return host;
138
+ }
139
+
140
+ protected get port(): number {
141
+ return this.env.EMAIL_PORT;
142
+ }
143
+
144
+ protected get secure(): boolean {
145
+ return this.env.EMAIL_SECURE;
146
+ }
147
+
148
+ protected get user(): string | undefined {
149
+ return this.env.EMAIL_USER;
150
+ }
151
+
152
+ protected get pass(): string | undefined {
153
+ return this.env.EMAIL_PASS;
154
+ }
155
+
156
+ protected get fromAddress(): string {
157
+ const from = this.env.EMAIL_FROM;
158
+ if (!from) {
159
+ throw new EmailError(
160
+ "Email from address not configured. Set EMAIL_FROM env var.",
161
+ );
162
+ }
163
+ return from;
164
+ }
63
165
 
64
- constructor() {
65
- this.fromAddress = this.options.from ?? this.env.EMAIL_FROM;
66
- this.transporter = this.createTransporter();
166
+ protected getTransporter(): Transporter {
167
+ if (!this.transporter) {
168
+ this.transporter = this.createTransporter();
169
+ }
170
+ return this.transporter;
67
171
  }
68
172
 
69
173
  public async send(options: EmailSendOptions): Promise<void> {
@@ -71,7 +175,7 @@ export class NodemailerEmailProvider implements EmailProvider {
71
175
  this.log.debug("Sending email via Nodemailer", { to, subject });
72
176
 
73
177
  try {
74
- const result = await this.transporter.sendMail({
178
+ const result = await this.getTransporter().sendMail({
75
179
  from: this.fromAddress,
76
180
  to,
77
181
  subject,
@@ -92,26 +196,30 @@ export class NodemailerEmailProvider implements EmailProvider {
92
196
  }
93
197
 
94
198
  protected createTransporter(): Transporter {
95
- if (this.options.transporter) {
96
- return this.options.transporter;
97
- }
98
-
99
199
  const transporterConfig = {
100
- host: this.env.EMAIL_HOST,
101
- port: this.env.EMAIL_PORT,
102
- secure: this.env.EMAIL_SECURE,
103
- auth: {
104
- user: this.env.EMAIL_USER,
105
- pass: this.env.EMAIL_PASS,
106
- },
107
- ...this.options.options,
200
+ host: this.host,
201
+ port: this.port,
202
+ secure: this.secure,
203
+ auth:
204
+ this.user && this.pass
205
+ ? {
206
+ user: this.user,
207
+ pass: this.pass,
208
+ }
209
+ : undefined,
210
+ pool: this.options.pool,
211
+ maxConnections: this.options.maxConnections,
212
+ maxMessages: this.options.maxMessages,
213
+ rateDelta: this.options.rateDelta,
214
+ rateLimit: this.options.rateLimit,
108
215
  };
109
216
 
110
217
  this.log.debug("Creating Nodemailer transporter", {
111
218
  host: transporterConfig.host,
112
219
  port: transporterConfig.port,
113
220
  secure: transporterConfig.secure,
114
- user: transporterConfig.auth.user,
221
+ user: transporterConfig.auth?.user,
222
+ pool: transporterConfig.pool,
115
223
  });
116
224
 
117
225
  return nodemailer.createTransport(transporterConfig);
@@ -122,7 +230,7 @@ export class NodemailerEmailProvider implements EmailProvider {
122
230
  */
123
231
  public async verify(): Promise<boolean> {
124
232
  try {
125
- await this.transporter.verify();
233
+ await this.getTransporter().verify();
126
234
  this.log.info("Email server connection verified");
127
235
  return true;
128
236
  } catch (error) {
@@ -135,7 +243,10 @@ export class NodemailerEmailProvider implements EmailProvider {
135
243
  * Close the transporter connection.
136
244
  */
137
245
  public close(): void {
138
- this.transporter.close();
246
+ if (this.transporter) {
247
+ this.transporter.close();
248
+ this.transporter = null;
249
+ }
139
250
  }
140
251
 
141
252
  protected readonly onStart = $hook({
@@ -0,0 +1,7 @@
1
+ export class FileError extends Error {
2
+ constructor(message: string, cause?: Error) {
3
+ super(message);
4
+ this.name = "FileError";
5
+ this.cause = cause;
6
+ }
7
+ }