better-auth 1.5.5 → 1.5.6

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 (90) hide show
  1. package/dist/api/index.d.mts +6 -10
  2. package/dist/api/index.mjs +19 -4
  3. package/dist/api/index.mjs.map +1 -1
  4. package/dist/api/middlewares/origin-check.mjs +17 -8
  5. package/dist/api/middlewares/origin-check.mjs.map +1 -1
  6. package/dist/api/routes/account.d.mts +1 -1
  7. package/dist/api/routes/email-verification.d.mts +0 -1
  8. package/dist/api/routes/session.d.mts +0 -1
  9. package/dist/api/routes/sign-in.d.mts +2 -2
  10. package/dist/api/routes/sign-up.d.mts +0 -1
  11. package/dist/api/routes/update-session.d.mts +0 -1
  12. package/dist/api/routes/update-user.d.mts +0 -1
  13. package/dist/api/to-auth-endpoints.mjs +49 -12
  14. package/dist/api/to-auth-endpoints.mjs.map +1 -1
  15. package/dist/auth/full.d.mts +0 -1
  16. package/dist/auth/minimal.d.mts +0 -1
  17. package/dist/client/index.d.mts +0 -2
  18. package/dist/client/path-to-object.d.mts +3 -1
  19. package/dist/client/session-refresh.d.mts +0 -1
  20. package/dist/client/session-refresh.mjs +12 -4
  21. package/dist/client/session-refresh.mjs.map +1 -1
  22. package/dist/client/types.d.mts +0 -1
  23. package/dist/context/create-context.mjs +4 -1
  24. package/dist/context/create-context.mjs.map +1 -1
  25. package/dist/context/helpers.mjs +10 -4
  26. package/dist/context/helpers.mjs.map +1 -1
  27. package/dist/cookies/index.d.mts +0 -1
  28. package/dist/cookies/session-store.d.mts +0 -2
  29. package/dist/db/index.d.mts +2 -2
  30. package/dist/db/internal-adapter.d.mts +2 -1
  31. package/dist/db/internal-adapter.mjs +1 -1
  32. package/dist/db/internal-adapter.mjs.map +1 -1
  33. package/dist/db/schema.d.mts +0 -1
  34. package/dist/db/with-hooks.d.mts +6 -2
  35. package/dist/db/with-hooks.mjs +72 -31
  36. package/dist/db/with-hooks.mjs.map +1 -1
  37. package/dist/index.d.mts +0 -2
  38. package/dist/integrations/node.d.mts +0 -1
  39. package/dist/oauth2/link-account.d.mts +0 -1
  40. package/dist/plugins/admin/access/statement.d.mts +0 -2
  41. package/dist/plugins/admin/admin.d.mts +0 -1
  42. package/dist/plugins/admin/client.d.mts +0 -2
  43. package/dist/plugins/admin/types.d.mts +0 -2
  44. package/dist/plugins/anonymous/types.d.mts +0 -1
  45. package/dist/plugins/email-otp/index.mjs +2 -1
  46. package/dist/plugins/email-otp/index.mjs.map +1 -1
  47. package/dist/plugins/email-otp/otp-token.mjs +31 -2
  48. package/dist/plugins/email-otp/otp-token.mjs.map +1 -1
  49. package/dist/plugins/email-otp/routes.mjs +60 -59
  50. package/dist/plugins/email-otp/routes.mjs.map +1 -1
  51. package/dist/plugins/email-otp/types.d.mts +12 -0
  52. package/dist/plugins/email-otp/utils.mjs +4 -1
  53. package/dist/plugins/email-otp/utils.mjs.map +1 -1
  54. package/dist/plugins/generic-oauth/client.d.mts +0 -1
  55. package/dist/plugins/generic-oauth/index.d.mts +0 -1
  56. package/dist/plugins/index.d.mts +0 -3
  57. package/dist/plugins/jwt/types.d.mts +0 -1
  58. package/dist/plugins/magic-link/index.d.mts +2 -0
  59. package/dist/plugins/magic-link/index.mjs +5 -3
  60. package/dist/plugins/magic-link/index.mjs.map +1 -1
  61. package/dist/plugins/mcp/index.d.mts +0 -1
  62. package/dist/plugins/oidc-provider/index.d.mts +0 -1
  63. package/dist/plugins/oidc-provider/types.d.mts +0 -1
  64. package/dist/plugins/one-time-token/index.d.mts +0 -1
  65. package/dist/plugins/organization/access/statement.d.mts +0 -2
  66. package/dist/plugins/organization/adapter.d.mts +0 -2
  67. package/dist/plugins/organization/adapter.mjs +2 -2
  68. package/dist/plugins/organization/adapter.mjs.map +1 -1
  69. package/dist/plugins/organization/client.d.mts +0 -5
  70. package/dist/plugins/organization/organization.d.mts +0 -2
  71. package/dist/plugins/organization/permission.d.mts +0 -1
  72. package/dist/plugins/organization/routes/crud-access-control.d.mts +0 -2
  73. package/dist/plugins/organization/routes/crud-invites.d.mts +0 -3
  74. package/dist/plugins/organization/routes/crud-members.d.mts +0 -3
  75. package/dist/plugins/organization/routes/crud-org.d.mts +0 -3
  76. package/dist/plugins/organization/routes/crud-team.d.mts +2 -3
  77. package/dist/plugins/organization/routes/crud-team.mjs +18 -14
  78. package/dist/plugins/organization/routes/crud-team.mjs.map +1 -1
  79. package/dist/plugins/organization/schema.d.mts +0 -1
  80. package/dist/plugins/organization/types.d.mts +0 -2
  81. package/dist/plugins/phone-number/types.d.mts +0 -1
  82. package/dist/plugins/siwe/index.d.mts +0 -1
  83. package/dist/plugins/test-utils/types.d.mts +0 -2
  84. package/dist/plugins/two-factor/client.d.mts +7 -0
  85. package/dist/plugins/two-factor/client.mjs +5 -1
  86. package/dist/plugins/two-factor/client.mjs.map +1 -1
  87. package/dist/plugins/two-factor/types.d.mts +0 -1
  88. package/dist/test-utils/test-instance.d.mts +18 -22
  89. package/dist/types/index.d.mts +0 -1
  90. package/package.json +13 -10
