better-auth 1.7.4 → 1.7.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 (46) hide show
  1. package/dist/api/index.d.mts +6 -6
  2. package/dist/api/routes/account.d.mts +1 -1
  3. package/dist/api/routes/callback.mjs +16 -64
  4. package/dist/api/routes/password.mjs +3 -5
  5. package/dist/api/routes/sign-in.d.mts +2 -2
  6. package/dist/api/routes/sign-in.mjs +2 -0
  7. package/dist/api/routes/sign-up.mjs +3 -10
  8. package/dist/api/routes/update-user.mjs +7 -20
  9. package/dist/client/config.d.mts +2 -2
  10. package/dist/client/query-atom.mjs +13 -0
  11. package/dist/client/query.mjs +37 -27
  12. package/dist/client/react/react-store.mjs +2 -1
  13. package/dist/client/session-atom.mjs +2 -1
  14. package/dist/context/create-context.mjs +3 -2
  15. package/dist/cookies/index.d.mts +3 -2
  16. package/dist/cookies/index.mjs +15 -36
  17. package/dist/db/get-migration.mjs +81 -60
  18. package/dist/oauth2/index.d.mts +2 -2
  19. package/dist/oauth2/index.mjs +2 -2
  20. package/dist/oauth2/link-account.d.mts +42 -14
  21. package/dist/oauth2/link-account.mjs +61 -1
  22. package/dist/package.mjs +1 -1
  23. package/dist/plugins/admin/admin.d.mts +3 -3
  24. package/dist/plugins/admin/admin.mjs +3 -1
  25. package/dist/plugins/admin/routes.mjs +4 -10
  26. package/dist/plugins/admin/types.d.mts +4 -1
  27. package/dist/plugins/captcha/constants.d.mts +1 -0
  28. package/dist/plugins/captcha/constants.mjs +2 -1
  29. package/dist/plugins/captcha/index.d.mts +2 -2
  30. package/dist/plugins/captcha/index.mjs +7 -0
  31. package/dist/plugins/captcha/types.d.mts +44 -2
  32. package/dist/plugins/captcha/utils.mjs +11 -1
  33. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +27 -8
  34. package/dist/plugins/captcha/verify-handlers/vercel-botid.mjs +27 -0
  35. package/dist/plugins/email-otp/routes.mjs +3 -4
  36. package/dist/plugins/index.d.mts +2 -2
  37. package/dist/plugins/oauth-proxy/index.mjs +19 -5
  38. package/dist/plugins/organization/routes/crud-members.d.mts +3 -3
  39. package/dist/plugins/organization/schema.d.mts +2 -2
  40. package/dist/plugins/phone-number/routes.mjs +4 -4
  41. package/dist/plugins/username/index.d.mts +4 -4
  42. package/dist/plugins/username/index.mjs +2 -0
  43. package/dist/plugins/username/schema.d.mts +4 -4
  44. package/dist/test-utils/http-test-instance.d.mts +2 -2
  45. package/dist/utils/password.mjs +16 -2
  46. package/package.json +10 -13
@@ -47,6 +47,48 @@ interface CaptchaFoxOptions extends BaseCaptchaOptions {
47
47
  provider: typeof Providers.CAPTCHAFOX;
48
48
  siteKey?: string | undefined;
49
49
  }
50
- type CaptchaOptions = GoogleRecaptchaOptions | CloudflareTurnstileOptions | HCaptchaOptions | CaptchaFoxOptions;
50
+ /**
51
+ * BotID verdict fields available to custom request validation.
52
+ *
53
+ * @see https://vercel.com/docs/botid/verified-bots
54
+ */
55
+ type BotIdVerification = {
56
+ isBot: boolean;
57
+ isVerifiedBot?: boolean | undefined;
58
+ verifiedBotName?: string | undefined;
59
+ verifiedBotCategory?: string | undefined;
60
+ };
61
+ type ValidateRequestContext = {
62
+ request: Request;
63
+ verification: BotIdVerification;
64
+ };
65
+ /**
66
+ * Protect auth requests with Vercel BotID. The browser must also protect the
67
+ * same request paths and methods with BotID's client SDK.
68
+ *
69
+ * @see https://vercel.com/docs/botid/get-started
70
+ */
71
+ interface VercelBotIdOptions {
72
+ provider: typeof Providers.VERCEL_BOTID;
73
+ /**
74
+ * Auth paths to verify, without the Better Auth base path.
75
+ *
76
+ * @default ["/sign-up/email", "/sign-in/email", "/request-password-reset"]
77
+ */
78
+ endpoints?: string[] | undefined;
79
+ /**
80
+ * Vercel's server-side check for the current request. Pass `checkBotId` from
81
+ * `botid/server`, or wrap it to supply SDK options.
82
+ */
83
+ checkBotId: () => Promise<BotIdVerification>;
84
+ /**
85
+ * Override the default `isBot === false` decision. Return `true` to allow
86
+ * the request, including a verified bot you trust.
87
+ *
88
+ * @see https://vercel.com/docs/botid/verified-bots
89
+ */
90
+ validateRequest?: (ctx: ValidateRequestContext) => boolean | Promise<boolean>;
91
+ }
92
+ type CaptchaOptions = GoogleRecaptchaOptions | CloudflareTurnstileOptions | HCaptchaOptions | CaptchaFoxOptions | VercelBotIdOptions;
51
93
  //#endregion
