better-auth 1.7.7 → 1.7.9

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.
@@ -4,8 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getSessionFromCtx } from "../../api/routes/session.mjs";
6
6
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
- import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
8
- import "../../utils/index.mjs";
7
+ import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword } from "../../utils/password.mjs";
9
8
  import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
10
9
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
11
10
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -176,15 +175,19 @@ const sendPhoneNumberOTP = (opts) => createAuthEndpoint("/phone-number/send-otp"
176
175
  async function verifyAndConsumePhoneNumberOTP(ctx, opts, body) {
177
176
  const { phoneNumber, code } = body;
178
177
  if (opts.verifyOTP) {
179
- if (!await opts.verifyOTP({
180
- phoneNumber,
181
- code
182
- }, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
183
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(phoneNumber);
178
+ await verifyWithCustomVerifier(ctx, opts.verifyOTP, phoneNumber, code, phoneNumber);
184
179
  return;
185
180
  }
186
181
  await verifyPhoneNumberOTP(ctx, opts, phoneNumber, code);
187
182
  }
183
+ /** Checks the code with `verifyOTP`, then drops the code stored under `identifier`. */
184
+ async function verifyWithCustomVerifier(ctx, verifyOTP, phoneNumber, code, identifier) {
185
+ if (!await verifyOTP({
186
+ phoneNumber,
187
+ code
188
+ }, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
189
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
190
+ }
188
191
  /**
189
192
  * ### API Methods
190
193
  *
@@ -460,7 +463,17 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
460
463
  } }
461
464
  } }
462
465
  }, async (ctx) => {
463
- await verifyPhoneNumberOTP(ctx, opts, `${ctx.body.phoneNumber}-request-password-reset`, ctx.body.otp);
466
+ const phoneResetIdentifier = `${ctx.body.phoneNumber}-request-password-reset`;
467
+ let otpConsumed = false;
468
+ if (opts.verifyOTP) {
469
+ assertPasswordNotTooShort(ctx, ctx.body.newPassword);
470
+ assertPasswordNotTooLong(ctx, ctx.body.newPassword);
471
+ await verifyWithCustomVerifier(ctx, opts.verifyOTP, ctx.body.phoneNumber, ctx.body.otp, phoneResetIdentifier);
472
+ otpConsumed = true;
473
+ } else if (!await matchesUsableStoredPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp)) {
474
+ await verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp);
475
+ otpConsumed = true;
476
+ }
464
477
  const user = await ctx.context.adapter.findOne({
465
478
  model: "user",
466
479
  where: [{
@@ -472,15 +485,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
472
485
  assertPasswordNotTooShort(ctx, ctx.body.newPassword);
473
486
  assertPasswordNotTooLong(ctx, ctx.body.newPassword);
474
487
  const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
475
- if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
476
- userId: user.id,
477
- providerId: "credential",
478
- accountId: user.id,
479
- password: hashedPassword
480
- });
481
- else await ctx.context.internalAdapter.updatePassword(user.id, hashedPassword);
488
+ await resetCredentialPassword(ctx, user.id, hashedPassword, otpConsumed ? void 0 : () => verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp));
482
489
  if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
483
- if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.id);
484
490
  return ctx.json({ status: true });
485
491
  });
486
492
  /**
@@ -519,6 +525,17 @@ async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode) {
519
525
  throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
520
526
  }
521
527
  }
528
+ /**
529
+ * Reports whether the provided code matches a stored, unexpired record with
530
+ * attempts left, without consuming it or counting an attempt.
531
+ */
532
+ async function matchesUsableStoredPhoneNumberOTP(ctx, opts, identifier, providedCode) {
533
+ const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
534
+ if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
535
+ const [otpValue, rawAttempts] = existing.value.split(":");
536
+ if (parseVerificationAttempts(rawAttempts) >= (opts?.allowedAttempts ?? 3)) return false;
537
+ return otpValue === providedCode;
538
+ }
522
539
  function parseVerificationAttempts(value) {
523
540
  const attempts = Number(value ?? 0);
524
541
  return Number.isSafeInteger(attempts) && attempts > 0 ? attempts : 0;
@@ -1,3 +1,4 @@
1
+ import { runWithTransaction } from "@better-auth/core/context";
1
2
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
2
3
  //#region src/utils/password.ts
3
4
  function assertPasswordNotTooShort(ctx, password) {
@@ -12,6 +13,37 @@ function assertPasswordNotTooLong(ctx, password) {
12
13
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
13
14
  }
14
15
  }
16
+ /**
17
+ * Sets a user's credential password after a reset, revoking their sessions
18
+ * when `emailAndPassword.revokeSessionsOnPasswordReset` is enabled.
19
+ *
20
+ * `consumeResetToken` spends the single-use reset token or OTP and throws when
21
+ * it is no longer valid. It runs first inside the same transaction as
22
+ * revocation and the password write, so a failure in any of them leaves all
23
+ * three unchanged and the user can retry. Verification values held only in
24
+ * secondary storage are consumed with `getAndDelete`, which can't roll back,
25
+ * so a later failure there still spends the token. Secondary-storage sessions are
26
+ * removed by an after-commit hook, so they can't roll back with the write.
27
+ * Sessions are revoked before the write so that on an adapter without
28
+ * transactions a failure still keeps the old password rather than committing a
29
+ * new one while existing sessions stay valid.
30
+ */
31
+ async function resetCredentialPassword(ctx, userId, passwordHash, consumeResetToken) {
32
+ await runWithTransaction(ctx.context.adapter, async () => {
33
+ await consumeResetToken?.();
34
+ if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(userId);
35
+ if (!await ctx.context.internalAdapter.findCredentialAccount(userId)) {
36
+ await ctx.context.internalAdapter.createAccount({
37
+ userId,
38
+ providerId: "credential",
39
+ accountId: userId,
40
+ password: passwordHash
41
+ });
42
+ return;
43
+ }
44
+ await ctx.context.internalAdapter.updatePassword(userId, passwordHash);
45
+ });
46
+ }
15
47
  async function validatePassword(ctx, data) {
16
48
  assertPasswordNotTooLong(ctx, data.password);
17
49
  const credentialAccount = await ctx.context.internalAdapter.findCredentialAccount(data.userId);
@@ -43,4 +75,4 @@ async function shouldRequirePassword(ctx, userId, allowPasswordless) {
43
75
  return Boolean(credentialAccount?.password);
44
76
  }
45
77
  //#endregion
46
- export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, shouldRequirePassword, validatePassword };
78
+ export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, resetCredentialPassword, shouldRequirePassword, validatePassword };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.7",
3
+ "version": "1.7.9",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -463,23 +463,23 @@
463
463
  }
464
464
  },
465
465
  "dependencies": {
466
- "@better-auth/utils": "0.4.2",
466
+ "@better-auth/utils": "0.5.0",
467
467
  "@better-fetch/fetch": "1.3.2",
468
468
  "@noble/ciphers": "^2.2.0",
469
469
  "@noble/hashes": "^2.2.0",
470
- "better-call": "1.4.0",
470
+ "better-call": "1.4.2",
471
471
  "defu": "^6.1.4",
472
472
  "jose": "^6.2.3",
473
473
  "kysely": "^0.28.17 || ^0.29.0",
474
474
  "nanostores": "^1.3.0",
475
475
  "zod": "^4.5.4",
476
- "@better-auth/core": "1.7.7",
477
- "@better-auth/drizzle-adapter": "1.7.7",
478
- "@better-auth/kysely-adapter": "1.7.7",
479
- "@better-auth/memory-adapter": "1.7.7",
480
- "@better-auth/mongo-adapter": "1.7.7",
481
- "@better-auth/prisma-adapter": "1.7.7",
482
- "@better-auth/telemetry": "1.7.7"
476
+ "@better-auth/core": "1.7.9",
477
+ "@better-auth/drizzle-adapter": "1.7.9",
478
+ "@better-auth/kysely-adapter": "1.7.9",
479
+ "@better-auth/memory-adapter": "1.7.9",
480
+ "@better-auth/mongo-adapter": "1.7.9",
481
+ "@better-auth/prisma-adapter": "1.7.9",
482
+ "@better-auth/telemetry": "1.7.9"
483
483
  },
484
484
  "devDependencies": {
485
485
  "@lynx-js/react": "^0.121.2",
@@ -487,8 +487,8 @@
487
487
  "@opentelemetry/sdk-trace-base": "^2.10.0",
488
488
  "@opentelemetry/sdk-trace-node": "^2.10.0",
489
489
  "@sveltejs/kit": "^2.70.1",
490
- "@tanstack/react-start": "^1.168.4",
491
- "@tanstack/solid-start": "^1.168.4",
490
+ "@tanstack/react-start": "^1.168.60",
491
+ "@tanstack/solid-start": "^1.168.57",
492
492
  "@types/bun": "^1.3.14",
493
493
  "@types/google.accounts": "^0.0.18",
494
494
  "@types/pg": "^8.20.0",
@@ -511,7 +511,7 @@
511
511
  "peerDependencies": {
512
512
  "@lynx-js/react": "*",
513
513
  "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
514
- "@sveltejs/kit": "^2.0.0",
514
+ "@sveltejs/kit": "^2.0.0 || ^3.0.0",
515
515
  "@tanstack/react-start": "^1.0.0",
516
516
  "@tanstack/solid-start": "^1.0.0",
517
517
  "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
@@ -1,5 +0,0 @@
1
- import "./url.mjs";
2
- import "../state.mjs";
3
- import "../oauth2/state.mjs";
4
- import "./hide-metadata.mjs";
5
- export {};