alepha 0.19.2 → 0.19.4

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 (241) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +90 -34
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +267 -44
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/notifications/index.browser.js +0 -1
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/notifications/index.js +0 -1
  18. package/dist/api/notifications/index.js.map +1 -1
  19. package/dist/api/parameters/index.browser.js +112 -1
  20. package/dist/api/parameters/index.browser.js.map +1 -1
  21. package/dist/api/parameters/index.d.ts +90 -3
  22. package/dist/api/parameters/index.d.ts.map +1 -1
  23. package/dist/api/parameters/index.js +79 -12
  24. package/dist/api/parameters/index.js.map +1 -1
  25. package/dist/{billing → api/payments}/index.d.ts +67 -49
  26. package/dist/api/payments/index.d.ts.map +1 -0
  27. package/dist/{billing → api/payments}/index.js +108 -74
  28. package/dist/api/payments/index.js.map +1 -0
  29. package/dist/api/subscriptions/index.d.ts +1692 -0
  30. package/dist/api/subscriptions/index.d.ts.map +1 -0
  31. package/dist/api/subscriptions/index.js +1870 -0
  32. package/dist/api/subscriptions/index.js.map +1 -0
  33. package/dist/api/users/index.d.ts +27 -21
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +167 -34
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/workflows/index.browser.js +246 -0
  38. package/dist/api/workflows/index.browser.js.map +1 -0
  39. package/dist/api/workflows/index.d.ts +1618 -0
  40. package/dist/api/workflows/index.d.ts.map +1 -0
  41. package/dist/api/workflows/index.js +1504 -0
  42. package/dist/api/workflows/index.js.map +1 -0
  43. package/dist/cli/config/index.d.ts +6 -28
  44. package/dist/cli/config/index.d.ts.map +1 -1
  45. package/dist/cli/config/index.js +5 -10
  46. package/dist/cli/config/index.js.map +1 -1
  47. package/dist/cli/core/index.d.ts +11669 -208
  48. package/dist/cli/core/index.d.ts.map +1 -1
  49. package/dist/cli/core/index.js +60 -69
  50. package/dist/cli/core/index.js.map +1 -1
  51. package/dist/cli/devtools/index.d.ts +5 -0
  52. package/dist/cli/devtools/index.d.ts.map +1 -1
  53. package/dist/cli/devtools/index.js +4 -0
  54. package/dist/cli/devtools/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +69 -64
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +6 -2
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +38 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +85 -26
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/core/index.browser.js +21 -2
  64. package/dist/core/index.browser.js.map +1 -1
  65. package/dist/core/index.d.ts +33 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +25 -2
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.native.js +25 -2
  70. package/dist/core/index.native.js.map +1 -1
  71. package/dist/core/index.workerd.js +25 -2
  72. package/dist/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js +24 -8
  74. package/dist/email/smtp/index.js.map +1 -1
  75. package/dist/logger/index.d.ts.map +1 -1
  76. package/dist/logger/index.js +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +25 -73
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +10 -32
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +25 -73
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts +2 -1
  89. package/dist/orm/postgres/index.d.ts.map +1 -1
  90. package/dist/orm/postgres/index.js +3 -3
  91. package/dist/orm/postgres/index.js.map +1 -1
  92. package/dist/react/router/index.browser.js +25 -3
  93. package/dist/react/router/index.browser.js.map +1 -1
  94. package/dist/react/router/index.d.ts +16 -1
  95. package/dist/react/router/index.d.ts.map +1 -1
  96. package/dist/react/router/index.js +25 -3
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/security/index.d.ts +28 -0
  99. package/dist/security/index.d.ts.map +1 -1
  100. package/dist/security/index.js +28 -0
  101. package/dist/security/index.js.map +1 -1
  102. package/package.json +37 -20
  103. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  104. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  105. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  106. package/src/api/invitations/entities/invitations.ts +33 -0
  107. package/src/api/invitations/index.ts +65 -0
  108. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  109. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  110. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  111. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  112. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  113. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  114. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  115. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  116. package/src/api/invitations/services/InvitationService.ts +556 -0
  117. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  118. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  119. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  120. package/src/api/jobs/index.ts +0 -3
  121. package/src/api/jobs/primitives/$job.ts +22 -11
  122. package/src/api/jobs/providers/JobProvider.ts +239 -25
  123. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  124. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  125. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  126. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  127. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  128. package/src/api/jobs/services/JobService.ts +51 -12
  129. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  130. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  131. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  132. package/src/api/parameters/index.browser.ts +12 -0
  133. package/src/api/parameters/primitives/$parameter.ts +20 -3
  134. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  135. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  136. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  137. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  138. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  139. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  140. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  141. package/src/{billing → api/payments}/index.ts +31 -25
  142. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  143. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  144. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  145. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  146. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  147. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  148. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  149. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  150. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  151. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  152. package/src/api/subscriptions/index.ts +144 -0
  153. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  154. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  155. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  156. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  157. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  158. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  159. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  160. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  161. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  162. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  163. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  164. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  165. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  166. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  167. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  168. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  169. package/src/api/subscriptions/services/BillingService.ts +437 -0
  170. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  171. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  172. package/src/api/subscriptions/services/UsageService.ts +118 -0
  173. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  174. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  175. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  176. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  177. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  178. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  179. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  180. package/src/api/users/controllers/UserController.ts +3 -8
  181. package/src/api/users/notifications/UserNotifications.ts +23 -0
  182. package/src/api/users/schemas/loginSchema.ts +1 -1
  183. package/src/api/users/services/CredentialService.ts +51 -4
  184. package/src/api/users/services/RegistrationService.ts +38 -9
  185. package/src/api/users/services/SessionService.ts +62 -9
  186. package/src/api/users/services/UserService.ts +21 -12
  187. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  188. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  189. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  190. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  191. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  192. package/src/api/workflows/index.browser.ts +22 -0
  193. package/src/api/workflows/index.ts +124 -0
  194. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  195. package/src/api/workflows/primitives/$workflow.ts +202 -0
  196. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  197. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  198. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  199. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  200. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  201. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  202. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  203. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  204. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  205. package/src/api/workflows/services/WorkflowService.ts +382 -0
  206. package/src/cli/config/defineConfig.ts +17 -46
  207. package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
  208. package/src/cli/core/services/PackageManagerUtils.ts +3 -1
  209. package/src/cli/core/services/ProjectScaffolder.ts +5 -5
  210. package/src/cli/core/templates/agentMd.ts +14 -5
  211. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  212. package/src/cli/devtools/index.ts +21 -1
  213. package/src/cli/platform/index.ts +23 -2
  214. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  215. package/src/cli/vendor/index.ts +20 -3
  216. package/src/cli/vendor/services/VendorService.ts +126 -27
  217. package/src/core/Alepha.ts +10 -0
  218. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  219. package/src/core/providers/SchemaValidator.ts +1 -1
  220. package/src/core/providers/TypeProvider.ts +46 -3
  221. package/src/logger/index.ts +6 -1
  222. package/src/orm/__tests__/enums.spec.ts +22 -29
  223. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  224. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  225. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  226. package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
  227. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  228. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  229. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  230. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  231. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  232. package/src/security/primitives/$secure.ts +28 -0
  233. package/tsconfig.base.json +0 -1
  234. package/dist/billing/index.d.ts.map +0 -1
  235. package/dist/billing/index.js.map +0 -1
  236. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  237. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  238. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  239. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  240. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  241. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -0,0 +1,118 @@
