better-auth 1.7.5 → 1.7.7

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 (44) hide show
  1. package/dist/api/rate-limiter/index.mjs +4 -1
  2. package/dist/api/routes/callback.mjs +16 -64
  3. package/dist/api/routes/password.mjs +3 -5
  4. package/dist/api/routes/sign-in.mjs +4 -1
  5. package/dist/api/routes/sign-up.mjs +3 -10
  6. package/dist/api/routes/update-user.mjs +7 -20
  7. package/dist/client/config.mjs +3 -4
  8. package/dist/client/query-atom.mjs +13 -0
  9. package/dist/client/query.mjs +37 -27
  10. package/dist/client/react/react-store.mjs +2 -1
  11. package/dist/client/session-atom.mjs +2 -1
  12. package/dist/context/create-context.mjs +3 -2
  13. package/dist/crypto/purpose.mjs +19 -0
  14. package/dist/index.mjs +1 -0
  15. package/dist/oauth2/index.d.mts +2 -2
  16. package/dist/oauth2/index.mjs +2 -2
  17. package/dist/oauth2/link-account.d.mts +42 -14
  18. package/dist/oauth2/link-account.mjs +61 -1
  19. package/dist/package.mjs +1 -1
  20. package/dist/plugins/admin/admin.mjs +3 -1
  21. package/dist/plugins/admin/routes.mjs +4 -10
  22. package/dist/plugins/admin/types.d.mts +4 -1
  23. package/dist/plugins/captcha/constants.d.mts +1 -0
  24. package/dist/plugins/captcha/constants.mjs +2 -1
  25. package/dist/plugins/captcha/index.d.mts +2 -2
  26. package/dist/plugins/captcha/index.mjs +6 -0
  27. package/dist/plugins/captcha/types.d.mts +44 -2
  28. package/dist/plugins/captcha/utils.mjs +11 -1
  29. package/dist/plugins/captcha/verify-handlers/vercel-botid.mjs +27 -0
  30. package/dist/plugins/email-otp/routes.mjs +3 -4
  31. package/dist/plugins/index.d.mts +2 -2
  32. package/dist/plugins/magic-link/index.mjs +23 -4
  33. package/dist/plugins/oauth-proxy/index.d.mts +5 -6
  34. package/dist/plugins/oauth-proxy/index.mjs +30 -15
  35. package/dist/plugins/open-api/index.mjs +1 -0
  36. package/dist/plugins/organization/client.mjs +2 -1
  37. package/dist/plugins/phone-number/routes.mjs +5 -4
  38. package/dist/plugins/username/index.mjs +2 -0
  39. package/dist/state.mjs +9 -6
  40. package/dist/utils/hashing.mjs +1 -1
  41. package/dist/utils/index.mjs +5 -0
  42. package/dist/utils/middleware-response.mjs +4 -1
  43. package/dist/utils/password.mjs +16 -2
  44. package/package.json +9 -8
@@ -42,8 +42,10 @@ const admin = (options) => {
42
42
  });
43
43
  return;
44
44
  }
45
+ const bannedUserMessage = opts.bannedUserMessage;
46
+ const message = typeof bannedUserMessage === "function" ? await bannedUserMessage(user) : bannedUserMessage;
45
47
  throw APIError.from("FORBIDDEN", {
46
- message: opts.bannedUserMessage,
48
+ message,
47
49
  code: "BANNED_USER"
48
50
  });
49
51
  }
@@ -2,6 +2,7 @@ import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
2
2
  import { getDate } from "../../utils/date.mjs";
3
3
  import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
4
4
  import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
5
+ import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
5
6
  import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
6
7
  import { hasPermission } from "./has-permission.mjs";
7
8
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -190,6 +191,7 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
190
191
  }
191
192
  const email = ctx.body.email.toLowerCase();
192
193
  if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
194
+ if (ctx.body.password) assertPasswordNotTooLong(ctx, ctx.body.password);
193
195
  if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
