better-auth 1.6.16 → 1.6.18

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 (93) hide show
  1. package/dist/api/index.d.mts +2 -2
  2. package/dist/api/index.mjs +3 -4
  3. package/dist/api/middlewares/origin-check.mjs +5 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +22 -7
  6. package/dist/api/routes/callback.mjs +2 -2
  7. package/dist/api/routes/index.d.mts +1 -1
  8. package/dist/api/routes/password.mjs +3 -4
  9. package/dist/api/routes/session.d.mts +12 -1
  10. package/dist/api/routes/session.mjs +13 -1
  11. package/dist/api/routes/sign-in.mjs +5 -5
  12. package/dist/api/routes/sign-up.mjs +2 -2
  13. package/dist/api/routes/update-session.mjs +2 -3
  14. package/dist/api/routes/update-user.mjs +10 -12
  15. package/dist/auth/base.mjs +11 -7
  16. package/dist/client/equality.d.mts +19 -0
  17. package/dist/client/equality.mjs +42 -0
  18. package/dist/client/index.d.mts +5 -4
  19. package/dist/client/index.mjs +2 -1
  20. package/dist/client/lynx/index.d.mts +4 -2
  21. package/dist/client/path-to-object.d.mts +5 -2
  22. package/dist/client/plugins/index.d.mts +4 -1
  23. package/dist/client/plugins/index.mjs +4 -1
  24. package/dist/client/query.d.mts +4 -3
  25. package/dist/client/query.mjs +27 -17
  26. package/dist/client/react/index.d.mts +4 -2
  27. package/dist/client/session-atom.mjs +129 -4
  28. package/dist/client/session-refresh.d.mts +3 -18
  29. package/dist/client/session-refresh.mjs +38 -49
  30. package/dist/client/solid/index.d.mts +4 -2
  31. package/dist/client/svelte/index.d.mts +4 -2
  32. package/dist/client/types.d.mts +27 -16
  33. package/dist/client/vanilla.d.mts +4 -2
  34. package/dist/client/vue/index.d.mts +4 -2
  35. package/dist/context/create-context.mjs +2 -1
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +25 -2
  38. package/dist/db/internal-adapter.mjs +51 -0
  39. package/dist/package.mjs +1 -1
  40. package/dist/plugins/access/access.mjs +49 -19
  41. package/dist/plugins/admin/routes.mjs +10 -3
  42. package/dist/plugins/captcha/constants.mjs +8 -1
  43. package/dist/plugins/captcha/index.mjs +8 -2
  44. package/dist/plugins/captcha/types.d.mts +21 -0
  45. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  46. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  47. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  48. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  49. package/dist/plugins/device-authorization/routes.mjs +16 -9
  50. package/dist/plugins/email-otp/routes.mjs +22 -52
  51. package/dist/plugins/generic-oauth/index.mjs +7 -2
  52. package/dist/plugins/generic-oauth/routes.mjs +16 -12
  53. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  54. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  55. package/dist/plugins/index.d.mts +6 -2
  56. package/dist/plugins/index.mjs +4 -1
  57. package/dist/plugins/jwt/index.mjs +2 -2
  58. package/dist/plugins/mcp/client/index.mjs +1 -0
  59. package/dist/plugins/mcp/index.mjs +8 -0
  60. package/dist/plugins/multi-session/index.mjs +7 -5
  61. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  62. package/dist/plugins/oauth-popup/client.mjs +203 -0
  63. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  64. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  65. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  66. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  67. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  68. package/dist/plugins/oauth-popup/index.mjs +227 -0
  69. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  70. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  71. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  72. package/dist/plugins/oidc-provider/index.mjs +10 -0
  73. package/dist/plugins/one-tap/client.mjs +12 -6
  74. package/dist/plugins/one-tap/index.d.mts +1 -0
  75. package/dist/plugins/one-tap/index.mjs +9 -5
  76. package/dist/plugins/one-time-token/index.mjs +1 -3
  77. package/dist/plugins/open-api/generator.d.mts +66 -57
  78. package/dist/plugins/open-api/generator.mjs +185 -67
  79. package/dist/plugins/open-api/index.d.mts +2 -2
  80. package/dist/plugins/organization/adapter.d.mts +29 -1
  81. package/dist/plugins/organization/adapter.mjs +66 -6
  82. package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
  83. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  84. package/dist/plugins/organization/routes/crud-team.mjs +36 -3
  85. package/dist/plugins/phone-number/routes.mjs +41 -36
  86. package/dist/plugins/siwe/index.mjs +2 -3
  87. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  88. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  89. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  90. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  91. package/dist/plugins/username/index.mjs +6 -6
  92. package/dist/test-utils/test-instance.d.mts +26 -23
  93. package/package.json +9 -9