1
+ import { $inject } from "alepha";
2
+ import { CacheProvider } from "alepha/cache";
3
+ import { DateTimeProvider } from "alepha/datetime";
4
+ import { SubscriptionService } from "./SubscriptionService.ts";
5
+
6
+ // -----------------------------------------------------------------------------------------------------------------
7
+
8
+ /**
9
+ * The result of a usage check or increment operation.
10
+ */
11
+ export interface UsageResult {
12
+ /**
13
+ * Whether the operation is allowed given the current usage and limit.
14
+ */
15
+ allowed: boolean;
16
+
17
+ /**
18
+ * Current usage count for the period.
19
+ */
20
+ current: number;
21
+
22
+ /**
23
+ * The plan limit for the resource. -1 means unlimited.
24
+ */
25
+ limit: number;
26
+
27
+ /**
28
+ * Remaining capacity. -1 means unlimited.
29
+ */
30
+ remaining: number;
31
+ }
32
+
33
+ // -----------------------------------------------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Tracks and enforces per-organization resource usage limits.
37
+ *
38
+ * Usage counters are keyed by `organizationId:resource:YYYY-MM` and stored in the cache.
39
+ * Limits are resolved from the organization's current subscription plan.
40
+ */
41
+ export class UsageService {
42
+ protected readonly cache = $inject(CacheProvider);
43
+ protected readonly dateTime = $inject(DateTimeProvider);
44
+ protected readonly subscriptionService = $inject(SubscriptionService);
45
+
46
+ /**
47
+ * Increment a resource counter for the current period and return the usage result.
48
+ *
49
+ * @param organizationId The organization to track usage for.
50
+ * @param resource The resource identifier (e.g., "api_calls", "seats").
51
+ * @param amount Amount to increment by (default: 1).
52
+ */
53
+ public async increment(
54
+ organizationId: string,
55
+ resource: string,
56
+ amount = 1,
57
+ ): Promise<UsageResult> {
58
+ const limit = await this.subscriptionService.limit(
59
+ organizationId,
60
+ resource,
61
+ );
62
+ const key = this.buildKey(organizationId, resource);
63
+ const current = await this.cache.incr("subscriptions:usage", key, amount);
64
+ const allowed = limit === -1 || current <= limit;
65
+ return {
66
+ allowed,
67
+ current,
68
+ limit,
69
+ remaining: limit === -1 ? -1 : Math.max(0, limit - current),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Get the current usage for a resource without incrementing.
75
+ *
76
+ * @param organizationId The organization to query usage for.
77
+ * @param resource The resource identifier.
78
+ */
79
+ public async getUsage(
80
+ organizationId: string,
81
+ resource: string,
82
+ ): Promise<UsageResult> {
83
+ const limit = await this.subscriptionService.limit(
84
+ organizationId,
85
+ resource,
86
+ );
87
+ const key = this.buildKey(organizationId, resource);
88
+ const current = await this.cache.incr("subscriptions:usage", key, 0);
89
+ return {
90
+ allowed: limit === -1 || current <= limit,
91
+ current,
92
+ limit,
93
+ remaining: limit === -1 ? -1 : Math.max(0, limit - current),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Reset all usage counters for an organization.
99
+ *
100
+ * Used at the start of a new billing period.
101
+ *
102
+ * @param organizationId The organization whose counters to reset.
103
+ */
104
+ public async resetForPeriod(organizationId: string): Promise<void> {
105
+ const pattern = this.buildKey(organizationId, "*");
106
+ await this.cache.invalidateKeys("subscriptions:usage", [pattern]);
107
+ }
108
+
109
+ /**
110
+ * Build the cache key for a usage counter.
111
+ *
112
+ * Format: `organizationId:resource:YYYY-MM`
113
+ */
114
+ protected buildKey(organizationId: string, resource: string): string {
115
+ const period = this.dateTime.now().format("YYYY-MM");
116
+ return `${organizationId}:${resource}:${period}`;
117
+ }
118
+ }
@@ -4,7 +4,12 @@ import { AlephaOrmPostgres } from "alepha/orm/postgres";
4
4
  import { AlephaSecurity, type UserAccountToken } from "alepha/security";
5
5
  import { BadRequestError } from "alepha/server";
6
6
  import { describe, it } from "vitest";
7
- import { AdminUserController, AlephaApiUsers, UserService } from "../index.ts";
7
+ import {
8
+ AdminUserController,
9
+ AlephaApiUsers,
10
+ SessionService,
11
+ UserService,
12
+ } from "../index.ts";
8
13
 
9
14
  const adminUser: UserAccountToken = {
10
15
  id: "00000000-0000-0000-0000-000000000001",
@@ -445,3 +450,77 @@ describe("alepha/api/users - AdminUserController CRUD", () => {
445
450
  expect(userIds.indexOf(user2.id)).toBeLessThan(userIds.indexOf(user1.id));
446
451
  });
447
452
  });
453
+
454
+ describe("alepha/api/users - AdminUserController delete cleanup", () => {
455
+ it("should clean up sessions and identities when deleting a user", async ({
456
+ expect,
457
+ }) => {
458
+ const { alepha, controller } = await setup();
459
+ const sessionService = alepha.inject(SessionService);
460
+
461
+ // Arrange: create a user
462
+ const user = await controller.createUser(
463
+ {
464
+ body: {
465
+ username: "cleanupuser",
466
+ email: "cleanupuser@example.com",
467
+ },
468
+ },
469
+ asAdmin,
470
+ );
471
+
472
+ // Arrange: create identities for the user (credentials + google)
473
+ await sessionService.identities().create({
474
+ userId: user.id,
475
+ provider: "credentials",
476
+ providerUserId: user.id,
477
+ });
478
+ await sessionService.identities().create({
479
+ userId: user.id,
480
+ provider: "google",
481
+ providerUserId: "google-123",
482
+ });
483
+
484
+ // Arrange: create sessions for the user
485
+ await sessionService.sessions().create({
486
+ userId: user.id,
487
+ refreshToken: "00000000-0000-0000-0000-000000000010",
488
+ expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
489
+ });
490
+ await sessionService.sessions().create({
491
+ userId: user.id,
492
+ refreshToken: "00000000-0000-0000-0000-000000000011",
493
+ expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
494
+ });
495
+
496
+ // Sanity check: sessions and identities exist before deletion
497
+ const sessionsBefore = await sessionService
498
+ .sessions()
499
+ .findMany({ where: { userId: { eq: user.id } } });
500
+ expect(sessionsBefore.length).toBe(2);
501
+
502
+ const identitiesBefore = await sessionService
503
+ .identities()
504
+ .findMany({ where: { userId: { eq: user.id } } });
505
+ expect(identitiesBefore.length).toBe(2);
506
+
507
+ // Act: delete the user
508
+ const result = await controller.deleteUser(
509
+ { params: { id: user.id } },
510
+ asAdmin,
511
+ );
512
+ expect(result.ok).toBe(true);
513
+
514
+ // Assert: sessions for that user should be gone
515
+ const sessionsAfter = await sessionService
516
+ .sessions()
517
+ .findMany({ where: { userId: { eq: user.id } } });
518
+ expect(sessionsAfter.length).toBe(0);
519
+
520
+ // Assert: identities for that user should be gone
521
+ const identitiesAfter = await sessionService
522
+ .identities()
523
+ .findMany({ where: { userId: { eq: user.id } } });
524
+ expect(identitiesAfter.length).toBe(0);
525
+ });
526
+ });
@@ -35,6 +35,9 @@ const setup = async () => {
35
35
  features: {
36
36
  notifications: true,
37
37
  },
38
+ settings: {
39
+ resetPasswordAllowed: true,
40
+ },
38
41
  });
39
42
 
40
43
  const emailProvider = alepha.inject(MemoryEmailProvider);
@@ -499,6 +502,180 @@ describe("alepha/api/users - CredentialService", () => {
499
502
  });
500
503
  });
