alepha 0.15.4 → 0.15.5

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 (282) hide show
  1. package/README.md +26 -11
  2. package/dist/api/audits/index.d.ts +3 -3
  3. package/dist/api/audits/index.js +3 -3
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +3 -3
  6. package/dist/api/files/index.js +3 -3
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +47 -4
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +100 -5
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/keys/index.d.ts +3 -3
  13. package/dist/api/keys/index.js +3 -3
  14. package/dist/api/keys/index.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.js +3 -3
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +263 -263
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/parameters/index.js +31 -30
  21. package/dist/api/parameters/index.js.map +1 -1
  22. package/dist/api/users/index.d.ts +373 -67
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +273 -72
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts +3 -3
  27. package/dist/api/verifications/index.js +3 -3
  28. package/dist/api/verifications/index.js.map +1 -1
  29. package/dist/batch/index.d.ts +7 -7
  30. package/dist/batch/index.js +3 -3
  31. package/dist/batch/index.js.map +1 -1
  32. package/dist/bucket/index.d.ts +3 -3
  33. package/dist/bucket/index.js +6 -6
  34. package/dist/bucket/index.js.map +1 -1
  35. package/dist/cache/core/index.d.ts +3 -3
  36. package/dist/cache/core/index.js +3 -3
  37. package/dist/cache/core/index.js.map +1 -1
  38. package/dist/cli/index.d.ts +5607 -20
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +103 -89
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/command/index.d.ts +11 -4
  43. package/dist/command/index.d.ts.map +1 -1
  44. package/dist/command/index.js +8 -6
  45. package/dist/command/index.js.map +1 -1
  46. package/dist/core/index.browser.js.map +1 -1
  47. package/dist/core/index.d.ts +4 -8
  48. package/dist/core/index.d.ts.map +1 -1
  49. package/dist/core/index.js +3 -3
  50. package/dist/core/index.js.map +1 -1
  51. package/dist/core/index.native.js.map +1 -1
  52. package/dist/datetime/index.d.ts +3 -3
  53. package/dist/datetime/index.js +3 -3
  54. package/dist/datetime/index.js.map +1 -1
  55. package/dist/email/index.d.ts +3 -3
  56. package/dist/email/index.js +8 -8
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +3 -3
  59. package/dist/fake/index.js +3 -3
  60. package/dist/fake/index.js.map +1 -1
  61. package/dist/lock/core/index.d.ts +3 -3
  62. package/dist/lock/core/index.js +3 -3
  63. package/dist/lock/core/index.js.map +1 -1
  64. package/dist/logger/index.d.ts +3 -3
  65. package/dist/logger/index.js +6 -3
  66. package/dist/logger/index.js.map +1 -1
  67. package/dist/mcp/index.d.ts +3 -3
  68. package/dist/mcp/index.js +3 -3
  69. package/dist/mcp/index.js.map +1 -1
  70. package/dist/orm/index.d.ts +12 -12
  71. package/dist/orm/index.js +4 -4
  72. package/dist/orm/index.js.map +1 -1
  73. package/dist/queue/core/index.d.ts +3 -3
  74. package/dist/queue/core/index.js +3 -3
  75. package/dist/queue/core/index.js.map +1 -1
  76. package/dist/react/auth/index.d.ts +3 -3
  77. package/dist/react/auth/index.js +3 -3
  78. package/dist/react/auth/index.js.map +1 -1
  79. package/dist/react/core/index.d.ts +3 -3
  80. package/dist/react/core/index.js +3 -3
  81. package/dist/react/core/index.js.map +1 -1
  82. package/dist/react/form/index.d.ts +3 -3
  83. package/dist/react/form/index.js +3 -3
  84. package/dist/react/form/index.js.map +1 -1
  85. package/dist/react/head/index.d.ts +3 -3
  86. package/dist/react/head/index.js +3 -3
  87. package/dist/react/head/index.js.map +1 -1
  88. package/dist/react/i18n/index.d.ts +3 -3
  89. package/dist/react/i18n/index.js +3 -3
  90. package/dist/react/i18n/index.js.map +1 -1
  91. package/dist/react/intro/index.css +337 -0
  92. package/dist/react/intro/index.css.map +1 -0
  93. package/dist/react/intro/index.d.ts +10 -0
  94. package/dist/react/intro/index.d.ts.map +1 -0
  95. package/dist/react/intro/index.js +222 -0
  96. package/dist/react/intro/index.js.map +1 -0
  97. package/dist/react/router/index.browser.js +2 -2
  98. package/dist/react/router/index.browser.js.map +1 -1
  99. package/dist/react/router/index.d.ts +1 -1
  100. package/dist/react/router/index.d.ts.map +1 -1
  101. package/dist/react/router/index.js +5 -5
  102. package/dist/react/router/index.js.map +1 -1
  103. package/dist/redis/index.d.ts +17 -17
  104. package/dist/redis/index.js +3 -3
  105. package/dist/redis/index.js.map +1 -1
  106. package/dist/retry/index.d.ts +3 -3
  107. package/dist/retry/index.js +3 -3
  108. package/dist/retry/index.js.map +1 -1
  109. package/dist/scheduler/index.d.ts +3 -3
  110. package/dist/scheduler/index.js +3 -3
  111. package/dist/scheduler/index.js.map +1 -1
  112. package/dist/security/index.d.ts +3 -3
  113. package/dist/security/index.js +5 -5
  114. package/dist/security/index.js.map +1 -1
  115. package/dist/server/auth/index.d.ts +3 -3
  116. package/dist/server/auth/index.js +3 -3
  117. package/dist/server/auth/index.js.map +1 -1
  118. package/dist/server/cache/index.d.ts +3 -3
  119. package/dist/server/cache/index.js +3 -3
  120. package/dist/server/cache/index.js.map +1 -1
  121. package/dist/server/compress/index.d.ts +3 -3
  122. package/dist/server/compress/index.js +3 -3
  123. package/dist/server/compress/index.js.map +1 -1
  124. package/dist/server/cookies/index.d.ts +3 -3
  125. package/dist/server/cookies/index.js +3 -3
  126. package/dist/server/cookies/index.js.map +1 -1
  127. package/dist/server/core/index.d.ts +5 -16
  128. package/dist/server/core/index.d.ts.map +1 -1
  129. package/dist/server/core/index.js +13 -29
  130. package/dist/server/core/index.js.map +1 -1
  131. package/dist/server/cors/index.d.ts +3 -3
  132. package/dist/server/cors/index.js +3 -3
  133. package/dist/server/cors/index.js.map +1 -1
  134. package/dist/server/health/index.d.ts +20 -20
  135. package/dist/server/health/index.js +3 -3
  136. package/dist/server/health/index.js.map +1 -1
  137. package/dist/server/helmet/index.d.ts +3 -3
  138. package/dist/server/helmet/index.js +3 -3
  139. package/dist/server/helmet/index.js.map +1 -1
  140. package/dist/server/links/index.d.ts +42 -42
  141. package/dist/server/links/index.d.ts.map +1 -1
  142. package/dist/server/links/index.js +3 -3
  143. package/dist/server/links/index.js.map +1 -1
  144. package/dist/server/metrics/index.d.ts +3 -3
  145. package/dist/server/metrics/index.js +3 -3
  146. package/dist/server/metrics/index.js.map +1 -1
  147. package/dist/server/multipart/index.d.ts +3 -3
  148. package/dist/server/multipart/index.js +3 -3
  149. package/dist/server/multipart/index.js.map +1 -1
  150. package/dist/server/proxy/index.d.ts +3 -3
  151. package/dist/server/proxy/index.js +3 -3
  152. package/dist/server/proxy/index.js.map +1 -1
  153. package/dist/server/rate-limit/index.d.ts +3 -3
  154. package/dist/server/rate-limit/index.js +3 -3
  155. package/dist/server/rate-limit/index.js.map +1 -1
  156. package/dist/server/static/index.d.ts +3 -3
  157. package/dist/server/static/index.js +6 -6
  158. package/dist/server/static/index.js.map +1 -1
  159. package/dist/server/swagger/index.d.ts +3 -3
  160. package/dist/server/swagger/index.js +6 -6
  161. package/dist/server/swagger/index.js.map +1 -1
  162. package/dist/sms/index.d.ts +3 -3
  163. package/dist/sms/index.js +6 -6
  164. package/dist/sms/index.js.map +1 -1
  165. package/dist/system/index.d.ts +3 -3
  166. package/dist/system/index.js +3 -3
  167. package/dist/system/index.js.map +1 -1
  168. package/dist/thread/index.d.ts +3 -3
  169. package/dist/thread/index.js +3 -3
  170. package/dist/thread/index.js.map +1 -1
  171. package/dist/topic/core/index.d.ts +3 -3
  172. package/dist/topic/core/index.js +3 -3
  173. package/dist/topic/core/index.js.map +1 -1
  174. package/dist/vite/index.d.ts +6284 -3
  175. package/dist/vite/index.d.ts.map +1 -1
  176. package/dist/websocket/index.d.ts +3 -3
  177. package/dist/websocket/index.js +3 -3
  178. package/dist/websocket/index.js.map +1 -1
  179. package/package.json +7 -2
  180. package/src/api/audits/index.ts +3 -3
  181. package/src/api/files/index.ts +3 -3
  182. package/src/api/jobs/controllers/AdminJobController.ts +15 -2
  183. package/src/api/jobs/index.ts +4 -3
  184. package/src/api/jobs/services/JobAudits.spec.ts +89 -0
  185. package/src/api/jobs/services/JobAudits.ts +101 -0
  186. package/src/api/keys/index.ts +3 -3
  187. package/src/api/notifications/index.ts +3 -3
  188. package/src/api/parameters/index.ts +5 -3
  189. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
  190. package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
  191. package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
  192. package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
  193. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
  194. package/src/api/users/index.ts +8 -9
  195. package/src/api/users/primitives/$realm.ts +117 -19
  196. package/src/api/users/providers/RealmProvider.ts +15 -7
  197. package/src/api/users/services/CredentialService.spec.ts +11 -0
  198. package/src/api/users/services/CredentialService.ts +47 -24
  199. package/src/api/users/services/IdentityService.ts +12 -4
  200. package/src/api/users/services/RegistrationService.spec.ts +11 -0
  201. package/src/api/users/services/RegistrationService.ts +33 -12
  202. package/src/api/users/services/SessionService.ts +83 -12
  203. package/src/api/users/services/UserAudits.ts +47 -0
  204. package/src/api/users/services/UserFiles.ts +19 -0
  205. package/src/api/users/services/UserJobs.spec.ts +107 -0
  206. package/src/api/users/services/UserJobs.ts +62 -0
  207. package/src/api/users/services/UserParameters.ts +23 -0
  208. package/src/api/users/services/UserService.ts +34 -17
  209. package/src/api/verifications/index.ts +3 -3
  210. package/src/batch/index.ts +3 -3
  211. package/src/bucket/index.ts +3 -3
  212. package/src/cache/core/index.ts +3 -3
  213. package/src/cli/commands/db.ts +9 -0
  214. package/src/cli/commands/init.spec.ts +2 -17
  215. package/src/cli/commands/init.ts +37 -1
  216. package/src/cli/providers/ViteDevServerProvider.ts +5 -2
  217. package/src/cli/services/AlephaCliUtils.ts +17 -0
  218. package/src/cli/services/PackageManagerUtils.ts +15 -1
  219. package/src/cli/services/ProjectScaffolder.ts +8 -13
  220. package/src/cli/templates/agentMd.ts +2 -25
  221. package/src/cli/templates/apiAppSecurityTs.ts +37 -2
  222. package/src/cli/templates/mainCss.ts +2 -32
  223. package/src/cli/templates/webAppRouterTs.ts +5 -5
  224. package/src/cli/templates/webHomeComponentTsx.ts +10 -0
  225. package/src/command/helpers/Runner.ts +14 -1
  226. package/src/command/index.ts +3 -3
  227. package/src/core/helpers/primitive.ts +0 -5
  228. package/src/core/index.ts +3 -3
  229. package/src/datetime/index.ts +3 -3
  230. package/src/email/index.ts +3 -3
  231. package/src/email/providers/LocalEmailProvider.ts +2 -2
  232. package/src/fake/index.ts +3 -3
  233. package/src/lock/core/index.ts +3 -3
  234. package/src/logger/index.ts +3 -3
  235. package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
  236. package/src/mcp/index.ts +3 -3
  237. package/src/orm/index.ts +3 -3
  238. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  239. package/src/queue/core/index.ts +3 -3
  240. package/src/react/auth/index.ts +3 -3
  241. package/src/react/core/index.ts +3 -3
  242. package/src/react/form/index.ts +3 -3
  243. package/src/react/head/index.ts +3 -3
  244. package/src/react/i18n/index.ts +3 -3
  245. package/src/react/intro/components/GettingStarted.css +334 -0
  246. package/src/react/intro/components/GettingStarted.tsx +276 -0
  247. package/src/react/intro/index.ts +1 -0
  248. package/src/react/router/index.browser.ts +2 -0
  249. package/src/react/router/index.ts +2 -0
  250. package/src/redis/index.ts +3 -3
  251. package/src/retry/index.ts +3 -3
  252. package/src/router/index.ts +3 -3
  253. package/src/scheduler/index.ts +3 -3
  254. package/src/security/index.ts +3 -3
  255. package/src/security/providers/JwtProvider.ts +2 -2
  256. package/src/server/auth/index.ts +3 -3
  257. package/src/server/cache/index.ts +3 -3
  258. package/src/server/compress/index.ts +3 -3
  259. package/src/server/cookies/index.ts +3 -3
  260. package/src/server/core/index.ts +3 -3
  261. package/src/server/core/primitives/$action.spec.ts +3 -2
  262. package/src/server/core/primitives/$action.ts +6 -2
  263. package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
  264. package/src/server/core/providers/ServerProvider.ts +4 -2
  265. package/src/server/core/providers/ServerRouterProvider.ts +5 -27
  266. package/src/server/cors/index.ts +3 -3
  267. package/src/server/health/index.ts +3 -3
  268. package/src/server/helmet/index.ts +3 -3
  269. package/src/server/links/index.ts +3 -3
  270. package/src/server/metrics/index.ts +3 -3
  271. package/src/server/multipart/index.ts +3 -3
  272. package/src/server/proxy/index.ts +3 -3
  273. package/src/server/rate-limit/index.ts +3 -3
  274. package/src/server/static/index.ts +3 -3
  275. package/src/server/swagger/index.ts +3 -3
  276. package/src/sms/index.ts +3 -3
  277. package/src/system/index.ts +3 -3
  278. package/src/thread/index.ts +3 -3
  279. package/src/topic/core/index.ts +3 -3
  280. package/src/websocket/index.ts +3 -3
  281. package/src/cli/templates/webHelloComponentTsx.ts +0 -30
  282. /package/src/api/users/{notifications → services}/UserNotifications.ts +0 -0
