better-auth 1.6.16 → 1.6.17

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 (84) 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/path-to-object.d.mts +5 -2
  21. package/dist/client/plugins/index.d.mts +4 -1
  22. package/dist/client/plugins/index.mjs +4 -1
  23. package/dist/client/query.d.mts +4 -3
  24. package/dist/client/query.mjs +27 -17
  25. package/dist/client/session-atom.mjs +129 -4
  26. package/dist/client/session-refresh.d.mts +3 -18
  27. package/dist/client/session-refresh.mjs +38 -49
  28. package/dist/client/types.d.mts +2 -2
  29. package/dist/context/create-context.mjs +2 -1
  30. package/dist/context/store-capabilities.mjs +12 -0
  31. package/dist/cookies/index.mjs +25 -2
  32. package/dist/db/internal-adapter.mjs +51 -0
  33. package/dist/package.mjs +1 -1
  34. package/dist/plugins/access/access.mjs +49 -19
  35. package/dist/plugins/admin/routes.mjs +10 -3
  36. package/dist/plugins/captcha/constants.mjs +8 -1
  37. package/dist/plugins/captcha/index.mjs +8 -2
  38. package/dist/plugins/captcha/types.d.mts +21 -0
  39. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  40. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  41. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  42. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  43. package/dist/plugins/device-authorization/routes.mjs +16 -9
  44. package/dist/plugins/email-otp/routes.mjs +22 -52
  45. package/dist/plugins/generic-oauth/index.mjs +7 -2
  46. package/dist/plugins/generic-oauth/routes.mjs +16 -12
  47. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  48. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  49. package/dist/plugins/index.d.mts +5 -1
  50. package/dist/plugins/index.mjs +4 -1
  51. package/dist/plugins/jwt/index.mjs +2 -2
  52. package/dist/plugins/mcp/client/index.mjs +1 -0
  53. package/dist/plugins/mcp/index.mjs +8 -0
  54. package/dist/plugins/multi-session/index.mjs +7 -5
  55. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  56. package/dist/plugins/oauth-popup/client.mjs +203 -0
  57. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  58. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  59. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  60. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  61. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  62. package/dist/plugins/oauth-popup/index.mjs +227 -0
  63. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  64. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  65. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  66. package/dist/plugins/oidc-provider/index.mjs +10 -0
  67. package/dist/plugins/one-tap/client.mjs +12 -6
  68. package/dist/plugins/one-tap/index.d.mts +1 -0
  69. package/dist/plugins/one-tap/index.mjs +9 -5
  70. package/dist/plugins/one-time-token/index.mjs +1 -3
  71. package/dist/plugins/open-api/generator.mjs +7 -4
  72. package/dist/plugins/organization/adapter.d.mts +29 -1
  73. package/dist/plugins/organization/adapter.mjs +66 -6
  74. package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
  75. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  76. package/dist/plugins/organization/routes/crud-team.mjs +36 -3
  77. package/dist/plugins/phone-number/routes.mjs +41 -36
  78. package/dist/plugins/siwe/index.mjs +2 -3
  79. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  80. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  81. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  82. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  83. package/dist/plugins/username/index.mjs +6 -6
  84. package/package.json +9 -9
@@ -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,6 +41,10 @@ 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";
@@ -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, 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;
@@ -302,6 +302,10 @@ const mcp = (options) => {
302
302
  error_description: "refresh token expired",
303
303
  error: "invalid_grant"
304
304
  });
305
+ if (!token.scopes?.split(" ").includes("offline_access")) throw new APIError("UNAUTHORIZED", {
306
+ error_description: "refresh token was not issued for the offline_access scope",
307
+ error: "invalid_grant"
308
+ });
305
309
  const refreshClient = await ctx.context.adapter.findOne({
306
310
  model: modelName.oauthClient,
307
311
  where: [{
@@ -693,6 +697,10 @@ const mcp = (options) => {
693
697
  }]
694
698
  });
695
699
  if (!accessTokenData) return c.json(null);
700
+ if (accessTokenData.accessTokenExpiresAt < /* @__PURE__ */ new Date()) {
701
+ c.headers?.set("WWW-Authenticate", "Bearer");
702
+ return c.json(null);
703
+ }
696
704
  return c.json(accessTokenData);
697
705
  })
698
706
  },
@@ -55,8 +55,9 @@ const multiSession = (options) => {
55
55
  }, async (ctx) => {
56
56
  const sessionToken = ctx.body.sessionToken;
57
57
  const multiSessionCookieName = `${ctx.context.authCookies.sessionToken.name}_multi-${sessionToken.toLowerCase()}`;
58
- if (!await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret)) throw APIError.from("UNAUTHORIZED", MULTI_SESSION_ERROR_CODES.INVALID_SESSION_TOKEN);
59
- const session = await ctx.context.internalAdapter.findSession(sessionToken);
58
+ const sessionCookie = await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret);
59
+ if (!sessionCookie) throw APIError.from("UNAUTHORIZED", MULTI_SESSION_ERROR_CODES.INVALID_SESSION_TOKEN);
60
+ const session = await ctx.context.internalAdapter.findSession(sessionCookie);
60
61
  if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