@@ -1,33 +1,63 @@
1
1
  import { BetterAuthError } from "@better-auth/core/error";
2
2
  //#region src/plugins/access/access.ts
3
+ function unknownResourceResponse(requestedResource) {
4
+ return {
5
+ success: false,
6
+ error: `You are not allowed to access resource: ${requestedResource}`
7
+ };
8
+ }
9
+ function unauthorizedResourceResponse(requestedResource) {
10
+ return {
11
+ success: false,
12
+ error: `unauthorized to access resource "${requestedResource}"`
13
+ };
14
+ }
15
+ function normalizeConnector(connector) {
16
+ return connector === "OR" ? "OR" : "AND";
17
+ }
18
+ function isActionList(actions) {
19
+ return Array.isArray(actions);
20
+ }
21
+ function normalizeActionRequest(requestedActions) {
22
+ if (isActionList(requestedActions)) return {
23
+ actions: requestedActions,
24
+ connector: "AND"
25
+ };
26
+ if (!requestedActions || typeof requestedActions !== "object") throw new BetterAuthError("Invalid access control request");
27
+ const { actions, connector } = requestedActions;
28
+ if (!isActionList(actions)) return {
29
+ actions: [],
30
+ connector: normalizeConnector(connector)
31
+ };
32
+ return {
33
+ actions,
34
+ connector: normalizeConnector(connector)
35
+ };
36
+ }
37
+ function hasAllowedAction(allowedActions, requestedAction) {
38
+ return typeof requestedAction === "string" && allowedActions.includes(requestedAction);
39
+ }
40
+ function isResourceAuthorized(allowedActions, { actions, connector }) {
41
+ if (actions.length === 0) return false;
42
+ if (connector === "OR") return actions.some((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
43
+ return actions.every((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
44
+ }
3
45
  function role(statements) {
4
46
  return {
5
47
  authorize(request, connector = "AND") {
6
- let success = false;
48
+ let hasAuthorizedResource = false;
7
49
  for (const [requestedResource, requestedActions] of Object.entries(request)) {
8
50
  const allowedActions = statements[requestedResource];
9
51
  if (!allowedActions) {
10
- if (connector === "AND") return {
11
- success: false,
12
- error: `You are not allowed to access resource: ${requestedResource}`
13
- };
14
- success = false;
52
+ if (connector === "AND") return unknownResourceResponse(requestedResource);
15
53
  continue;
16
54
  }
17
- if (Array.isArray(requestedActions)) success = requestedActions.length > 0 && requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
18
- else if (typeof requestedActions === "object") {
19
- const actions = requestedActions;
20
- if (!Array.isArray(actions.actions) || actions.actions.length === 0) success = false;
21
- else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
22
- else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
23
- } else throw new BetterAuthError("Invalid access control request");
24
- if (success && connector === "OR") return { success };
25
- if (!success && connector === "AND") return {
26
- success: false,
27
- error: `unauthorized to access resource "${requestedResource}"`
28
- };
55
+ const isAuthorized = isResourceAuthorized(allowedActions, normalizeActionRequest(requestedActions));
56
+ if (isAuthorized) hasAuthorizedResource = true;
57
+ if (isAuthorized && connector === "OR") return { success: true };
58
+ if (!isAuthorized && connector === "AND") return unauthorizedResourceResponse(requestedResource);
29
59
  }
30
- if (success) return { success };
60
+ if (hasAuthorizedResource) return { success: true };
31
61
  return {
32
62
  success: false,
33
63
  error: "Not authorized"
@@ -816,16 +816,23 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
816
816
  const { newPassword, userId } = ctx.body;
817
817
  const minPasswordLength = ctx.context.password.config.minPasswordLength;
818
818
  if (newPassword.length < minPasswordLength) {
819
- ctx.context.logger.error("Password is too short");
819
+ ctx.context.logger.warn("Password is too short");
820
820
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
821
821
  }
822
822
  const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
823
823
  if (newPassword.length > maxPasswordLength) {
824
- ctx.context.logger.error("Password is too long");
824
+ ctx.context.logger.warn("Password is too long");
825
825
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
826
826
  }
827
+ if (!await ctx.context.internalAdapter.findUserById(userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
827
828
  const hashedPassword = await ctx.context.password.hash(newPassword);
828
- await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
829
+ if ((await ctx.context.internalAdapter.findAccounts(userId)).find((account) => account.providerId === "credential")) await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
830
+ else await ctx.context.internalAdapter.createAccount({
831
+ userId,
832
+ providerId: "credential",
833
+ accountId: userId,
834
+ password: hashedPassword
835
+ });
829
836
  return ctx.json({ status: true });
830
837
  });
831
838
  const userHasPermissionBodySchema = z.object({
@@ -1,4 +1,11 @@
1
1
  //#region src/plugins/captcha/constants.ts
2
+ /**
3
+ * Upper bound (in milliseconds) for a single provider verification request.
4
+ * Without it, a hanging provider would tie up the request indefinitely before
5
+ * any rate limiting applies, so every verify handler aborts at this deadline
6
+ * and fails closed.
7
+ */
8
+ const CAPTCHA_VERIFY_TIMEOUT_MS = 1e4;
2
9
  const defaultEndpoints = [
3
10
  "/sign-up/email",
4
11
  "/sign-in/email",
@@ -17,4 +24,4 @@ const siteVerifyMap = {
17
24
  [Providers.CAPTCHAFOX]: "https://api.captchafox.com/siteverify"
18
25
  };
19
26
  //#endregion
20
- export { Providers, defaultEndpoints, siteVerifyMap };
27
+ export { CAPTCHA_VERIFY_TIMEOUT_MS, Providers, defaultEndpoints, siteVerifyMap };
@@ -42,10 +42,16 @@ const captcha = (options) => ({
42
42
  secretKey: options.secretKey,
43
43
  remoteIP: remoteUserIP
44
44
  };
45
- if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile(handlerParams);
45
+ if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile({
46
+ ...handlerParams,
47
+ expectedAction: options.expectedAction,
48
+ allowedHostnames: options.allowedHostnames
49
+ });
46
50
  if (options.provider === Providers.GOOGLE_RECAPTCHA) return await googleRecaptcha({
47
51
  ...handlerParams,
48
- minScore: options.minScore
52
+ minScore: options.minScore,
53
+ expectedAction: options.expectedAction,
54
+ allowedHostnames: options.allowedHostnames
49
55
  });
50
56
  if (options.provider === Providers.HCAPTCHA) return await hCaptcha({
51
57
  ...handlerParams,
@@ -10,9 +10,30 @@ interface BaseCaptchaOptions {
10
10
  interface GoogleRecaptchaOptions extends BaseCaptchaOptions {
11
11
  provider: typeof Providers.GOOGLE_RECAPTCHA;
12
12
  minScore?: number | undefined;
13
+ /**
14
+ * Expected reCAPTCHA v3 `action`. When set, a verification whose action does
15
+ * not match is rejected, preventing a token minted for another action on the
16
+ * same site key from being replayed against this endpoint.
17
+ */
18
+ expectedAction?: string | undefined;
19
+ /**
20
+ * Allow-list of hostnames the token must have been issued for. When set, a
21
+ * verification reporting a different hostname is rejected.
22
+ */
23
+ allowedHostnames?: string[] | undefined;
13
24
  }
14
25
  interface CloudflareTurnstileOptions extends BaseCaptchaOptions {
15
26
  provider: typeof Providers.CLOUDFLARE_TURNSTILE;
27
+ /**
28
+ * Expected Turnstile `action`. When set, a verification whose action does
29
+ * not match is rejected, preventing cross-context token reuse.
30
+ */
31
+ expectedAction?: string | undefined;
32
+ /**
33
+ * Allow-list of hostnames the token must have been issued for. When set, a
34
+ * verification reporting a different or missing hostname is rejected.
35
+ */
36
+ allowedHostnames?: string[] | undefined;
16
37
  }
17
38
  interface HCaptchaOptions extends BaseCaptchaOptions {
18
39
  provider: typeof Providers.HCAPTCHA;
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const captchaFox = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
7
8
  const response = await betterFetch(siteVerifyURL, {
8
9
  method: "POST",
10
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
9
11
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
10
12
  body: encodeToURLParams({
11
13
  secret: secretKey,
@@ -1,10 +1,12 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { betterFetch } from "@better-fetch/fetch";
4
5
  //#region src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
5
- const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP }) => {
6
+ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, expectedAction, allowedHostnames }) => {
6
7
  const response = await betterFetch(siteVerifyURL, {
7
8
  method: "POST",
9
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
8
10
  headers: { "Content-Type": "application/json" },
9
11
  body: JSON.stringify({
10
12
  secret: secretKey,
@@ -13,11 +15,14 @@ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey,
13
15
  })
14
16
  });
15
17
  if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
16
- if (!response.data.success) return middlewareResponse({
18
+ const verificationFailed = () => middlewareResponse({
17
19
  message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
18
20
  code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
19
21
  status: 403
20
22
  });
23
+ if (!response.data.success) return verificationFailed();
24
+ if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
25
+ if (allowedHostnames && allowedHostnames.length > 0 && !(response.data.hostname && allowedHostnames.includes(response.data.hostname))) return verificationFailed();
21
26
  };
22
27
  //#endregion
23
28
  export { cloudflareTurnstile };
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,9 +7,10 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const isV3 = (response) => {
7
8
  return "score" in response && typeof response.score === "number";
8
9
  };
9
- const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP }) => {
10
+ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP, expectedAction, allowedHostnames }) => {
10
11
  const response = await betterFetch(siteVerifyURL, {
11
12
  method: "POST",
13
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
12
14
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
13
15
  body: encodeToURLParams({
14
16
  secret: secretKey,
@@ -17,11 +19,14 @@ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minS
17
19
  })
18
20
  });
19
21
  if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
20
- if (!response.data.success || isV3(response.data) && response.data.score < minScore) return middlewareResponse({
22
+ const verificationFailed = () => middlewareResponse({
21
23
  message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
22
24
  code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
23
25
  status: 403
24
26
  });
27
+ if (!response.data.success || isV3(response.data) && response.data.score < minScore) return verificationFailed();
28
+ if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
29
+ if (allowedHostnames && allowedHostnames.length > 0 && !allowedHostnames.includes(response.data.hostname)) return verificationFailed();
25
30
  };
26
31
  //#endregion
27
32
  export { googleRecaptcha };
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const hCaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
7
8
  const response = await betterFetch(siteVerifyURL, {
8
9
  method: "POST",
10
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
9
11
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
10
12
  body: encodeToURLParams({
11
13
  secret: secretKey,
@@ -243,7 +243,21 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
243
243
  });
244
244
  }
245
245
  if (deviceCodeRecord.status === "approved" && deviceCodeRecord.userId) {
246
- const user = await ctx.context.internalAdapter.findUserById(deviceCodeRecord.userId);
246
+ const claimedDeviceCode = await ctx.context.adapter.consumeOne({
247
+ model: "deviceCode",
248
+ where: [{
249
+ field: "deviceCode",
250
+ value: device_code
251
+ }, {
252
+ field: "status",
253
+ value: "approved"
254
+ }]
255
+ });
256
+ if (!claimedDeviceCode?.userId) throw new APIError("BAD_REQUEST", {
257
+ error: "invalid_grant",
258
+ error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE.message
259
+ });
260
+ const user = await ctx.context.internalAdapter.findUserById(claimedDeviceCode.userId);
247
261
  if (!user) throw new APIError("INTERNAL_SERVER_ERROR", {
248
262
  error: "server_error",
249
263
  error_description: DEVICE_AUTHORIZATION_ERROR_CODES.USER_NOT_FOUND.message
@@ -261,18 +275,11 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
261
275
  user,
262
276
  session
263
277
  }), Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / 1e3));
264
- await ctx.context.adapter.delete({
265
- model: "deviceCode",
266
- where: [{
267
- field: "id",
268
- value: deviceCodeRecord.id
269
- }]
270
- });
271
278
  return ctx.json({
272
279
  access_token: session.token,
273
280
  token_type: "Bearer",
274
281
  expires_in: Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / 1e3),
275
- scope: deviceCodeRecord.scope || ""
282
+ scope: claimedDeviceCode.scope || ""
276
283
  }, { headers: {
277
284
  "Cache-Control": "no-store",
278
285
  Pragma: "no-cache"
@@ -115,7 +115,7 @@ const createVerificationOTPBodySchema = z.object({
115
115
  description: "Type of the OTP"
116
116
  })
117
117
  });
118
- const createVerificationOTP = (opts) => createAuthEndpoint({
118
+ const createVerificationOTP = (opts) => createAuthEndpoint.serverOnly({
119
119
  method: "POST",
120
120
  body: createVerificationOTPBodySchema,
121
121
  metadata: { openapi: {
@@ -159,7 +159,7 @@ const getVerificationOTPBodySchema = z.object({
159
159
  *
160
160
  * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-get-verification-otp)
161
161
  */
162
- const getVerificationOTP = (opts) => createAuthEndpoint({
162
+ const getVerificationOTP = (opts) => createAuthEndpoint.serverOnly({
163
163
  method: "GET",
164
164
  query: getVerificationOTPBodySchema,
165
165
  metadata: { openapi: {
@@ -636,24 +636,7 @@ const requestEmailChangeEmailOTP = (opts) => createAuthEndpoint("/email-otp/requ
636
636
  }
637
637
  if (opts.changeEmail?.verifyCurrentEmail) {
638
638
  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(toOTPIdentifier("email-verification", email));
640
- if (!currentEmailVerificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
641
- const currentEmailIdentifier = toOTPIdentifier("email-verification", email);
642
- if (currentEmailVerificationValue.expiresAt < /* @__PURE__ */ new Date()) {
643
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
644
- throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
645
- }
646
- const [otpValue, attempts] = splitAtLastColon(currentEmailVerificationValue.value);
647
- const allowedAttempts = opts?.allowedAttempts || 3;
648
- if (attempts && parseInt(attempts) >= allowedAttempts) {
649
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
650
- throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
651
- }
652
- if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
653
- await ctx.context.internalAdapter.updateVerificationByIdentifier(currentEmailIdentifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
654
- throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
655
- }
656
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
639
+ await atomicVerifyOTP(ctx, opts, toOTPIdentifier("email-verification", email), ctx.body.otp);
657
640
  } else if (ctx.body.otp) ctx.context.logger.warn("OTP provided but not required for verifying current email. If you want to require OTP verification for current email, please set the changeEmail.verifyCurrentEmail option to true in the configuration");
658
641
  const otp = opts.generateOTP({
659
642
  email: newEmail,
@@ -723,24 +706,7 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
723
706
  ctx.context.logger.error("Email is the same");
724
707
  throw APIError$1.fromStatus("BAD_REQUEST", { message: "Email is the same" });
725
708
  }
726
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(toOTPIdentifier("change-email", `${email}-${newEmail}`));
727
- if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
728
- const changeEmailIdentifier = toOTPIdentifier("change-email", `${email}-${newEmail}`);
729
- if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
730
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
731
- throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
732
- }
733
- const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
734
- const allowedAttempts = opts?.allowedAttempts || 3;
735
- if (attempts && parseInt(attempts) >= allowedAttempts) {
736
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
737
- throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
738
- }
739
- if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
740
- await ctx.context.internalAdapter.updateVerificationByIdentifier(changeEmailIdentifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
741
- throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
742
- }
743
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
709
+ await atomicVerifyOTP(ctx, opts, toOTPIdentifier("change-email", `${email}-${newEmail}`), ctx.body.otp);
744
710
  const currentUser = await ctx.context.internalAdapter.findUserByEmail(email);
745
711
  if (!currentUser)
746
712
  /**
@@ -770,29 +736,33 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
770
736
  });
771
737
  const defaultOTPGenerator = (options) => generateRandomString(options.otpLength ?? 6, "0-9");
772
738
  /**
773
- * Atomically verifies OTP with race condition protection.
774
- * Deletes token before verification to prevent concurrent reuse.
775
- * Re-creates token with incremented attempts on failure.
739
+ * Verifies a single-use OTP with race-condition protection.
740
+ *
741
+ * The atomic consume is the single gate: only the first concurrent caller
742
+ * receives the record, every later racer receives `null` and is rejected, so
743
+ * a correct OTP can only ever be accepted once. When the submitted code is
744
+ * wrong the record is recreated with the same value and expiry and an
745
+ * incremented attempt count so the next try can still find it; the budget is
746
+ * enforced before verification, and a record whose attempts are exhausted is
747
+ * left consumed (no recreate), locking the identifier out.
776
748
  */
777
749
  async function atomicVerifyOTP(ctx, opts, identifier, providedOTP) {
778
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
779
- if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
780
- if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
750
+ const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
751
+ if (existing && existing.expiresAt < /* @__PURE__ */ new Date()) {
781
752
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
782
753
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
783
754
  }
784
- const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
755
+ const consumed = await ctx.context.internalAdapter.consumeVerificationValue(identifier);
756
+ if (!consumed) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
757
+ const [otpValue, attempts] = splitAtLastColon(consumed.value);
785
758
  const allowedAttempts = opts?.allowedAttempts || 3;
786
- if (attempts && parseInt(attempts) >= allowedAttempts) {
787
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
788
- throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
789
- }
790
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
759
+ const usedAttempts = parseInt(attempts || "0");
760
+ if (usedAttempts >= allowedAttempts) throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
791
761
  if (!await verifyStoredOTP(ctx, opts, otpValue, providedOTP)) {
792
762
  await ctx.context.internalAdapter.createVerificationValue({
793
- value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
763
+ value: `${otpValue}:${usedAttempts + 1}`,
794
764
  identifier,
795
- expiresAt: verificationValue.expiresAt
765
+ expiresAt: consumed.expiresAt
796
766
  });
797
767
  throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
798
768
  }
@@ -14,6 +14,9 @@ import { APIError } from "@better-auth/core/error";
14
14
  import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
15
15
  import { betterFetch } from "@better-fetch/fetch";
16
16
  //#region src/plugins/generic-oauth/index.ts
17
+ function isNonEmptyOAuthId(id) {
18
+ return id !== void 0 && id !== null && id !== "";
19
+ }
17
20
  /**
18
21
  * A generic OAuth plugin that can be used to add OAuth support to any provider
19
22
  */
@@ -114,14 +117,16 @@ const genericOAuth = (options) => {
114
117
  const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
115
118
  if (!userInfo) return null;
116
119
  const userMap = await c.mapProfileToUser?.(userInfo);
120
+ const rawId = isNonEmptyOAuthId(userMap?.id) ? userMap.id : isNonEmptyOAuthId(userInfo.id) ? userInfo.id : isNonEmptyOAuthId(userInfo.sub) ? userInfo.sub : void 0;
121
+ if (rawId === void 0) return null;
117
122
  return {
118
123
  user: {
119
- id: userInfo?.id,
120
124
  email: userInfo?.email,
121
125
  emailVerified: userInfo?.emailVerified,
122
126
  image: userInfo?.image,
123
127
  name: userInfo?.name,
124
- ...userMap
128
+ ...userMap,
129
+ id: String(rawId)
125
130
  },
126
131
  data: userInfo
127
132
  };
@@ -15,6 +15,9 @@ import * as z from "zod";
15
15
  import { decodeJwt } from "jose";
16
16
  import { betterFetch } from "@better-fetch/fetch";
17
17
  //#region src/plugins/generic-oauth/routes.ts
18
+ function isNonEmptyOAuthId(id) {
19
+ return id !== void 0 && id !== null && id !== "";
20
+ }
18
21
  const signInWithOAuth2BodySchema = z.object({
19
22
  providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
20
23
  callbackURL: z.string().meta({ description: "The URL to redirect to after sign in" }).optional(),
@@ -209,8 +212,8 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
209
212
  ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
210
213
  redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
211
214
  }
212
- const rawId = mapUser.id !== void 0 && mapUser.id !== null && mapUser.id !== "" ? mapUser.id : userInfo.id;
213
- const id = rawId !== void 0 && rawId !== null ? String(rawId) : "";
215
+ const rawId = isNonEmptyOAuthId(mapUser.id) ? mapUser.id : isNonEmptyOAuthId(userInfo.id) ? userInfo.id : isNonEmptyOAuthId(userInfo.sub) ? userInfo.sub : void 0;
216
+ const id = rawId !== void 0 ? String(rawId) : "";
214
217
  if (!id) {
215
218
  ctx.context.logger.error("Provider did not return an account id (e.g. `sub`). Unable to sign in.", userInfo);
216
219
  redirectOnError(ctx, resolvedErrorURL, "id_is_missing");
@@ -399,19 +402,20 @@ async function getUserInfo(tokens, finalUserInfoUrl) {
399
402
  }
400
403
  }
401
404
  if (!finalUserInfoUrl) return null;
402
- const userInfo = await betterFetch(finalUserInfoUrl, {
405
+ const profile = (await betterFetch(finalUserInfoUrl, {
403
406
  method: "GET",
404
407
  headers: { Authorization: `Bearer ${tokens.accessToken}` }
405
- });
406
- const subjectId = userInfo.data?.sub ?? userInfo.data?.id;
407
- if (subjectId === void 0 || subjectId === null || subjectId === "") return null;
408
+ })).data;
409
+ if (!profile) return null;
410
+ const { id: profileId, ...profileFields } = profile;
411
+ const subjectId = isNonEmptyOAuthId(profileId) ? profileId : isNonEmptyOAuthId(profile.sub) ? profile.sub : void 0;
408
412
  return {
409
- id: subjectId,
410
- emailVerified: userInfo.data?.email_verified ?? false,
411
- email: userInfo.data?.email,
412
- image: userInfo.data?.picture,
413
- name: userInfo.data?.name,
414
- ...userInfo.data
413
+ ...profileFields,
414
+ ...subjectId !== void 0 ? { id: subjectId } : {},
415
+ email: profile?.email,
416
+ emailVerified: profile?.email_verified ?? false,
417
+ image: profile?.picture,
418
+ name: profile?.name
415
419
  };
416
420
  }
417
421
  //#endregion
@@ -17,7 +17,7 @@ interface HaveIBeenPwnedOptions {
17
17
  /**
18
18
  * Paths to check for password
19
19
  *
20
- * @default ["/sign-up/email", "/change-password", "/reset-password"]
20
+ * @default ["/sign-up/email", "/change-password", "/reset-password", "/email-otp/reset-password", "/phone-number/reset-password", "/admin/create-user", "/admin/set-user-password"]
21
21
  */
22
22
  paths?: string[];
23
23
  /**
@@ -31,7 +31,11 @@ const haveIBeenPwned = (options) => {
31
31
  const paths = options?.paths || [
32
32
  "/sign-up/email",
33
33
  "/change-password",
34
- "/reset-password"
34
+ "/reset-password",
35
+ "/email-otp/reset-password",
36
+ "/phone-number/reset-password",
37
+ "/admin/create-user",
38
+ "/admin/set-user-password"
35
39
  ];
36
40
  return {
37
41
  id: "have-i-been-pwned",
@@ -41,10 +41,14 @@ import { getClient, getMetadata, oidcProvider } from "./oidc-provider/index.mjs"
41
41
  import { getMCPProtectedResourceMetadata, getMCPProviderMetadata, mcp, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, withMcpAuth } from "./mcp/index.mjs";
42
42
  import { MULTI_SESSION_ERROR_CODES } from "./multi-session/error-codes.mjs";
43
43
  import { MultiSessionConfig, multiSession } from "./multi-session/index.mjs";
44
+ import { OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_MESSAGE_TYPE, POPUP_MARKER_COOKIE } from "./oauth-popup/constants.mjs";
45
+ import { OAUTH_POPUP_ERROR_CODES } from "./oauth-popup/error-codes.mjs";
46
+ import { OAuthPopupData, OAuthPopupMessage } from "./oauth-popup/types.mjs";
47
+ import { OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_SCRIPT_CSP_HASH, oauthPopup } from "./oauth-popup/index.mjs";
44
48
  import { OAuthProxyOptions, oAuthProxy } from "./oauth-proxy/index.mjs";
45
49
  import { OneTapOptions, oneTap } from "./one-tap/index.mjs";
46
50
  import { OneTimeTokenOptions, oneTimeToken } from "./one-time-token/index.mjs";
47
- import { FieldSchema, OpenAPIModelSchema, Path, generator } from "./open-api/generator.mjs";
51
+ import { FieldSchema, OpenAPIModelSchema, OpenAPIParameter, OpenAPISchema, Path, generator } from "./open-api/generator.mjs";
48
52
  import { OpenAPIOptions, openAPI } from "./open-api/index.mjs";
49
53
  import { PhoneNumberOptions, UserWithPhoneNumber } from "./phone-number/types.mjs";
50
54
  import { phoneNumber } from "./phone-number/index.mjs";
@@ -62,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
62
66
  import { UsernameOptions, username } from "./username/index.mjs";
63
67
  import { hasPermission } from "./organization/has-permission.mjs";
64
68
  import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
65
- export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
69
+ export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthAccessToken, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
@@ -1,6 +1,8 @@
1
1
  import { HIDE_METADATA } from "../utils/hide-metadata.mjs";
2
2
  import { createAccessControl, role } from "./access/access.mjs";
3
3
  import { MULTI_SESSION_ERROR_CODES } from "./multi-session/error-codes.mjs";
4
+ import { OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_MESSAGE_TYPE, POPUP_MARKER_COOKIE } from "./oauth-popup/constants.mjs";
5
+ import { OAUTH_POPUP_ERROR_CODES } from "./oauth-popup/error-codes.mjs";
4
6
  import { TWO_FACTOR_ERROR_CODES } from "./two-factor/error-code.mjs";
5
7
  import { twoFactorClient } from "./two-factor/client.mjs";
6
8
  import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
@@ -31,6 +33,7 @@ import { magicLink } from "./magic-link/index.mjs";
31
33
  import { getClient, getMetadata, oidcProvider } from "./oidc-provider/index.mjs";
32
34
  import { getMCPProtectedResourceMetadata, getMCPProviderMetadata, mcp, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, withMcpAuth } from "./mcp/index.mjs";
33
35
  import { multiSession } from "./multi-session/index.mjs";
36
+ import { OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_SCRIPT_CSP_HASH, oauthPopup } from "./oauth-popup/index.mjs";
34
37
  import { oAuthProxy } from "./oauth-proxy/index.mjs";
35
38
  import { oneTap } from "./one-tap/index.mjs";
36
39
  import { oneTimeToken } from "./one-time-token/index.mjs";
@@ -43,4 +46,4 @@ import { siwe } from "./siwe/index.mjs";
43
46
  import { testUtils } from "./test-utils/index.mjs";
44
47
  import { twoFactor } from "./two-factor/index.mjs";
45
48
  import { username } from "./username/index.mjs";
46
- export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
49
+ export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
@@ -144,7 +144,7 @@ const jwt = (options) => {
144
144
  const jwt = await getJwtToken(ctx, options);
145
145
  return ctx.json({ token: jwt });
146
146
  }),
147
- signJWT: createAuthEndpoint({
147
+ signJWT: createAuthEndpoint.serverOnly({
148
148
  method: "POST",
149
149
  metadata: { $Infer: { body: {} } },
150
150
  body: signJWTBodySchema
@@ -158,7 +158,7 @@ const jwt = (options) => {
158
158
  });
159
159
  return c.json({ token: jwt });
160
160
  }),
161
- verifyJWT: createAuthEndpoint({
161
+ verifyJWT: createAuthEndpoint.serverOnly({
162
162
  method: "POST",
163
163
  metadata: { $Infer: {
164
164
  body: {},
@@ -65,6 +65,7 @@ function createMcpAuthClient(options) {
65
65
  if (!response.ok) return null;
66
66
  const data = await response.json();
67
67
  if (!data || !data.userId) return null;
68
+ if (data.accessTokenExpiresAt && new Date(data.accessTokenExpiresAt).getTime() < Date.now()) return null;
68
69
  return data;
69
70
  } catch {
70
71
  return null;