@@ -0,0 +1,107 @@
1
+ import { Alepha } from "alepha";
2
+ import { AlephaApiJobs } from "alepha/api/jobs";
3
+ import { $repository, AlephaOrm } from "alepha/orm";
4
+ import { describe, test } from "vitest";
5
+ import { sessions } from "../entities/sessions.ts";
6
+ import { users } from "../entities/users.ts";
7
+ import { UserJobs } from "./UserJobs.ts";
8
+
9
+ describe("UserJobs", () => {
10
+ describe("purgeExpiredSessions", () => {
11
+ test("should delete expired sessions", async ({ expect }) => {
12
+ const alepha = Alepha.create().with(AlephaOrm).with(AlephaApiJobs);
13
+
14
+ class TestRepositories {
15
+ userRepository = $repository(users);
16
+ sessionRepository = $repository(sessions);
17
+ }
18
+
19
+ const userJobs = alepha.inject(UserJobs);
20
+ const repos = alepha.inject(TestRepositories);
21
+ await alepha.start();
22
+
23
+ // Create a test user
24
+ const user = await repos.userRepository.create({
25
+ email: "test@example.com",
26
+ });
27
+
28
+ // Create expired sessions (expiresAt in the past)
29
+ const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // 1 day ago
30
+ await repos.sessionRepository.create({
31
+ userId: user.id,
32
+ refreshToken: crypto.randomUUID(),
33
+ expiresAt: pastDate,
34
+ });
35
+ await repos.sessionRepository.create({
36
+ userId: user.id,
37
+ refreshToken: crypto.randomUUID(),
38
+ expiresAt: pastDate,
39
+ });
40
+
41
+ // Create a valid session (expiresAt in the future)
42
+ const futureDate = new Date(
43
+ Date.now() + 24 * 60 * 60 * 1000,
44
+ ).toISOString(); // 1 day from now
45
+ await repos.sessionRepository.create({
46
+ userId: user.id,
47
+ refreshToken: crypto.randomUUID(),
48
+ expiresAt: futureDate,
49
+ });
50
+
51
+ // Verify we have 3 sessions
52
+ const sessionsBefore = await repos.sessionRepository.findMany();
53
+ expect(sessionsBefore).toHaveLength(3);
54
+
55
+ // Trigger the job
56
+ await userJobs.purgeExpiredSessions.trigger();
57
+
58
+ // Verify only the valid session remains
59
+ const sessionsAfter = await repos.sessionRepository.findMany();
60
+ expect(sessionsAfter).toHaveLength(1);
61
+ expect(new Date(sessionsAfter[0].expiresAt).getTime()).toBeGreaterThan(
62
+ Date.now(),
63
+ );
64
+ });
65
+
66
+ test("should handle case when no expired sessions exist", async ({
67
+ expect,
68
+ }) => {
69
+ const alepha = Alepha.create().with(AlephaOrm).with(AlephaApiJobs);
70
+
71
+ class TestRepositories {
72
+ userRepository = $repository(users);
73
+ sessionRepository = $repository(sessions);
74
+ }
75
+
76
+ const userJobs = alepha.inject(UserJobs);
77
+ const repos = alepha.inject(TestRepositories);
78
+ await alepha.start();
79
+
80
+ // Create a test user
81
+ const user = await repos.userRepository.create({
82
+ email: "test2@example.com",
83
+ });
84
+
85
+ // Create only valid sessions
86
+ const futureDate = new Date(
87
+ Date.now() + 24 * 60 * 60 * 1000,
88
+ ).toISOString();
89
+ await repos.sessionRepository.create({
90
+ userId: user.id,
91
+ refreshToken: crypto.randomUUID(),
92
+ expiresAt: futureDate,
93
+ });
94
+
95
+ // Verify we have 1 session
96
+ const sessionsBefore = await repos.sessionRepository.findMany();
97
+ expect(sessionsBefore).toHaveLength(1);
98
+
99
+ // Trigger the job - should not throw
100
+ await userJobs.purgeExpiredSessions.trigger();
101
+
102
+ // Session should still exist
103
+ const sessionsAfter = await repos.sessionRepository.findMany();
104
+ expect(sessionsAfter).toHaveLength(1);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,62 @@
1
+ import { $inject } from "alepha";
2
+ import { $job } from "alepha/api/jobs";
3
+ import { DateTimeProvider } from "alepha/datetime";
4
+ import { $logger } from "alepha/logger";
5
+ import { $repository } from "alepha/orm";
6
+ import { sessions } from "../entities/sessions.ts";
7
+
8
+ /**
9
+ * User-specific jobs wrapper service.
10
+ *
11
+ * This service handles user-related scheduled jobs such as:
12
+ * - Session purge (cleaning up expired sessions)
13
+ * - Verification code cleanup
14
+ * - Inactive user notifications
15
+ *
16
+ * It is lazy-loaded when the `jobs` feature is enabled in the realm.
17
+ */
18
+ export class UserJobs {
19
+ protected readonly log = $logger();
20
+ protected readonly dateTimeProvider = $inject(DateTimeProvider);
21
+ protected readonly sessionRepository = $repository(sessions);
22
+
23
+ /**
24
+ * Purge expired sessions from the database.
25
+ *
26
+ * This job runs daily at 3:00 AM and removes all sessions
27
+ * where the `expiresAt` timestamp has passed.
28
+ */
29
+ public readonly purgeExpiredSessions = $job({
30
+ name: "users.purgeExpiredSessions",
31
+ description: "Remove expired user sessions from the database",
32
+ cron: "0 3 * * *", // Daily at 3:00 AM
33
+ handler: async () => {
34
+ const now = this.dateTimeProvider.nowISOString();
35
+
36
+ this.log.info("Starting expired sessions purge", { cutoffTime: now });
37
+
38
+ const expiredSessions = await this.sessionRepository.findMany({
39
+ where: {
40
+ expiresAt: { lt: now },
41
+ },
42
+ });
43
+
44
+ if (expiredSessions.length === 0) {
45
+ this.log.info("No expired sessions found");
46
+ return;
47
+ }
48
+
49
+ this.log.info("Found expired sessions", {
50
+ count: expiredSessions.length,
51
+ });
52
+
53
+ const deletedIds = await this.sessionRepository.deleteMany({
54
+ expiresAt: { lt: now },
55
+ });
56
+
57
+ this.log.info("Expired sessions purged successfully", {
58
+ deletedCount: deletedIds.length,
59
+ });
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,23 @@
1
+ import { $config } from "alepha/api/parameters";
2
+ import { realmAuthSettingsAtom } from "../atoms/realmAuthSettingsAtom.ts";
3
+
4
+ /**
5
+ * User-specific configuration service.
6
+ *
7
+ * This service wraps the core ConfigStore to provide realm settings management.
8
+ * It is lazy-loaded when the `parameters` feature is enabled in the realm.
9
+ */
10
+ export class UserParameters {
11
+ /**
12
+ * Realm authentication settings configuration.
13
+ *
14
+ * Controls user registration, login methods, verification requirements,
15
+ * and password policies for the realm.
16
+ */
17
+ public readonly realmSettings = $config({
18
+ name: "alepha.api.users.realmSettings",
19
+ description: "Realm authentication and registration settings",
20
+ schema: realmAuthSettingsAtom.schema,
21
+ default: realmAuthSettingsAtom.options.default,
22
+ });
23
+ }
@@ -1,23 +1,38 @@
1
- import { $inject } from "alepha";
2
- import { AuditService } from "alepha/api/audits";
1
+ import { $inject, Alepha } from "alepha";
3
2
  import type { VerificationController } from "alepha/api/verifications";
4
3
  import { $logger } from "alepha/logger";
5
4
  import { type Page, parseQueryString } from "alepha/orm";
6
5
  import { BadRequestError } from "alepha/server";
7
6
  import { $client } from "alepha/server/links";
8
7
  import type { UserEntity } from "../entities/users.ts";
9
- import { UserNotifications } from "../notifications/UserNotifications.ts";
10
8
  import { RealmProvider } from "../providers/RealmProvider.ts";
11
9
  import type { CreateUser } from "../schemas/createUserSchema.ts";
12
10
  import type { UpdateUser } from "../schemas/updateUserSchema.ts";
13
11
  import type { UserQuery } from "../schemas/userQuerySchema.ts";
12
+ import { UserAudits } from "./UserAudits.ts";
13
+ import { UserNotifications } from "./UserNotifications.ts";
14
14
 
15
15
  export class UserService {
16
+ protected readonly alepha = $inject(Alepha);
16
17
  protected readonly log = $logger();
17
18
  protected readonly verificationController = $client<VerificationController>();
18
- protected readonly userNotifications = $inject(UserNotifications);
19
19
  protected readonly realmProvider = $inject(RealmProvider);
20
- protected readonly auditService = $inject(AuditService);
20
+
21
+ protected userAudits(realmName?: string) {
22
+ const realm = this.realmProvider.getRealm(realmName);
23
+ if (realm.features.audits) {
24
+ return this.alepha.inject(UserAudits);
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ protected userNotifications(realmName?: string) {
30
+ const realm = this.realmProvider.getRealm(realmName);
31
+ if (realm.features.notifications) {
32
+ return this.alepha.inject(UserNotifications);
33
+ }
34
+ return undefined;
35
+ }
21
36
 
22
37
  public users(userRealmName?: string) {
23
38
  return this.realmProvider.userRepository(userRealmName);
@@ -79,21 +94,23 @@ export class UserService {
79
94
  ? `${verifyUrl}${url.search}`
80
95
  : url.pathname + url.search;
81
96
 
82
- await this.userNotifications.emailVerificationLink.push({
83
- contact: email,
84
- variables: {
85
- email,
86
- verifyUrl: fullVerifyUrl,
87
- expiresInMinutes: Math.floor(verification.codeExpiration / 60),
97
+ await this.userNotifications(userRealmName)?.emailVerificationLink.push(
98
+ {
99
+ contact: email,
100
+ variables: {
101
+ email,
102
+ verifyUrl: fullVerifyUrl,
103
+ expiresInMinutes: Math.floor(verification.codeExpiration / 60),
104
+ },
88
105
  },
89
- });
106
+ );
90
107
 
91
108
  this.log.debug("Email verification link sent", {
92
109
  email,
93
110
  userId: user.id,
94
111
  });
95
112
  } else {
96
- await this.userNotifications.emailVerification.push({
113
+ await this.userNotifications(userRealmName)?.emailVerification.push({
97
114
  contact: email,
98
115
  variables: {
99
116
  email,
@@ -158,7 +175,7 @@ export class UserService {
158
175
 
159
176
  const realm = this.realmProvider.getRealm(userRealmName);
160
177
 
161
- await this.auditService.recordUser("update", {
178
+ await this.userAudits(userRealmName)?.recordUser("update", {
162
179
  userId: user.id,
163
180
  userEmail: email,
164
181
  userRealm: realm.name,
@@ -314,7 +331,7 @@ export class UserService {
314
331
  email: user.email,
315
332
  });
316
333
 
317
- await this.auditService.recordUser("create", {
334
+ await this.userAudits(userRealmName)?.recordUser("create", {
318
335
  userRealm: realm.name,
319
336
  resourceId: user.id,
320
337
  description: "User created",
@@ -357,7 +374,7 @@ export class UserService {
357
374
  data.roles !== undefined &&
358
375
  JSON.stringify(before.roles) !== JSON.stringify(data.roles);
359
376
 
360
- await this.auditService.recordUser(
377
+ await this.userAudits(userRealmName)?.recordUser(
361
378
  isRoleChange ? "role_change" : "update",
362
379
  {
363
380
  userRealm: realm.name,
@@ -384,7 +401,7 @@ export class UserService {
384
401
 
385
402
  const realm = this.realmProvider.getRealm(userRealmName);
386
403
 
387
- await this.auditService.recordUser("delete", {
404
+ await this.userAudits(userRealmName)?.recordUser("delete", {
388
405
  userRealm: realm.name,
389
406
  resourceId: id,
390
407
  severity: "warning",
@@ -18,9 +18,9 @@ export * from "./services/VerificationService.ts";
18
18
  // ---------------------------------------------------------------------------------------------------------------------
19
19
 
20
20
  /**
21
- * | type | quality | stability |
22
- * |------|---------|-----------|
23
- * | backend | standard | stable |
21
+ * | Stability | Since | Runtime |
22
+ * |-----------|-------|---------|
23
+ * | 3 - stable | 0.13.0 | node, bun, workerd|
24
24
  *
25
25
  * Email and phone verification workflows.
26
26
  *
@@ -10,9 +10,9 @@ export * from "./providers/BatchProvider.ts";
10
10
  // ---------------------------------------------------------------------------------------------------------------------
11
11
 
12
12
  /**
13
- * | type | quality | stability |
14
- * |------|---------|-----------|
15
- * | backend | standard | stable |
13
+ * | Stability | Since | Runtime |
14
+ * |-----------|-------|---------|
15
+ * | 3 - stable | 0.8.0 | node, bun|
16
16
  *
17
17
  * Batch accumulation and processing.
18
18
  *
@@ -43,9 +43,9 @@ declare module "alepha" {
43
43
  // ---------------------------------------------------------------------------------------------------------------------
44
44
 
45
45
  /**
46
- * | type | quality | stability |
47
- * |------|---------|-----------|
48
- * | backend | rare | stable |
46
+ * | Stability | Since | Runtime |
47
+ * |-----------|-------|---------|
48
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
49
49
  *
50
50
  * Unified file storage abstraction across multiple backends.
51
51
  *
@@ -12,9 +12,9 @@ export * from "./providers/MemoryCacheProvider.ts";
12
12
  // ---------------------------------------------------------------------------------------------------------------------
13
13
 
14
14
  /**
15
- * | type | quality | stability |
16
- * |------|---------|-----------|
17
- * | backend | rare | stable |
15
+ * | Stability | Since | Runtime |
16
+ * |-----------|-------|---------|
17
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
18
18
  *
19
19
  * Type-safe caching with TTL support.
20
20
  *
@@ -9,6 +9,7 @@ import type {
9
9
  import { FileSystemProvider } from "alepha/system";
10
10
  import { AppEntryProvider } from "../providers/AppEntryProvider.ts";
11
11
  import { AlephaCliUtils } from "../services/AlephaCliUtils.ts";
12
+ import { PackageManagerUtils } from "../services/PackageManagerUtils.ts";
12
13
 
13
14
  const drizzleCommandFlags = t.object({
14
15
  provider: t.optional(
@@ -29,6 +30,7 @@ export class DbCommand {
29
30
  protected readonly log = $logger();
30
31
  protected readonly fs = $inject(FileSystemProvider);
31
32
  protected readonly utils = $inject(AlephaCliUtils);
33
+ protected readonly pm = $inject(PackageManagerUtils);
32
34
  protected readonly entryProvider = $inject(AppEntryProvider);
33
35
 
34
36
  /**
@@ -316,6 +318,13 @@ export class DbCommand {
316
318
  this.log.info("");
317
319
  this.log.info(options.logMessage(providerName, dialect));
318
320
 
321
+ if (dialect === "sqlite") {
322
+ await this.pm.ensureDependency(options.root, "better-sqlite3", {
323
+ dev: true,
324
+ exec: (cmd, opts) => this.utils.exec(cmd, opts),
325
+ });
326
+ }
327
+
319
328
  const drizzleConfigJsPath = await this.prepareDrizzleConfig({
320
329
  kit: drizzleKitProvider,
321
330
  provider,
@@ -316,7 +316,7 @@ describe("alepha init", () => {
316
316
 
317
317
  expect(fs.wasWritten("/project/src/web/index.ts")).toBe(true);
318
318
  expect(fs.wasWritten("/project/src/web/AppRouter.ts")).toBe(true);
319
- expect(fs.wasWritten("/project/src/web/components/Hello.tsx")).toBe(true);
319
+ expect(fs.wasWritten("/project/src/web/components/Home.tsx")).toBe(true);
320
320
  });
321
321
 
322
322
  it("should create main.browser.ts for client-side entry", async () => {
@@ -331,28 +331,13 @@ describe("alepha init", () => {
331
331
  ).toBe(true);
332
332
  });
333
333
 
334
- it("should create main.css with base styles", async () => {
334
+ it("should create main.css", async () => {
335
335
  const { fs, cli, cmd, json } = createTestEnv();
336
336
  await setupProject(fs, json);
337
337
 
338
338
  await cli.run(cmd.init, { argv: "--react", root: "/project" });
339
339
 
340
340
  expect(fs.wasWritten("/project/src/main.css")).toBe(true);
341
- expect(fs.wasWrittenMatching("/project/src/main.css", /box-sizing/)).toBe(
342
- true,
343
- );
344
- });
345
-
346
- it("should create main.css with @alepha/ui import when --ui", async () => {
347
- const { fs, cli, cmd, json } = createTestEnv();
348
- await setupProject(fs, json);
349
-
350
- await cli.run(cmd.init, { argv: "--react --ui", root: "/project" });
351
-
352
- expect(fs.wasWritten("/project/src/main.css")).toBe(true);
353
- expect(
354
- fs.wasWrittenMatching("/project/src/main.css", /@alepha\/ui\/styles/),
355
- ).toBe(true);
356
341
  });
357
342
 
358
343
  it("should not create api structure without --api flag", async () => {
@@ -1,11 +1,14 @@
1
1
  import { $inject, t } from "alepha";
2
2
  import { $command } from "alepha/command";
3
+ import { $logger, ConsoleColorProvider } from "alepha/logger";
3
4
  import { FileSystemProvider } from "alepha/system";
4
5
  import { AlephaCliUtils } from "../services/AlephaCliUtils.ts";
5
6
  import { PackageManagerUtils } from "../services/PackageManagerUtils.ts";
6
7
  import { ProjectScaffolder } from "../services/ProjectScaffolder.ts";
7
8
 
8
9
  export class InitCommand {
10
+ protected readonly log = $logger();
11
+ protected readonly colors = $inject(ConsoleColorProvider);
9
12
  protected readonly utils = $inject(AlephaCliUtils);
10
13
  protected readonly pm = $inject(PackageManagerUtils);
11
14
  protected readonly scaffolder = $inject(ProjectScaffolder);
@@ -106,6 +109,11 @@ export class InitCommand {
106
109
 
107
110
  const isExpo = await this.pm.hasExpo(root);
108
111
 
112
+ // Get git email for admin auto-promotion (if auth enabled)
113
+ const adminEmail = flags.auth
114
+ ? await this.utils.getGitEmail()
115
+ : undefined;
116
+
109
117
  const force = !!flags.force;
110
118
 
111
119
  await run({
@@ -113,9 +121,9 @@ export class InitCommand {
113
121
  handler: async () => {
114
122
  await this.scaffolder.ensureConfig(root, {
115
123
  force,
116
- tsconfigJson: !workspace.config.tsconfigJson,
117
124
  packageJson: { ...flags, isPackage: workspace.isPackage },
118
125
  // Skip workspace-level configs if they exist at workspace root
126
+ tsconfigJson: !workspace.config.tsconfigJson,
119
127
  biomeJson: !workspace.config.biomeJson,
120
128
  editorconfig: !workspace.config.editorconfig,
121
129
  agentMd: agentType
@@ -132,6 +140,7 @@ export class InitCommand {
132
140
  if (flags.api) {
133
141
  await this.scaffolder.ensureApiProject(root, {
134
142
  auth: !!flags.auth,
143
+ adminEmail,
135
144
  force,
136
145
  });
137
146
  }
@@ -196,6 +205,33 @@ export class InitCommand {
196
205
  });
197
206
  }
198
207
  }
208
+
209
+ run.end();
210
+
211
+ // Success message
212
+ const projectName = args || ".";
213
+ const pmRun = pmName === "npm" ? "npm run" : pmName;
214
+ const c = this.colors;
215
+
216
+ this.log.info("");
217
+ this.log.info(` ${c.set("GREEN", "✓")} Project ready!`);
218
+ this.log.info("");
219
+ this.log.info(
220
+ ` ${c.set("GREY_DARK", "$")} cd ${c.set("CYAN", projectName)}`,
221
+ );
222
+ this.log.info(
223
+ ` ${c.set("GREY_DARK", "$")} ${c.set("CYAN", `${pmRun} dev`)}`,
224
+ );
225
+
226
+ if (adminEmail) {
227
+ this.log.info("");
228
+ this.log.info(` Admin email: ${c.set("GREEN", adminEmail)}`);
229
+ this.log.info(
230
+ ` ${c.set("GREY_DARK", "(from git config, change in src/api/AppSecurity.ts)")}`,
231
+ );
232
+ }
233
+
234
+ this.log.info("");
199
235
  },
200
236
  });
201
237
  }
@@ -219,6 +219,9 @@ export class ViteDevServerProvider {
219
219
  );
220
220
  }
221
221
 
222
+ // expose Vite server to Alepha for Logger SSR Fix stack traces
223
+ alepha.store.set("alepha.vite.server" as any, this.server);
224
+
222
225
  this.alepha = alepha;
223
226
  await this.setupAlepha();
224
227
 
@@ -343,8 +346,8 @@ export class ViteDevServerProvider {
343
346
  const originalEnd = res.end.bind(res);
344
347
 
345
348
  const guardedCall = <T>(fn: (...args: any[]) => T, ...args: any[]): T => {
346
- if (resolved && !ctx.metadata.vite) {
347
- // Vite didn't handle this request, silently ignore late writes
349
+ if (resolved && ctx.metadata.vite) {
350
+ // Vite already handled this request, ignore late writes from framework
348
351
  return undefined as T;
349
352
  }
350
353
  return fn(...args);
@@ -161,4 +161,21 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
161
161
  return "unknown";
162
162
  }
163
163
  }
164
+
165
+ /**
166
+ * Get the user's email from git config.
167
+ *
168
+ * @returns The git user email or undefined if not configured
169
+ */
170
+ public async getGitEmail(): Promise<string | undefined> {
171
+ try {
172
+ const result = await this.shell.run("git config user.email", {
173
+ capture: true,
174
+ });
175
+ const email = result.trim();
176
+ return email || undefined;
177
+ } catch {
178
+ return undefined;
179
+ }
180
+ }
164
181
  }
@@ -48,6 +48,7 @@ export class PackageManagerUtils {
48
48
 
49
49
  /**
50
50
  * Detect the package manager used in the project.
51
+ * Checks current directory first, then workspace root if in a monorepo.
51
52
  */
52
53
  public async getPackageManager(
53
54
  root: string,
@@ -55,10 +56,21 @@ export class PackageManagerUtils {
55
56
  ): Promise<"yarn" | "pnpm" | "npm" | "bun"> {
56
57
  if (pm) return pm;
57
58
  if (this.alepha.isBun()) return "bun";
59
+
60
+ // Check current directory first
58
61
  if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
59
62
  if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
60
63
  if (await this.fs.exists(this.fs.join(root, "pnpm-lock.yaml")))
61
64
  return "pnpm";
65
+ if (await this.fs.exists(this.fs.join(root, "package-lock.json")))
66
+ return "npm";
67
+
68
+ // Check workspace root (for monorepo packages like apps/blog)
69
+ const workspace = await this.getWorkspaceContext(root);
70
+ if (workspace.packageManager) {
71
+ return workspace.packageManager;
72
+ }
73
+
62
74
  return "npm";
63
75
  }
64
76
 
@@ -358,7 +370,9 @@ export class PackageManagerUtils {
358
370
  alepha: `^${version}`,
359
371
  };
360
372
 
361
- const devDependencies: Record<string, string> = {};
373
+ const devDependencies: Record<string, string> = {
374
+ vite: alephaDeps.vite,
375
+ };
362
376
 
363
377
  // Add biome/vitest only if not a workspace package (workspace root has them)
364
378
  if (!modes.isPackage) {
@@ -19,7 +19,7 @@ import { mainCss } from "../templates/mainCss.ts";
19
19
  import { mainServerTs } from "../templates/mainServerTs.ts";
20
20
  import { tsconfigJson } from "../templates/tsconfigJson.ts";
21
21
  import { webAppRouterTs } from "../templates/webAppRouterTs.ts";
22
- import { webHelloComponentTsx } from "../templates/webHelloComponentTsx.ts";
22
+ import { webHomeComponentTsx } from "../templates/webHomeComponentTsx.ts";
23
23
  import { webIndexTs } from "../templates/webIndexTs.ts";
24
24
  import { AlephaCliUtils } from "./AlephaCliUtils.ts";
25
25
  import {
@@ -34,7 +34,7 @@ import {
34
34
  * - Project structure (src/api, src/web)
35
35
  * - Configuration files (tsconfig, biome, editorconfig)
36
36
  * - Entry points (main.server.ts, main.browser.ts)
37
- * - Example code (HelloController, Hello component)
37
+ * - Example code (HelloController, Home component)
38
38
  */
39
39
  export class ProjectScaffolder {
40
40
  protected readonly log = $logger();
@@ -228,7 +228,7 @@ export class ProjectScaffolder {
228
228
  */
229
229
  public async ensureApiProject(
230
230
  root: string,
231
- opts: { auth?: boolean; force?: boolean } = {},
231
+ opts: { auth?: boolean; adminEmail?: string; force?: boolean } = {},
232
232
  ): Promise<void> {
233
233
  const appName = this.getAppName(root);
234
234
 
@@ -256,7 +256,7 @@ export class ProjectScaffolder {
256
256
  await this.ensureFile(
257
257
  root,
258
258
  "src/api/AppSecurity.ts",
259
- apiAppSecurityTs(),
259
+ apiAppSecurityTs({ adminEmail: opts.adminEmail }),
260
260
  opts.force,
261
261
  );
262
262
  }
@@ -272,7 +272,7 @@ export class ProjectScaffolder {
272
272
  * Creates:
273
273
  * - src/main.browser.ts
274
274
  * - src/main.css
275
- * - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
275
+ * - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Home.tsx
276
276
  */
277
277
  public async ensureWebProject(
278
278
  root: string,
@@ -292,12 +292,7 @@ export class ProjectScaffolder {
292
292
  });
293
293
 
294
294
  // src/main.css
295
- await this.ensureFile(
296
- root,
297
- "src/main.css",
298
- mainCss({ ui: opts.ui }),
299
- opts.force,
300
- );
295
+ await this.ensureFile(root, "src/main.css", mainCss(), opts.force);
301
296
 
302
297
  // Web structure
303
298
  await this.ensureFile(
@@ -319,8 +314,8 @@ export class ProjectScaffolder {
319
314
  );
320
315
  await this.ensureFile(
321
316
  root,
322
- "src/web/components/Hello.tsx",
323
- webHelloComponentTsx({ auth: opts.auth }),
317
+ "src/web/components/Home.tsx",
318
+ webHomeComponentTsx(),
324
319
  opts.force,
325
320
  );
326
321
  await this.ensureFile(