52
- export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider };
94
+ export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions };
@@ -5,5 +5,15 @@ const encodeToURLParams = (obj) => {
5
5
  for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) params.append(key, String(value));
6
6
  return params.toString();
7
7
  };
8
+ /**
9
+ * Rejects after `timeoutMs` without cancelling the underlying promise.
10
+ */
11
+ const withTimeout = (promise, timeoutMs) => {
12
+ let timer;
13
+ const timeout = new Promise((_resolve, reject) => {
14
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Operation timed out")), timeoutMs);
15
+ });
16
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
17
+ };
8
18
  //#endregion
9
- export { encodeToURLParams };
19
+ export { encodeToURLParams, withTimeout };
@@ -3,7 +3,7 @@ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
3
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
4
4
  import { betterFetch } from "@better-fetch/fetch";
5
5
  //#region src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
6
- const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, expectedAction, allowedHostnames }) => {
6
+ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, logger, remoteIP, expectedAction, allowedHostnames }) => {
7
7
  const response = await betterFetch(siteVerifyURL, {
8
8
  method: "POST",
9
9
  timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
@@ -15,14 +15,33 @@ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey,
15
15
  })
16
16
  });
17
17
  if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
18
- const verificationFailed = () => middlewareResponse({
19
- message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
20
- code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
21
- status: 403
18
+ const verificationFailed = (details) => {
19
+ logger.warn("Cloudflare Turnstile verification failed", {
20
+ provider: "cloudflare-turnstile",
21
+ ...details
22
+ });
23
+ return middlewareResponse({
24
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
25
+ code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
26
+ status: 403
27
+ });
28
+ };
29
+ if (!response.data.success) return verificationFailed({
30
+ reason: "siteverify_rejected",
31
+ errorCodes: response.data["error-codes"] ?? [],
32
+ ...response.data.hostname && { hostname: response.data.hostname },
33
+ ...response.data.action && { action: response.data.action }
34
+ });
35
+ if (expectedAction && response.data.action !== expectedAction) return verificationFailed({
36
+ reason: "action_mismatch",
37
+ expectedAction,
38
+ actualAction: response.data.action
39
+ });
40
+ if (allowedHostnames && allowedHostnames.length > 0 && !(response.data.hostname && allowedHostnames.includes(response.data.hostname))) return verificationFailed({
41
+ reason: "hostname_mismatch",
42
+ allowedHostnames,
43
+ actualHostname: response.data.hostname
22
44
  });
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();
26
45
  };
27
46
  //#endregion
28
47
  export { cloudflareTurnstile };
@@ -0,0 +1,27 @@
1
+ import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
3
+ import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
4
+ import { withTimeout } from "../utils.mjs";
5
+ //#region src/plugins/captcha/verify-handlers/vercel-botid.ts
6
+ const vercelBotId = async ({ request, checkBotId, validateRequest }) => {
7
+ let isValid;
8
+ try {
9
+ isValid = await withTimeout((async () => {
10
+ const verification = await checkBotId();
11
+ return validateRequest ? validateRequest({
12
+ request,
13
+ verification
14
+ }) : verification.isBot === false;
15
+ })(), CAPTCHA_VERIFY_TIMEOUT_MS);
16
+ } catch (error) {
17
+ throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message, { cause: error });
18
+ }
19
+ if (isValid) return void 0;
20
+ return middlewareResponse({
21
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
22
+ code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
23
+ status: 403
24
+ });
25
+ };
26
+ //#endregion
27
+ export { vercelBotId };
@@ -6,6 +6,7 @@ import { symmetricDecrypt } from "../../crypto/index.mjs";
6
6
  import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