61
62
  expireCookie(ctx, {
62
63
  name: multiSessionCookieName,
@@ -88,13 +89,14 @@ const multiSession = (options) => {
88
89
  }, async (ctx) => {
89
90
  const sessionToken = ctx.body.sessionToken;
90
91
  const multiSessionCookieName = `${ctx.context.authCookies.sessionToken.name}_multi-${sessionToken.toLowerCase()}`;
91
- if (!await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret)) throw APIError.from("UNAUTHORIZED", MULTI_SESSION_ERROR_CODES.INVALID_SESSION_TOKEN);
92
- await ctx.context.internalAdapter.deleteSession(sessionToken);
92
+ const sessionCookie = await ctx.getSignedCookie(multiSessionCookieName, ctx.context.secret);
93
+ if (!sessionCookie) throw APIError.from("UNAUTHORIZED", MULTI_SESSION_ERROR_CODES.INVALID_SESSION_TOKEN);
94
+ await ctx.context.internalAdapter.deleteSession(sessionCookie);
93
95
  expireCookie(ctx, {
94
96
  name: multiSessionCookieName,
95
97
  attributes: ctx.context.authCookies.sessionToken.attributes
96
98
  });
97
- if (!(ctx.context.session?.session.token === sessionToken)) return ctx.json({ status: true });
99
+ if (!(ctx.context.session?.session.token === sessionCookie)) return ctx.json({ status: true });
98
100
  const cookieHeader = ctx.headers?.get("cookie");
99
101
  if (cookieHeader) {
100
102
  const cookies = Object.fromEntries(parseCookies(cookieHeader));
@@ -0,0 +1,82 @@
1
+ import { POPUP_TOKEN_STORAGE_KEY } from "./constants.mjs";
2
+ import { OAUTH_POPUP_ERROR_CODES } from "./error-codes.mjs";
3
+ import { oauthPopup } from "./index.mjs";
4
+ import { BetterAuthClientOptions, ClientStore } from "@better-auth/core";
5
+ import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
6
+ import { BetterFetch, BetterFetchPlugin } from "@better-fetch/fetch";
7
+
8
+ //#region src/plugins/oauth-popup/client.d.ts
9
+ /** Inputs for `authClient.signIn.popup`; mirror the redirect sign-in. */
10
+ interface SignInPopupOptions {
11
+ /** Built-in social provider id (e.g. `"google"`). */
12
+ provider?: string;
13
+ /** Generic OAuth provider id (registered via `genericOAuth`). */
14
+ providerId?: string;
15
+ callbackURL?: string;
16
+ errorCallbackURL?: string;
17
+ newUserCallbackURL?: string;
18
+ requestSignUp?: boolean;
19
+ scopes?: string[];
20
+ additionalData?: Record<string, unknown>;
21
+ /** `window.open` feature string; defaults to a centered 500x600 window. */
22
+ windowFeatures?: string;
23
+ /** How long (ms) to wait for the popup to complete. Default 5 minutes. */
24
+ timeoutMs?: number;
25
+ }
26
+ interface SignInPopupResult {
27
+ data: {
28
+ success: boolean;
29
+ } | null;
30
+ error: {
31
+ code: string;
32
+ message: string;
33
+ status?: number;
34
+ } | null;
35
+ }
36
+ /** Reads the stored popup token (browser-only; null otherwise). */
37
+ declare function getStoredPopupToken(): string | null;
38
+ /**
39
+ * Attaches the popup token as a bearer header when embedded (where the cookie is
40
+ * partitioned), and clears it once the session ends so it can't be reused.
41
+ */
42
+ declare const popupBearerFetchPlugin: BetterFetchPlugin;
43
+ interface SignInPopupDeps {
44
+ $fetch: BetterFetch;
45
+ options?: BetterAuthClientOptions | undefined;
46
+ /** Refreshes the reactive session, as the redirect flow's atom listeners do. */
47
+ notifySessionSignal: () => void;
48
+ }
49
+ /**
50
+ * Builds `signIn.popup`. Runs the sign-in in the popup's own first-party
51
+ * context (so the OAuth state cookie lands there), waits for the completion
52
+ * page to post the session token back, stores it for the bearer fetch plugin,
53
+ * and refreshes the reactive session.
54
+ */
55
+ declare function createSignInPopup({
56
+ $fetch,
57
+ options,
58
+ notifySessionSignal
59
+ }: SignInPopupDeps): (opts: SignInPopupOptions) => Promise<SignInPopupResult>;
60
+ /**
61
+ * Client plugin for popup OAuth sign-in. Adds `authClient.signIn.popup`. Pair
62
+ * with the server `oauthPopup` and `bearer` plugins.
63
+ */
64
+ declare const oauthPopupClient: () => {
65
+ id: "oauth-popup";
66
+ version: string;
67
+ $InferServerPlugin: ReturnType<typeof oauthPopup>;
68
+ $ERROR_CODES: {
69
+ POPUP_SIGN_IN_FAILED: _better_auth_core_utils_error_codes0.RawError<"POPUP_SIGN_IN_FAILED">;
70
+ POPUP_BLOCKED: _better_auth_core_utils_error_codes0.RawError<"POPUP_BLOCKED">;
71
+ POPUP_CLOSED: _better_auth_core_utils_error_codes0.RawError<"POPUP_CLOSED">;
72
+ POPUP_TIMEOUT: _better_auth_core_utils_error_codes0.RawError<"POPUP_TIMEOUT">;
73
+ };
74
+ fetchPlugins: BetterFetchPlugin[];
75
+ getActions: ($fetch: BetterFetch, $store: ClientStore, options: BetterAuthClientOptions | undefined) => {
76
+ signIn: {
77
+ popup: (opts: SignInPopupOptions) => Promise<SignInPopupResult>;
78
+ };
79
+ };
80
+ };
81
+ //#endregion
82
+ export { SignInPopupOptions, SignInPopupResult, createSignInPopup, getStoredPopupToken, oauthPopupClient, popupBearerFetchPlugin };