@@ -3,6 +3,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
3
3
  import "../../crypto/index.mjs";
4
4
  import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
5
5
  import { getEndpointResponse } from "../../utils/plugin-helper.mjs";
6
+ import { toOTPIdentifier } from "./utils.mjs";
6
7
  import { storeOTP } from "./otp-token.mjs";
7
8
  import { changeEmailEmailOTP, checkVerificationOTP, createVerificationOTP, forgetPasswordEmailOTP, getVerificationOTP, requestEmailChangeEmailOTP, requestPasswordResetEmailOTP, resetPasswordEmailOTP, sendVerificationOTP, signInEmailOTP, verifyEmailOTP } from "./routes.mjs";
8
9
  import { createAuthMiddleware } from "@better-auth/core/api";
@@ -60,7 +61,7 @@ const emailOTP = (options) => {
60
61
  const storedOTP = await storeOTP(ctx, opts, otp);
61
62
  await ctx.context.internalAdapter.createVerificationValue({
62
63
  value: `${storedOTP}:0`,
63
- identifier: `email-verification-otp-${email}`,
64
+ identifier: toOTPIdentifier("email-verification", email),
64
65
  expiresAt: getDate(opts.expiresIn, "sec")
65
66
  });
66
67
  await ctx.context.runInBackgroundOrAwait(options.sendVerificationOTP({
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/plugins/email-otp/index.ts"],"sourcesContent":["import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { createAuthMiddleware } from \"@better-auth/core/api\";\nimport { generateRandomString } from \"../../crypto\";\nimport { getDate } from \"../../utils/date\";\nimport { getEndpointResponse } from \"../../utils/plugin-helper\";\nimport { EMAIL_OTP_ERROR_CODES } from \"./error-codes\";\nimport { storeOTP } from \"./otp-token\";\nimport {\n\tchangeEmailEmailOTP,\n\tcheckVerificationOTP,\n\tcreateVerificationOTP,\n\tforgetPasswordEmailOTP,\n\tgetVerificationOTP,\n\trequestEmailChangeEmailOTP,\n\trequestPasswordResetEmailOTP,\n\tresetPasswordEmailOTP,\n\tsendVerificationOTP,\n\tsignInEmailOTP,\n\tverifyEmailOTP,\n} from \"./routes\";\nimport type { EmailOTPOptions } from \"./types\";\n\ndeclare module \"@better-auth/core\" {\n\tinterface BetterAuthPluginRegistry<AuthOptions, Options> {\n\t\t\"email-otp\": {\n\t\t\tcreator: typeof emailOTP;\n\t\t};\n\t}\n}\n\nexport type { EmailOTPOptions } from \"./types\";\n\nconst defaultOTPGenerator = (options: EmailOTPOptions) =>\n\tgenerateRandomString(options.otpLength ?? 6, \"0-9\");\n\nexport const emailOTP = (options: EmailOTPOptions) => {\n\tconst opts = {\n\t\texpiresIn: 5 * 60,\n\t\tgenerateOTP: () => defaultOTPGenerator(options),\n\t\tstoreOTP: \"plain\",\n\t\t...options,\n\t} satisfies EmailOTPOptions;\n\n\tconst sendVerificationOTPAction = sendVerificationOTP(opts);\n\n\treturn {\n\t\tid: \"email-otp\",\n\t\tinit(ctx) {\n\t\t\tif (!opts.overrideDefaultEmailVerification) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toptions: {\n\t\t\t\t\temailVerification: {\n\t\t\t\t\t\tasync sendVerificationEmail(data, request) {\n\t\t\t\t\t\t\tawait ctx.runInBackgroundOrAwait(\n\t\t\t\t\t\t\t\tsendVerificationOTPAction({\n\t\t\t\t\t\t\t\t\tcontext: ctx,\n\t\t\t\t\t\t\t\t\trequest: request,\n\t\t\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\t\t\temail: data.user.email,\n\t\t\t\t\t\t\t\t\t\ttype: \"email-verification\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\tendpoints: {\n\t\t\tsendVerificationOTP: sendVerificationOTPAction,\n\t\t\tcreateVerificationOTP: createVerificationOTP(opts),\n\t\t\tgetVerificationOTP: getVerificationOTP(opts),\n\t\t\tcheckVerificationOTP: checkVerificationOTP(opts),\n\t\t\tverifyEmailOTP: verifyEmailOTP(opts),\n\t\t\tsignInEmailOTP: signInEmailOTP(opts),\n\t\t\trequestPasswordResetEmailOTP: requestPasswordResetEmailOTP(opts),\n\t\t\tforgetPasswordEmailOTP: forgetPasswordEmailOTP(opts),\n\t\t\tresetPasswordEmailOTP: resetPasswordEmailOTP(opts),\n\t\t\trequestEmailChangeEmailOTP: requestEmailChangeEmailOTP(opts),\n\t\t\tchangeEmailEmailOTP: changeEmailEmailOTP(opts),\n\t\t},\n\t\thooks: {\n\t\t\tafter: [\n\t\t\t\t{\n\t\t\t\t\tmatcher(context) {\n\t\t\t\t\t\treturn !!(\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/sign-up\") &&\n\t\t\t\t\t\t\topts.sendVerificationOnSignUp &&\n\t\t\t\t\t\t\t!opts.overrideDefaultEmailVerification\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tconst response = await getEndpointResponse<{\n\t\t\t\t\t\t\tuser: { email: string };\n\t\t\t\t\t\t}>(ctx);\n\t\t\t\t\t\tconst email = response?.user.email;\n\t\t\t\t\t\tif (email) {\n\t\t\t\t\t\t\tconst otp =\n\t\t\t\t\t\t\t\topts.generateOTP({ email, type: ctx.body.type }, ctx) ||\n\t\t\t\t\t\t\t\tdefaultOTPGenerator(opts);\n\t\t\t\t\t\t\tconst storedOTP = await storeOTP(ctx, opts, otp);\n\t\t\t\t\t\t\tawait ctx.context.internalAdapter.createVerificationValue({\n\t\t\t\t\t\t\t\tvalue: `${storedOTP}:0`,\n\t\t\t\t\t\t\t\tidentifier: `email-verification-otp-${email}`,\n\t\t\t\t\t\t\t\texpiresAt: getDate(opts.expiresIn, \"sec\"),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tawait ctx.context.runInBackgroundOrAwait(\n\t\t\t\t\t\t\t\toptions.sendVerificationOTP(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\temail,\n\t\t\t\t\t\t\t\t\t\totp,\n\t\t\t\t\t\t\t\t\t\ttype: \"email-verification\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\n\t\trateLimit: [\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/send-verification-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/check-verification-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/verify-email\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/sign-in/email-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/request-password-reset\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/reset-password\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/forget-password/email-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/request-email-change\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/change-email\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t],\n\t\toptions,\n\t\t$ERROR_CODES: EMAIL_OTP_ERROR_CODES,\n\t} satisfies BetterAuthPlugin;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/plugins/email-otp/index.ts"],"sourcesContent":["import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { createAuthMiddleware } from \"@better-auth/core/api\";\nimport { generateRandomString } from \"../../crypto\";\nimport { getDate } from \"../../utils/date\";\nimport { getEndpointResponse } from \"../../utils/plugin-helper\";\nimport { EMAIL_OTP_ERROR_CODES } from \"./error-codes\";\nimport { storeOTP } from \"./otp-token\";\nimport {\n\tchangeEmailEmailOTP,\n\tcheckVerificationOTP,\n\tcreateVerificationOTP,\n\tforgetPasswordEmailOTP,\n\tgetVerificationOTP,\n\trequestEmailChangeEmailOTP,\n\trequestPasswordResetEmailOTP,\n\tresetPasswordEmailOTP,\n\tsendVerificationOTP,\n\tsignInEmailOTP,\n\tverifyEmailOTP,\n} from \"./routes\";\nimport type { EmailOTPOptions } from \"./types\";\nimport { toOTPIdentifier } from \"./utils\";\n\ndeclare module \"@better-auth/core\" {\n\tinterface BetterAuthPluginRegistry<AuthOptions, Options> {\n\t\t\"email-otp\": {\n\t\t\tcreator: typeof emailOTP;\n\t\t};\n\t}\n}\n\nexport type { EmailOTPOptions } from \"./types\";\n\nconst defaultOTPGenerator = (options: EmailOTPOptions) =>\n\tgenerateRandomString(options.otpLength ?? 6, \"0-9\");\n\nexport const emailOTP = (options: EmailOTPOptions) => {\n\tconst opts = {\n\t\texpiresIn: 5 * 60,\n\t\tgenerateOTP: () => defaultOTPGenerator(options),\n\t\tstoreOTP: \"plain\",\n\t\t...options,\n\t} satisfies EmailOTPOptions;\n\n\tconst sendVerificationOTPAction = sendVerificationOTP(opts);\n\n\treturn {\n\t\tid: \"email-otp\",\n\t\tinit(ctx) {\n\t\t\tif (!opts.overrideDefaultEmailVerification) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toptions: {\n\t\t\t\t\temailVerification: {\n\t\t\t\t\t\tasync sendVerificationEmail(data, request) {\n\t\t\t\t\t\t\tawait ctx.runInBackgroundOrAwait(\n\t\t\t\t\t\t\t\tsendVerificationOTPAction({\n\t\t\t\t\t\t\t\t\tcontext: ctx,\n\t\t\t\t\t\t\t\t\trequest: request,\n\t\t\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\t\t\temail: data.user.email,\n\t\t\t\t\t\t\t\t\t\ttype: \"email-verification\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\tendpoints: {\n\t\t\tsendVerificationOTP: sendVerificationOTPAction,\n\t\t\tcreateVerificationOTP: createVerificationOTP(opts),\n\t\t\tgetVerificationOTP: getVerificationOTP(opts),\n\t\t\tcheckVerificationOTP: checkVerificationOTP(opts),\n\t\t\tverifyEmailOTP: verifyEmailOTP(opts),\n\t\t\tsignInEmailOTP: signInEmailOTP(opts),\n\t\t\trequestPasswordResetEmailOTP: requestPasswordResetEmailOTP(opts),\n\t\t\tforgetPasswordEmailOTP: forgetPasswordEmailOTP(opts),\n\t\t\tresetPasswordEmailOTP: resetPasswordEmailOTP(opts),\n\t\t\trequestEmailChangeEmailOTP: requestEmailChangeEmailOTP(opts),\n\t\t\tchangeEmailEmailOTP: changeEmailEmailOTP(opts),\n\t\t},\n\t\thooks: {\n\t\t\tafter: [\n\t\t\t\t{\n\t\t\t\t\tmatcher(context) {\n\t\t\t\t\t\treturn !!(\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/sign-up\") &&\n\t\t\t\t\t\t\topts.sendVerificationOnSignUp &&\n\t\t\t\t\t\t\t!opts.overrideDefaultEmailVerification\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tconst response = await getEndpointResponse<{\n\t\t\t\t\t\t\tuser: { email: string };\n\t\t\t\t\t\t}>(ctx);\n\t\t\t\t\t\tconst email = response?.user.email;\n\t\t\t\t\t\tif (email) {\n\t\t\t\t\t\t\tconst otp =\n\t\t\t\t\t\t\t\topts.generateOTP({ email, type: ctx.body.type }, ctx) ||\n\t\t\t\t\t\t\t\tdefaultOTPGenerator(opts);\n\t\t\t\t\t\t\tconst storedOTP = await storeOTP(ctx, opts, otp);\n\t\t\t\t\t\t\tawait ctx.context.internalAdapter.createVerificationValue({\n\t\t\t\t\t\t\t\tvalue: `${storedOTP}:0`,\n\t\t\t\t\t\t\t\tidentifier: toOTPIdentifier(\"email-verification\", email),\n\t\t\t\t\t\t\t\texpiresAt: getDate(opts.expiresIn, \"sec\"),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tawait ctx.context.runInBackgroundOrAwait(\n\t\t\t\t\t\t\t\toptions.sendVerificationOTP(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\temail,\n\t\t\t\t\t\t\t\t\t\totp,\n\t\t\t\t\t\t\t\t\t\ttype: \"email-verification\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\n\t\trateLimit: [\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/send-verification-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/check-verification-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/verify-email\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/sign-in/email-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/request-password-reset\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/reset-password\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/forget-password/email-otp\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/request-email-change\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tpathMatcher(path) {\n\t\t\t\t\treturn path === \"/email-otp/change-email\";\n\t\t\t\t},\n\t\t\t\twindow: opts.rateLimit?.window || 60,\n\t\t\t\tmax: opts.rateLimit?.max || 3,\n\t\t\t},\n\t\t],\n\t\toptions,\n\t\t$ERROR_CODES: EMAIL_OTP_ERROR_CODES,\n\t} satisfies BetterAuthPlugin;\n};\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
+ import { getDate } from "../../utils/date.mjs";
1
2
  import { constantTimeEqual } from "../../crypto/buffer.mjs";
2
3
  import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
3
- import { defaultKeyHasher } from "./utils.mjs";
4
+ import { defaultKeyHasher, splitAtLastColon } from "./utils.mjs";
4
5
 
5
6
  //#region src/plugins/email-otp/otp-token.ts
6
7
  async function storeOTP(ctx, opts, otp) {
@@ -23,7 +24,35 @@ async function verifyStoredOTP(ctx, opts, storedOtp, otp) {
23
24
  if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) return constantTimeEqual(await opts.storeOTP.decrypt(storedOtp), otp);
24
25
  return constantTimeEqual(otp, storedOtp);
25
26
  }
27
+ /**
28
+ * Retrieves the plain-text OTP from a stored value.
29
+ * Returns `null` if the OTP is hashed and cannot be recovered.
30
+ */
31
+ async function retrieveOTP(ctx, opts, storedOtp) {
32
+ if (opts.storeOTP === "plain" || opts.storeOTP === void 0) return storedOtp;
33
+ if (opts.storeOTP === "encrypted") return await symmetricDecrypt({
34
+ key: ctx.context.secretConfig,
35
+ data: storedOtp
36
+ });
37
+ if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) return await opts.storeOTP.decrypt(storedOtp);
38
+ return null;
39
+ }
40
+ /**
41
+ * Tries to reuse an existing unexpired OTP.
42
+ * Returns the plain-text OTP if reusable, `null` otherwise.
43
+ */
44
+ async function tryReuseOTP(ctx, opts, identifier) {
45
+ const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
46
+ if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return null;
47
+ const [storedOtpValue, attempts] = splitAtLastColon(existing.value);
48
+ const allowedAttempts = opts.allowedAttempts || 3;
49
+ if (attempts && parseInt(attempts) >= allowedAttempts) return null;
50
+ const plainOtp = await retrieveOTP(ctx, opts, storedOtpValue);
51
+ if (!plainOtp) return null;
52
+ await ctx.context.internalAdapter.updateVerificationByIdentifier(identifier, { expiresAt: getDate(opts.expiresIn, "sec") });
53
+ return plainOtp;
54
+ }
26
55
 
27
56
  //#endregion
28
- export { storeOTP, verifyStoredOTP };
57
+ export { storeOTP, tryReuseOTP, verifyStoredOTP };
29
58
  //# sourceMappingURL=otp-token.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"otp-token.mjs","names":[],"sources":["../../../src/plugins/email-otp/otp-token.ts"],"sourcesContent":["import type { GenericEndpointContext } from \"@better-auth/core\";\nimport {\n\tconstantTimeEqual,\n\tsymmetricDecrypt,\n\tsymmetricEncrypt,\n} from \"../../crypto\";\nimport type { EmailOTPOptions } from \"./types\";\nimport { defaultKeyHasher } from \"./utils\";\n\nexport async function storeOTP(\n\tctx: GenericEndpointContext,\n\topts: EmailOTPOptions,\n\totp: string,\n) {\n\tif (opts.storeOTP === \"encrypted\") {\n\t\treturn await symmetricEncrypt({\n\t\t\tkey: ctx.context.secretConfig,\n\t\t\tdata: otp,\n\t\t});\n\t}\n\tif (opts.storeOTP === \"hashed\") {\n\t\treturn await defaultKeyHasher(otp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"hash\" in opts.storeOTP) {\n\t\treturn await opts.storeOTP.hash(otp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"encrypt\" in opts.storeOTP) {\n\t\treturn await opts.storeOTP.encrypt(otp);\n\t}\n\n\treturn otp;\n}\n\nexport async function verifyStoredOTP(\n\tctx: GenericEndpointContext,\n\topts: EmailOTPOptions,\n\tstoredOtp: string,\n\totp: string,\n): Promise<boolean> {\n\tif (opts.storeOTP === \"encrypted\") {\n\t\tconst decryptedOtp = await symmetricDecrypt({\n\t\t\tkey: ctx.context.secretConfig,\n\t\t\tdata: storedOtp,\n\t\t});\n\t\treturn constantTimeEqual(decryptedOtp, otp);\n\t}\n\tif (opts.storeOTP === \"hashed\") {\n\t\tconst hashedOtp = await defaultKeyHasher(otp);\n\t\treturn constantTimeEqual(hashedOtp, storedOtp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"hash\" in opts.storeOTP) {\n\t\tconst hashedOtp = await opts.storeOTP.hash(otp);\n\t\treturn constantTimeEqual(hashedOtp, storedOtp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"decrypt\" in opts.storeOTP) {\n\t\tconst decryptedOtp = await opts.storeOTP.decrypt(storedOtp);\n\t\treturn constantTimeEqual(decryptedOtp, otp);\n\t}\n\n\treturn constantTimeEqual(otp, storedOtp);\n}\n"],"mappings":";;;;;AASA,eAAsB,SACrB,KACA,MACA,KACC;AACD,KAAI,KAAK,aAAa,YACrB,QAAO,MAAM,iBAAiB;EAC7B,KAAK,IAAI,QAAQ;EACjB,MAAM;EACN,CAAC;AAEH,KAAI,KAAK,aAAa,SACrB,QAAO,MAAM,iBAAiB,IAAI;AAEnC,KAAI,OAAO,KAAK,aAAa,YAAY,UAAU,KAAK,SACvD,QAAO,MAAM,KAAK,SAAS,KAAK,IAAI;AAErC,KAAI,OAAO,KAAK,aAAa,YAAY,aAAa,KAAK,SAC1D,QAAO,MAAM,KAAK,SAAS,QAAQ,IAAI;AAGxC,QAAO;;AAGR,eAAsB,gBACrB,KACA,MACA,WACA,KACmB;AACnB,KAAI,KAAK,aAAa,YAKrB,QAAO,kBAJc,MAAM,iBAAiB;EAC3C,KAAK,IAAI,QAAQ;EACjB,MAAM;EACN,CAAC,EACqC,IAAI;AAE5C,KAAI,KAAK,aAAa,SAErB,QAAO,kBADW,MAAM,iBAAiB,IAAI,EACT,UAAU;AAE/C,KAAI,OAAO,KAAK,aAAa,YAAY,UAAU,KAAK,SAEvD,QAAO,kBADW,MAAM,KAAK,SAAS,KAAK,IAAI,EACX,UAAU;AAE/C,KAAI,OAAO,KAAK,aAAa,YAAY,aAAa,KAAK,SAE1D,QAAO,kBADc,MAAM,KAAK,SAAS,QAAQ,UAAU,EACpB,IAAI;AAG5C,QAAO,kBAAkB,KAAK,UAAU"}
1
+ {"version":3,"file":"otp-token.mjs","names":[],"sources":["../../../src/plugins/email-otp/otp-token.ts"],"sourcesContent":["import type { GenericEndpointContext } from \"@better-auth/core\";\nimport {\n\tconstantTimeEqual,\n\tsymmetricDecrypt,\n\tsymmetricEncrypt,\n} from \"../../crypto\";\nimport { getDate } from \"../../utils/date\";\nimport type { EmailOTPOptions, RequiredEmailOTPOptions } from \"./types\";\nimport { defaultKeyHasher, splitAtLastColon } from \"./utils\";\n\nexport async function storeOTP(\n\tctx: GenericEndpointContext,\n\topts: EmailOTPOptions,\n\totp: string,\n) {\n\tif (opts.storeOTP === \"encrypted\") {\n\t\treturn await symmetricEncrypt({\n\t\t\tkey: ctx.context.secretConfig,\n\t\t\tdata: otp,\n\t\t});\n\t}\n\tif (opts.storeOTP === \"hashed\") {\n\t\treturn await defaultKeyHasher(otp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"hash\" in opts.storeOTP) {\n\t\treturn await opts.storeOTP.hash(otp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"encrypt\" in opts.storeOTP) {\n\t\treturn await opts.storeOTP.encrypt(otp);\n\t}\n\n\treturn otp;\n}\n\nexport async function verifyStoredOTP(\n\tctx: GenericEndpointContext,\n\topts: EmailOTPOptions,\n\tstoredOtp: string,\n\totp: string,\n): Promise<boolean> {\n\tif (opts.storeOTP === \"encrypted\") {\n\t\tconst decryptedOtp = await symmetricDecrypt({\n\t\t\tkey: ctx.context.secretConfig,\n\t\t\tdata: storedOtp,\n\t\t});\n\t\treturn constantTimeEqual(decryptedOtp, otp);\n\t}\n\tif (opts.storeOTP === \"hashed\") {\n\t\tconst hashedOtp = await defaultKeyHasher(otp);\n\t\treturn constantTimeEqual(hashedOtp, storedOtp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"hash\" in opts.storeOTP) {\n\t\tconst hashedOtp = await opts.storeOTP.hash(otp);\n\t\treturn constantTimeEqual(hashedOtp, storedOtp);\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"decrypt\" in opts.storeOTP) {\n\t\tconst decryptedOtp = await opts.storeOTP.decrypt(storedOtp);\n\t\treturn constantTimeEqual(decryptedOtp, otp);\n\t}\n\n\treturn constantTimeEqual(otp, storedOtp);\n}\n\n/**\n * Retrieves the plain-text OTP from a stored value.\n * Returns `null` if the OTP is hashed and cannot be recovered.\n */\nasync function retrieveOTP(\n\tctx: GenericEndpointContext,\n\topts: EmailOTPOptions,\n\tstoredOtp: string,\n): Promise<string | null> {\n\tif (opts.storeOTP === \"plain\" || opts.storeOTP === undefined) {\n\t\treturn storedOtp;\n\t}\n\tif (opts.storeOTP === \"encrypted\") {\n\t\treturn await symmetricDecrypt({\n\t\t\tkey: ctx.context.secretConfig,\n\t\t\tdata: storedOtp,\n\t\t});\n\t}\n\tif (typeof opts.storeOTP === \"object\" && \"decrypt\" in opts.storeOTP) {\n\t\treturn await opts.storeOTP.decrypt(storedOtp);\n\t}\n\t// hashed or custom hash -> cannot recover\n\treturn null;\n}\n\n/**\n * Tries to reuse an existing unexpired OTP.\n * Returns the plain-text OTP if reusable, `null` otherwise.\n */\nexport async function tryReuseOTP(\n\tctx: GenericEndpointContext,\n\topts: RequiredEmailOTPOptions,\n\tidentifier: string,\n): Promise<string | null> {\n\tconst existing =\n\t\tawait ctx.context.internalAdapter.findVerificationValue(identifier);\n\tif (!existing || existing.expiresAt < new Date()) return null;\n\n\tconst [storedOtpValue, attempts] = splitAtLastColon(existing.value);\n\tconst allowedAttempts = opts.allowedAttempts || 3;\n\tif (attempts && parseInt(attempts) >= allowedAttempts) return null;\n\n\tconst plainOtp = await retrieveOTP(ctx, opts, storedOtpValue);\n\tif (!plainOtp) return null;\n\n\tawait ctx.context.internalAdapter.updateVerificationByIdentifier(identifier, {\n\t\texpiresAt: getDate(opts.expiresIn, \"sec\"),\n\t});\n\n\treturn plainOtp;\n}\n"],"mappings":";;;;;;AAUA,eAAsB,SACrB,KACA,MACA,KACC;AACD,KAAI,KAAK,aAAa,YACrB,QAAO,MAAM,iBAAiB;EAC7B,KAAK,IAAI,QAAQ;EACjB,MAAM;EACN,CAAC;AAEH,KAAI,KAAK,aAAa,SACrB,QAAO,MAAM,iBAAiB,IAAI;AAEnC,KAAI,OAAO,KAAK,aAAa,YAAY,UAAU,KAAK,SACvD,QAAO,MAAM,KAAK,SAAS,KAAK,IAAI;AAErC,KAAI,OAAO,KAAK,aAAa,YAAY,aAAa,KAAK,SAC1D,QAAO,MAAM,KAAK,SAAS,QAAQ,IAAI;AAGxC,QAAO;;AAGR,eAAsB,gBACrB,KACA,MACA,WACA,KACmB;AACnB,KAAI,KAAK,aAAa,YAKrB,QAAO,kBAJc,MAAM,iBAAiB;EAC3C,KAAK,IAAI,QAAQ;EACjB,MAAM;EACN,CAAC,EACqC,IAAI;AAE5C,KAAI,KAAK,aAAa,SAErB,QAAO,kBADW,MAAM,iBAAiB,IAAI,EACT,UAAU;AAE/C,KAAI,OAAO,KAAK,aAAa,YAAY,UAAU,KAAK,SAEvD,QAAO,kBADW,MAAM,KAAK,SAAS,KAAK,IAAI,EACX,UAAU;AAE/C,KAAI,OAAO,KAAK,aAAa,YAAY,aAAa,KAAK,SAE1D,QAAO,kBADc,MAAM,KAAK,SAAS,QAAQ,UAAU,EACpB,IAAI;AAG5C,QAAO,kBAAkB,KAAK,UAAU;;;;;;AAOzC,eAAe,YACd,KACA,MACA,WACyB;AACzB,KAAI,KAAK,aAAa,WAAW,KAAK,aAAa,OAClD,QAAO;AAER,KAAI,KAAK,aAAa,YACrB,QAAO,MAAM,iBAAiB;EAC7B,KAAK,IAAI,QAAQ;EACjB,MAAM;EACN,CAAC;AAEH,KAAI,OAAO,KAAK,aAAa,YAAY,aAAa,KAAK,SAC1D,QAAO,MAAM,KAAK,SAAS,QAAQ,UAAU;AAG9C,QAAO;;;;;;AAOR,eAAsB,YACrB,KACA,MACA,YACyB;CACzB,MAAM,WACL,MAAM,IAAI,QAAQ,gBAAgB,sBAAsB,WAAW;AACpE,KAAI,CAAC,YAAY,SAAS,4BAAY,IAAI,MAAM,CAAE,QAAO;CAEzD,MAAM,CAAC,gBAAgB,YAAY,iBAAiB,SAAS,MAAM;CACnE,MAAM,kBAAkB,KAAK,mBAAmB;AAChD,KAAI,YAAY,SAAS,SAAS,IAAI,gBAAiB,QAAO;CAE9D,MAAM,WAAW,MAAM,YAAY,KAAK,MAAM,eAAe;AAC7D,KAAI,CAAC,SAAU,QAAO;AAEtB,OAAM,IAAI,QAAQ,gBAAgB,+BAA+B,YAAY,EAC5E,WAAW,QAAQ,KAAK,WAAW,MAAM,EACzC,CAAC;AAEF,QAAO"}
@@ -6,8 +6,8 @@ import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
6
6
  import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
7
7
  import { APIError as APIError$1 } from "../../api/index.mjs";
8
8
  import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
9
- import { splitAtLastColon } from "./utils.mjs";
10
- import { storeOTP, verifyStoredOTP } from "./otp-token.mjs";
9
+ import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
10
+ import { storeOTP, tryReuseOTP, verifyStoredOTP } from "./otp-token.mjs";
11
11
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
12
12
  import { createAuthEndpoint } from "@better-auth/core/api";
13
13
  import { deprecate } from "@better-auth/core/utils/deprecate";
@@ -20,6 +20,37 @@ const types = [
20
20
  "forget-password",
21
21
  "change-email"
22
22
  ];
23
+ /**
24
+ * Resolves the OTP to send: reuses an existing one if possible,
25
+ * otherwise generates and stores a new one.
26
+ *
27
+ * @internal
28
+ */
29
+ async function resolveOTP(ctx, opts, email, type) {
30
+ const identifier = toOTPIdentifier(type, email);
31
+ if (opts.resendStrategy === "reuse") {
32
+ const reused = await tryReuseOTP(ctx, opts, identifier);
33
+ if (reused) return reused;
34
+ }
35
+ const otp = opts.generateOTP({
36
+ email,
37
+ type
38
+ }, ctx) || defaultOTPGenerator(opts);
39
+ const storedOTP = await storeOTP(ctx, opts, otp);
40
+ await ctx.context.internalAdapter.createVerificationValue({
41
+ value: `${storedOTP}:0`,
42
+ identifier,
43
+ expiresAt: getDate(opts.expiresIn, "sec")
44
+ }).catch(async () => {
45
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
46
+ await ctx.context.internalAdapter.createVerificationValue({
47
+ value: `${storedOTP}:0`,
48
+ identifier,
49
+ expiresAt: getDate(opts.expiresIn, "sec")
50
+ });
51
+ });
52
+ return otp;
53
+ }
23
54
  const sendVerificationOTPBodySchema = z.object({
24
55
  email: z.string({}).meta({ description: "Email address to send the OTP" }),
25
56
  type: z.enum(types).meta({ description: "Type of the OTP" })
@@ -64,25 +95,11 @@ const sendVerificationOTP = (opts) => createAuthEndpoint("/email-otp/send-verifi
64
95
  ctx.context.logger.error("Use the /email-otp/request-email-change endpoint to send OTP for changing email");
65
96
  throw APIError$1.fromStatus("BAD_REQUEST", { message: "Invalid OTP type" });
66
97
  }
67
- const otp = opts.generateOTP({
68
- email,
69
- type: ctx.body.type
70
- }, ctx) || defaultOTPGenerator(opts);
71
- const storedOTP = await storeOTP(ctx, opts, otp);
72
- await ctx.context.internalAdapter.createVerificationValue({
73
- value: `${storedOTP}:0`,
74
- identifier: `${ctx.body.type}-otp-${email}`,
75
- expiresAt: getDate(opts.expiresIn, "sec")
76
- }).catch(async (error) => {
77
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${ctx.body.type}-otp-${email}`);
78
- await ctx.context.internalAdapter.createVerificationValue({
79
- value: `${storedOTP}:0`,
80
- identifier: `${ctx.body.type}-otp-${email}`,
81
- expiresAt: getDate(opts.expiresIn, "sec")
82
- });
83
- });
84
- if (!await ctx.context.internalAdapter.findUserByEmail(email)) if (ctx.body.type === "sign-in" && !opts.disableSignUp) {} else {
85
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${ctx.body.type}-otp-${email}`);
98
+ const identifier = toOTPIdentifier(ctx.body.type, email);
99
+ const otp = await resolveOTP(ctx, opts, email, ctx.body.type);
100
+ const shouldSendOTP = ctx.body.type === "sign-in" && !opts.disableSignUp;
101
+ if (!await ctx.context.internalAdapter.findUserByEmail(email) && !shouldSendOTP) {
102
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
86
103
  return ctx.json({ success: true });
87
104
  }
88
105
  await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
@@ -119,7 +136,7 @@ const createVerificationOTP = (opts) => createAuthEndpoint({
119
136
  const storedOTP = await storeOTP(ctx, opts, otp);
120
137
  await ctx.context.internalAdapter.createVerificationValue({
121
138
  value: `${storedOTP}:0`,
122
- identifier: `${ctx.body.type}-otp-${email}`,
139
+ identifier: toOTPIdentifier(ctx.body.type, email),
123
140
  expiresAt: getDate(opts.expiresIn, "sec")
124
141
  });
125
142
  return otp;
@@ -164,7 +181,7 @@ const getVerificationOTP = (opts) => createAuthEndpoint({
164
181
  } }
165
182
  }, async (ctx) => {
166
183
  const email = ctx.query.email.toLowerCase();
167
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(`${ctx.query.type}-otp-${email}`);
184
+ const verificationValue = await ctx.context.internalAdapter.findVerificationValue(toOTPIdentifier(ctx.query.type, email));
168
185
  if (!verificationValue || verificationValue.expiresAt < /* @__PURE__ */ new Date()) return ctx.json({ otp: null });
169
186
  if (opts.storeOTP === "hashed" || typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) throw APIError$1.fromStatus("BAD_REQUEST", { message: "OTP is hashed, cannot return the plain text OTP" });
170
187
  const [storedOtp, _attempts] = splitAtLastColon(verificationValue.value);
@@ -217,21 +234,21 @@ const checkVerificationOTP = (opts) => createAuthEndpoint("/email-otp/check-veri
217
234
  const email = ctx.body.email.toLowerCase();
218
235
  if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
219
236
  if (!await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
220
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(`${ctx.body.type}-otp-${email}`);
237
+ const identifier = toOTPIdentifier(ctx.body.type, email);
238
+ const verificationValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
221
239
  if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
222
- const otpIdentifier = `${ctx.body.type}-otp-${email}`;
223
240
  if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
224
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(otpIdentifier);
241
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
225
242
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
226
243
  }
227
244
  const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
228
245
  const allowedAttempts = opts?.allowedAttempts || 3;
229
246
  if (attempts && parseInt(attempts) >= allowedAttempts) {
230
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(otpIdentifier);
247
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
231
248
  throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
232
249
  }
233
250
  if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
234
- await ctx.context.internalAdapter.updateVerificationByIdentifier(otpIdentifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
251
+ await ctx.context.internalAdapter.updateVerificationByIdentifier(identifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
235
252
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
236
253
  }
237
254
  return ctx.json({ success: true });
@@ -291,7 +308,7 @@ const verifyEmailOTP = (opts) => createAuthEndpoint("/email-otp/verify-email", {
291
308
  }, async (ctx) => {
292
309
  const email = ctx.body.email.toLowerCase();
293
310
  if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
294
- await atomicVerifyOTP(ctx, opts, `email-verification-otp-${email}`, ctx.body.otp);
311
+ await atomicVerifyOTP(ctx, opts, toOTPIdentifier("email-verification", email), ctx.body.otp);
295
312
  const user = await ctx.context.internalAdapter.findUserByEmail(email);
296
313
  if (!user)
297
314
  /**
@@ -382,7 +399,7 @@ const signInEmailOTP = (opts) => createAuthEndpoint("/sign-in/email-otp", {
382
399
  }, async (ctx) => {
383
400
  const { email: rawEmail, otp, name, image, ...rest } = ctx.body;
384
401
  const email = rawEmail.toLowerCase();
385
- await atomicVerifyOTP(ctx, opts, `sign-in-otp-${email}`, otp);
402
+ await atomicVerifyOTP(ctx, opts, toOTPIdentifier("sign-in", email), otp);
386
403
  const user = await ctx.context.internalAdapter.findUserByEmail(email);
387
404
  if (!user) {
388
405
  if (opts.disableSignUp) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
@@ -450,18 +467,10 @@ const requestPasswordResetEmailOTP = (opts) => createAuthEndpoint("/email-otp/re
450
467
  } }
451
468
  }, async (ctx) => {
452
469
  const email = ctx.body.email;
453
- const otp = opts.generateOTP({
454
- email,
455
- type: "forget-password"
456
- }, ctx) || defaultOTPGenerator(opts);
457
- const storedOTP = await storeOTP(ctx, opts, otp);
458
- await ctx.context.internalAdapter.createVerificationValue({
459
- value: `${storedOTP}:0`,
460
- identifier: `forget-password-otp-${email}`,
461
- expiresAt: getDate(opts.expiresIn, "sec")
462
- });
470
+ const identifier = toOTPIdentifier("forget-password", email);
471
+ const otp = await resolveOTP(ctx, opts, email, "forget-password");
463
472
  if (!await ctx.context.internalAdapter.findUserByEmail(email)) {
464
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`forget-password-otp-${email}`);
473
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
465
474
  return ctx.json({ success: true });
466
475
  }
467
476
  await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
@@ -510,18 +519,10 @@ const forgetPasswordEmailOTP = (opts) => {
510
519
  }, async (ctx) => {
511
520
  warnDeprecation();
512
521
  const email = ctx.body.email;
513
- const otp = opts.generateOTP({
514
- email,
515
- type: "forget-password"
516
- }, ctx) || defaultOTPGenerator(opts);
517
- const storedOTP = await storeOTP(ctx, opts, otp);
518
- await ctx.context.internalAdapter.createVerificationValue({
519
- value: `${storedOTP}:0`,
520
- identifier: `forget-password-otp-${email}`,
521
- expiresAt: getDate(opts.expiresIn, "sec")
522
- });
522
+ const identifier = toOTPIdentifier("forget-password", email);
523
+ const otp = await resolveOTP(ctx, opts, email, "forget-password");
523
524
  if (!await ctx.context.internalAdapter.findUserByEmail(email)) {
524
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`forget-password-otp-${email}`);
525
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
525
526
  return ctx.json({ success: true });
526
527
  }
527
528
  await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
@@ -568,7 +569,7 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
568
569
  } }
569
570
  }, async (ctx) => {
570
571
  const email = ctx.body.email;
571
- await atomicVerifyOTP(ctx, opts, `forget-password-otp-${email}`, ctx.body.otp);
572
+ await atomicVerifyOTP(ctx, opts, toOTPIdentifier("forget-password", email), ctx.body.otp);
572
573
  const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
573
574
  if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
574
575
  const minPasswordLength = ctx.context.password.config.minPasswordLength;
@@ -636,9 +637,9 @@ const requestEmailChangeEmailOTP = (opts) => createAuthEndpoint("/email-otp/requ
636
637
  }
637
638
  if (opts.changeEmail?.verifyCurrentEmail) {
638
639
  if (!ctx.body.otp) throw APIError$1.fromStatus("BAD_REQUEST", { message: "OTP is required to verify current email" });
639
- const currentEmailVerificationValue = await ctx.context.internalAdapter.findVerificationValue(`email-verification-otp-${email}`);
640
+ const currentEmailVerificationValue = await ctx.context.internalAdapter.findVerificationValue(toOTPIdentifier("email-verification", email));
640
641
  if (!currentEmailVerificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
641
- const currentEmailIdentifier = `email-verification-otp-${email}`;
642
+ const currentEmailIdentifier = toOTPIdentifier("email-verification", email);
642
643
  if (currentEmailVerificationValue.expiresAt < /* @__PURE__ */ new Date()) {
643
644
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
644
645
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
@@ -662,11 +663,11 @@ const requestEmailChangeEmailOTP = (opts) => createAuthEndpoint("/email-otp/requ
662
663
  const storedOTP = await storeOTP(ctx, opts, otp);
663
664
  await ctx.context.internalAdapter.createVerificationValue({
664
665
  value: `${storedOTP}:0`,
665
- identifier: `change-email-otp-${email}-${newEmail}`,
666
+ identifier: toOTPIdentifier("change-email", `${email}-${newEmail}`),
666
667
  expiresAt: getDate(opts.expiresIn, "sec")
667
668
  });
668
669
  if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
669
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`change-email-otp-${email}-${newEmail}`);
670
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(toOTPIdentifier("change-email", `${email}-${newEmail}`));
670
671
  return ctx.json({ success: true });
671
672
  }
672
673
  await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
@@ -723,9 +724,9 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
723
724
  ctx.context.logger.error("Email is the same");
724
725
  throw APIError$1.fromStatus("BAD_REQUEST", { message: "Email is the same" });
725
726
  }
726
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(`change-email-otp-${email}-${newEmail}`);
727
+ const verificationValue = await ctx.context.internalAdapter.findVerificationValue(toOTPIdentifier("change-email", `${email}-${newEmail}`));
727
728
  if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
728
- const changeEmailIdentifier = `change-email-otp-${email}-${newEmail}`;
729
+ const changeEmailIdentifier = toOTPIdentifier("change-email", `${email}-${newEmail}`);
729
730
  if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
730
731
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
731
732
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);