501
504
 
505
+ describe("resetPasswordAllowed=false", () => {
506
+ it("should silently return intent when resetPasswordAllowed is false", async ({
507
+ expect,
508
+ }) => {
509
+ const { alepha, credentialService, userService, cryptoProvider } =
510
+ await setup();
511
+
512
+ // Register a separate realm with resetPasswordAllowed disabled
513
+ const realmProvider = alepha.inject(RealmProvider);
514
+ realmProvider.register("no-reset", {
515
+ features: { notifications: true },
516
+ settings: { resetPasswordAllowed: false },
517
+ });
518
+
519
+ // Create a user in the no-reset realm
520
+ const user = await credentialService.users("no-reset").create({
521
+ email: "noreset@example.com",
522
+ username: "noresetuser",
523
+ roles: ["user"],
524
+ });
525
+
526
+ const hashedPassword = await cryptoProvider.hashPassword("Password123!");
527
+ await credentialService.identities("no-reset").create({
528
+ userId: user.id,
529
+ provider: "credentials",
530
+ password: hashedPassword,
531
+ });
532
+
533
+ // Request password reset - should return an intentId (security: doesn't reveal it's disabled)
534
+ const result = await credentialService.createPasswordResetIntent(
535
+ "noreset@example.com",
536
+ "no-reset",
537
+ );
538
+
539
+ expect(result.intentId).toBeDefined();
540
+ expect(result.expiresAt).toBeDefined();
541
+
542
+ // No email should be sent
543
+ const emailProvider = alepha.inject(MemoryEmailProvider);
544
+ expect(emailProvider.records.length).toBe(0);
545
+
546
+ // Completing the reset with the returned intentId should fail with 410
547
+ // because the intent was never actually stored in the cache
548
+ await expect(
549
+ credentialService.completePasswordReset({
550
+ intentId: result.intentId,
551
+ code: "123456",
552
+ newPassword: "NewPassword456!",
553
+ }),
554
+ ).rejects.toThrow(HttpError);
555
+ });
556
+ });
557
+
558
+ describe("Password policy enforcement during reset", () => {
559
+ it("should reject password that violates realm policy", async ({
560
+ expect,
561
+ }) => {
562
+ const { alepha, credentialService, userService, cryptoProvider } =
563
+ await setup();
564
+
565
+ // Register a realm with strict password policy
566
+ const realmProvider = alepha.inject(RealmProvider);
567
+ realmProvider.register("strict-policy", {
568
+ features: { notifications: true },
569
+ settings: {
570
+ resetPasswordAllowed: true,
571
+ passwordPolicy: {
572
+ minLength: 8,
573
+ requireUppercase: true,
574
+ requireLowercase: true,
575
+ requireNumbers: true,
576
+ requireSpecialCharacters: false,
577
+ },
578
+ },
579
+ });
580
+
581
+ // Create a user in the strict-policy realm
582
+ const user = await credentialService.users("strict-policy").create({
583
+ email: "strict@example.com",
584
+ username: "strictuser",
585
+ roles: ["user"],
586
+ });
587
+
588
+ const hashedPassword =
589
+ await cryptoProvider.hashPassword("OldPassword123!");
590
+ await credentialService.identities("strict-policy").create({
591
+ userId: user.id,
592
+ provider: "credentials",
593
+ password: hashedPassword,
594
+ });
595
+
596
+ // Create intent
597
+ const intent = await credentialService.createPasswordResetIntent(
598
+ "strict@example.com",
599
+ "strict-policy",
600
+ );
601
+
602
+ // Extract code from email
603
+ const emailProvider = alepha.inject(MemoryEmailProvider);
604
+ await expect.poll(() => emailProvider.records.length).toBe(1);
605
+ const code = extractCode(emailProvider.records[0].body);
606
+
607
+ // Try to complete with a password that has no uppercase and no numbers
608
+ await expect(
609
+ credentialService.completePasswordReset({
610
+ intentId: intent.intentId,
611
+ code,
612
+ newPassword: "alllowercase",
613
+ }),
614
+ ).rejects.toThrowError(BadRequestError);
615
+
616
+ // The verification code should NOT be consumed - can retry with a valid password
617
+ await credentialService.completePasswordReset({
618
+ intentId: intent.intentId,
619
+ code,
620
+ newPassword: "ValidRetry123",
621
+ });
622
+ });
623
+
624
+ it("should accept password that meets realm policy", async ({ expect }) => {
625
+ const { alepha, credentialService, userService, cryptoProvider } =
626
+ await setup();
627
+
628
+ // Register a realm with strict password policy
629
+ const realmProvider = alepha.inject(RealmProvider);
630
+ realmProvider.register("strict-accept", {
631
+ features: { notifications: true },
632
+ settings: {
633
+ resetPasswordAllowed: true,
634
+ passwordPolicy: {
635
+ minLength: 8,
636
+ requireUppercase: true,
637
+ requireLowercase: true,
638
+ requireNumbers: true,
639
+ requireSpecialCharacters: false,
640
+ },
641
+ },
642
+ });
643
+
644
+ // Create a user in the strict-accept realm
645
+ const user = await credentialService.users("strict-accept").create({
646
+ email: "accept@example.com",
647
+ username: "acceptuser",
648
+ roles: ["user"],
649
+ });
650
+
651
+ const hashedPassword =
652
+ await cryptoProvider.hashPassword("OldPassword123!");
653
+ await credentialService.identities("strict-accept").create({
654
+ userId: user.id,
655
+ provider: "credentials",
656
+ password: hashedPassword,
657
+ });
658
+
659
+ // Create intent
660
+ const intent = await credentialService.createPasswordResetIntent(
661
+ "accept@example.com",
662
+ "strict-accept",
663
+ );
664
+
665
+ // Extract code from email
666
+ const emailProvider = alepha.inject(MemoryEmailProvider);
667
+ await expect.poll(() => emailProvider.records.length).toBe(1);
668
+ const code = extractCode(emailProvider.records[0].body);
669
+
670
+ // Complete with a password that meets the policy
671
+ await credentialService.completePasswordReset({
672
+ intentId: intent.intentId,
673
+ code,
674
+ newPassword: "ValidPass123",
675
+ });
676
+ });
677
+ });
678
+
502
679
  describe("Legacy methods (backward compatibility)", () => {
503
680
  it("requestPasswordReset should work as before", async ({ expect }) => {
504
681
  const { credentialService, userService, cryptoProvider, emailProvider } =
@@ -3,7 +3,7 @@ import { AlephaApiVerification } from "alepha/api/verifications";
3
3
  import { DateTimeProvider } from "alepha/datetime";
4
4
  import { AlephaEmail, MemoryEmailProvider } from "alepha/email";
5
5
  import { AlephaOrmPostgres } from "alepha/orm/postgres";
6
- import { AlephaSecurity } from "alepha/security";
6
+ import { AlephaSecurity, currentUserAtom } from "alepha/security";
7
7
  import { BadRequestError } from "alepha/server";
8
8
  import { describe, it } from "vitest";
9
9
  import {
@@ -264,7 +264,7 @@ describe("alepha/api/users - Email Verification", () => {
264
264
  });
265
265
 
266
266
  it("should check email verification status", async ({ expect }) => {
267
- const { userService, actions } = await setup();
267
+ const { alepha, userService, actions } = await setup();
268
268
 
269
269
  // Create a test user with unverified email
270
270
  await userService.users().create({
@@ -282,23 +282,34 @@ describe("alepha/api/users - Email Verification", () => {
282
282
  emailVerified: true,
283
283
  });
284
284
 
285
- // Check unverified email
286
- const unverifiedResult = await actions.checkEmailVerification({
287
- query: { email: "unverified@example.com" },
288
- });
289
- expect(unverifiedResult.verified).toBe(false);
290
-
291
- // Check verified email
292
- const verifiedResult = await actions.checkEmailVerification({
293
- query: { email: "verified@example.com" },
294
- });
295
- expect(verifiedResult.verified).toBe(true);
296
-
297
- // Check non-existent email
298
- const nonExistentResult = await actions.checkEmailVerification({
299
- query: { email: "nonexistent@example.com" },
285
+ // checkEmailVerification requires $secure() authentication
286
+ await alepha.context.run(async () => {
287
+ alepha.store.set(currentUserAtom, {
288
+ id: "admin-1",
289
+ name: "Admin",
290
+ email: "admin@example.com",
291
+ realm: "default",
292
+ roles: ["admin"],
293
+ });
294
+
295
+ // Check unverified email
296
+ const unverifiedResult = await actions.checkEmailVerification({
297
+ query: { email: "unverified@example.com" },
298
+ });
299
+ expect(unverifiedResult.verified).toBe(false);
300
+
301
+ // Check verified email
302
+ const verifiedResult = await actions.checkEmailVerification({
303
+ query: { email: "verified@example.com" },
304
+ });
305
+ expect(verifiedResult.verified).toBe(true);
306
+
307
+ // Check non-existent email
308
+ const nonExistentResult = await actions.checkEmailVerification({
309
+ query: { email: "nonexistent@example.com" },
310
+ });
311
+ expect(nonExistentResult.verified).toBe(false);
300
312
  });
301
- expect(nonExistentResult.verified).toBe(false);
302
313
  });
303
314
 
304
315
  it("should respect rate limiting on verification requests", async ({
@@ -35,6 +35,9 @@ const setup = async () => {
35
35
  features: {
36
36
  notifications: true,
37
37
  },
38
+ settings: {
39
+ resetPasswordAllowed: true,
40
+ },
38
41
  });
39
42
 
40
43
  const emailProvider = alepha.inject(MemoryEmailProvider);
@@ -245,8 +245,9 @@ describe("alepha/api/users - RegistrationService", () => {
245
245
  } as never,
246
246
  });
247
247
 
248
- // Create existing user with phone
248
+ // Create existing user with phone in the same realm
249
249
  await userService.users("phone-realm").create({
250
+ realm: "phone-realm",
250
251
  username: "existinguser",
251
252
  email: "existing@example.com",
252
253
  phoneNumber: "+1234567890",
@@ -488,6 +489,7 @@ describe("alepha/api/users - RegistrationService", () => {
488
489
 
489
490
  // Simulate another user registering with same email while verification pending
490
491
  await userService.users("race-realm").create({
492
+ realm: "race-realm",
491
493
  email: "race@example.com",
492
494
  username: "racewinner",
493
495
  roles: ["user"],
@@ -644,4 +646,149 @@ describe("alepha/api/users - RegistrationService", () => {
644
646
  expect(session?.id).toBe(user.id);
645
647
  });
646
648
  });
649
+
650
+ describe("Password policy enforcement during registration", () => {
651
+ it("should reject registration when password violates realm policy", async ({
652
+ expect,
653
+ }) => {
654
+ const { registrationService, realmProvider } = await setup();
655
+
656
+ realmProvider.register("strict-policy-realm", {
657
+ settings: {
658
+ passwordPolicy: {
659
+ minLength: 10,
660
+ requireUppercase: true,
661
+ requireLowercase: false,
662
+ requireNumbers: false,
663
+ requireSpecialCharacters: false,
664
+ },
665
+ } as never,
666
+ });
667
+
668
+ await expect(
669
+ registrationService.createRegistrationIntent(
670
+ {
671
+ email: "weakpass@example.com",
672
+ password: "shortpw!",
673
+ },
674
+ "strict-policy-realm",
675
+ ),
676
+ ).rejects.toThrowError(BadRequestError);
677
+ });
678
+
679
+ it("should accept registration when password meets realm policy", async ({
680
+ expect,
681
+ }) => {
682
+ const { registrationService, realmProvider } = await setup();
683
+
684
+ realmProvider.register("strict-policy-realm", {
685
+ settings: {
686
+ passwordPolicy: {
687
+ minLength: 10,
688
+ requireUppercase: true,
689
+ requireLowercase: false,
690
+ requireNumbers: false,
691
+ requireSpecialCharacters: false,
692
+ },
693
+ } as never,
694
+ });
695
+
696
+ const result = await registrationService.createRegistrationIntent(
697
+ {
698
+ email: "strongpass@example.com",
699
+ password: "StrongPass123",
700
+ },
701
+ "strict-policy-realm",
702
+ );
703
+
704
+ expect(result.intentId).toBeDefined();
705
+ });
706
+ });
707
+
708
+ describe("Registration rate limiting", () => {
709
+ it("should rate limit registration attempts by IP", async ({ expect }) => {
710
+ const { alepha, registrationService } = await setup();
711
+
712
+ await alepha.fork(async () => {
713
+ alepha.store.set("alepha.http.request", { ip: "10.0.0.1" } as never);
714
+
715
+ // Make 10 registration attempts (they may fail for duplicate email, that's fine)
716
+ for (let i = 0; i < 10; i++) {
717
+ await registrationService
718
+ .createRegistrationIntent({
719
+ email: `ratelimit-${i}@example.com`,
720
+ password: "SecurePassword123!",
721
+ })
722
+ .catch(() => {});
723
+ }
724
+
725
+ // 11th attempt should be rate limited
726
+ await expect(
727
+ registrationService.createRegistrationIntent({
728
+ email: "ratelimit-overflow@example.com",
729
+ password: "SecurePassword123!",
730
+ }),
731
+ ).rejects.toThrowError(BadRequestError);
732
+ });
733
+ });
734
+ });
735
+
736
+ describe("Default roles from realm settings", () => {
737
+ it("should assign defaultRoles from realm settings to new users", async ({
738
+ expect,
739
+ }) => {
740
+ const { registrationService, realmProvider } = await setup();
741
+
742
+ realmProvider.register("custom-roles-realm", {
743
+ settings: {
744
+ defaultRoles: ["member", "viewer"],
745
+ } as never,
746
+ });
747
+
748
+ const intent = await registrationService.createRegistrationIntent(
749
+ {
750
+ email: "roleuser@example.com",
751
+ password: "SecurePassword123!",
752
+ },
753
+ "custom-roles-realm",
754
+ );
755
+
756
+ const user = await registrationService.completeRegistration({
757
+ intentId: intent.intentId,
758
+ });
759
+
760
+ expect(user.roles).toEqual(["member", "viewer"]);
761
+ });
762
+ });
763
+
764
+ describe("Cross-realm email uniqueness", () => {
765
+ it("should allow same email in different realms", async ({ expect }) => {
766
+ const { registrationService, userService, realmProvider } = await setup();
767
+
768
+ realmProvider.register("realm-a");
769
+ realmProvider.register("realm-b");
770
+
771
+ // Create a user with this email in realm-a directly
772
+ await userService.users("realm-a").create({
773
+ realm: "realm-a",
774
+ email: "shared@example.com",
775
+ roles: ["user"],
776
+ });
777
+
778
+ // Register a new user with the same email in realm-b via registration flow
779
+ const intent = await registrationService.createRegistrationIntent(
780
+ {
781
+ email: "shared@example.com",
782
+ password: "SecurePassword123!",
783
+ },
784
+ "realm-b",
785
+ );
786
+
787
+ const user = await registrationService.completeRegistration({
788
+ intentId: intent.intentId,
789
+ });
790
+
791
+ expect(user.email).toBe("shared@example.com");
792
+ });
793
+ });
647
794
  });