alepha 0.14.4 → 0.15.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 (322) hide show
  1. package/README.md +44 -102
  2. package/dist/api/audits/index.d.ts +331 -443
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +2 -2
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +0 -113
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -3
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +151 -262
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/notifications/index.browser.js +4 -4
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +164 -276
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/notifications/index.js +4 -4
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +265 -377
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.browser.js +1 -2
  21. package/dist/api/users/index.browser.js.map +1 -1
  22. package/dist/api/users/index.d.ts +195 -301
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +203 -184
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts.map +1 -1
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/batch/index.js +1 -2
  29. package/dist/batch/index.js.map +1 -1
  30. package/dist/bucket/index.d.ts.map +1 -1
  31. package/dist/cache/core/index.d.ts.map +1 -1
  32. package/dist/cache/redis/index.d.ts.map +1 -1
  33. package/dist/cache/redis/index.js +2 -2
  34. package/dist/cache/redis/index.js.map +1 -1
  35. package/dist/cli/index.d.ts +5900 -165
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +1481 -639
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +8 -4
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +29 -25
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +563 -54
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +175 -8
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +564 -54
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +563 -54
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts.map +1 -1
  52. package/dist/datetime/index.js +4 -4
  53. package/dist/datetime/index.js.map +1 -1
  54. package/dist/email/index.d.ts +89 -42
  55. package/dist/email/index.d.ts.map +1 -1
  56. package/dist/email/index.js +129 -33
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +7969 -2
  59. package/dist/fake/index.d.ts.map +1 -1
  60. package/dist/fake/index.js +22 -22
  61. package/dist/fake/index.js.map +1 -1
  62. package/dist/file/index.d.ts +134 -1
  63. package/dist/file/index.d.ts.map +1 -1
  64. package/dist/file/index.js +253 -1
  65. package/dist/file/index.js.map +1 -1
  66. package/dist/lock/core/index.d.ts.map +1 -1
  67. package/dist/lock/redis/index.d.ts.map +1 -1
  68. package/dist/logger/index.d.ts +1 -2
  69. package/dist/logger/index.d.ts.map +1 -1
  70. package/dist/logger/index.js +1 -5
  71. package/dist/logger/index.js.map +1 -1
  72. package/dist/mcp/index.d.ts +19 -1
  73. package/dist/mcp/index.d.ts.map +1 -1
  74. package/dist/mcp/index.js +28 -4
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/orm/chunk-DH6iiROE.js +38 -0
  77. package/dist/orm/index.browser.js +9 -9
  78. package/dist/orm/index.browser.js.map +1 -1
  79. package/dist/orm/index.bun.js +2821 -0
  80. package/dist/orm/index.bun.js.map +1 -0
  81. package/dist/orm/index.d.ts +318 -169
  82. package/dist/orm/index.d.ts.map +1 -1
  83. package/dist/orm/index.js +2086 -1776
  84. package/dist/orm/index.js.map +1 -1
  85. package/dist/queue/core/index.d.ts +4 -4
  86. package/dist/queue/core/index.d.ts.map +1 -1
  87. package/dist/queue/redis/index.d.ts.map +1 -1
  88. package/dist/redis/index.bun.js +285 -0
  89. package/dist/redis/index.bun.js.map +1 -0
  90. package/dist/redis/index.d.ts +13 -31
  91. package/dist/redis/index.d.ts.map +1 -1
  92. package/dist/redis/index.js +18 -38
  93. package/dist/redis/index.js.map +1 -1
  94. package/dist/retry/index.d.ts.map +1 -1
  95. package/dist/router/index.d.ts.map +1 -1
  96. package/dist/scheduler/index.d.ts +83 -1
  97. package/dist/scheduler/index.d.ts.map +1 -1
  98. package/dist/scheduler/index.js +393 -1
  99. package/dist/scheduler/index.js.map +1 -1
  100. package/dist/security/index.browser.js +5 -1
  101. package/dist/security/index.browser.js.map +1 -1
  102. package/dist/security/index.d.ts +598 -112
  103. package/dist/security/index.d.ts.map +1 -1
  104. package/dist/security/index.js +1808 -97
  105. package/dist/security/index.js.map +1 -1
  106. package/dist/server/auth/index.d.ts +1200 -175
  107. package/dist/server/auth/index.d.ts.map +1 -1
  108. package/dist/server/auth/index.js +1268 -37
  109. package/dist/server/auth/index.js.map +1 -1
  110. package/dist/server/cache/index.d.ts +6 -3
  111. package/dist/server/cache/index.d.ts.map +1 -1
  112. package/dist/server/cache/index.js +1 -1
  113. package/dist/server/cache/index.js.map +1 -1
  114. package/dist/server/compress/index.d.ts.map +1 -1
  115. package/dist/server/cookies/index.d.ts.map +1 -1
  116. package/dist/server/cookies/index.js +3 -3
  117. package/dist/server/cookies/index.js.map +1 -1
  118. package/dist/server/core/index.d.ts +115 -13
  119. package/dist/server/core/index.d.ts.map +1 -1
  120. package/dist/server/core/index.js +321 -139
  121. package/dist/server/core/index.js.map +1 -1
  122. package/dist/server/cors/index.d.ts +0 -1
  123. package/dist/server/cors/index.d.ts.map +1 -1
  124. package/dist/server/health/index.d.ts +0 -1
  125. package/dist/server/health/index.d.ts.map +1 -1
  126. package/dist/server/helmet/index.d.ts.map +1 -1
  127. package/dist/server/links/index.browser.js +9 -1
  128. package/dist/server/links/index.browser.js.map +1 -1
  129. package/dist/server/links/index.d.ts +1 -2
  130. package/dist/server/links/index.d.ts.map +1 -1
  131. package/dist/server/links/index.js +14 -7
  132. package/dist/server/links/index.js.map +1 -1
  133. package/dist/server/metrics/index.d.ts +514 -1
  134. package/dist/server/metrics/index.d.ts.map +1 -1
  135. package/dist/server/metrics/index.js +4462 -4
  136. package/dist/server/metrics/index.js.map +1 -1
  137. package/dist/server/multipart/index.d.ts.map +1 -1
  138. package/dist/server/proxy/index.d.ts +0 -1
  139. package/dist/server/proxy/index.d.ts.map +1 -1
  140. package/dist/server/rate-limit/index.d.ts.map +1 -1
  141. package/dist/server/static/index.d.ts.map +1 -1
  142. package/dist/server/swagger/index.d.ts +1 -2
  143. package/dist/server/swagger/index.d.ts.map +1 -1
  144. package/dist/server/swagger/index.js +1 -2
  145. package/dist/server/swagger/index.js.map +1 -1
  146. package/dist/sms/index.d.ts +3 -1
  147. package/dist/sms/index.d.ts.map +1 -1
  148. package/dist/sms/index.js +10 -10
  149. package/dist/sms/index.js.map +1 -1
  150. package/dist/thread/index.d.ts +0 -1
  151. package/dist/thread/index.d.ts.map +1 -1
  152. package/dist/thread/index.js +2 -2
  153. package/dist/thread/index.js.map +1 -1
  154. package/dist/topic/core/index.d.ts.map +1 -1
  155. package/dist/topic/redis/index.d.ts.map +1 -1
  156. package/dist/vite/index.d.ts +6315 -149
  157. package/dist/vite/index.d.ts.map +1 -1
  158. package/dist/vite/index.js +140 -469
  159. package/dist/vite/index.js.map +1 -1
  160. package/dist/websocket/index.browser.js +9 -9
  161. package/dist/websocket/index.browser.js.map +1 -1
  162. package/dist/websocket/index.d.ts +28 -28
  163. package/dist/websocket/index.d.ts.map +1 -1
  164. package/dist/websocket/index.js +9 -9
  165. package/dist/websocket/index.js.map +1 -1
  166. package/package.json +13 -18
  167. package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
  168. package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
  169. package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
  170. package/src/api/users/entities/users.ts +1 -1
  171. package/src/api/users/index.ts +8 -8
  172. package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
  173. package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
  174. package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
  175. package/src/api/users/services/CredentialService.ts +7 -7
  176. package/src/api/users/services/IdentityService.ts +4 -4
  177. package/src/api/users/services/RegistrationService.spec.ts +25 -27
  178. package/src/api/users/services/RegistrationService.ts +38 -27
  179. package/src/api/users/services/SessionCrudService.ts +3 -3
  180. package/src/api/users/services/SessionService.spec.ts +3 -3
  181. package/src/api/users/services/SessionService.ts +27 -18
  182. package/src/api/users/services/UserService.ts +7 -7
  183. package/src/batch/providers/BatchProvider.ts +1 -2
  184. package/src/cli/apps/AlephaCli.ts +2 -2
  185. package/src/cli/apps/AlephaPackageBuilderCli.ts +47 -20
  186. package/src/cli/assets/apiHelloControllerTs.ts +19 -0
  187. package/src/cli/assets/apiIndexTs.ts +16 -0
  188. package/src/cli/assets/biomeJson.ts +2 -1
  189. package/src/cli/assets/claudeMd.ts +308 -0
  190. package/src/cli/assets/dummySpecTs.ts +2 -1
  191. package/src/cli/assets/editorconfig.ts +2 -1
  192. package/src/cli/assets/mainBrowserTs.ts +4 -3
  193. package/src/cli/assets/mainCss.ts +24 -0
  194. package/src/cli/assets/mainServerTs.ts +24 -0
  195. package/src/cli/assets/tsconfigJson.ts +2 -1
  196. package/src/cli/assets/webAppRouterTs.ts +16 -0
  197. package/src/cli/assets/webHelloComponentTsx.ts +20 -0
  198. package/src/cli/assets/webIndexTs.ts +16 -0
  199. package/src/cli/atoms/appEntryOptions.ts +13 -0
  200. package/src/cli/atoms/buildOptions.ts +1 -1
  201. package/src/cli/atoms/changelogOptions.ts +1 -1
  202. package/src/cli/commands/build.ts +97 -61
  203. package/src/cli/commands/db.ts +21 -18
  204. package/src/cli/commands/deploy.ts +17 -5
  205. package/src/cli/commands/dev.ts +26 -47
  206. package/src/cli/commands/gen/env.ts +1 -1
  207. package/src/cli/commands/init.ts +79 -25
  208. package/src/cli/commands/lint.ts +9 -3
  209. package/src/cli/commands/test.ts +8 -2
  210. package/src/cli/commands/typecheck.ts +5 -1
  211. package/src/cli/commands/verify.ts +4 -2
  212. package/src/cli/defineConfig.ts +9 -0
  213. package/src/cli/index.ts +2 -1
  214. package/src/cli/providers/AppEntryProvider.ts +131 -0
  215. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  216. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  217. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  218. package/src/cli/services/AlephaCliUtils.ts +72 -602
  219. package/src/cli/services/PackageManagerUtils.ts +308 -0
  220. package/src/cli/services/ProjectScaffolder.ts +329 -0
  221. package/src/command/helpers/Runner.ts +15 -3
  222. package/src/core/Alepha.ts +2 -8
  223. package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
  224. package/src/core/index.shared.ts +1 -0
  225. package/src/core/index.ts +2 -0
  226. package/src/core/primitives/$hook.ts +6 -2
  227. package/src/core/primitives/$module.spec.ts +4 -0
  228. package/src/core/primitives/$module.ts +12 -0
  229. package/src/core/providers/AlsProvider.ts +1 -1
  230. package/src/core/providers/CodecManager.spec.ts +12 -6
  231. package/src/core/providers/CodecManager.ts +26 -6
  232. package/src/core/providers/EventManager.ts +169 -13
  233. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +878 -0
  234. package/src/core/providers/KeylessJsonSchemaCodec.ts +789 -0
  235. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  236. package/src/core/providers/StateManager.spec.ts +27 -16
  237. package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
  238. package/src/email/providers/LocalEmailProvider.ts +52 -15
  239. package/src/email/providers/NodemailerEmailProvider.ts +167 -56
  240. package/src/file/errors/FileError.ts +7 -0
  241. package/src/file/index.ts +9 -1
  242. package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
  243. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  244. package/src/mcp/errors/McpError.ts +30 -0
  245. package/src/mcp/index.ts +3 -0
  246. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  247. package/src/orm/index.browser.ts +1 -19
  248. package/src/orm/index.bun.ts +77 -0
  249. package/src/orm/index.shared-server.ts +22 -0
  250. package/src/orm/index.shared.ts +15 -0
  251. package/src/orm/index.ts +19 -39
  252. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  253. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
  254. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  255. package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
  256. package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
  257. package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
  258. package/src/orm/services/Repository.ts +19 -0
  259. package/src/redis/index.bun.ts +35 -0
  260. package/src/redis/providers/BunRedisProvider.ts +12 -43
  261. package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
  262. package/src/redis/providers/NodeRedisProvider.ts +16 -34
  263. package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
  264. package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
  265. package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
  266. package/src/security/index.browser.ts +5 -0
  267. package/src/security/index.ts +90 -7
  268. package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
  269. package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
  270. package/src/security/primitives/$role.ts +5 -5
  271. package/src/security/primitives/$serviceAccount.spec.ts +5 -5
  272. package/src/security/primitives/$serviceAccount.ts +3 -3
  273. package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
  274. package/src/server/auth/primitives/$auth.ts +10 -10
  275. package/src/server/auth/primitives/$authCredentials.ts +3 -3
  276. package/src/server/auth/primitives/$authGithub.ts +3 -3
  277. package/src/server/auth/primitives/$authGoogle.ts +3 -3
  278. package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
  279. package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
  280. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
  281. package/src/server/core/index.ts +1 -1
  282. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  283. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  284. package/src/server/core/providers/NodeHttpServerProvider.ts +92 -24
  285. package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
  286. package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
  287. package/src/server/core/providers/ServerProvider.ts +144 -24
  288. package/src/server/core/providers/ServerRouterProvider.ts +259 -115
  289. package/src/server/core/providers/ServerTimingProvider.ts +2 -2
  290. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  291. package/src/server/links/index.browser.ts +2 -0
  292. package/src/server/links/index.ts +3 -1
  293. package/src/server/links/providers/LinkProvider.ts +1 -1
  294. package/src/server/swagger/index.ts +1 -1
  295. package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
  296. package/src/sms/providers/LocalSmsProvider.ts +8 -7
  297. package/src/vite/index.ts +3 -2
  298. package/src/vite/tasks/buildClient.ts +0 -1
  299. package/src/vite/tasks/buildServer.ts +80 -22
  300. package/src/vite/tasks/copyAssets.ts +5 -4
  301. package/src/vite/tasks/generateCloudflare.ts +7 -0
  302. package/src/vite/tasks/generateSitemap.ts +64 -23
  303. package/src/vite/tasks/index.ts +0 -2
  304. package/src/vite/tasks/prerenderPages.ts +49 -24
  305. package/dist/server/security/index.browser.js +0 -13
  306. package/dist/server/security/index.browser.js.map +0 -1
  307. package/dist/server/security/index.d.ts +0 -173
  308. package/dist/server/security/index.d.ts.map +0 -1
  309. package/dist/server/security/index.js +0 -311
  310. package/dist/server/security/index.js.map +0 -1
  311. package/src/cli/assets/appRouterTs.ts +0 -9
  312. package/src/cli/assets/indexHtml.ts +0 -15
  313. package/src/cli/assets/mainTs.ts +0 -13
  314. package/src/cli/commands/format.ts +0 -17
  315. package/src/server/security/index.browser.ts +0 -10
  316. package/src/server/security/index.ts +0 -94
  317. package/src/vite/helpers/boot.ts +0 -106
  318. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  319. package/src/vite/tasks/devServer.ts +0 -69
  320. package/src/vite/tasks/runAlepha.ts +0 -270
  321. /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
  322. /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