194
196
  const user = await ctx.context.internalAdapter.createUser({
195
197
  ...userData,
@@ -823,16 +825,8 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
823
825
  permissions: { user: ["set-password"] }
824
826
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD);
825
827
  const { newPassword, userId } = ctx.body;
826
- const minPasswordLength = ctx.context.password.config.minPasswordLength;
827
- if (newPassword.length < minPasswordLength) {
828
- ctx.context.logger.warn("Password is too short");
829
- throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
830
- }
831
- const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
832
- if (newPassword.length > maxPasswordLength) {
833
- ctx.context.logger.warn("Password is too long");
834
- throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
835
- }
828
+ assertPasswordNotTooShort(ctx, newPassword);
829
+ assertPasswordNotTooLong(ctx, newPassword);
836
830
  const user = await ctx.context.internalAdapter.findUserById(userId);
837
831
  if (!user) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
838
832
  const hashedPassword = await ctx.context.password.hash(newPassword);
@@ -2,6 +2,7 @@ import { Session, User } from "../../types/models.mjs";
2
2
  import { InferOptionSchema } from "../../types/plugins.mjs";
3
3
  import { AccessControl, Role } from "../access/types.mjs";
4
4
  import { AdminSchema } from "./schema.mjs";
5
+ import { Awaitable } from "@better-auth/core";
5
6
 
6
7
  //#region src/plugins/admin/types.d.ts
7
8
  interface UserWithRole extends User {
@@ -70,8 +71,10 @@ interface AdminOptions {
70
71
  * Message to show when a user is banned
71
72
  *
72
73
  * By default, the message is "You have been banned from this application"
74
+ *
75
+ * Can also be a function that receives the banned user and returns a message.
73
76
  */
74
- bannedUserMessage?: string | undefined;
77
+ bannedUserMessage?: string | ((user: UserWithRole & Record<string, unknown>) => Awaitable<string>) | undefined;
75
78
  /**
76
79
  * Whether to allow impersonating other admins.
77
80
  *
@@ -4,6 +4,7 @@ declare const Providers: {
4
4
  readonly GOOGLE_RECAPTCHA: "google-recaptcha";
5
5
  readonly HCAPTCHA: "hcaptcha";
6
6
  readonly CAPTCHAFOX: "captchafox";
7
+ readonly VERCEL_BOTID: "vercel-botid";
7
8
  };
8
9
  //#endregion
9
10
  export { Providers };
@@ -15,7 +15,8 @@ const Providers = {
15
15
  CLOUDFLARE_TURNSTILE: "cloudflare-turnstile",
16
16
  GOOGLE_RECAPTCHA: "google-recaptcha",
17
17
  HCAPTCHA: "hcaptcha",
18
- CAPTCHAFOX: "captchafox"
18
+ CAPTCHAFOX: "captchafox",
19
+ VERCEL_BOTID: "vercel-botid"
19
20
  };
20
21
  const siteVerifyMap = {
21
22
  [Providers.CLOUDFLARE_TURNSTILE]: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
@@ -1,4 +1,4 @@
1
- import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./types.mjs";
1
+ import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./types.mjs";
2
2
  import * as _$_better_auth_core0 from "@better-auth/core";
3
3
  import * as _$_better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
4
4
 
@@ -24,4 +24,4 @@ declare const captcha: (options: CaptchaOptions) => {
24
24
  options: CaptchaOptions;
25
25
  };
26
26
  //#endregion
27
- export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, captcha };
27
+ export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions, captcha };
@@ -7,6 +7,7 @@ import { captchaFox } from "./verify-handlers/captchafox.mjs";
7
7
  import { cloudflareTurnstile } from "./verify-handlers/cloudflare-turnstile.mjs";
8
8
  import { googleRecaptcha } from "./verify-handlers/google-recaptcha.mjs";
9
9
  import { hCaptcha } from "./verify-handlers/h-captcha.mjs";
10
+ import { vercelBotId } from "./verify-handlers/vercel-botid.mjs";
10
11
  import { getIP } from "@better-auth/core/utils/ip";
11
12
  //#region src/plugins/captcha/index.ts
12
13
  const normalizeEndpointPath = (pathname, basePath) => {
@@ -26,6 +27,11 @@ const captcha = (options) => ({
26
27
  const basePath = ctx.options.basePath ?? "/api/auth";
27
28
  const pathname = normalizeEndpointPath(url.pathname, basePath);
28
29
  if (!endpoints.some((endpoint) => endpoint.includes("*") ? wildcardMatch(endpoint)(pathname) : endpoint === pathname)) return;
30
+ if (options.provider === Providers.VERCEL_BOTID) return await vercelBotId({
31
+ request,
32
+ checkBotId: options.checkBotId,
33
+ validateRequest: options.validateRequest
34
+ });
29
35
  if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
30
36
  const captchaResponse = request.headers.get("x-captcha-response");
31
37
  const remoteUserIP = getIP(request, ctx.options) ?? void 0;
@@ -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 };
@@ -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 };
@@ -23,6 +23,20 @@ const magicLinkVerifyQuerySchema = z.object({
23
23
  errorCallbackURL: z.string().meta({ description: "URL to redirect after error." }).optional(),
24
24
  newUserCallbackURL: z.string().meta({ description: "URL to redirect after new user signup. Only used if the user is registering for the first time." }).optional()
25
25
  });
26
+ const magicLinkRecordSchema = z.object({
27
+ type: z.literal("magic-link"),
28
+ email: z.email(),
29
+ name: z.string().optional()
30
+ }).strict();
31
+ function parseMagicLinkRecord(value) {
32
+ try {
33
+ const parsed = JSON.parse(value);
34
+ return magicLinkRecordSchema.safeParse(parsed);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ const magicLinkIdentifier = (storedToken) => `magic-link:${storedToken}`;
26
40
  const magicLink = (options) => {
27
41
  const opts = {
28
42
  storeToken: "plain",
@@ -75,8 +89,9 @@ const magicLink = (options) => {
75
89
  const verificationToken = opts?.generateToken ? await opts.generateToken(email) : generateRandomString(32, "a-z", "A-Z");
76
90
  const storedToken = await storeToken(ctx, verificationToken);
77
91
  await ctx.context.internalAdapter.createVerificationValue({
78
- identifier: storedToken,
92
+ identifier: magicLinkIdentifier(storedToken),
79
93
  value: JSON.stringify({
94
+ type: "magic-link",
80
95
  email,
81
96
  name: ctx.body.name
82
97
  }),
@@ -152,10 +167,14 @@ const magicLink = (options) => {
152
167
  throw ctx.redirect(errorCallbackURL.toString());
153
168
  }
154
169
  const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : callbackURL, ctx.context.baseURL).toString();
155
- const storedToken = await storeToken(ctx, token);
156
- const tokenValue = await ctx.context.internalAdapter.consumeVerificationValue(storedToken);
170
+ const identifier = magicLinkIdentifier(await storeToken(ctx, token));
171
+ const pendingValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
172
+ if (!pendingValue || !parseMagicLinkRecord(pendingValue.value)?.success) redirectWithError("INVALID_TOKEN");
173
+ const tokenValue = await ctx.context.internalAdapter.consumeVerificationValue(identifier);
157
174
  if (!tokenValue) redirectWithError("INVALID_TOKEN");
158
- const { email, name } = JSON.parse(tokenValue.value);
175
+ const record = parseMagicLinkRecord(tokenValue.value);
176
+ if (!record?.success) redirectWithError("INVALID_TOKEN");
177
+ const { email, name } = record.data;
159
178
  let isNewUser = false;
160
179
  let user = await ctx.context.internalAdapter.findUserByEmail(email).then((res) => res?.user);
161
180
  if (!user) if (!opts.disableSignUp) {
@@ -41,12 +41,11 @@ interface OAuthProxyOptions {
41
41
  * A dedicated secret used to encrypt and decrypt data passed between
42
42
  * servers during the OAuth proxy flow.
43
43
  *
44
- * When set, this secret is used **instead of** the global
45
- * `BETTER_AUTH_SECRET` for all OAuth proxy encryption operations.
46
- * This limits the blast radius if the secret is shared across
47
- * environments (production, preview, development): a leaked proxy
48
- * secret cannot be used to forge sessions or decrypt other data
49
- * protected by the main secret.
44
+ * When set, keys derived from this secret are used **instead of** keys
45
+ * derived from `BETTER_AUTH_SECRET` for OAuth proxy encryption.
46
+ * This limits exposure of data protected only by the main secret if
47
+ * the proxy secret is shared across environments (production, preview,
48
+ * development). A compromised proxy secret can still affect OAuth flows.
50
49
  *
51
50
  * All environments participating in the OAuth proxy flow must share
52
51
  * the same `secret` value.
@@ -7,9 +7,10 @@ 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
- import { parseGenericState } from "../../state.mjs";
12
+ import { derivePurposeKey } from "../../crypto/purpose.mjs";
13
+ import { getAuthStateVerificationIdentifier, parseGenericState } from "../../state.mjs";
13
14
  import { PACKAGE_VERSION } from "../../version.mjs";
14
15
  import { parseJSON } from "../../client/parser.mjs";
15
16
  import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
@@ -37,6 +38,7 @@ const passthroughPayloadSchema = z.looseObject({
37
38
  updatedAt: true
38
39
  }).shape),
39
40
  profile: z.record(z.string(), z.unknown()).optional(),
41
+ scopes: z.array(z.string()).optional(),
40
42
  state: z.string().min(1),
41
43
  callbackURL: z.string().min(1),
42
44
  newUserURL: z.string().optional(),
@@ -65,7 +67,7 @@ const oauthCallbackQuerySchema = z.object({
65
67
  });
66
68
  const oAuthProxy = (opts) => {
67
69
  const maxAge = opts?.maxAge ?? 60;
68
- const getEncryptionKey = (ctx) => opts?.secret ?? ctx.context.secretConfig;
70
+ const getEncryptionKey = (ctx, purpose) => derivePurposeKey(opts?.secret ?? ctx.context.secretConfig, purpose);
69
71
  const oauthProxyCompletion = createAuthEndpoint("/callback/:id/oauth-proxy", {
70
72
  method: "GET",
71
73
  operationId: "oauthProxyCompletion",
@@ -83,7 +85,7 @@ const oAuthProxy = (opts) => {
83
85
  let decryptedPayload;
84
86
  try {
85
87
  decryptedPayload = await symmetricDecrypt({
86
- key: getEncryptionKey(ctx),
88
+ key: getEncryptionKey(ctx, "oauth-proxy-profile"),
87
89
  data: encryptedProfile
88
90
  });
89
91
  } catch (e) {
@@ -107,7 +109,19 @@ const oAuthProxy = (opts) => {
107
109
  ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
108
110
  throw redirectOnError(ctx, errorURL, "payload_expired");
109
111
  }
110
- if (!await restoreOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
112
+ const stateData = await restoreOAuthProxyState(ctx, payload.state);
113
+ if (!stateData) throw redirectOnError(ctx, errorURL, "state_mismatch");
114
+ if (stateData.link) {
115
+ const linkResult = await linkOAuthAccount(ctx, {
116
+ link: stateData.link,
117
+ userInfo: payload.userInfo,
118
+ account: payload.account,
119
+ profile: payload.profile ?? {},
120
+ scopes: payload.scopes ?? payload.account.scope?.split(",")
121
+ });
122
+ if (!linkResult.linked) throw redirectOnError(ctx, errorURL, linkResult.error.code, linkResult.error.message);
123
+ throw ctx.redirect(payload.callbackURL);
124
+ }
111
125
  let result;
112
126
  try {
113
127
  result = await handleOAuthUserInfo(ctx, {
@@ -185,7 +199,7 @@ const oAuthProxy = (opts) => {
185
199
  hooks: {
186
200
  before: [{
187
201
  matcher(context) {
188
- return !!context.path?.startsWith("/sign-in/social");
202
+ return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
189
203
  },
190
204
  handler: createAuthMiddleware(async (ctx) => {
191
205
  if (checkSkipProxy(ctx, opts)) return;
@@ -212,7 +226,7 @@ const oAuthProxy = (opts) => {
212
226
  let statePackage;
213
227
  try {
214
228
  statePackage = parseJSON(await symmetricDecrypt({
215
- key: getEncryptionKey(ctx),
229
+ key: getEncryptionKey(ctx, "oauth-proxy-package"),
216
230
  data: state
217
231
  }));
218
232
  } catch {
@@ -232,7 +246,7 @@ const oAuthProxy = (opts) => {
232
246
  let stateData;
233
247
  try {
234
248
  stateData = parseJSON(await symmetricDecrypt({
235
- key: getEncryptionKey(ctx),
249
+ key: getEncryptionKey(ctx, "oauth-proxy-state"),
236
250
  data: statePackage.stateCookie
237
251
  }));
238
252
  } catch (e) {
@@ -307,6 +321,7 @@ const oAuthProxy = (opts) => {
307
321
  emailVerified: userInfo.emailVerified
308
322
  },
309
323
  profile: providerProfile,
324
+ scopes: tokens.scopes,
310
325
  account: {
311
326
  ...accountKey,
312
327
  accessToken: tokens.accessToken,
@@ -324,7 +339,7 @@ const oAuthProxy = (opts) => {
324
339
  timestamp: Date.now()
325
340
  };
326
341
  const encryptedPayload = await symmetricEncrypt({
327
- key: getEncryptionKey(ctx),
342
+ key: getEncryptionKey(ctx, "oauth-proxy-profile"),
328
343
  data: JSON.stringify(payload)
329
344
  });
330
345
  proxyCallbackURL.searchParams.set("profile", encryptedPayload);
@@ -333,14 +348,14 @@ const oAuthProxy = (opts) => {
333
348
  }],
334
349
  after: [{
335
350
  matcher(context) {
336
- return !!context.path?.startsWith("/sign-in/social");
351
+ return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
337
352
  },
338
353
  handler: createAuthMiddleware(async (ctx) => {
339
354
  if (checkSkipProxy(ctx, opts)) return;
340
355
  const signInResponse = ctx.context.returned;
341
356
  if (!signInResponse || typeof signInResponse !== "object" || !("url" in signInResponse)) return;
342
357
  const { url: providerURL } = signInResponse;
343
- if (typeof providerURL !== "string") return;
358
+ if (typeof providerURL !== "string" || providerURL.length === 0) return;
344
359
  const oauthURL = new URL(providerURL);
345
360
  const originalState = oauthURL.searchParams.get("state");
346
361
  if (!originalState) return;
@@ -352,11 +367,11 @@ const oAuthProxy = (opts) => {
352
367
  const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
353
368
  const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
354
369
  if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
355
- key: ctx.context.secretConfig,
370
+ key: derivePurposeKey(ctx.context.secretConfig, "oauth-state-cookie"),
356
371
  data: encryptedCookieValue
357
372
  });
358
373
  }
359
- } else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
374
+ } else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(originalState)))?.value;
360
375
  if (!plaintextState) {
361
376
  ctx.context.logger.warn("No OAuth state found for proxy");
362
377
  return;
@@ -364,13 +379,13 @@ const oAuthProxy = (opts) => {
364
379
  const statePackage = {
365
380
  state: originalState,
366
381
  stateCookie: await symmetricEncrypt({
367
- key: getEncryptionKey(ctx),
382
+ key: getEncryptionKey(ctx, "oauth-proxy-state"),
368
383
  data: plaintextState
369
384
  }),
370
385
  isOAuthProxy: true
371
386
  };
372
387
  const encryptedPackage = await symmetricEncrypt({
373
- key: getEncryptionKey(ctx),
388
+ key: getEncryptionKey(ctx, "oauth-proxy-package"),
374
389
  data: JSON.stringify(statePackage)
375
390
  });
376
391
  oauthURL.searchParams.set("state", encryptedPackage);
@@ -1,4 +1,5 @@
1
1
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
2
+ import "../../utils/index.mjs";
2
3
  import { APIError } from "../../api/index.mjs";
3
4
  import { PACKAGE_VERSION } from "../../version.mjs";
4
5
  import { generator } from "./generator.mjs";
@@ -1,5 +1,6 @@
1
1
  import { PACKAGE_VERSION } from "../../version.mjs";
2
2
  import { useAuthQuery } from "../../client/query.mjs";
3
+ import { matchesSessionSignal } from "../../client/config.mjs";
3
4
  import { adminAc, defaultRoles, memberAc, ownerAc } from "./access/statement.mjs";
4
5
  import { ORGANIZATION_ERROR_CODES } from "./error-codes.mjs";
5
6
  import { hasPermissionFn } from "./permission.mjs";
@@ -71,7 +72,7 @@ const organizationClient = (options) => {
71
72
  },
72
73
  {
73
74
  matcher(path) {
74
- return path === "/sign-out" || path.startsWith("/organization");
75
+ return matchesSessionSignal(path) || path.startsWith("/organization");
75
76
  },
76
77
  signal: "$activeOrgSignal"
77
78
  },
@@ -4,6 +4,8 @@ import { generateRandomString } from "../../crypto/random.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getSessionFromCtx } from "../../api/routes/session.mjs";
6
6
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
+ import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
8
+ import "../../utils/index.mjs";
7
9
  import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
8
10
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
9
11
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -54,6 +56,7 @@ const signInPhoneNumber = (opts) => createAuthEndpoint("/sign-in/phone-number",
54
56
  if (opts.phoneNumberValidator) {
55
57
  if (!await opts.phoneNumberValidator(ctx.body.phoneNumber)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER);
56
58
  }
59
+ assertPasswordNotTooLong(ctx, password);
57
60
  const user = await ctx.context.adapter.findOne({
58
61
  model: "user",
59
62
  where: [{
@@ -466,10 +469,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
466
469
  }]
467
470
  });
468
471
  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);
472
+ assertPasswordNotTooShort(ctx, ctx.body.newPassword);
473
+ assertPasswordNotTooLong(ctx, ctx.body.newPassword);
473
474
  const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
474
475
  if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
475
476
  userId: user.id,
@@ -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: [{
package/dist/state.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { generateRandomString } from "./crypto/random.mjs";
2
2
  import { symmetricDecrypt, symmetricEncrypt } from "./crypto/index.mjs";
3
3
  import { expireCookie } from "./cookies/index.mjs";
4
+ import { derivePurposeKey } from "./crypto/purpose.mjs";
4
5
  import { BetterAuthError } from "@better-auth/core/error";
5
6
  import * as z from "zod";
6
7
  //#region src/state.ts
@@ -32,6 +33,8 @@ const stateDataSchema = z.looseObject({
32
33
  */
33
34
  serverContext: z.record(z.string(), z.unknown()).optional()
34
35
  });
36
+ /** Verification key for database-backed OAuth and SAML relay state. */
37
+ const getAuthStateVerificationIdentifier = (state) => `auth-state:${state}`;
35
38
  const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
36
39
  var StateError = class extends BetterAuthError {
37
40
  code;
@@ -59,7 +62,7 @@ async function generateGenericState(c, stateData, settings) {
59
62
  oauthState: state
60
63
  };
61
64
  const encryptedData = await symmetricEncrypt({
62
- key: c.context.secretConfig,
65
+ key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
63
66
  data: JSON.stringify(payload)
64
67
  });
65
68
  const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 });