7
7
  import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
8
8
  import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
9
+ import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
9
10
  import { APIError as APIError$1 } from "../../api/index.mjs";
10
11
  import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
11
12
  import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
@@ -581,10 +582,8 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
581
582
  } }
582
583
  }, async (ctx) => {
583
584
  const email = ctx.body.email.toLowerCase();
584
- const minPasswordLength = ctx.context.password.config.minPasswordLength;
585
- if (ctx.body.password.length < minPasswordLength) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
586
- const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
587
- if (ctx.body.password.length > maxPasswordLength) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
585
+ assertPasswordNotTooShort(ctx, ctx.body.password);
586
+ assertPasswordNotTooLong(ctx, ctx.body.password);
588
587
  await atomicVerifyOTP(ctx, opts, toOTPIdentifier("forget-password", email), ctx.body.otp);
589
588
  const user = await ctx.context.internalAdapter.findUserByEmail(email);
590
589
  if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
@@ -10,7 +10,7 @@ import { admin } from "./admin/admin.mjs";
10
10
  import { AnonymousOptions, AnonymousSession, UserWithAnonymous } from "./anonymous/types.mjs";
11
11
  import { anonymous } from "./anonymous/index.mjs";
12
12
  import { BearerOptions, bearer } from "./bearer/index.mjs";
13
- import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./captcha/types.mjs";
13
+ import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./captcha/types.mjs";
14
14
  import { captcha } from "./captcha/index.mjs";
15
15
  import { CustomSessionPluginOptions, customSession } from "./custom-session/index.mjs";
16
16
  import { TimeString, ms, sec } from "../utils/time.mjs";
@@ -66,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
66
66
  import { UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, username } from "./username/index.mjs";
67
67
  import { hasPermission } from "./organization/has-permission.mjs";
68
68
  import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
69
- export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, 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, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
69
+ export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, 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, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, ValidateRequestContext, VercelBotIdOptions, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
@@ -7,7 +7,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
7
7
  import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
8
8
  import { redirectOnError } from "../../oauth2/errors.mjs";
9
9
  import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
10
- import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
10
+ import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
11
11
  import { setOAuthState } from "../../api/state/oauth.mjs";
12
12
  import { parseGenericState } from "../../state.mjs";
13
13
  import { PACKAGE_VERSION } from "../../version.mjs";
@@ -37,6 +37,7 @@ const passthroughPayloadSchema = z.looseObject({
37
37
  updatedAt: true
38
38
  }).shape),
39
39
  profile: z.record(z.string(), z.unknown()).optional(),
40
+ scopes: z.array(z.string()).optional(),
40
41
  state: z.string().min(1),
41
42
  callbackURL: z.string().min(1),
42
43
  newUserURL: z.string().optional(),
@@ -107,7 +108,19 @@ const oAuthProxy = (opts) => {
107
108
  ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
108
109
  throw redirectOnError(ctx, errorURL, "payload_expired");
109
110
  }