@@ -0,0 +1,236 @@
1
+ import { Alepha, t } from "alepha";
2
+ import { describe, test } from "vitest";
3
+ import { SchemaValidator } from "./SchemaValidator.ts";
4
+
5
+ describe("SchemaValidator", () => {
6
+ describe("Basic validation", () => {
7
+ test("should validate simple objects", async ({ expect }) => {
8
+ const alepha = Alepha.create();
9
+ const validator = alepha.inject(SchemaValidator);
10
+
11
+ const schema = t.object({
12
+ name: t.text(),
13
+ age: t.integer(),
14
+ });
15
+
16
+ const data = {
17
+ name: "Alice",
18
+ age: 30,
19
+ };
20
+
21
+ const result = validator.validate(schema, data);
22
+ expect(result).toEqual(data);
23
+ });
24
+
25
+ test("should trim strings when schema has trim option", async ({
26
+ expect,
27
+ }) => {
28
+ const alepha = Alepha.create();
29
+ const validator = alepha.inject(SchemaValidator);
30
+
31
+ const schema = t.object({
32
+ name: t.text({ trim: true }),
33
+ });
34
+
35
+ const data = {
36
+ name: " Alice ",
37
+ };
38
+
39
+ const result = validator.validate(schema, data);
40
+ expect(result.name).toBe("Alice");
41
+ });
42
+
43
+ test("should convert null to undefined for non-nullable fields", async ({
44
+ expect,
45
+ }) => {
46
+ const alepha = Alepha.create();
47
+ const validator = alepha.inject(SchemaValidator);
48
+
49
+ const schema = t.object({
50
+ name: t.text(),
51
+ bio: t.optional(t.text()),
52
+ });
53
+
54
+ const data = {
55
+ name: "Alice",
56
+ bio: null,
57
+ };
58
+
59
+ const result = validator.validate(schema, data);
60
+ expect(result.name).toBe("Alice");
61
+ expect(result.bio).toBeUndefined();
62
+ });
63
+ });
64
+
65
+ describe("Security - Prototype Pollution Protection", () => {
66
+ test("should filter out __proto__ key from input data", async ({
67
+ expect,
68
+ }) => {
69
+ const alepha = Alepha.create();
70
+ const validator = alepha.inject(SchemaValidator);
71
+
72
+ const schema = t.object({
73
+ name: t.text(),
74
+ });
75
+
76
+ // Input data with __proto__ key via JSON.parse (bypasses literal protection)
77
+ const data = JSON.parse('{"name":"Alice","__proto__":{"isAdmin":true}}');
78
+
79
+ const result = validator.validate(schema, data);
80
+
81
+ // __proto__ should not be an own property in the result
82
+ expect(result.name).toBe("Alice");
83
+ // Using hasOwnProperty because 'in' operator has special behavior for __proto__
84
+ expect(Object.hasOwn(result, "__proto__")).toBe(false);
85
+ });
86
+
87
+ test("should filter out constructor key from input data", async ({
88
+ expect,
89
+ }) => {
90
+ const alepha = Alepha.create();
91
+ const validator = alepha.inject(SchemaValidator);
92
+
93
+ const schema = t.object({
94
+ name: t.text(),
95
+ });
96
+
97
+ const data = {
98
+ name: "Alice",
99
+ constructor: { prototype: { isAdmin: true } },
100
+ };
101
+
102
+ const result = validator.validate(schema, data);
103
+
104
+ expect(result.name).toBe("Alice");
105
+ expect(Object.hasOwn(result, "constructor")).toBe(false);
106
+ });
107
+
108
+ test("should filter out prototype key from input data", async ({
109
+ expect,
110
+ }) => {
111
+ const alepha = Alepha.create();
112
+ const validator = alepha.inject(SchemaValidator);
113
+
114
+ const schema = t.object({
115
+ name: t.text(),
116
+ });
117
+
118
+ const data = {
119
+ name: "Alice",
120
+ prototype: { isAdmin: true },
121
+ };
122
+
123
+ const result = validator.validate(schema, data);
124
+
125
+ expect(result.name).toBe("Alice");
126
+ expect(Object.hasOwn(result, "prototype")).toBe(false);
127
+ });
128
+
129
+ test("should filter unsafe keys from nested objects", async ({
130
+ expect,
131
+ }) => {
132
+ const alepha = Alepha.create();
133
+ const validator = alepha.inject(SchemaValidator);
134
+
135
+ const schema = t.object({
136
+ user: t.object({
137
+ name: t.text(),
138
+ }),
139
+ });
140
+
141
+ const data = {
142
+ user: {
143
+ name: "Alice",
144
+ __proto__: { isAdmin: true },
145
+ },
146
+ };
147
+
148
+ const result = validator.validate(schema, data);
149
+
150
+ expect(result.user.name).toBe("Alice");
151
+ expect(Object.hasOwn(result.user, "__proto__")).toBe(false);
152
+ });
153
+
154
+ test("should not pollute Object.prototype", async ({ expect }) => {
155
+ const alepha = Alepha.create();
156
+ const validator = alepha.inject(SchemaValidator);
157
+
158
+ const schema = t.object({
159
+ name: t.text(),
160
+ });
161
+
162
+ // Attempt to pollute Object.prototype via __proto__
163
+ const maliciousData = JSON.parse(
164
+ '{"name":"Alice","__proto__":{"polluted":"yes"}}',
165
+ );
166
+
167
+ validator.validate(schema, maliciousData);
168
+
169
+ // Object.prototype should not be polluted
170
+ expect(({} as any).polluted).toBeUndefined();
171
+ });
172
+
173
+ test("should create objects without prototype chain", async ({
174
+ expect,
175
+ }) => {
176
+ const alepha = Alepha.create();
177
+ const validator = alepha.inject(SchemaValidator);
178
+
179
+ const schema = t.object({
180
+ name: t.text(),
181
+ });
182
+
183
+ const data = {
184
+ name: "Alice",
185
+ };
186
+
187
+ const result = validator.validate(schema, data);
188
+
189
+ // The result should not have Object.prototype methods directly accessible
190
+ // via hasOwnProperty (it should use Object.create(null))
191
+ expect(result.name).toBe("Alice");
192
+ });
193
+ });
194
+
195
+ describe("beforeParse", () => {
196
+ test("should handle arrays correctly", async ({ expect }) => {
197
+ const alepha = Alepha.create();
198
+ const validator = alepha.inject(SchemaValidator);
199
+
200
+ const schema = t.object({
201
+ tags: t.array(t.text({ trim: true })),
202
+ });
203
+
204
+ const data = {
205
+ tags: [" tag1 ", " tag2 "],
206
+ };
207
+
208
+ const result = validator.validate(schema, data);
209
+ expect(result.tags).toEqual(["tag1", "tag2"]);
210
+ });
211
+
212
+ test("should delete undefined values when option is enabled", async ({
213
+ expect,
214
+ }) => {
215
+ const alepha = Alepha.create();
216
+ const validator = alepha.inject(SchemaValidator);
217
+
218
+ const schema = t.object({
219
+ name: t.text(),
220
+ bio: t.optional(t.text()),
221
+ });
222
+
223
+ const data = {
224
+ name: "Alice",
225
+ bio: undefined,
226
+ };
227
+
228
+ const result = validator.validate(schema, data, {
229
+ deleteUndefined: true,
230
+ });
231
+
232
+ expect(result.name).toBe("Alice");
233
+ expect("bio" in result).toBe(false);
234
+ });
235
+ });
236
+ });
@@ -209,12 +209,16 @@ describe("StateManager", () => {
209
209
  });