@@ -78,7 +81,7 @@ async function generateGenericState(c, stateData, settings) {
78
81
  ...stateData,
79
82
  oauthState: state
80
83
  }),
81
- identifier: state,
84
+ identifier: getAuthStateVerificationIdentifier(state),
82
85
  expiresAt
83
86
  })) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" });
84
87
  return {
@@ -99,7 +102,7 @@ async function parseGenericState(c, state, settings) {
99
102
  });
100
103
  try {
101
104
  const decryptedData = await symmetricDecrypt({
102
- key: c.context.secretConfig,
105
+ key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
103
106
  data: encryptedData
104
107
  });
105
108
  parsedData = stateDataSchema.parse(JSON.parse(decryptedData));
@@ -117,7 +120,7 @@ async function parseGenericState(c, state, settings) {
117
120
  });
118
121
  expireCookie(c, stateCookie);
119
122
  } else {
120
- const data = await c.context.internalAdapter.findVerificationValue(state);
123
+ const data = await c.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(state));
121
124
  if (!data) throw new StateError("State mismatch: verification not found", {
122
125
  code: "state_mismatch",
123
126
  details: { state }
@@ -136,7 +139,7 @@ async function parseGenericState(c, state, settings) {
136
139
  errorURL: parsedData.errorURL
137
140
  });
138
141
  expireCookie(c, stateCookie);
139
- await c.context.internalAdapter.deleteVerificationByIdentifier(state);
142
+ await c.context.internalAdapter.deleteVerificationByIdentifier(getAuthStateVerificationIdentifier(state));
140
143
  }
141
144
  if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
142
145
  code: "state_mismatch",
@@ -146,4 +149,4 @@ async function parseGenericState(c, state, settings) {
146
149
  return parsedData;
147
150
  }
148
151
  //#endregion
149
- export { INTERNAL_STATE_KEYS, StateError, generateGenericState, parseGenericState };
152
+ export { INTERNAL_STATE_KEYS, StateError, generateGenericState, getAuthStateVerificationIdentifier, parseGenericState };
@@ -1,5 +1,5 @@
1
- import { keccak_256 } from "@noble/hashes/sha3.js";
2
1
  import { utf8ToBytes } from "@noble/hashes/utils.js";
2
+ import { keccak_256 } from "@noble/hashes/sha3.js";
3
3
  //#region src/utils/hashing.ts
4
4
  /**
5
5
  * TS implementation of ERC-55 ("Mixed-case checksum address encoding") using @noble/hashes
@@ -0,0 +1,5 @@
1
+ import "./url.mjs";
2
+ import "../state.mjs";
3
+ import "../oauth2/state.mjs";
4
+ import "./hide-metadata.mjs";
5
+ export {};