alepha 0.19.3 → 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 (215) 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 +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -168,6 +168,8 @@ const realmAuthSettingsAtom = $atom({
168
168
  resetPasswordAllowed: t.boolean({ description: "Enable forgot password functionality" }),
169
169
  adminEmails: t.array(t.email(), { description: "List of email addresses that are automatically promoted to admin role on login" }),
170
170
  adminUsernames: t.array(t.text(), { description: "List of usernames that are automatically promoted to admin role on login" }),
171
+ defaultRoles: t.array(t.string(), { description: "Default roles assigned to newly registered users" }),
172
+ verifyEmailUrl: t.optional(t.string({ description: "Base URL for email verification links (used when verification method is 'link'). Token and email are appended as query params." })),
171
173
  passwordPolicy: t.object({
172
174
  minLength: t.integer({
173
175
  description: "Minimum password length",
@@ -209,6 +211,7 @@ const realmAuthSettingsAtom = $atom({
209
211
  firstNameLastName: "none",
210
212
  adminEmails: [],
211
213
  adminUsernames: [],
214
+ defaultRoles: ["user"],
212
215
  passwordPolicy: {
213
216
  minLength: 8,
214
217
  requireUppercase: true,
@@ -723,6 +726,27 @@ var UserNotifications = class {
723
726
  expiresInMinutes: t.number()
724
727
  })
725
728
  });
729
+ accountLockout = $notification({
730
+ category: "security",
731
+ description: "Email sent to users when their account is temporarily locked due to too many failed login attempts.",
732
+ critical: true,
733
+ sensitive: true,
734
+ email: {
735
+ subject: "Account temporarily locked",
736
+ body: (it) => `
737
+ <h1>Account Temporarily Locked</h1>
738
+ <p>Hi ${it.email},</p>
739
+ <p>Your account has been temporarily locked due to too many failed login attempts.</p>
740
+ <p>If this was you, please wait ${it.lockoutMinutes} minutes before trying again. If you've forgotten your password, you can reset it using the password reset feature.</p>
741
+ <p>If this wasn't you, someone may be trying to access your account. We recommend changing your password as soon as possible.</p>
742
+ <p>Best regards,<br>The Team</p>
743
+ `
744
+ },
745
+ schema: t.object({
746
+ email: t.string({ format: "email" }),
747
+ lockoutMinutes: t.number()
748
+ })
749
+ });
726
750
  emailVerificationLink = $notification({
727
751
  category: "security",
728
752
  description: "Email sent to users with a link to verify their email address.",
@@ -776,7 +800,7 @@ var UserService = class {
776
800
  * @param method - The verification method: "code" (default) or "link".
777
801
  * @param verifyUrl - Base URL for verification link (required when method is "link").
778
802
  */
779
- async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
803
+ async requestEmailVerification(email, userRealmName, method = "code") {
780
804
  this.log.trace("Requesting email verification", {
781
805
  email,
782
806
  userRealmName,
@@ -800,10 +824,12 @@ var UserService = class {
800
824
  body: { target: email }
801
825
  });
802
826
  if (method === "link") {
803
- const url = new URL(verifyUrl || "/verify-email", "http://localhost");
827
+ const realmSettings = await this.realmProvider.getRealm(userRealmName).getSettings();
828
+ const baseUrl = realmSettings.verifyEmailUrl ?? "/verify-email";
829
+ const url = new URL(baseUrl, "http://localhost");
804
830
  url.searchParams.set("email", email);
805
831
  url.searchParams.set("token", verification.token);
806
- const fullVerifyUrl = verifyUrl ? `${verifyUrl}${url.search}` : url.pathname + url.search;
832
+ const fullVerifyUrl = realmSettings.verifyEmailUrl ? `${baseUrl}${url.search}` : url.pathname + url.search;
807
833
  await this.userNotifications(userRealmName)?.emailVerificationLink.push({
808
834
  contact: email,
809
835
  variables: {
@@ -936,27 +962,37 @@ var UserService = class {
936
962
  userRealmName
937
963
  });
938
964
  const realm = this.realmProvider.getRealm(userRealmName);
965
+ const realmSettings = await realm.getSettings();
939
966
  if (data.username) {
940
- if (await this.users(userRealmName).findOne({ where: { username: { ilike: data.username } } })) {
967
+ if (await this.users(userRealmName).findOne({ where: {
968
+ realm: realm.name,
969
+ username: { ilike: data.username }
970
+ } })) {
941
971
  this.log.debug("Username already taken", { username: data.username });
942
972
  throw new BadRequestError("User with this username already exists");
943
973
  }
944
974
  }
945
975
  if (data.email) {
946
- if (await this.users(userRealmName).findOne({ where: { email: { eq: data.email } } })) {
976
+ if (await this.users(userRealmName).findOne({ where: {
977
+ realm: realm.name,
978
+ email: { eq: data.email }
979
+ } })) {
947
980
  this.log.debug("Email already taken", { email: data.email });
948
981
  throw new BadRequestError("User with this email already exists");
949
982
  }
950
983
  }
951
984
  if (data.phoneNumber) {
952
- if (await this.users(userRealmName).findOne({ where: { phoneNumber: { eq: data.phoneNumber } } })) {
985
+ if (await this.users(userRealmName).findOne({ where: {
986
+ realm: realm.name,
987
+ phoneNumber: { eq: data.phoneNumber }
988
+ } })) {
953
989
  this.log.debug("Phone number already taken", { phoneNumber: data.phoneNumber });
954
990
  throw new BadRequestError("User with this phone number already exists");
955
991
  }
956
992
  }
957
993
  const user = await this.users(userRealmName).create({
958
994
  ...data,
959
- roles: data.roles ?? ["user"],
995
+ roles: data.roles ?? realmSettings.defaultRoles,
960
996
  realm: realm.name
961
997
  });
962
998
  this.log.info("User created", {
@@ -1011,6 +1047,8 @@ var UserService = class {
1011
1047
  userRealmName
1012
1048
  });
1013
1049
  const user = await this.getUserById(id, userRealmName);
1050
+ await this.realmProvider.sessionRepository(userRealmName).deleteMany({ userId: { eq: id } });
1051
+ await this.realmProvider.identityRepository(userRealmName).deleteMany({ userId: { eq: id } });
1014
1052
  await this.users(userRealmName).deleteById(id);
1015
1053
  this.log.info("User deleted", { userId: id });
1016
1054
  const realm = this.realmProvider.getRealm(userRealmName);
@@ -1282,6 +1320,16 @@ var CredentialService = class {
1282
1320
  return this.realmProvider.identityRepository(userRealmName);
1283
1321
  }
1284
1322
  /**
1323
+ * Validate a password against the realm's password policy.
1324
+ */
1325
+ validatePasswordPolicy(password, policy) {
1326
+ if (password.length < policy.minLength) throw new BadRequestError(`Password must be at least ${policy.minLength} characters`);
1327
+ if (policy.requireUppercase && !/[A-Z]/.test(password)) throw new BadRequestError("Password must contain at least one uppercase letter");
1328
+ if (policy.requireLowercase && !/[a-z]/.test(password)) throw new BadRequestError("Password must contain at least one lowercase letter");
1329
+ if (policy.requireNumbers && !/\d/.test(password)) throw new BadRequestError("Password must contain at least one number");
1330
+ if (policy.requireSpecialCharacters && !/[^a-zA-Z0-9]/.test(password)) throw new BadRequestError("Password must contain at least one special character");
1331
+ }
1332
+ /**
1285
1333
  * Phase 1: Create a password reset intent.
1286
1334
  *
1287
1335
  * Validates the email, checks for existing user with credentials,
@@ -1298,6 +1346,13 @@ var CredentialService = class {
1298
1346
  });
1299
1347
  const intentId = randomUUID();
1300
1348
  const expiresAt = this.dateTimeProvider.now().add(INTENT_TTL_MINUTES$1, "minutes").toISOString();
1349
+ if ((await this.realmProvider.getRealm(userRealmName).getSettings()).resetPasswordAllowed === false) {
1350
+ this.log.debug("Password reset not allowed for realm", { userRealmName });
1351
+ return {
1352
+ intentId,
1353
+ expiresAt
1354
+ };
1355
+ }
1301
1356
  const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } });
1302
1357
  if (!user) {
1303
1358
  this.log.debug("Password reset requested for non-existent email", { email });
@@ -1369,6 +1424,9 @@ var CredentialService = class {
1369
1424
  message: "Invalid or expired password reset intent"
1370
1425
  });
1371
1426
  }
1427
+ const realm = this.realmProvider.getRealm(intent.realmName);
1428
+ const realmSettings = await realm.getSettings();
1429
+ this.validatePasswordPolicy(body.newPassword, realmSettings.passwordPolicy);
1372
1430
  if ((await this.verificationController.validateVerificationCode({
1373
1431
  params: { type: "code" },
1374
1432
  body: {
@@ -1396,7 +1454,6 @@ var CredentialService = class {
1396
1454
  userId: intent.userId,
1397
1455
  email: intent.email
1398
1456
  });
1399
- const realm = this.realmProvider.getRealm(intent.realmName);
1400
1457
  await this.userAudits(intent.realmName)?.recordUser("update", {
1401
1458
  userId: intent.userId,
1402
1459
  userEmail: intent.email,
@@ -1447,6 +1504,9 @@ var CredentialService = class {
1447
1504
  }).catch(() => {
1448
1505
  throw new BadRequestError("Invalid or expired reset token");
1449
1506
  })).alreadyVerified) throw new BadRequestError("Invalid or expired reset token");
1507
+ const realm = this.realmProvider.getRealm(userRealmName);
1508
+ const realmSettings = await realm.getSettings();
1509
+ this.validatePasswordPolicy(newPassword, realmSettings.passwordPolicy);
1450
1510
  const user = await this.users(userRealmName).getOne({ where: { email: { eq: email } } });
1451
1511
  const identity = await this.identities(userRealmName).getOne({ where: {
1452
1512
  userId: { eq: user.id },
@@ -1455,7 +1515,6 @@ var CredentialService = class {
1455
1515
  const hashedPassword = await this.cryptoProvider.hashPassword(newPassword);
1456
1516
  await this.identities(userRealmName).updateById(identity.id, { password: hashedPassword });
1457
1517
  await this.sessions(userRealmName).deleteMany({ userId: { eq: user.id } });
1458
- const realm = this.realmProvider.getRealm(userRealmName);
1459
1518
  await this.userAudits(userRealmName)?.recordUser("update", {
1460
1519
  userId: user.id,
1461
1520
  userEmail: email,
@@ -1484,10 +1543,15 @@ var RegistrationService = class {
1484
1543
  cryptoProvider = $inject(CryptoProvider);
1485
1544
  verificationController = $client();
1486
1545
  realmProvider = $inject(RealmProvider);
1546
+ credentialService = $inject(CredentialService);
1487
1547
  intentCache = $cache({
1488
1548
  name: "api:users:registrations",
1489
1549
  ttl: [INTENT_TTL_MINUTES, "minutes"]
1490
1550
  });
1551
+ rateLimitCache = $cache({
1552
+ name: "api:users:registration-rate-limit",
1553
+ ttl: [15, "minutes"]
1554
+ });
1491
1555
  userAudits(realmName) {
1492
1556
  if (this.realmProvider.getRealm(realmName).features.audits) return this.alepha.inject(UserAudits);
1493
1557
  }
@@ -1507,6 +1571,16 @@ var RegistrationService = class {
1507
1571
  username: body.username,
1508
1572
  userRealmName
1509
1573
  });
1574
+ const request = this.alepha.store.get("alepha.http.request");
1575
+ const ipKey = request?.ip ? `register:ip:${request.ip}` : void 0;
1576
+ if (ipKey) {
1577
+ const count = await this.rateLimitCache.get(ipKey) ?? 0;
1578
+ if (count >= 10) {
1579
+ this.log.warn("Registration rate limit exceeded", { ip: request?.ip });
1580
+ throw new BadRequestError("Too many registration attempts, please try again later");
1581
+ }
1582
+ await this.rateLimitCache.set(ipKey, count + 1);
1583
+ }
1510
1584
  const realmSettings = await this.realmProvider.getRealm(userRealmName).getSettings();
1511
1585
  if (realmSettings?.registrationAllowed === false) {
1512
1586
  this.log.warn("Registration not allowed for realm", { userRealmName });
@@ -1537,6 +1611,7 @@ var RegistrationService = class {
1537
1611
  throw new BadRequestError("Phone number is required");
1538
1612
  }
1539
1613
  await this.checkUserAvailability(body, userRealmName);
1614
+ this.credentialService.validatePasswordPolicy(body.password, realmSettings.passwordPolicy);
1540
1615
  const passwordHash = await this.cryptoProvider.hashPassword(body.password);
1541
1616
  const requirements = {
1542
1617
  email: realmSettings?.verifyEmailRequired === true && !!body.email,
@@ -1620,19 +1695,21 @@ var RegistrationService = class {
1620
1695
  email: intent.data.email,
1621
1696
  phoneNumber: intent.data.phoneNumber
1622
1697
  }, userRealmName);
1623
- await this.intentCache.invalidate(body.intentId);
1698
+ const realm = this.realmProvider.getRealm(userRealmName);
1699
+ const realmSettings = await realm.getSettings();
1624
1700
  const user = await userRepository.create({
1625
- realm: userRealmName,
1701
+ realm: realm.name,
1626
1702
  username: intent.data.username,
1627
1703
  email: intent.data.email,
1628
1704
  phoneNumber: intent.data.phoneNumber,
1629
1705
  firstName: intent.data.firstName,
1630
1706
  lastName: intent.data.lastName,
1631
1707
  picture: intent.data.picture,
1632
- roles: ["user"],
1708
+ roles: realmSettings.defaultRoles,
1633
1709
  enabled: true,
1634
1710
  emailVerified: intent.requirements.email
1635
1711
  });
1712
+ await this.intentCache.invalidate(body.intentId);
1636
1713
  await identityRepository.create({
1637
1714
  userId: user.id,
1638
1715
  provider: "credentials",
@@ -1643,7 +1720,6 @@ var RegistrationService = class {
1643
1720
  email: user.email,
1644
1721
  username: user.username
1645
1722
  });
1646
- const realm = this.realmProvider.getRealm(userRealmName);
1647
1723
  await this.userAudits(userRealmName)?.recordUser("create", {
1648
1724
  userId: user.id,
1649
1725
  userEmail: user.email ?? void 0,
@@ -1663,21 +1739,31 @@ var RegistrationService = class {
1663
1739
  * Check if username, email, and phone are available.
1664
1740
  */
1665
1741
  async checkUserAvailability(body, userRealmName) {
1742
+ const realm = this.realmProvider.getRealm(userRealmName);
1666
1743
  const userRepository = this.realmProvider.userRepository(userRealmName);
1667
1744
  if (body.username) {
1668
- if (await userRepository.findOne({ where: { username: { ilike: body.username } } })) {
1745
+ if (await userRepository.findOne({ where: {
1746
+ realm: realm.name,
1747
+ username: { ilike: body.username }
1748
+ } })) {
1669
1749
  this.log.debug("Username already taken", { username: body.username });
1670
1750
  throw new ConflictError("User with this username already exists");
1671
1751
  }
1672
1752
  }
1673
1753
  if (body.email) {
1674
- if (await userRepository.findOne({ where: { email: { eq: body.email } } })) {
1754
+ if (await userRepository.findOne({ where: {
1755
+ realm: realm.name,
1756
+ email: { eq: body.email }
1757
+ } })) {
1675
1758
  this.log.debug("Email already taken", { email: body.email });
1676
1759
  throw new ConflictError("User with this email already exists");
1677
1760
  }
1678
1761
  }
1679
1762
  if (body.phoneNumber) {
1680
- if (await userRepository.findOne({ where: { phoneNumber: { eq: body.phoneNumber } } })) {
1763
+ if (await userRepository.findOne({ where: {
1764
+ realm: realm.name,
1765
+ phoneNumber: { eq: body.phoneNumber }
1766
+ } })) {
1681
1767
  this.log.debug("Phone number already taken", { phoneNumber: body.phoneNumber });
1682
1768
  throw new ConflictError("User with this phone number already exists");
1683
1769
  }
@@ -1924,9 +2010,8 @@ var UserController = class {
1924
2010
  userRealmName: t.optional(t.string()),
1925
2011
  method: t.optional(t.enum(["code", "link"], {
1926
2012
  default: "code",
1927
- description: "Verification method: \"code\" sends a 6-digit code, \"link\" sends a clickable verification link."
1928
- })),
1929
- verifyUrl: t.optional(t.string({ description: "Base URL for verification link. Required when method is \"link\". Token and email will be appended as query params." }))
2013
+ description: "Verification method: \"code\" sends a 6-digit code, \"link\" sends a clickable verification link. When using \"link\", configure verifyEmailUrl in realm settings."
2014
+ }))
1930
2015
  }),
1931
2016
  body: t.object({ email: t.email() }),
1932
2017
  response: t.object({
@@ -1936,7 +2021,7 @@ var UserController = class {
1936
2021
  },
1937
2022
  handler: async ({ body, query }) => {
1938
2023
  const method = query.method ?? "code";
1939
- await this.userService.requestEmailVerification(body.email, query.userRealmName, method, query.verifyUrl);
2024
+ await this.userService.requestEmailVerification(body.email, query.userRealmName, method);
1940
2025
  return {
1941
2026
  success: true,
1942
2027
  message: method === "link" ? "If an account exists with this email, a verification link has been sent." : "If an account exists with this email, a verification code has been sent."
@@ -1975,6 +2060,7 @@ var UserController = class {
1975
2060
  checkEmailVerification = $action({
1976
2061
  path: "/users/email-verification/check",
1977
2062
  group: this.group,
2063
+ use: [$secure()],
1978
2064
  schema: {
1979
2065
  query: t.object({
1980
2066
  email: t.email(),
@@ -2039,6 +2125,9 @@ var SessionService = class SessionService {
2039
2125
  userAudits(realmName) {
2040
2126
  if (this.realmProvider.getRealm(realmName).features.audits) return this.alepha.inject(UserAudits);
2041
2127
  }
2128
+ userNotifications(realmName) {
2129
+ if (this.realmProvider.getRealm(realmName).features.notifications) return this.alepha.inject(UserNotifications);
2130
+ }
2042
2131
  users(userRealmName) {
2043
2132
  return this.realmProvider.userRepository(userRealmName);
2044
2133
  }
@@ -2103,10 +2192,7 @@ var SessionService = class SessionService {
2103
2192
  if (candidate.length < 3) candidate = `user${candidate}`;
2104
2193
  candidate = candidate.slice(0, maxLength - 2);
2105
2194
  const isAvailable = async (name) => {
2106
- return !(await users.findMany({
2107
- where: { username: { contains: name } },
2108
- limit: 1
2109
- })).some((u) => u.username?.toLowerCase() === name.toLowerCase());
2195
+ return !await users.findOne({ where: { username: { ilike: name } } });
2110
2196
  };
2111
2197
  if (await isAvailable(candidate)) return candidate;
2112
2198
  for (let i = 2; i <= maxSuffixAttempts + 1; i++) {
@@ -2240,6 +2326,22 @@ var SessionService = class SessionService {
2240
2326
  }
2241
2327
  throw new InvalidCredentialsError();
2242
2328
  }
2329
+ if (!user.enabled) {
2330
+ this.log.warn("Login attempt for disabled account", {
2331
+ userId: user.id,
2332
+ realm: name
2333
+ });
2334
+ await this.userAudits(userRealmName)?.recordAuth("login_failed", {
2335
+ userRealm: name,
2336
+ resourceId: user.id,
2337
+ description: "Login attempt for disabled account",
2338
+ metadata: {
2339
+ provider,
2340
+ username
2341
+ }
2342
+ });
2343
+ throw new InvalidCredentialsError();
2344
+ }
2243
2345
  const accountKey = `login:account:${name}:${user.id}`;
2244
2346
  if (await this.isLoginLocked(accountKey, loginRateLimit.accountMaxAttempts)) {
2245
2347
  this.log.warn("Login blocked — account rate limit exceeded", {
@@ -2285,13 +2387,25 @@ var SessionService = class SessionService {
2285
2387
  metadata: { ip: request?.ip }
2286
2388
  });
2287
2389
  }
2288
- if (await this.recordFailedLogin(accountKey, loginRateLimit.accountMaxAttempts, loginRateLimit.windowMs)) await this.userAudits(userRealmName)?.record("security", "rate_limited", {
2289
- userRealm: name,
2290
- resourceId: user.id,
2291
- success: false,
2292
- description: "Account temporarily locked due to too many failed login attempts",
2293
- metadata: { userId: user.id }
2294
- });
2390
+ if (await this.recordFailedLogin(accountKey, loginRateLimit.accountMaxAttempts, loginRateLimit.windowMs)) {
2391
+ await this.userAudits(userRealmName)?.record("security", "rate_limited", {
2392
+ userRealm: name,
2393
+ resourceId: user.id,
2394
+ success: false,
2395
+ description: "Account temporarily locked due to too many failed login attempts",
2396
+ metadata: { userId: user.id }
2397
+ });
2398
+ if (user.email) {
2399
+ const lockoutMinutes = Math.round(loginRateLimit.windowMs / 6e4);
2400
+ await this.userNotifications(userRealmName)?.accountLockout.push({
2401
+ contact: user.email,
2402
+ variables: {
2403
+ email: user.email,
2404
+ lockoutMinutes
2405
+ }
2406
+ });
2407
+ }
2408
+ }
2295
2409
  throw new InvalidCredentialsError();
2296
2410
  }
2297
2411
  await this.userAudits(userRealmName)?.recordAuth("login", {
@@ -2352,6 +2466,14 @@ var SessionService = class SessionService {
2352
2466
  throw new UnauthorizedError("Session expired");
2353
2467
  }
2354
2468
  const user = await this.users(userRealmName).getOne({ where: { id: { eq: session.userId } } });
2469
+ if (!user.enabled) {
2470
+ this.log.warn("Session refresh for disabled account", {
2471
+ userId: user.id,
2472
+ sessionId: session.id
2473
+ });
2474
+ await this.sessions(userRealmName).deleteById(session.id);
2475
+ throw new UnauthorizedError("Account disabled");
2476
+ }
2355
2477
  await this.ensureAdminRole(user, userRealmName);
2356
2478
  this.log.debug("Session refreshed", {
2357
2479
  sessionId: session.id,
@@ -2422,8 +2544,19 @@ var SessionService = class SessionService {
2422
2544
  ...profile
2423
2545
  };
2424
2546
  }
2425
- const existing = await users.findOne({ where: { email: profile.email } });
2547
+ const existing = await users.findOne({ where: {
2548
+ realm: realm.name,
2549
+ email: profile.email
2550
+ } });
2426
2551
  if (existing) {
2552
+ if (profile.email_verified === false) {
2553
+ this.log.warn("OAuth2 profile email not verified by provider, refusing auto-link", {
2554
+ provider,
2555
+ email: profile.email,
2556
+ userId: existing.id
2557
+ });
2558
+ throw new BadRequestError("Cannot link account: email not verified by provider");
2559
+ }
2427
2560
  this.log.debug("Linking OAuth2 profile to existing user by email", {
2428
2561
  provider,
2429
2562
  profileSub: profile.sub,
@@ -2466,7 +2599,7 @@ var SessionService = class SessionService {
2466
2599
  username,
2467
2600
  email: profile.email,
2468
2601
  emailVerified: true,
2469
- roles: ["user"]
2602
+ roles: realmSettings.defaultRoles
2470
2603
  });
2471
2604
  if (profile.picture) {
2472
2605
  this.log.debug("Fetching user profile picture from OAuth2 provider", {
@@ -2645,7 +2778,7 @@ const loginSchema = t.object({
2645
2778
  description: "Username or email address for login"
2646
2779
  }),
2647
2780
  password: t.text({
2648
- minLength: 6,
2781
+ minLength: 8,
2649
2782
  description: "User password"
2650
2783
  })
2651
2784
  });