210
210
 
211
211
  it("should set values in ALS when context exists", async () => {
212
- const store: any = { context: "test-context" };
213
- alepha.context.run(() => {
214
- stateManager.set("name", "ALS Value");
215
- }, store);
212
+ // Note: AlsProvider spreads the store object, so we check values inside the callback
213
+ const result = alepha.context.run(
214
+ () => {
215
+ stateManager.set("name", "ALS Value");
216
+ return stateManager.get("name");
217
+ },
218
+ { context: "test-context" },
219
+ );
216
220
 
217
- expect(store.name).toBe("ALS Value");
221
+ expect(result).toBe("ALS Value");
218
222
  });
219
223
 
220
224
  it("should set values in local store when no ALS context", () => {
@@ -267,14 +271,16 @@ describe("StateManager", () => {
267
271
  stateManager.set("name", "Local Value");
268
272
 
269
273
  // In ALS context, ALS values should take priority
270
- const store: any = { context: "test-context" };
271
- const result = alepha.context.run(() => {
272
- stateManager.set("name", "ALS Priority");
273
- return stateManager.get("name");
274
- }, store);
274
+ // Note: AlsProvider spreads the store object, so we check values inside the callback
275
+ const result = alepha.context.run(
276
+ () => {
277
+ stateManager.set("name", "ALS Priority");
278
+ return stateManager.get("name");
279
+ },
280
+ { context: "test-context" },
281
+ );
275
282
 
276
283
  expect(result).toBe("ALS Priority");
277
- expect(store.name).toBe("ALS Priority");
278
284
  });
279
285
 
280
286
  it("should not skip event emission when values are the same", async () => {
@@ -291,12 +297,17 @@ describe("StateManager", () => {
291
297
  });
292
298
 
293
299
  it("should delete values from ALS when context exists", async () => {
294
- const store = { name: "To Delete", context: "test-context" };
295
- alepha.context.run(() => {
296
- stateManager.del("name");
297
- }, store);
300
+ // Note: AlsProvider spreads the store object, so we check values inside the callback
301
+ const result = alepha.context.run(
302
+ () => {
303
+ stateManager.set("name", "To Delete");
304
+ stateManager.del("name");
305
+ return stateManager.get("name");
306
+ },
307
+ { context: "test-context" },
308
+ );
298
309
 
299
- expect(store.name).toBeUndefined();
310
+ expect(result).toBeUndefined();
300
311
  });
301
312
 
302
313
  it("should clear only local store, not ALS", async () => {
@@ -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", () => {