110
- if (!await restoreOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
111
+ const stateData = await restoreOAuthProxyState(ctx, payload.state);
112
+ if (!stateData) throw redirectOnError(ctx, errorURL, "state_mismatch");
113
+ if (stateData.link) {
114
+ const linkResult = await linkOAuthAccount(ctx, {
115
+ link: stateData.link,
116
+ userInfo: payload.userInfo,
117
+ account: payload.account,
118
+ profile: payload.profile ?? {},
119
+ scopes: payload.scopes ?? payload.account.scope?.split(",")
120
+ });
121
+ if (!linkResult.linked) throw redirectOnError(ctx, errorURL, linkResult.error.code, linkResult.error.message);
122
+ throw ctx.redirect(payload.callbackURL);
123
+ }
111
124
  let result;
112
125
  try {
113
126
  result = await handleOAuthUserInfo(ctx, {
@@ -185,7 +198,7 @@ const oAuthProxy = (opts) => {
185
198
  hooks: {
186
199
  before: [{
187
200
  matcher(context) {
188
- return !!context.path?.startsWith("/sign-in/social");
201
+ return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
189
202
  },
190
203
  handler: createAuthMiddleware(async (ctx) => {
191
204
  if (checkSkipProxy(ctx, opts)) return;
@@ -307,6 +320,7 @@ const oAuthProxy = (opts) => {
307
320
  emailVerified: userInfo.emailVerified
308
321
  },
309
322
  profile: providerProfile,
323
+ scopes: tokens.scopes,
310
324
  account: {
311
325
  ...accountKey,
312
326
  accessToken: tokens.accessToken,
@@ -333,14 +347,14 @@ const oAuthProxy = (opts) => {
333
347
  }],
334
348
  after: [{
335
349
  matcher(context) {
336
- return !!context.path?.startsWith("/sign-in/social");
350
+ return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
337
351
  },
338
352
  handler: createAuthMiddleware(async (ctx) => {
339
353
  if (checkSkipProxy(ctx, opts)) return;
340
354
  const signInResponse = ctx.context.returned;
341
355
  if (!signInResponse || typeof signInResponse !== "object" || !("url" in signInResponse)) return;
342
356
  const { url: providerURL } = signInResponse;
343
- if (typeof providerURL !== "string") return;
357
+ if (typeof providerURL !== "string" || providerURL.length === 0) return;
344
358
  const oauthURL = new URL(providerURL);
345
359
  const originalState = oauthURL.searchParams.get("state");
346
360
  if (!originalState) return;
@@ -736,13 +736,13 @@ declare const listMembers: <O extends OrganizationOptions>(options: O) => _$bett
736
736
  filterField: z.ZodOptional<z.ZodString>;
737
737
  filterValue: z.ZodOptional<z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodNumber]>, z.ZodBoolean]>, z.ZodArray<z.ZodString>]>, z.ZodArray<z.ZodNumber>]>>;
738
738
  filterOperator: z.ZodOptional<z.ZodEnum<{
739
+ in: "in";
740
+ lte: "lte";
739
741
  eq: "eq";
740
742
  ne: "ne";
743
+ lt: "lt";
741
744
  gt: "gt";
742
745
  gte: "gte";
743
- lt: "lt";
744
- lte: "lte";
745
- in: "in";
746
746
  not_in: "not_in";
747
747
  contains: "contains";
748
748
  starts_with: "starts_with";
@@ -244,10 +244,10 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
244
244
  };
245
245
  declare const roleSchema: z.ZodString;
246
246
  declare const invitationStatus: z.ZodDefault<z.ZodEnum<{
247
- canceled: "canceled";
248
247
  pending: "pending";
249
248
  accepted: "accepted";
250
249
  rejected: "rejected";
250
+ canceled: "canceled";
251
251
  }>>;
252
252
  declare const organizationSchema: z.ZodObject<{
253
253
  id: z.ZodDefault<z.ZodString>;
@@ -270,10 +270,10 @@ declare const invitationSchema: z.ZodObject<{
270
270
  email: z.ZodString;
271
271
  role: z.ZodString;
272
272
  status: z.ZodDefault<z.ZodEnum<{
273
- canceled: "canceled";
274
273
  pending: "pending";
275
274
  accepted: "accepted";
276
275
  rejected: "rejected";
276
+ canceled: "canceled";
277
277
  }>>;
278
278
  teamId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
279
279
  inviterId: z.ZodString;
@@ -4,6 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getSessionFromCtx } from "../../api/routes/session.mjs";
6
6
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
+ import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
7
8
  import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
8
9
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
9
10
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -54,6 +55,7 @@ const signInPhoneNumber = (opts) => createAuthEndpoint("/sign-in/phone-number",
54
55
  if (opts.phoneNumberValidator) {
55
56
  if (!await opts.phoneNumberValidator(ctx.body.phoneNumber)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER);
56
57
  }
58
+ assertPasswordNotTooLong(ctx, password);
57
59
  const user = await ctx.context.adapter.findOne({
58
60
  model: "user",
59
61
  where: [{
@@ -466,10 +468,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
466
468
  }]
467
469
  });
468
470
  if (!user) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR);
469
- const minLength = ctx.context.password.config.minPasswordLength;
470
- const maxLength = ctx.context.password.config.maxPasswordLength;
471
- if (ctx.body.newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
472
- if (ctx.body.newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
471
+ assertPasswordNotTooShort(ctx, ctx.body.newPassword);
472
+ assertPasswordNotTooLong(ctx, ctx.body.newPassword);
473
473
  const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
474
474
  if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
475
475
  userId: user.id,
@@ -266,14 +266,14 @@ declare const usernameImpl: <IncludeDisplayUsername extends boolean>(options: Us
266
266
  unique: true;
267
267
  returned: true;
268
268
  transform: {
269
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
269
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
270
270
  };
271
271
  };
272
272
  displayUsername: {
273
273
  type: "string";
274
274
  required: false;
275
275
  transform: {
276
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
276
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
277
277
  };
278
278
  };
279
279
  }, "username"> : {
@@ -284,14 +284,14 @@ declare const usernameImpl: <IncludeDisplayUsername extends boolean>(options: Us
284
284
  unique: true;
285
285
  returned: true;
286
286
  transform: {
287
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
287
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
288
288
  };
289
289
  };
290
290
  displayUsername: {
291
291
  type: "string";
292
292
  required: false;
293
293
  transform: {
294
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
294
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
295
295
  };
296
296
  };
297
297
  };
@@ -2,6 +2,7 @@ import { mergeSchema, parseUserOutput } from "../../db/schema.mjs";
2
2
  import { setSessionCookie } from "../../cookies/index.mjs";
3
3
  import { getSessionFromCtx } from "../../api/routes/session.mjs";
4
4
  import { createEmailVerificationToken } from "../../api/routes/email-verification.mjs";
5
+ import { assertPasswordNotTooLong } from "../../utils/password.mjs";
5
6
  import { PACKAGE_VERSION } from "../../version.mjs";
6
7
  import { USERNAME_ERROR_CODES } from "./error-codes.mjs";
7
8
  import { getSchema } from "./schema.mjs";
@@ -166,6 +167,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
166
167
  throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
167
168
  }
168
169
  if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
170
+ assertPasswordNotTooLong(ctx, ctx.body.password);
169
171
  const user = await ctx.context.adapter.findOne({
170
172
  model: "user",
171
173
  where: [{
@@ -14,14 +14,14 @@ declare const getSchema: <IncludeDisplayUsername extends boolean = true>(normali
14
14
  unique: true;
15
15
  returned: true;
16
16
  transform: {
17
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
17
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
18
18
  };
19
19
  };
20
20
  displayUsername: {
21
21
  type: "string";
22
22
  required: false;
23
23
  transform: {
24
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
24
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
25
25
  };
26
26
  };
27
27
  }, "username"> : {
@@ -32,14 +32,14 @@ declare const getSchema: <IncludeDisplayUsername extends boolean = true>(normali
32
32
  unique: true;
33
33
  returned: true;
34
34
  transform: {
35
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
35
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
36
36
  };
37
37
  };
38
38
  displayUsername: {
39
39
  type: "string";
40
40
  required: false;
41
41
  transform: {
42
- input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | Record<string, unknown> | unknown[] | null | undefined;
42
+ input(value: _$_better_auth_core_db0.DBPrimitive): string | number | boolean | Date | unknown[] | Record<string, unknown> | null | undefined;
43
43
  };
44
44
  };
45
45
  };
@@ -88,17 +88,17 @@ declare function getHttpTestInstance<O extends Partial<BetterAuthOptions>, C ext
88
88
  onResponse: ((context: _$_better_fetch_fetch0.ResponseContext) => Promise<Response | void | _$_better_fetch_fetch0.ResponseContext> | Response | _$_better_fetch_fetch0.ResponseContext | void) | undefined;
89
89
  };
90
90
  })[];
91
- priority?: RequestPriority | undefined;
92
91
  cache?: RequestCache | undefined;
93
92
  credentials?: RequestCredentials;
94
93
  integrity?: string | undefined;
95
94
  keepalive?: boolean | undefined;
96
95
  method: string;
97
96
  mode?: RequestMode | undefined;
97
+ priority?: RequestPriority | undefined;
98
98
  redirect?: RequestRedirect | undefined;
99
99
  referrer?: string | undefined;
100
100
  referrerPolicy?: ReferrerPolicy | undefined;
101
- signal?: (AbortSignal | null) | undefined;
101
+ signal?: AbortSignal | null;
102
102
  window?: null | undefined;
103
103
  onRetry?: ((response: _$_better_fetch_fetch0.ResponseContext) => Promise<void> | void) | undefined;
104
104
  hookOptions?: {
@@ -1,6 +1,19 @@
1
1
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
2
2
  //#region src/utils/password.ts
3
+ function assertPasswordNotTooShort(ctx, password) {
4
+ if (password.length < ctx.context.password.config.minPasswordLength) {
5
+ ctx.context.logger.warn("Password is too short");
6
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
7
+ }
8
+ }
9
+ function assertPasswordNotTooLong(ctx, password) {
10
+ if (password.length > ctx.context.password.config.maxPasswordLength) {
11
+ ctx.context.logger.warn("Password is too long");
12
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
13
+ }
14
+ }
3
15
  async function validatePassword(ctx, data) {
16
+ assertPasswordNotTooLong(ctx, data.password);
4
17
  const credentialAccount = await ctx.context.internalAdapter.findCredentialAccount(data.userId);
5
18
  const currentPassword = credentialAccount?.password;
6
19
  if (!credentialAccount || !currentPassword) return false;
@@ -10,9 +23,10 @@ async function validatePassword(ctx, data) {
10
23
  });
11
24
  }
12
25
  async function checkPassword(userId, c) {
26
+ const password = c.body.password;
27
+ if (typeof password === "string") assertPasswordNotTooLong(c, password);
13
28
  const credentialAccount = await c.context.internalAdapter.findCredentialAccount(userId);
14
29
  const currentPassword = credentialAccount?.password;
15
- const password = c.body.password;
16
30
  if (!credentialAccount || !currentPassword || !password) {
17
31
  if (password) await c.context.password.hash(password);
18
32
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
@@ -29,4 +43,4 @@ async function shouldRequirePassword(ctx, userId, allowPasswordless) {
29
43
  return Boolean(credentialAccount?.password);
30
44
  }
31
45
  //#endregion
32
- export { checkPassword, shouldRequirePassword, validatePassword };
46
+ export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, shouldRequirePassword, validatePassword };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -464,7 +464,7 @@
464
464
  },
465
465
  "dependencies": {
466
466
  "@better-auth/utils": "0.4.2",
467
- "@better-fetch/fetch": "1.3.1",
467
+ "@better-fetch/fetch": "1.3.2",
468
468
  "@noble/ciphers": "^2.2.0",
469
469
  "@noble/hashes": "^2.2.0",
470
470
  "better-call": "1.4.0",
@@ -473,13 +473,13 @@
473
473
  "kysely": "^0.28.17 || ^0.29.0",
474
474
  "nanostores": "^1.3.0",
475
475
  "zod": "^4.5.4",
476
- "@better-auth/core": "1.7.4",
477
- "@better-auth/drizzle-adapter": "1.7.4",
478
- "@better-auth/kysely-adapter": "1.7.4",
479
- "@better-auth/memory-adapter": "1.7.4",
480
- "@better-auth/mongo-adapter": "1.7.4",
481
- "@better-auth/prisma-adapter": "1.7.4",
482
- "@better-auth/telemetry": "1.7.4"
476
+ "@better-auth/core": "1.7.6",
477
+ "@better-auth/drizzle-adapter": "1.7.6",
478
+ "@better-auth/kysely-adapter": "1.7.6",
479
+ "@better-auth/memory-adapter": "1.7.6",
480
+ "@better-auth/mongo-adapter": "1.7.6",
481
+ "@better-auth/prisma-adapter": "1.7.6",
482
+ "@better-auth/telemetry": "1.7.6"
483
483
  },
484
484
  "devDependencies": {
485
485
  "@lynx-js/react": "^0.121.2",
@@ -494,6 +494,7 @@
494
494
  "@types/pg": "^8.20.0",
495
495
  "@types/react": "^19.2.17",
496
496
  "@types/react-dom": "^19.2.3",
497
+ "botid": "^1.5.10",
497
498
  "happy-dom": "^20.8.9",
498
499
  "msw": "^2.14.6",
499
500
  "next": "^16.3.4",
@@ -513,7 +514,6 @@
513
514
  "@sveltejs/kit": "^2.0.0",
514
515
  "@tanstack/react-start": "^1.0.0",
515
516
  "@tanstack/solid-start": "^1.0.0",
516
- "better-sqlite3": "^12.0.0",
517
517
  "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
518
518
  "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
519
519
  "mongodb": "^6.0.0 || ^7.0.0",
@@ -580,9 +580,6 @@
580
580
  "prisma": {
581
581
  "optional": true
582
582
  },
583
- "better-sqlite3": {
584
- "optional": true
585
- },
586
583
  "vitest": {
587
584
  "optional": true
588
585
  }