@mesob/auth-hono 0.0.3 → 0.0.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.
package/dist/index.js CHANGED
@@ -467,8 +467,8 @@ var findUserById = (db, tenantId, userId) => {
467
467
 
468
468
  // src/handler.ts
469
469
  import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
470
- import { getCookie as getCookie2 } from "hono/cookie";
471
- import { HTTPException as HTTPException11 } from "hono/http-exception";
470
+ import { getCookie as getCookie3 } from "hono/cookie";
471
+ import { HTTPException as HTTPException16 } from "hono/http-exception";
472
472
 
473
473
  // src/lib/crypto.ts
474
474
  import { scrypt } from "@noble/hashes/scrypt.js";
@@ -643,6 +643,9 @@ var changePasswordSchema = z.object({
643
643
  currentPassword: passwordField,
644
644
  newPassword: passwordField
645
645
  });
646
+ var verifyPasswordSchema = z.object({
647
+ password: passwordField
648
+ });
646
649
  var messageWithVerificationIdSchema = messageSchema.extend({
647
650
  verificationId: z.string().uuid().optional()
648
651
  });
@@ -652,9 +655,175 @@ var checkUserSchema = z.object({
652
655
  var checkUserResponseSchema = z.object({
653
656
  exists: z.boolean()
654
657
  });
658
+ var updateProfileSchema = z.object({
659
+ fullName: z.string().min(1).max(255).optional().describe("User full name")
660
+ });
661
+ var updateEmailSchema = z.object({
662
+ email: z.string().email().describe("New email address")
663
+ });
664
+ var updatePhoneSchema = z.object({
665
+ phone: z.string().min(6).max(30).describe("New phone number")
666
+ });
667
+ var profileResponseSchema = z.object({
668
+ user: userSchema.describe("Updated user")
669
+ });
670
+ var pendingAccountChangeSchema = z.object({
671
+ changeType: z.enum(["email", "phone"]),
672
+ newEmail: z.string().email().nullable(),
673
+ newPhone: z.string().nullable(),
674
+ expiresAt: z.string().datetime()
675
+ });
676
+ var pendingAccountChangeResponseSchema = z.object({
677
+ accountChange: pendingAccountChangeSchema.nullable(),
678
+ verificationId: z.string().uuid().nullable()
679
+ });
680
+
681
+ // src/routes/handler/account-change-pending.ts
682
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
683
+
684
+ // src/db/orm/iam/account-changes/expire-pending-account-changes.ts
685
+ import { and as and3, eq as eq3, lte } from "drizzle-orm";
686
+ var expirePendingAccountChanges = (db, tenantId, userId) => {
687
+ const now = (/* @__PURE__ */ new Date()).toISOString();
688
+ return db.update(accountChangesInIam).set({
689
+ status: "expired",
690
+ updatedAt: now
691
+ }).where(
692
+ and3(
693
+ eq3(accountChangesInIam.tenantId, tenantId),
694
+ eq3(accountChangesInIam.userId, userId),
695
+ eq3(accountChangesInIam.status, "pending"),
696
+ lte(accountChangesInIam.expiresAt, now)
697
+ )
698
+ );
699
+ };
700
+
701
+ // src/db/orm/iam/account-changes/find-pending-account-change.ts
702
+ import { and as and4, desc, eq as eq4, gt as gt2 } from "drizzle-orm";
703
+ var findPendingAccountChange = async (db, tenantId, userId) => {
704
+ const now = (/* @__PURE__ */ new Date()).toISOString();
705
+ return await db.select({
706
+ changeType: accountChangesInIam.changeType,
707
+ newEmail: accountChangesInIam.newEmail,
708
+ newPhone: accountChangesInIam.newPhone,
709
+ expiresAt: accountChangesInIam.expiresAt
710
+ }).from(accountChangesInIam).where(
711
+ and4(
712
+ eq4(accountChangesInIam.tenantId, tenantId),
713
+ eq4(accountChangesInIam.userId, userId),
714
+ eq4(accountChangesInIam.status, "pending"),
715
+ gt2(accountChangesInIam.expiresAt, now)
716
+ )
717
+ ).orderBy(desc(accountChangesInIam.createdAt)).limit(1).then(([row]) => {
718
+ if (!row) {
719
+ return null;
720
+ }
721
+ if (row.changeType !== "email" && row.changeType !== "phone") {
722
+ return null;
723
+ }
724
+ return {
725
+ changeType: row.changeType,
726
+ newEmail: row.newEmail ?? null,
727
+ newPhone: row.newPhone ?? null,
728
+ expiresAt: row.expiresAt
729
+ };
730
+ });
731
+ };
732
+
733
+ // src/db/orm/iam/verifications/find-active-verification-id.ts
734
+ import { and as and5, desc as desc2, eq as eq5, gt as gt3 } from "drizzle-orm";
735
+ var findActiveVerificationId = async (db, tenantId, userId, type, to) => {
736
+ const now = (/* @__PURE__ */ new Date()).toISOString();
737
+ return await db.select({
738
+ verificationId: verificationsInIam.id,
739
+ expiresAt: verificationsInIam.expiresAt
740
+ }).from(verificationsInIam).where(
741
+ and5(
742
+ eq5(verificationsInIam.tenantId, tenantId),
743
+ eq5(verificationsInIam.userId, userId),
744
+ eq5(verificationsInIam.type, type),
745
+ eq5(verificationsInIam.to, to),
746
+ gt3(verificationsInIam.expiresAt, now)
747
+ )
748
+ ).orderBy(desc2(verificationsInIam.createdAt)).limit(1).then(([row]) => row ? row : null);
749
+ };
750
+
751
+ // src/errors.ts
752
+ var AUTH_ERRORS = {
753
+ USER_NOT_FOUND: "USER_NOT_FOUND",
754
+ INVALID_PASSWORD: "INVALID_PASSWORD",
755
+ USER_EXISTS: "USER_EXISTS",
756
+ VERIFICATION_EXPIRED: "VERIFICATION_EXPIRED",
757
+ VERIFICATION_MISMATCH: "VERIFICATION_MISMATCH",
758
+ VERIFICATION_NOT_FOUND: "VERIFICATION_NOT_FOUND",
759
+ TOO_MANY_ATTEMPTS: "TOO_MANY_ATTEMPTS",
760
+ REQUIRES_VERIFICATION: "REQUIRES_VERIFICATION",
761
+ UNAUTHORIZED: "UNAUTHORIZED",
762
+ ACCESS_DENIED: "ACCESS_DENIED",
763
+ HAS_NO_PASSWORD: "HAS_NO_PASSWORD"
764
+ };
765
+
766
+ // src/lib/tenant.ts
767
+ import { HTTPException } from "hono/http-exception";
768
+ var ensureTenantId = (config, tenantId) => {
769
+ if (config.enableTenant) {
770
+ if (!tenantId) {
771
+ throw new HTTPException(400, {
772
+ message: "Missing tenantId. Tenant isolation is enabled."
773
+ });
774
+ }
775
+ return tenantId;
776
+ }
777
+ if (!config.tenantId) {
778
+ throw new HTTPException(500, {
779
+ message: "tenantId must be provided in config when enableTenant is false."
780
+ });
781
+ }
782
+ return config.tenantId;
783
+ };
784
+
785
+ // src/routes/handler/account-change-pending.ts
786
+ var accountChangePendingHandler = async (c) => {
787
+ const { config, database, tenantId, userId } = c.var;
788
+ if (!userId) {
789
+ throw new HTTPException2(401, { message: AUTH_ERRORS.UNAUTHORIZED });
790
+ }
791
+ const resolvedTenantId = ensureTenantId(config, tenantId);
792
+ await expirePendingAccountChanges(database, resolvedTenantId, userId);
793
+ const accountChange = await findPendingAccountChange(database, resolvedTenantId, userId);
794
+ if (!accountChange) {
795
+ return c.json({ accountChange: null, verificationId: null });
796
+ }
797
+ let verification = null;
798
+ if (accountChange.changeType === "email" && accountChange.newEmail) {
799
+ verification = await findActiveVerificationId(
800
+ database,
801
+ resolvedTenantId,
802
+ userId,
803
+ "email-verification",
804
+ accountChange.newEmail
805
+ );
806
+ }
807
+ if (accountChange.changeType === "phone" && accountChange.newPhone) {
808
+ verification = await findActiveVerificationId(
809
+ database,
810
+ resolvedTenantId,
811
+ userId,
812
+ "phone-otp-change-phone",
813
+ accountChange.newPhone
814
+ );
815
+ }
816
+ if (!verification) {
817
+ return c.json({ accountChange: null, verificationId: null });
818
+ }
819
+ return c.json({
820
+ accountChange,
821
+ verificationId: verification.verificationId
822
+ });
823
+ };
655
824
 
656
825
  // src/db/orm/iam/users/find-user-by-email.ts
657
- import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
826
+ import { and as and6, eq as eq6, sql as sql2 } from "drizzle-orm";
658
827
  var findUserByEmail = (db, tenantId, email) => {
659
828
  return db.select({
660
829
  id: usersInIam.id,
@@ -668,15 +837,15 @@ var findUserByEmail = (db, tenantId, email) => {
668
837
  phoneVerified: usersInIam.phoneVerified,
669
838
  lastSignInAt: usersInIam.lastSignInAt
670
839
  }).from(usersInIam).where(
671
- and3(
672
- eq3(usersInIam.tenantId, tenantId),
840
+ and6(
841
+ eq6(usersInIam.tenantId, tenantId),
673
842
  sql2`lower(${usersInIam.email}) = lower(${email})`
674
843
  )
675
844
  ).limit(1).then(([user]) => user || null);
676
845
  };
677
846
 
678
847
  // src/db/orm/iam/users/find-user-by-phone.ts
679
- import { and as and4, eq as eq4 } from "drizzle-orm";
848
+ import { and as and7, eq as eq7 } from "drizzle-orm";
680
849
  var findUserByPhone = (db, tenantId, phone) => {
681
850
  return db.select({
682
851
  id: usersInIam.id,
@@ -689,7 +858,7 @@ var findUserByPhone = (db, tenantId, phone) => {
689
858
  emailVerified: usersInIam.emailVerified,
690
859
  phoneVerified: usersInIam.phoneVerified,
691
860
  lastSignInAt: usersInIam.lastSignInAt
692
- }).from(usersInIam).where(and4(eq4(usersInIam.tenantId, tenantId), eq4(usersInIam.phone, phone))).limit(1).then(([user]) => user || null);
861
+ }).from(usersInIam).where(and7(eq7(usersInIam.tenantId, tenantId), eq7(usersInIam.phone, phone))).limit(1).then(([user]) => user || null);
693
862
  };
694
863
 
695
864
  // src/db/orm/iam/users/find-user-by-identifier.ts
@@ -703,25 +872,6 @@ var findUserByIdentifier = async (db, tenantId, identifier) => {
703
872
  return { user, type: "phone" };
704
873
  };
705
874
 
706
- // src/lib/tenant.ts
707
- import { HTTPException } from "hono/http-exception";
708
- var ensureTenantId = (config, tenantId) => {
709
- if (config.enableTenant) {
710
- if (!tenantId) {
711
- throw new HTTPException(400, {
712
- message: "Missing tenantId. Tenant isolation is enabled."
713
- });
714
- }
715
- return tenantId;
716
- }
717
- if (!config.tenantId) {
718
- throw new HTTPException(500, {
719
- message: "tenantId must be provided in config when enableTenant is false."
720
- });
721
- }
722
- return config.tenantId;
723
- };
724
-
725
875
  // src/routes/handler/check-user.ts
726
876
  var checkUserHandler = async (c) => {
727
877
  const body = c.req.valid("json");
@@ -738,7 +888,7 @@ var checkUserHandler = async (c) => {
738
888
 
739
889
  // src/routes/handler/email-verification-confirm.ts
740
890
  import { setCookie } from "hono/cookie";
741
- import { HTTPException as HTTPException2 } from "hono/http-exception";
891
+ import { HTTPException as HTTPException3 } from "hono/http-exception";
742
892
 
743
893
  // src/db/orm/iam/sessions/insert-session.ts
744
894
  var insertSession = (db, data) => {
@@ -763,22 +913,22 @@ var insertSession = (db, data) => {
763
913
  };
764
914
 
765
915
  // src/db/orm/iam/users/update-user-verified.ts
766
- import { and as and5, eq as eq5 } from "drizzle-orm";
916
+ import { and as and8, eq as eq8 } from "drizzle-orm";
767
917
  var updateUserVerified = (db, tenantId, userId, type) => {
768
918
  return db.update(usersInIam).set({
769
919
  [type === "email" ? "emailVerified" : "phoneVerified"]: true,
770
920
  lastSignInAt: (/* @__PURE__ */ new Date()).toISOString()
771
- }).where(and5(eq5(usersInIam.id, userId), eq5(usersInIam.tenantId, tenantId)));
921
+ }).where(and8(eq8(usersInIam.id, userId), eq8(usersInIam.tenantId, tenantId)));
772
922
  };
773
923
 
774
924
  // src/db/orm/iam/verifications/consume-verification.ts
775
- import { eq as eq6 } from "drizzle-orm";
925
+ import { eq as eq9 } from "drizzle-orm";
776
926
  var consumeVerification = (db, verificationId) => {
777
- return db.delete(verificationsInIam).where(eq6(verificationsInIam.id, verificationId));
927
+ return db.delete(verificationsInIam).where(eq9(verificationsInIam.id, verificationId));
778
928
  };
779
929
 
780
930
  // src/db/orm/iam/verifications/find-verification-by-id.ts
781
- import { eq as eq7 } from "drizzle-orm";
931
+ import { eq as eq10 } from "drizzle-orm";
782
932
  var findVerificationById = (db, verificationId) => {
783
933
  return db.select({
784
934
  id: verificationsInIam.id,
@@ -790,32 +940,17 @@ var findVerificationById = (db, verificationId) => {
790
940
  expiresAt: verificationsInIam.expiresAt,
791
941
  createdAt: verificationsInIam.createdAt,
792
942
  attempt: verificationsInIam.attempt
793
- }).from(verificationsInIam).where(eq7(verificationsInIam.id, verificationId)).limit(1).then(([verification]) => verification || null);
943
+ }).from(verificationsInIam).where(eq10(verificationsInIam.id, verificationId)).limit(1).then(([verification]) => verification || null);
794
944
  };
795
945
 
796
946
  // src/db/orm/iam/verifications/update-verification-attempt.ts
797
- import { eq as eq8 } from "drizzle-orm";
947
+ import { eq as eq11 } from "drizzle-orm";
798
948
  var updateVerificationAttempt = async (db, verificationId) => {
799
949
  const verification = await findVerificationById(db, verificationId);
800
950
  if (!verification) {
801
951
  return;
802
952
  }
803
- await db.update(verificationsInIam).set({ attempt: (verification.attempt || 0) + 1 }).where(eq8(verificationsInIam.id, verificationId));
804
- };
805
-
806
- // src/errors.ts
807
- var AUTH_ERRORS = {
808
- USER_NOT_FOUND: "USER_NOT_FOUND",
809
- INVALID_PASSWORD: "INVALID_PASSWORD",
810
- USER_EXISTS: "USER_EXISTS",
811
- VERIFICATION_EXPIRED: "VERIFICATION_EXPIRED",
812
- VERIFICATION_MISMATCH: "VERIFICATION_MISMATCH",
813
- VERIFICATION_NOT_FOUND: "VERIFICATION_NOT_FOUND",
814
- TOO_MANY_ATTEMPTS: "TOO_MANY_ATTEMPTS",
815
- REQUIRES_VERIFICATION: "REQUIRES_VERIFICATION",
816
- UNAUTHORIZED: "UNAUTHORIZED",
817
- ACCESS_DENIED: "ACCESS_DENIED",
818
- HAS_NO_PASSWORD: "HAS_NO_PASSWORD"
953
+ await db.update(verificationsInIam).set({ attempt: (verification.attempt || 0) + 1 }).where(eq11(verificationsInIam.id, verificationId));
819
954
  };
820
955
 
821
956
  // src/lib/session.ts
@@ -860,19 +995,19 @@ var emailVerificationConfirmHandler = async (c) => {
860
995
  const { verificationId, code } = body;
861
996
  const verification = await findVerificationById(database, verificationId);
862
997
  if (!verification) {
863
- throw new HTTPException2(400, {
998
+ throw new HTTPException3(400, {
864
999
  message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
865
1000
  });
866
1001
  }
867
1002
  if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
868
- throw new HTTPException2(400, {
1003
+ throw new HTTPException3(400, {
869
1004
  message: AUTH_ERRORS.VERIFICATION_EXPIRED
870
1005
  });
871
1006
  }
872
1007
  const hashedCode = await hashToken(code, config.secret);
873
1008
  if (verification.code !== hashedCode) {
874
1009
  await updateVerificationAttempt(database, verificationId);
875
- throw new HTTPException2(400, {
1010
+ throw new HTTPException3(400, {
876
1011
  message: AUTH_ERRORS.VERIFICATION_MISMATCH
877
1012
  });
878
1013
  }
@@ -889,7 +1024,7 @@ var emailVerificationConfirmHandler = async (c) => {
889
1024
  verification.userId
890
1025
  );
891
1026
  if (!user) {
892
- throw new HTTPException2(500, { message: "User not found" });
1027
+ throw new HTTPException3(500, { message: "User not found" });
893
1028
  }
894
1029
  const sessionToken = crypto.randomUUID();
895
1030
  const hashedToken = await hashToken(sessionToken, config.secret);
@@ -925,16 +1060,52 @@ var emailVerificationConfirmHandler = async (c) => {
925
1060
  };
926
1061
 
927
1062
  // src/routes/handler/email-verification-request.ts
928
- import { HTTPException as HTTPException3 } from "hono/http-exception";
1063
+ import { HTTPException as HTTPException4 } from "hono/http-exception";
1064
+
1065
+ // src/db/orm/iam/account-changes/cancel-pending-account-changes.ts
1066
+ import { and as and9, eq as eq12 } from "drizzle-orm";
1067
+ var cancelPendingAccountChanges = (db, tenantId, userId, changeType) => {
1068
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1069
+ return db.update(accountChangesInIam).set({
1070
+ status: "cancelled",
1071
+ cancelledAt: now,
1072
+ updatedAt: now,
1073
+ reason: "replaced"
1074
+ }).where(
1075
+ and9(
1076
+ eq12(accountChangesInIam.tenantId, tenantId),
1077
+ eq12(accountChangesInIam.userId, userId),
1078
+ eq12(accountChangesInIam.changeType, changeType),
1079
+ eq12(accountChangesInIam.status, "pending")
1080
+ )
1081
+ );
1082
+ };
1083
+
1084
+ // src/db/orm/iam/account-changes/insert-pending-email-change.ts
1085
+ var insertPendingEmailChange = (db, data) => {
1086
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1087
+ return db.insert(accountChangesInIam).values({
1088
+ tenantId: data.tenantId,
1089
+ userId: data.userId,
1090
+ changeType: "email",
1091
+ oldEmail: data.oldEmail,
1092
+ newEmail: data.newEmail,
1093
+ oldPhone: null,
1094
+ newPhone: null,
1095
+ status: "pending",
1096
+ expiresAt: data.expiresAt,
1097
+ updatedAt: now
1098
+ });
1099
+ };
929
1100
 
930
1101
  // src/db/orm/iam/verifications/delete-verifications-by-user-and-type.ts
931
- import { and as and6, eq as eq9 } from "drizzle-orm";
1102
+ import { and as and10, eq as eq13 } from "drizzle-orm";
932
1103
  var deleteVerificationsByUserAndType = (db, tenantId, userId, type) => {
933
1104
  return db.delete(verificationsInIam).where(
934
- and6(
935
- eq9(verificationsInIam.tenantId, tenantId),
936
- eq9(verificationsInIam.userId, userId),
937
- eq9(verificationsInIam.type, type)
1105
+ and10(
1106
+ eq13(verificationsInIam.tenantId, tenantId),
1107
+ eq13(verificationsInIam.userId, userId),
1108
+ eq13(verificationsInIam.type, type)
938
1109
  )
939
1110
  );
940
1111
  };
@@ -999,8 +1170,6 @@ var ResendEmailProvider = class {
999
1170
  }
1000
1171
  async sendVerificationEmail(email, code, tenantName) {
1001
1172
  const subject = this.config.verificationSubject || `Verify your email${tenantName ? ` for ${tenantName}` : ""}`;
1002
- const verificationPath = this.config.verificationPath || "/verify-email";
1003
- const verificationUrl = `${this.config.frontendBaseUrl}${verificationPath}?token=${code}`;
1004
1173
  const html = `
1005
1174
  <!DOCTYPE html>
1006
1175
  <html>
@@ -1016,12 +1185,7 @@ var ResendEmailProvider = class {
1016
1185
  <div style="background: #f3f4f6; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 20px 0;">
1017
1186
  ${code}
1018
1187
  </div>
1019
- <p>Or click the link below:</p>
1020
- <p>
1021
- <a href="${verificationUrl}" style="background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
1022
- Verify Email
1023
- </a>
1024
- </p>
1188
+
1025
1189
  <p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
1026
1190
  This code will expire in 1 hour. If you didn't request this, please ignore this email.
1027
1191
  </p>
@@ -1031,8 +1195,6 @@ var ResendEmailProvider = class {
1031
1195
  `;
1032
1196
  const text2 = `Your verification code is: ${code}
1033
1197
 
1034
- Or visit: ${verificationUrl}
1035
-
1036
1198
  This code will expire in 1 hour.`;
1037
1199
  await this.sendEmail({
1038
1200
  to: [email],
@@ -1043,8 +1205,6 @@ This code will expire in 1 hour.`;
1043
1205
  }
1044
1206
  async sendPasswordResetEmail(email, code, tenantName) {
1045
1207
  const subject = this.config.resetPasswordSubject || `Reset your password${tenantName ? ` for ${tenantName}` : ""}`;
1046
- const resetPath = this.config.resetPasswordPath || "/reset-password";
1047
- const resetUrl = `${this.config.frontendBaseUrl}${resetPath}?token=${code}`;
1048
1208
  const html = `
1049
1209
  <!DOCTYPE html>
1050
1210
  <html>
@@ -1060,12 +1220,6 @@ This code will expire in 1 hour.`;
1060
1220
  <div style="background: #f3f4f6; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 20px 0;">
1061
1221
  ${code}
1062
1222
  </div>
1063
- <p>Or click the link below:</p>
1064
- <p>
1065
- <a href="${resetUrl}" style="background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
1066
- Reset Password
1067
- </a>
1068
- </p>
1069
1223
  <p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
1070
1224
  This code will expire in 1 hour. If you didn't request this, please ignore this email.
1071
1225
  </p>
@@ -1075,8 +1229,6 @@ This code will expire in 1 hour.`;
1075
1229
  `;
1076
1230
  const text2 = `Your password reset code is: ${code}
1077
1231
 
1078
- Or visit: ${resetUrl}
1079
-
1080
1232
  This code will expire in 1 hour.`;
1081
1233
  await this.sendEmail({
1082
1234
  to: [email],
@@ -1108,7 +1260,7 @@ var emailVerificationRequestHandler = async (c) => {
1108
1260
  const resolvedTenantId = ensureTenantId(config, tenantId);
1109
1261
  const email = body.email || user?.email;
1110
1262
  if (!email) {
1111
- throw new HTTPException3(400, { message: "Email required" });
1263
+ throw new HTTPException4(400, { message: "Email required" });
1112
1264
  }
1113
1265
  let userId = user?.id;
1114
1266
  if (!userId) {
@@ -1118,7 +1270,7 @@ var emailVerificationRequestHandler = async (c) => {
1118
1270
  email
1119
1271
  );
1120
1272
  if (!lookup.user) {
1121
- throw new HTTPException3(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1273
+ throw new HTTPException4(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1122
1274
  }
1123
1275
  userId = lookup.user.id;
1124
1276
  }
@@ -1139,26 +1291,42 @@ var emailVerificationRequestHandler = async (c) => {
1139
1291
  expiresAt,
1140
1292
  to: email
1141
1293
  });
1294
+ if (user?.id && body.email) {
1295
+ await cancelPendingAccountChanges(
1296
+ database,
1297
+ resolvedTenantId,
1298
+ user.id,
1299
+ "email"
1300
+ );
1301
+ await insertPendingEmailChange(database, {
1302
+ tenantId: resolvedTenantId,
1303
+ userId: user.id,
1304
+ oldEmail: user.email ?? "",
1305
+ newEmail: body.email,
1306
+ expiresAt: verification.expiresAt
1307
+ });
1308
+ }
1142
1309
  const emailProvider = new ResendEmailProvider(config.email.resend);
1143
1310
  await emailProvider.sendVerificationEmail(email, code);
1144
1311
  return c.json({ verificationId: verification.id });
1145
1312
  };
1146
1313
 
1147
1314
  // src/routes/handler/me.ts
1148
- import { HTTPException as HTTPException4 } from "hono/http-exception";
1315
+ import { HTTPException as HTTPException5 } from "hono/http-exception";
1149
1316
  var meHandler = (c) => {
1150
1317
  const { user } = c.var;
1151
1318
  if (!user) {
1152
- throw new HTTPException4(401, { message: "Unauthorized" });
1319
+ throw new HTTPException5(401, { message: "Unauthorized" });
1153
1320
  }
1154
1321
  return c.json({ user });
1155
1322
  };
1156
1323
 
1157
1324
  // src/routes/handler/password-change.ts
1158
- import { HTTPException as HTTPException5 } from "hono/http-exception";
1325
+ import { getCookie } from "hono/cookie";
1326
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
1159
1327
 
1160
1328
  // src/db/orm/iam/accounts/find-account-by-provider.ts
1161
- import { and as and7, eq as eq10 } from "drizzle-orm";
1329
+ import { and as and11, eq as eq14 } from "drizzle-orm";
1162
1330
  var findAccountByProvider = (db, tenantId, userId, provider) => {
1163
1331
  return db.select({
1164
1332
  id: accountsInIam.id,
@@ -1168,34 +1336,34 @@ var findAccountByProvider = (db, tenantId, userId, provider) => {
1168
1336
  providerAccountId: accountsInIam.providerAccountId,
1169
1337
  password: accountsInIam.password
1170
1338
  }).from(accountsInIam).where(
1171
- and7(
1172
- eq10(accountsInIam.tenantId, tenantId),
1173
- eq10(accountsInIam.userId, userId),
1174
- eq10(accountsInIam.provider, provider)
1339
+ and11(
1340
+ eq14(accountsInIam.tenantId, tenantId),
1341
+ eq14(accountsInIam.userId, userId),
1342
+ eq14(accountsInIam.provider, provider)
1175
1343
  )
1176
1344
  ).limit(1).then(([account]) => account || null);
1177
1345
  };
1178
1346
 
1179
1347
  // src/db/orm/iam/accounts/update-account-password.ts
1180
- import { and as and8, eq as eq11 } from "drizzle-orm";
1348
+ import { and as and12, eq as eq15 } from "drizzle-orm";
1181
1349
  var updateAccountPassword = (db, tenantId, userId, password) => {
1182
1350
  return db.update(accountsInIam).set({ password }).where(
1183
- and8(
1184
- eq11(accountsInIam.tenantId, tenantId),
1185
- eq11(accountsInIam.userId, userId),
1186
- eq11(accountsInIam.provider, "credentials")
1351
+ and12(
1352
+ eq15(accountsInIam.tenantId, tenantId),
1353
+ eq15(accountsInIam.userId, userId),
1354
+ eq15(accountsInIam.provider, "credentials")
1187
1355
  )
1188
1356
  );
1189
1357
  };
1190
1358
 
1191
1359
  // src/db/orm/iam/sessions/delete-session-by-id.ts
1192
- import { eq as eq12 } from "drizzle-orm";
1360
+ import { eq as eq16 } from "drizzle-orm";
1193
1361
  var deleteSessionById = (db, sessionId) => {
1194
- return db.delete(sessionsInIam).where(eq12(sessionsInIam.id, sessionId));
1362
+ return db.delete(sessionsInIam).where(eq16(sessionsInIam.id, sessionId));
1195
1363
  };
1196
1364
 
1197
1365
  // src/db/orm/iam/sessions/list-sessions-for-user.ts
1198
- import { and as and9, asc, eq as eq13, gt as gt2 } from "drizzle-orm";
1366
+ import { and as and13, asc, eq as eq17, gt as gt4 } from "drizzle-orm";
1199
1367
  var listSessionsForUser = (db, tenantId, userId) => {
1200
1368
  return db.select({
1201
1369
  id: sessionsInIam.id,
@@ -1207,10 +1375,10 @@ var listSessionsForUser = (db, tenantId, userId) => {
1207
1375
  userAgent: sessionsInIam.userAgent,
1208
1376
  ip: sessionsInIam.ip
1209
1377
  }).from(sessionsInIam).where(
1210
- and9(
1211
- eq13(sessionsInIam.tenantId, tenantId),
1212
- eq13(sessionsInIam.userId, userId),
1213
- gt2(sessionsInIam.expiresAt, (/* @__PURE__ */ new Date()).toISOString())
1378
+ and13(
1379
+ eq17(sessionsInIam.tenantId, tenantId),
1380
+ eq17(sessionsInIam.userId, userId),
1381
+ gt4(sessionsInIam.expiresAt, (/* @__PURE__ */ new Date()).toISOString())
1214
1382
  )
1215
1383
  ).orderBy(asc(sessionsInIam.createdAt)).then((sessions) => sessions);
1216
1384
  };
@@ -1220,10 +1388,10 @@ var changePasswordHandler = async (c) => {
1220
1388
  const body = c.req.valid("json");
1221
1389
  const { config, database, tenantId, userId, user } = c.var;
1222
1390
  if (!userId) {
1223
- throw new HTTPException5(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1391
+ throw new HTTPException6(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1224
1392
  }
1225
1393
  if (!user) {
1226
- throw new HTTPException5(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1394
+ throw new HTTPException6(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1227
1395
  }
1228
1396
  const resolvedTenantId = ensureTenantId(config, tenantId);
1229
1397
  const { currentPassword, newPassword } = body;
@@ -1234,11 +1402,11 @@ var changePasswordHandler = async (c) => {
1234
1402
  "credentials"
1235
1403
  );
1236
1404
  if (!account?.password) {
1237
- throw new HTTPException5(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
1405
+ throw new HTTPException6(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
1238
1406
  }
1239
1407
  const passwordValid = await verifyPassword(currentPassword, account.password);
1240
1408
  if (!passwordValid) {
1241
- throw new HTTPException5(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
1409
+ throw new HTTPException6(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
1242
1410
  }
1243
1411
  const passwordHash = await hashPassword(newPassword);
1244
1412
  await updateAccountPassword(database, resolvedTenantId, userId, passwordHash);
@@ -1247,7 +1415,7 @@ var changePasswordHandler = async (c) => {
1247
1415
  resolvedTenantId,
1248
1416
  userId
1249
1417
  );
1250
- const currentSessionToken = c.req.cookie("session_token");
1418
+ const currentSessionToken = getCookie(c, "session_token");
1251
1419
  if (currentSessionToken) {
1252
1420
  const hashedToken = await hashToken(currentSessionToken, config.secret);
1253
1421
  const currentSession = await findSessionByToken(database, hashedToken);
@@ -1372,7 +1540,7 @@ var forgotPasswordHandler = async (c) => {
1372
1540
 
1373
1541
  // src/routes/handler/password-reset.ts
1374
1542
  import { setCookie as setCookie2 } from "hono/cookie";
1375
- import { HTTPException as HTTPException6 } from "hono/http-exception";
1543
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
1376
1544
  var resetPasswordHandler = async (c) => {
1377
1545
  const body = c.req.valid("json");
1378
1546
  const { config, database, tenantId } = c.var;
@@ -1380,17 +1548,17 @@ var resetPasswordHandler = async (c) => {
1380
1548
  const { verificationId, code, password } = body;
1381
1549
  const verification = await findVerificationById(database, verificationId);
1382
1550
  if (!verification) {
1383
- throw new HTTPException6(400, {
1551
+ throw new HTTPException7(400, {
1384
1552
  message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
1385
1553
  });
1386
1554
  }
1387
1555
  if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
1388
- throw new HTTPException6(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
1556
+ throw new HTTPException7(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
1389
1557
  }
1390
1558
  const hashedCode = await hashToken(code, config.secret);
1391
1559
  if (verification.code !== hashedCode) {
1392
1560
  await updateVerificationAttempt(database, verificationId);
1393
- throw new HTTPException6(400, {
1561
+ throw new HTTPException7(400, {
1394
1562
  message: AUTH_ERRORS.VERIFICATION_MISMATCH
1395
1563
  });
1396
1564
  }
@@ -1428,7 +1596,7 @@ var resetPasswordHandler = async (c) => {
1428
1596
  verification.userId
1429
1597
  );
1430
1598
  if (!user) {
1431
- throw new HTTPException6(500, { message: "User not found" });
1599
+ throw new HTTPException7(500, { message: "User not found" });
1432
1600
  }
1433
1601
  setCookie2(c, "session_token", sessionToken, {
1434
1602
  httpOnly: true,
@@ -1451,9 +1619,38 @@ var resetPasswordHandler = async (c) => {
1451
1619
  });
1452
1620
  };
1453
1621
 
1622
+ // src/routes/handler/password-verify.ts
1623
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
1624
+ var verifyPasswordHandler = async (c) => {
1625
+ const body = c.req.valid("json");
1626
+ const { config, database, tenantId, userId, user } = c.var;
1627
+ if (!userId) {
1628
+ throw new HTTPException8(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1629
+ }
1630
+ if (!user) {
1631
+ throw new HTTPException8(401, { message: AUTH_ERRORS.UNAUTHORIZED });
1632
+ }
1633
+ const resolvedTenantId = ensureTenantId(config, tenantId);
1634
+ const { password } = body;
1635
+ const account = await findAccountByProvider(
1636
+ database,
1637
+ resolvedTenantId,
1638
+ userId,
1639
+ "credentials"
1640
+ );
1641
+ if (!account?.password) {
1642
+ throw new HTTPException8(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
1643
+ }
1644
+ const passwordValid = await verifyPassword(password, account.password);
1645
+ if (!passwordValid) {
1646
+ throw new HTTPException8(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
1647
+ }
1648
+ return c.json({ message: "Password verified" });
1649
+ };
1650
+
1454
1651
  // src/routes/handler/phone-verification-confirm.ts
1455
1652
  import { setCookie as setCookie3 } from "hono/cookie";
1456
- import { HTTPException as HTTPException7 } from "hono/http-exception";
1653
+ import { HTTPException as HTTPException9 } from "hono/http-exception";
1457
1654
  var phoneVerificationConfirmHandler = async (c) => {
1458
1655
  const body = c.req.valid("json");
1459
1656
  const { config, database, tenantId } = c.var;
@@ -1461,17 +1658,17 @@ var phoneVerificationConfirmHandler = async (c) => {
1461
1658
  const { verificationId, code, context } = body;
1462
1659
  const verification = await findVerificationById(database, verificationId);
1463
1660
  if (!verification) {
1464
- throw new HTTPException7(400, {
1661
+ throw new HTTPException9(400, {
1465
1662
  message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
1466
1663
  });
1467
1664
  }
1468
1665
  if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
1469
- throw new HTTPException7(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
1666
+ throw new HTTPException9(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
1470
1667
  }
1471
1668
  const hashedCode = await hashToken(code, config.secret);
1472
1669
  if (verification.code !== hashedCode) {
1473
1670
  await updateVerificationAttempt(database, verificationId);
1474
- throw new HTTPException7(400, {
1671
+ throw new HTTPException9(400, {
1475
1672
  message: AUTH_ERRORS.VERIFICATION_MISMATCH
1476
1673
  });
1477
1674
  }
@@ -1500,7 +1697,7 @@ var phoneVerificationConfirmHandler = async (c) => {
1500
1697
  }
1501
1698
  const user = verification.userId ? await findUserById(database, resolvedTenantId, verification.userId) : null;
1502
1699
  if (!user) {
1503
- throw new HTTPException7(500, { message: "User not found" });
1700
+ throw new HTTPException9(500, { message: "User not found" });
1504
1701
  }
1505
1702
  if (context === "sign-in" || context === "change-phone" || context === "sign-up") {
1506
1703
  const sessionToken = crypto.randomUUID();
@@ -1545,14 +1742,33 @@ var phoneVerificationConfirmHandler = async (c) => {
1545
1742
  };
1546
1743
 
1547
1744
  // src/routes/handler/phone-verification-request.ts
1548
- import { HTTPException as HTTPException8 } from "hono/http-exception";
1745
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
1746
+
1747
+ // src/db/orm/iam/account-changes/insert-pending-phone-change.ts
1748
+ var insertPendingPhoneChange = (db, data) => {
1749
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1750
+ return db.insert(accountChangesInIam).values({
1751
+ tenantId: data.tenantId,
1752
+ userId: data.userId,
1753
+ changeType: "phone",
1754
+ oldPhone: data.oldPhone,
1755
+ newPhone: data.newPhone,
1756
+ oldEmail: null,
1757
+ newEmail: null,
1758
+ status: "pending",
1759
+ expiresAt: data.expiresAt,
1760
+ updatedAt: now
1761
+ });
1762
+ };
1763
+
1764
+ // src/routes/handler/phone-verification-request.ts
1549
1765
  var phoneVerificationRequestHandler = async (c) => {
1550
1766
  const body = c.req.valid("json");
1551
1767
  const { config, database, tenantId, user } = c.var;
1552
1768
  const resolvedTenantId = ensureTenantId(config, tenantId);
1553
1769
  const { phone, context } = body;
1554
1770
  if (!phone) {
1555
- throw new HTTPException8(400, { message: "Phone required" });
1771
+ throw new HTTPException10(400, { message: "Phone required" });
1556
1772
  }
1557
1773
  let userId = user?.id;
1558
1774
  if (!userId) {
@@ -1562,12 +1778,12 @@ var phoneVerificationRequestHandler = async (c) => {
1562
1778
  phone
1563
1779
  );
1564
1780
  if (!lookup.user) {
1565
- throw new HTTPException8(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1781
+ throw new HTTPException10(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1566
1782
  }
1567
1783
  userId = lookup.user.id;
1568
1784
  }
1569
1785
  if (!userId) {
1570
- throw new HTTPException8(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1786
+ throw new HTTPException10(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
1571
1787
  }
1572
1788
  await deleteVerificationsByUserAndType(
1573
1789
  database,
@@ -1586,6 +1802,21 @@ var phoneVerificationRequestHandler = async (c) => {
1586
1802
  expiresAt,
1587
1803
  to: phone
1588
1804
  });
1805
+ if (context === "change-phone" && user?.id) {
1806
+ await cancelPendingAccountChanges(
1807
+ database,
1808
+ resolvedTenantId,
1809
+ user.id,
1810
+ "phone"
1811
+ );
1812
+ await insertPendingPhoneChange(database, {
1813
+ tenantId: resolvedTenantId,
1814
+ userId: user.id,
1815
+ oldPhone: user.phone ?? "",
1816
+ newPhone: phone,
1817
+ expiresAt: verification.expiresAt
1818
+ });
1819
+ }
1589
1820
  const smsProvider = new AfroSmsProvider(config.phone.smsConfig);
1590
1821
  await smsProvider.sendVerificationSms(phone, code);
1591
1822
  return c.json({ verificationId: verification.id });
@@ -1608,7 +1839,7 @@ var sessionHandler = (c) => {
1608
1839
 
1609
1840
  // src/routes/handler/sign-in.ts
1610
1841
  import { setCookie as setCookie4 } from "hono/cookie";
1611
- import { HTTPException as HTTPException9 } from "hono/http-exception";
1842
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
1612
1843
 
1613
1844
  // src/db/orm/iam/sessions/delete-oldest-sessions.ts
1614
1845
  var deleteOldestSessions = async (db, tenantId, userId, keepCount) => {
@@ -1623,9 +1854,9 @@ var deleteOldestSessions = async (db, tenantId, userId, keepCount) => {
1623
1854
  };
1624
1855
 
1625
1856
  // src/db/orm/iam/users/update-last-sign-in.ts
1626
- import { and as and10, eq as eq14 } from "drizzle-orm";
1857
+ import { and as and14, eq as eq18 } from "drizzle-orm";
1627
1858
  var updateLastSignIn = (db, tenantId, userId) => {
1628
- return db.update(usersInIam).set({ lastSignInAt: (/* @__PURE__ */ new Date()).toISOString(), loginAttempt: 0 }).where(and10(eq14(usersInIam.id, userId), eq14(usersInIam.tenantId, tenantId)));
1859
+ return db.update(usersInIam).set({ lastSignInAt: (/* @__PURE__ */ new Date()).toISOString(), loginAttempt: 0 }).where(and14(eq18(usersInIam.id, userId), eq18(usersInIam.tenantId, tenantId)));
1629
1860
  };
1630
1861
 
1631
1862
  // src/routes/handler/sign-in.ts
@@ -1640,7 +1871,7 @@ var signInHandler = async (c) => {
1640
1871
  identifier
1641
1872
  );
1642
1873
  if (!lookup.user) {
1643
- throw new HTTPException9(401, { message: AUTH_ERRORS.USER_NOT_FOUND });
1874
+ throw new HTTPException11(401, { message: AUTH_ERRORS.USER_NOT_FOUND });
1644
1875
  }
1645
1876
  const account = await findAccountByProvider(
1646
1877
  database,
@@ -1649,13 +1880,14 @@ var signInHandler = async (c) => {
1649
1880
  "credentials"
1650
1881
  );
1651
1882
  if (!account?.password) {
1652
- throw new HTTPException9(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
1883
+ throw new HTTPException11(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
1653
1884
  }
1654
1885
  const passwordValid = await verifyPassword(password, account.password);
1655
1886
  if (!passwordValid) {
1656
- throw new HTTPException9(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
1887
+ throw new HTTPException11(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
1657
1888
  }
1658
1889
  const isEmail = lookup.type === "email";
1890
+ const isPhone = lookup.type === "phone";
1659
1891
  const isVerified = isEmail ? lookup.user.emailVerified : lookup.user.phoneVerified;
1660
1892
  if (isEmail && config.email.verificationRequired && !isVerified) {
1661
1893
  const code = generateOtpCode(6);
@@ -1676,7 +1908,7 @@ var signInHandler = async (c) => {
1676
1908
  requiresVerification: true
1677
1909
  });
1678
1910
  }
1679
- if (!isEmail && config.phone.verificationRequired && !isVerified) {
1911
+ if (isPhone && config.phone.verificationRequired && !isVerified) {
1680
1912
  const code = generateOtpCode(config.phone.otpLength);
1681
1913
  const hashedCode = await hashToken(code, config.secret);
1682
1914
  const expiresAt2 = addDuration(config.phone.otpExpiresIn);
@@ -1738,11 +1970,11 @@ var signInHandler = async (c) => {
1738
1970
  };
1739
1971
 
1740
1972
  // src/routes/handler/sign-out.ts
1741
- import { deleteCookie, getCookie } from "hono/cookie";
1973
+ import { deleteCookie, getCookie as getCookie2 } from "hono/cookie";
1742
1974
  var signOutHandler = async (c) => {
1743
1975
  const { config, database, tenantId } = c.var;
1744
1976
  ensureTenantId(config, tenantId);
1745
- const sessionToken = getCookie(c, "session_token");
1977
+ const sessionToken = getCookie2(c, "session_token");
1746
1978
  if (!sessionToken) {
1747
1979
  return c.json({ message: "Signed out" });
1748
1980
  }
@@ -1763,7 +1995,7 @@ var signOutHandler = async (c) => {
1763
1995
 
1764
1996
  // src/routes/handler/sign-up.ts
1765
1997
  import { setCookie as setCookie5 } from "hono/cookie";
1766
- import { HTTPException as HTTPException10 } from "hono/http-exception";
1998
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
1767
1999
 
1768
2000
  // src/db/orm/iam/accounts/insert-credentials-account.ts
1769
2001
  var insertCredentialsAccount = (db, data) => {
@@ -1777,7 +2009,7 @@ var insertCredentialsAccount = (db, data) => {
1777
2009
  };
1778
2010
 
1779
2011
  // src/db/orm/iam/users/find-user-by-handle.ts
1780
- import { and as and11, eq as eq15, sql as sql3 } from "drizzle-orm";
2012
+ import { and as and15, eq as eq19, sql as sql3 } from "drizzle-orm";
1781
2013
  var findUserByHandle = (db, tenantId, handle) => {
1782
2014
  return db.select({
1783
2015
  id: usersInIam.id,
@@ -1791,8 +2023,8 @@ var findUserByHandle = (db, tenantId, handle) => {
1791
2023
  phoneVerified: usersInIam.phoneVerified,
1792
2024
  lastSignInAt: usersInIam.lastSignInAt
1793
2025
  }).from(usersInIam).where(
1794
- and11(
1795
- eq15(usersInIam.tenantId, tenantId),
2026
+ and15(
2027
+ eq19(usersInIam.tenantId, tenantId),
1796
2028
  sql3`lower(${usersInIam.handle}) = lower(${handle})`
1797
2029
  )
1798
2030
  ).limit(1).then(([user]) => user || null);
@@ -1830,7 +2062,7 @@ var signUpHandler = async (c) => {
1830
2062
  const resolvedTenantId = ensureTenantId(config, tenantId);
1831
2063
  const { email, phone, password, fullName, handle } = body;
1832
2064
  if (!(email || phone)) {
1833
- throw new HTTPException10(400, {
2065
+ throw new HTTPException12(400, {
1834
2066
  message: "Either email or phone is required"
1835
2067
  });
1836
2068
  }
@@ -1841,7 +2073,7 @@ var signUpHandler = async (c) => {
1841
2073
  identifier
1842
2074
  );
1843
2075
  if (existing.user) {
1844
- throw new HTTPException10(409, { message: AUTH_ERRORS.USER_EXISTS });
2076
+ throw new HTTPException12(409, { message: AUTH_ERRORS.USER_EXISTS });
1845
2077
  }
1846
2078
  const userHandle = handle || generateHandle(email || phone);
1847
2079
  const existingHandle = await findUserByHandle(
@@ -1850,7 +2082,7 @@ var signUpHandler = async (c) => {
1850
2082
  userHandle
1851
2083
  );
1852
2084
  if (existingHandle) {
1853
- throw new HTTPException10(409, { message: "Handle already taken" });
2085
+ throw new HTTPException12(409, { message: "Handle already taken" });
1854
2086
  }
1855
2087
  const user = await insertUser(database, {
1856
2088
  tenantId: resolvedTenantId,
@@ -1943,6 +2175,227 @@ var signUpHandler = async (c) => {
1943
2175
  });
1944
2176
  };
1945
2177
 
2178
+ // src/routes/handler/update-email.ts
2179
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
2180
+
2181
+ // src/db/orm/iam/account-changes/mark-pending-account-change-applied.ts
2182
+ import { and as and16, eq as eq20 } from "drizzle-orm";
2183
+ var markPendingAccountChangeApplied = (db, tenantId, userId, changeType, newValue) => {
2184
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2185
+ const valueCondition = changeType === "email" ? eq20(accountChangesInIam.newEmail, newValue) : eq20(accountChangesInIam.newPhone, newValue);
2186
+ return db.update(accountChangesInIam).set({
2187
+ status: "applied",
2188
+ confirmedAt: now,
2189
+ updatedAt: now
2190
+ }).where(
2191
+ and16(
2192
+ eq20(accountChangesInIam.tenantId, tenantId),
2193
+ eq20(accountChangesInIam.userId, userId),
2194
+ eq20(accountChangesInIam.changeType, changeType),
2195
+ eq20(accountChangesInIam.status, "pending"),
2196
+ valueCondition
2197
+ )
2198
+ );
2199
+ };
2200
+
2201
+ // src/db/orm/iam/accounts/update-credentials-provider-account-id.ts
2202
+ import { and as and17, eq as eq21 } from "drizzle-orm";
2203
+ var updateCredentialsProviderAccountId = async (db, tenantId, userId, providerAccountId) => {
2204
+ const updated = await db.update(accountsInIam).set({ providerAccountId }).where(
2205
+ and17(
2206
+ eq21(accountsInIam.tenantId, tenantId),
2207
+ eq21(accountsInIam.userId, userId),
2208
+ eq21(accountsInIam.provider, "credentials")
2209
+ )
2210
+ ).returning({ id: accountsInIam.id }).then(([row]) => row?.id);
2211
+ return Boolean(updated);
2212
+ };
2213
+
2214
+ // src/db/orm/iam/sessions/delete-other-sessions.ts
2215
+ import { and as and18, eq as eq22, ne } from "drizzle-orm";
2216
+ var deleteOtherSessions = (db, tenantId, userId, currentSessionId) => {
2217
+ return db.delete(sessionsInIam).where(
2218
+ and18(
2219
+ eq22(sessionsInIam.tenantId, tenantId),
2220
+ eq22(sessionsInIam.userId, userId),
2221
+ ne(sessionsInIam.id, currentSessionId)
2222
+ )
2223
+ );
2224
+ };
2225
+
2226
+ // src/db/orm/iam/users/update-user-email.ts
2227
+ import { and as and19, eq as eq23, sql as sql4 } from "drizzle-orm";
2228
+ var updateUserEmail = (db, tenantId, userId, email) => {
2229
+ return db.update(usersInIam).set({
2230
+ email,
2231
+ emailVerified: true,
2232
+ updatedAt: sql4`CURRENT_TIMESTAMP`
2233
+ }).where(and19(eq23(usersInIam.id, userId), eq23(usersInIam.tenantId, tenantId))).returning({
2234
+ id: usersInIam.id,
2235
+ tenantId: usersInIam.tenantId,
2236
+ fullName: usersInIam.fullName,
2237
+ email: usersInIam.email,
2238
+ phone: usersInIam.phone,
2239
+ handle: usersInIam.handle,
2240
+ image: usersInIam.image,
2241
+ emailVerified: usersInIam.emailVerified,
2242
+ phoneVerified: usersInIam.phoneVerified,
2243
+ lastSignInAt: usersInIam.lastSignInAt
2244
+ }).then(([user]) => user || null);
2245
+ };
2246
+
2247
+ // src/routes/handler/update-email.ts
2248
+ var updateEmailHandler = async (c) => {
2249
+ const body = c.req.valid("json");
2250
+ const { config, database, tenantId, userId, user, session } = c.var;
2251
+ if (!userId) {
2252
+ throw new HTTPException13(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2253
+ }
2254
+ if (!user) {
2255
+ throw new HTTPException13(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2256
+ }
2257
+ const resolvedTenantId = ensureTenantId(config, tenantId);
2258
+ if (user.email && session?.id) {
2259
+ await deleteOtherSessions(database, resolvedTenantId, userId, session.id);
2260
+ }
2261
+ const updatedUser = await updateUserEmail(
2262
+ database,
2263
+ resolvedTenantId,
2264
+ userId,
2265
+ body.email
2266
+ );
2267
+ if (!updatedUser) {
2268
+ throw new HTTPException13(404, { message: "User not found" });
2269
+ }
2270
+ await markPendingAccountChangeApplied(
2271
+ database,
2272
+ resolvedTenantId,
2273
+ userId,
2274
+ "email",
2275
+ body.email
2276
+ );
2277
+ await updateCredentialsProviderAccountId(
2278
+ database,
2279
+ resolvedTenantId,
2280
+ userId,
2281
+ body.email
2282
+ );
2283
+ return c.json({ user: updatedUser }, 200);
2284
+ };
2285
+
2286
+ // src/routes/handler/update-phone.ts
2287
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
2288
+
2289
+ // src/db/orm/iam/users/update-user-phone.ts
2290
+ import { and as and20, eq as eq24, sql as sql5 } from "drizzle-orm";
2291
+ var updateUserPhone = (db, tenantId, userId, phone) => {
2292
+ return db.update(usersInIam).set({
2293
+ phone,
2294
+ phoneVerified: true,
2295
+ updatedAt: sql5`CURRENT_TIMESTAMP`
2296
+ }).where(and20(eq24(usersInIam.id, userId), eq24(usersInIam.tenantId, tenantId))).returning({
2297
+ id: usersInIam.id,
2298
+ tenantId: usersInIam.tenantId,
2299
+ fullName: usersInIam.fullName,
2300
+ email: usersInIam.email,
2301
+ phone: usersInIam.phone,
2302
+ handle: usersInIam.handle,
2303
+ image: usersInIam.image,
2304
+ emailVerified: usersInIam.emailVerified,
2305
+ phoneVerified: usersInIam.phoneVerified,
2306
+ lastSignInAt: usersInIam.lastSignInAt
2307
+ }).then(([user]) => user || null);
2308
+ };
2309
+
2310
+ // src/routes/handler/update-phone.ts
2311
+ var updatePhoneHandler = async (c) => {
2312
+ const body = c.req.valid("json");
2313
+ const { config, database, tenantId, userId, user, session } = c.var;
2314
+ if (!userId) {
2315
+ throw new HTTPException14(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2316
+ }
2317
+ if (!user) {
2318
+ throw new HTTPException14(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2319
+ }
2320
+ const resolvedTenantId = ensureTenantId(config, tenantId);
2321
+ if (user.phone && session?.id) {
2322
+ await deleteOtherSessions(database, resolvedTenantId, userId, session.id);
2323
+ }
2324
+ const updatedUser = await updateUserPhone(
2325
+ database,
2326
+ resolvedTenantId,
2327
+ userId,
2328
+ body.phone
2329
+ );
2330
+ if (!updatedUser) {
2331
+ throw new HTTPException14(404, { message: "User not found" });
2332
+ }
2333
+ await markPendingAccountChangeApplied(
2334
+ database,
2335
+ resolvedTenantId,
2336
+ userId,
2337
+ "phone",
2338
+ body.phone
2339
+ );
2340
+ await updateCredentialsProviderAccountId(
2341
+ database,
2342
+ resolvedTenantId,
2343
+ userId,
2344
+ body.phone
2345
+ );
2346
+ return c.json({ user: updatedUser }, 200);
2347
+ };
2348
+
2349
+ // src/routes/handler/update-profile.ts
2350
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
2351
+
2352
+ // src/db/orm/iam/users/update-user-profile.ts
2353
+ import { and as and21, eq as eq25, sql as sql6 } from "drizzle-orm";
2354
+ var updateUserProfile = async (db, tenantId, userId, data) => {
2355
+ const updateData = {};
2356
+ if (data.fullName !== void 0) {
2357
+ updateData.fullName = data.fullName;
2358
+ }
2359
+ return await db.update(usersInIam).set({
2360
+ ...updateData,
2361
+ updatedAt: sql6`CURRENT_TIMESTAMP`
2362
+ }).where(and21(eq25(usersInIam.id, userId), eq25(usersInIam.tenantId, tenantId))).returning({
2363
+ id: usersInIam.id,
2364
+ tenantId: usersInIam.tenantId,
2365
+ fullName: usersInIam.fullName,
2366
+ email: usersInIam.email,
2367
+ phone: usersInIam.phone,
2368
+ handle: usersInIam.handle,
2369
+ image: usersInIam.image,
2370
+ emailVerified: usersInIam.emailVerified,
2371
+ phoneVerified: usersInIam.phoneVerified,
2372
+ lastSignInAt: usersInIam.lastSignInAt
2373
+ }).then(([user]) => user || null);
2374
+ };
2375
+
2376
+ // src/routes/handler/update-profile.ts
2377
+ var updateProfileHandler = async (c) => {
2378
+ const body = c.req.valid("json");
2379
+ const { config, database, tenantId, userId, user } = c.var;
2380
+ if (!userId) {
2381
+ throw new HTTPException15(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2382
+ }
2383
+ if (!user) {
2384
+ throw new HTTPException15(401, { message: AUTH_ERRORS.UNAUTHORIZED });
2385
+ }
2386
+ const resolvedTenantId = ensureTenantId(config, tenantId);
2387
+ const updatedUser = await updateUserProfile(
2388
+ database,
2389
+ resolvedTenantId,
2390
+ userId,
2391
+ body
2392
+ );
2393
+ if (!updatedUser) {
2394
+ throw new HTTPException15(404, { message: "User not found" });
2395
+ }
2396
+ return c.json({ user: updatedUser }, 200);
2397
+ };
2398
+
1946
2399
  // src/routes/auth.route.ts
1947
2400
  var signUpRoute = createRoute({
1948
2401
  method: "post",
@@ -2088,6 +2541,26 @@ var sessionRoute = createRoute({
2088
2541
  }
2089
2542
  }
2090
2543
  });
2544
+ var accountChangePendingRoute = createRoute({
2545
+ method: "get",
2546
+ path: "/account-change/pending",
2547
+ tags: ["Profile"],
2548
+ summary: "Get pending account change (email/phone)",
2549
+ responses: {
2550
+ 200: {
2551
+ content: {
2552
+ "application/json": {
2553
+ schema: pendingAccountChangeResponseSchema
2554
+ }
2555
+ },
2556
+ description: "Pending account change"
2557
+ },
2558
+ 401: {
2559
+ content: { "application/json": { schema: errorResponseSchema } },
2560
+ description: "Unauthorized"
2561
+ }
2562
+ }
2563
+ });
2091
2564
  var emailVerificationRequestRoute = createRoute({
2092
2565
  method: "post",
2093
2566
  path: "/email/verification/request",
@@ -2238,6 +2711,31 @@ var resetPasswordRoute = createRoute({
2238
2711
  }
2239
2712
  }
2240
2713
  });
2714
+ var verifyPasswordRoute = createRoute({
2715
+ method: "post",
2716
+ path: "/password/verify",
2717
+ tags: ["Password"],
2718
+ summary: "Verify password",
2719
+ request: {
2720
+ body: {
2721
+ content: {
2722
+ "application/json": {
2723
+ schema: verifyPasswordSchema
2724
+ }
2725
+ }
2726
+ }
2727
+ },
2728
+ responses: {
2729
+ 200: {
2730
+ content: { "application/json": { schema: messageSchema } },
2731
+ description: "Password verified"
2732
+ },
2733
+ 401: {
2734
+ content: { "application/json": { schema: errorResponseSchema } },
2735
+ description: "Invalid password"
2736
+ }
2737
+ }
2738
+ });
2241
2739
  var changePasswordRoute = createRoute({
2242
2740
  method: "post",
2243
2741
  path: "/password/change",
@@ -2263,8 +2761,131 @@ var changePasswordRoute = createRoute({
2263
2761
  }
2264
2762
  }
2265
2763
  });
2764
+ var updateProfileRoute = createRoute({
2765
+ method: "put",
2766
+ path: "/profile",
2767
+ tags: ["Profile"],
2768
+ summary: "Update user profile",
2769
+ request: {
2770
+ body: {
2771
+ content: {
2772
+ "application/json": {
2773
+ schema: updateProfileSchema
2774
+ }
2775
+ }
2776
+ }
2777
+ },
2778
+ responses: {
2779
+ 200: {
2780
+ content: {
2781
+ "application/json": {
2782
+ schema: profileResponseSchema
2783
+ }
2784
+ },
2785
+ description: "Profile updated"
2786
+ },
2787
+ 401: {
2788
+ content: {
2789
+ "application/json": {
2790
+ schema: errorResponseSchema
2791
+ }
2792
+ },
2793
+ description: "Unauthorized"
2794
+ },
2795
+ 404: {
2796
+ content: {
2797
+ "application/json": {
2798
+ schema: errorResponseSchema
2799
+ }
2800
+ },
2801
+ description: "User not found"
2802
+ }
2803
+ }
2804
+ });
2805
+ var updateEmailRoute = createRoute({
2806
+ method: "put",
2807
+ path: "/profile/email",
2808
+ tags: ["Profile"],
2809
+ summary: "Update user email",
2810
+ request: {
2811
+ body: {
2812
+ content: {
2813
+ "application/json": {
2814
+ schema: updateEmailSchema
2815
+ }
2816
+ }
2817
+ }
2818
+ },
2819
+ responses: {
2820
+ 200: {
2821
+ content: {
2822
+ "application/json": {
2823
+ schema: profileResponseSchema
2824
+ }
2825
+ },
2826
+ description: "Email updated"
2827
+ },
2828
+ 401: {
2829
+ content: {
2830
+ "application/json": {
2831
+ schema: errorResponseSchema
2832
+ }
2833
+ },
2834
+ description: "Unauthorized"
2835
+ },
2836
+ 404: {
2837
+ content: {
2838
+ "application/json": {
2839
+ schema: errorResponseSchema
2840
+ }
2841
+ },
2842
+ description: "User not found"
2843
+ }
2844
+ }
2845
+ });
2846
+ var updatePhoneRoute = createRoute({
2847
+ method: "put",
2848
+ path: "/profile/phone",
2849
+ tags: ["Profile"],
2850
+ summary: "Update user phone",
2851
+ request: {
2852
+ body: {
2853
+ content: {
2854
+ "application/json": {
2855
+ schema: updatePhoneSchema
2856
+ }
2857
+ }
2858
+ }
2859
+ },
2860
+ responses: {
2861
+ 200: {
2862
+ content: {
2863
+ "application/json": {
2864
+ schema: profileResponseSchema
2865
+ }
2866
+ },
2867
+ description: "Phone updated"
2868
+ },
2869
+ 401: {
2870
+ content: {
2871
+ "application/json": {
2872
+ schema: errorResponseSchema
2873
+ }
2874
+ },
2875
+ description: "Unauthorized"
2876
+ },
2877
+ 404: {
2878
+ content: {
2879
+ "application/json": {
2880
+ schema: errorResponseSchema
2881
+ }
2882
+ },
2883
+ description: "User not found"
2884
+ }
2885
+ }
2886
+ });
2266
2887
  var createAuthRoutes = () => {
2267
- const authRoutes = new OpenAPIHono().openapi(signUpRoute, signUpHandler).openapi(signInRoute, signInHandler).openapi(checkUserRoute, checkUserHandler).openapi(signOutRoute, signOutHandler).openapi(meRoute, meHandler).openapi(sessionRoute, sessionHandler).openapi(emailVerificationRequestRoute, emailVerificationRequestHandler).openapi(emailVerificationConfirmRoute, emailVerificationConfirmHandler).openapi(phoneVerificationRequestRoute, phoneVerificationRequestHandler).openapi(phoneVerificationConfirmRoute, phoneVerificationConfirmHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(changePasswordRoute, changePasswordHandler);
2888
+ const authRoutes = new OpenAPIHono().openapi(signUpRoute, signUpHandler).openapi(signInRoute, signInHandler).openapi(checkUserRoute, checkUserHandler).openapi(signOutRoute, signOutHandler).openapi(meRoute, meHandler).openapi(sessionRoute, sessionHandler).openapi(accountChangePendingRoute, accountChangePendingHandler).openapi(emailVerificationRequestRoute, emailVerificationRequestHandler).openapi(emailVerificationConfirmRoute, emailVerificationConfirmHandler).openapi(phoneVerificationRequestRoute, phoneVerificationRequestHandler).openapi(phoneVerificationConfirmRoute, phoneVerificationConfirmHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(verifyPasswordRoute, verifyPasswordHandler).openapi(changePasswordRoute, changePasswordHandler).openapi(updateProfileRoute, updateProfileHandler).openapi(updateEmailRoute, updateEmailHandler).openapi(updatePhoneRoute, updatePhoneHandler);
2268
2889
  return authRoutes;
2269
2890
  };
2270
2891
 
@@ -2272,18 +2893,18 @@ var createAuthRoutes = () => {
2272
2893
  var createAuthMiddleware = (config, database, getTenantId) => {
2273
2894
  const enableTenant = config.enableTenant ?? true;
2274
2895
  return async (c, next) => {
2275
- const sessionToken = getCookie2(c, "session_token") || void 0;
2896
+ const sessionToken = getCookie3(c, "session_token") || void 0;
2276
2897
  let tenantId = getTenantId(c);
2277
2898
  if (enableTenant) {
2278
2899
  if (!tenantId) {
2279
- throw new HTTPException11(400, {
2900
+ throw new HTTPException16(400, {
2280
2901
  message: "Missing tenantId. Tenant isolation is enabled."
2281
2902
  });
2282
2903
  }
2283
2904
  } else {
2284
2905
  tenantId = config.tenantId;
2285
2906
  if (!tenantId) {
2286
- throw new HTTPException11(500, {
2907
+ throw new HTTPException16(500, {
2287
2908
  message: "tenantId must be provided in config when enableTenant is false."
2288
2909
  });
2289
2910
  }