better-auth 1.6.8 → 1.6.10

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 (40) hide show
  1. package/README.md +14 -15
  2. package/dist/api/index.d.mts +0 -2
  3. package/dist/api/routes/callback.mjs +6 -5
  4. package/dist/api/routes/email-verification.mjs +2 -2
  5. package/dist/api/routes/error.mjs +1 -1
  6. package/dist/api/routes/sign-in.d.mts +0 -1
  7. package/dist/api/routes/sign-in.mjs +4 -11
  8. package/dist/api/routes/sign-up.mjs +1 -1
  9. package/dist/api/to-auth-endpoints.mjs +7 -1
  10. package/dist/client/plugins/index.d.mts +2 -1
  11. package/dist/cookies/cookie-utils.d.mts +10 -1
  12. package/dist/cookies/cookie-utils.mjs +19 -1
  13. package/dist/cookies/index.d.mts +2 -2
  14. package/dist/cookies/index.mjs +2 -2
  15. package/dist/db/internal-adapter.mjs +14 -6
  16. package/dist/integrations/cookie-plugin-guard.mjs +18 -0
  17. package/dist/integrations/next-js.mjs +6 -0
  18. package/dist/integrations/svelte-kit.mjs +6 -0
  19. package/dist/integrations/tanstack-start-solid.mjs +6 -0
  20. package/dist/integrations/tanstack-start.mjs +6 -0
  21. package/dist/package.mjs +1 -1
  22. package/dist/plugins/admin/client.d.mts +5 -0
  23. package/dist/plugins/admin/client.mjs +5 -0
  24. package/dist/plugins/bearer/index.mjs +2 -4
  25. package/dist/plugins/captcha/index.mjs +14 -1
  26. package/dist/plugins/email-otp/routes.mjs +3 -3
  27. package/dist/plugins/generic-oauth/routes.mjs +3 -3
  28. package/dist/plugins/one-tap/index.mjs +3 -2
  29. package/dist/plugins/organization/client.mjs +1 -1
  30. package/dist/plugins/organization/routes/crud-invites.d.mts +8 -1
  31. package/dist/plugins/organization/routes/crud-invites.mjs +1 -1
  32. package/dist/plugins/organization/routes/crud-team.mjs +7 -2
  33. package/dist/plugins/siwe/client.d.mts +4 -0
  34. package/dist/plugins/siwe/client.mjs +5 -1
  35. package/dist/plugins/siwe/index.d.mts +13 -2
  36. package/dist/plugins/siwe/index.mjs +179 -165
  37. package/dist/plugins/username/index.d.mts +11 -0
  38. package/dist/plugins/username/index.mjs +18 -2
  39. package/dist/test-utils/test-instance.d.mts +0 -6
  40. package/package.json +9 -9
package/README.md CHANGED
@@ -1,32 +1,31 @@
1
- <p align="center">
1
+ <div align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/better-auth/better-auth/blob/main/banner-dark.png?raw=true" />
4
- <source media="(prefers-color-scheme: light)" srcset="https://github.com/better-auth/better-auth/blob/main/banner.png?raw=true" />
5
- <img alt="Better Auth Logo" src="https://github.com/better-auth/better-auth/blob/main/banner.png?raw=true" />
3
+ <source srcset="https://github.com/better-auth/better-auth/blob/main/banner-dark.png?raw=true" media="(prefers-color-scheme: dark)"/>
4
+ <source srcset="https://github.com/better-auth/better-auth/blob/main/banner-light.png?raw=true" media="(prefers-color-scheme: light)"/>
5
+ <img src="https://github.com/better-auth/better-auth/blob/main/banner-light.png?raw=true" alt="Better Auth Logo"/>
6
6
  </picture>
7
7
 
8
- <h1 align="center">
9
- Better Auth
10
- </h1>
8
+ [![npm](https://img.shields.io/npm/dm/better-auth?style=flat&colorA=000000&colorB=000000)](https://npm.chart.dev/better-auth?primary=neutral&gray=neutral&theme=dark)
9
+ [![npm version](https://img.shields.io/npm/v/better-auth.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/better-auth)
10
+ [![GitHub stars](https://img.shields.io/github/stars/better-auth/better-auth?style=flat&colorA=000000&colorB=000000)](https://github.com/better-auth/better-auth/stargazers)
11
11
 
12
- <p align="center">
13
- The most comprehensive authentication framework for TypeScript
14
- <br />
15
- <a href="https://better-auth.com"><strong>Learn more »</strong></a>
16
- <br />
17
- <br />
12
+ <p>
18
13
  <a href="https://discord.gg/better-auth">Discord</a>
19
14
  ·
20
15
  <a href="https://better-auth.com">Website</a>
21
16
  ·
22
17
  <a href="https://github.com/better-auth/better-auth/issues">Issues</a>
23
18
  </p>
24
- </p>
19
+ </div>
20
+
21
+ ## Better Auth
22
+
23
+ Better Auth is a framework-agnostic authentication (and authorization) framework for TypeScript. It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities with minimal code in a short amount of time. Whether you need 2FA, multi-tenant support, or other complex features, it lets you focus on building your actual application instead of reinventing the wheel.
25
24
 
26
25
  ## Getting Started
27
26
 
28
27
  ```bash
29
- pnpm install better-auth
28
+ npm i better-auth
30
29
  ```
31
30
 
32
31
  Read the [Installation Guide](https://better-auth.com/docs/installation) to
@@ -178,7 +178,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
178
178
  };
179
179
  redirect: {
180
180
  type: string;
181
- enum: boolean[];
182
181
  };
183
182
  };
184
183
  required: string[];
@@ -2167,7 +2166,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2167
2166
  };
2168
2167
  redirect: {
2169
2168
  type: string;
2170
- enum: boolean[];
2171
2169
  };
2172
2170
  };
2173
2171
  required: string[];
@@ -91,10 +91,11 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
91
91
  ...tokens,
92
92
  user: parsedUserData ?? void 0
93
93
  }).then((res) => res?.user);
94
- if (!userInfo) {
94
+ if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
95
95
  c.context.logger.error("Unable to get user info");
96
96
  return redirectOnError("unable_to_get_user_info");
97
97
  }
98
+ const providerAccountId = String(userInfo.id);
98
99
  if (!callbackURL) {
99
100
  c.context.logger.error("No callback URL found");
100
101
  throw redirectOnError("no_callback_url");
@@ -105,7 +106,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
105
106
  return redirectOnError("unable_to_link_account");
106
107
  }
107
108
  if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
108
- const existingAccount = await c.context.internalAdapter.findAccountByProviderId(String(userInfo.id), provider.id);
109
+ const existingAccount = await c.context.internalAdapter.findAccountByProviderId(providerAccountId, provider.id);
109
110
  if (existingAccount) {
110
111
  if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
111
112
  const updateData = Object.fromEntries(Object.entries({
@@ -120,7 +121,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
120
121
  } else if (!await c.context.internalAdapter.createAccount({
121
122
  userId: link.userId,
122
123
  providerId: provider.id,
123
- accountId: String(userInfo.id),
124
+ accountId: providerAccountId,
124
125
  ...tokens,
125
126
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
126
127
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
@@ -140,14 +141,14 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
140
141
  }
141
142
  const accountData = {
142
143
  providerId: provider.id,
143
- accountId: String(userInfo.id),
144
+ accountId: providerAccountId,
144
145
  ...tokens,
145
146
  scope: tokens.scopes?.join(",")
146
147
  };
147
148
  const result = await handleOAuthUserInfo(c, {
148
149
  userInfo: {
149
150
  ...userInfo,
150
- id: String(userInfo.id),
151
+ id: providerAccountId,
151
152
  email: userInfo.email,
152
153
  name: userInfo.name || ""
153
154
  },
@@ -12,7 +12,7 @@ import { JWTExpired } from "jose/errors";
12
12
  async function createEmailVerificationToken(secret, email, updateTo, expiresIn = 3600, extraPayload) {
13
13
  return await signJWT({
14
14
  email: email.toLowerCase(),
15
- updateTo,
15
+ updateTo: updateTo?.toLowerCase(),
16
16
  ...extraPayload
17
17
  }, secret, expiresIn);
18
18
  }
@@ -101,7 +101,7 @@ const sendVerificationEmail = createAuthEndpoint("/send-verification-email", {
101
101
  await sendVerificationEmailFn(ctx, user.user);
102
102
  return ctx.json({ status: true });
103
103
  }
104
- if (session?.user.email !== email) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_MISMATCH);
104
+ if (session?.user.email.toLowerCase() !== email.toLowerCase()) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_MISMATCH);
105
105
  if (session?.user.emailVerified) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_ALREADY_VERIFIED);
106
106
  await sendVerificationEmailFn(ctx, session.user);
107
107
  return ctx.json({ status: true });
@@ -282,7 +282,7 @@ ${custom?.disableBackgroundGrid ? "" : `
282
282
  text-wrap: pretty;
283
283
  "
284
284
  >
285
- ${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find more information about the error <a href='https://better-auth.com/docs/reference/errors/${encodeURIComponent(code)}' target='_blank' rel="noopener noreferrer" style='color: var(--foreground); text-decoration: underline;'>here</a>.` : description}
285
+ ${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find <a href='https://better-auth.com/docs/reference/errors/${encodeURIComponent(code)}' target='_blank' rel="noopener noreferrer" style='color: var(--foreground); text-decoration: underline;'>more information about the error</a>.` : description}
286
286
  </p>
287
287
  </div>
288
288
 
@@ -91,7 +91,6 @@ declare const signInSocial: <O extends BetterAuthOptions>() => better_call0.Stri
91
91
  };
92
92
  redirect: {
93
93
  type: string;
94
- enum: boolean[];
95
94
  };
96
95
  };
97
96
  required: string[];
@@ -49,10 +49,10 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
49
49
  description: "Sign in with a social provider",
50
50
  operationId: "socialSignIn",
51
51
  responses: { "200": {
52
- description: "Success - Returns either session details or redirect URL",
52
+ description: "Success - Returns session details (idToken branch) or an authorize URL (redirect branch)",
53
53
  content: { "application/json": { schema: {
54
54
  type: "object",
55
- description: "Session response when idToken is provided",
55
+ description: "Returns session details when idToken is provided, or an authorize URL otherwise",
56
56
  properties: {
57
57
  token: { type: "string" },
58
58
  user: {
@@ -60,16 +60,9 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
60
60
  $ref: "#/components/schemas/User"
61
61
  },
62
62
  url: { type: "string" },
63
- redirect: {
64
- type: "boolean",
65
- enum: [false]
66
- }
63
+ redirect: { type: "boolean" }
67
64
  },
68
- required: [
69
- "redirect",
70
- "token",
71
- "user"
72
- ]
65
+ required: ["redirect"]
73
66
  } } }
74
67
  } }
75
68
  }
@@ -157,7 +157,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
157
157
  ctx.context.logger.error("Password is too long");
158
158
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
159
159
  }
160
- const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification;
160
+ const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification || ctx.context.options.emailAndPassword.autoSignIn === false;
161
161
  const shouldSkipAutoSignIn = ctx.context.options.emailAndPassword.autoSignIn === false || shouldReturnGenericDuplicateResponse;
162
162
  const additionalUserFields = parseUserInput(ctx.context.options, rest, "create");
163
163
  const normalizedEmail = email.toLowerCase();
@@ -117,7 +117,13 @@ function toAuthEndpoints(endpoints, ctx) {
117
117
  * headers override while cookies accumulate.
118
118
  */
119
119
  const ctxHeaders = e[kAPIErrorHeaderSymbol];
120
- const errHeaders = e.headers ? new Headers(e.headers) : null;
120
+ /**
121
+ * `c.redirect()` (and similar APIError throws) reuse
122
+ * `ctx.responseHeaders` as `e.headers`, so when both sources
123
+ * reference the same Headers, iterating both duplicates every
124
+ * `set-cookie`. Skip the `errHeaders` copy in that case.
125
+ */
126
+ const errHeaders = e.headers && e.headers !== ctxHeaders ? new Headers(e.headers) : null;
121
127
  let headers = null;
122
128
  if (ctxHeaders || errHeaders) {
123
129
  headers = new Headers();
@@ -1,3 +1,4 @@
1
+ import { FieldAttributeToObject, RemoveFieldsWithReturnedFalse } from "../../db/field.mjs";
1
2
  import { ExtractPluginField, HasRequiredKeys, InferPluginFieldFromTuple, IsAny, OverrideMerge, Prettify, PrettifyDeep, RequiredKeysOf, StripEmptyObjects, UnionToIntersection } from "../../types/helper.mjs";
2
3
  import { InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferTeam, Invitation, InvitationInput, InvitationStatus, Member, MemberInput, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, Team, TeamInput, TeamMember, TeamMemberInput, defaultRolesSchema, invitationSchema, invitationStatus, memberSchema, organizationRoleSchema, organizationSchema, roleSchema, teamMemberSchema, teamSchema } from "../../plugins/organization/schema.mjs";
3
4
  import { AdminOptions, InferAdminRolesFromOption, SessionWithImpersonatedBy, UserWithRole } from "../../plugins/admin/types.mjs";
@@ -52,4 +53,4 @@ import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
52
53
  import { siweClient } from "../../plugins/siwe/client.mjs";
53
54
  import { usernameClient } from "../../plugins/username/client.mjs";
54
55
  import { InferServerPlugin } from "./infer-plugin.mjs";
55
- export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
56
+ export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, type FieldAttributeToObject, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, type RemoveFieldsWithReturnedFalse, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
@@ -33,8 +33,17 @@ declare function stripSecureCookiePrefix(cookieName: string): string;
33
33
  declare function splitSetCookieHeader(setCookie: string): string[];
34
34
  declare function parseSetCookieHeader(setCookie: string): Map<string, CookieAttributes>;
35
35
  declare function toCookieOptions(attributes: CookieAttributes): ParsedCookieOptions;
36
+ /**
37
+ * Add or replace a cookie in the request `Cookie` header.
38
+ *
39
+ * Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
40
+ * joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
41
+ * breaks downstream cookie parsing. This builds the header value via
42
+ * parse-mutate-serialize.
43
+ */
44
+ declare function setRequestCookie(headers: Headers, name: string, value: string): void;
36
45
  declare function setCookieToHeader(headers: Headers): (context: {
37
46
  response: Response;
38
47
  }) => void;
39
48
  //#endregion
40
- export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
49
+ export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -102,6 +102,24 @@ function toCookieOptions(attributes) {
102
102
  partitioned: attributes.partitioned
103
103
  };
104
104
  }
105
+ /**
106
+ * Add or replace a cookie in the request `Cookie` header.
107
+ *
108
+ * Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
109
+ * joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
110
+ * breaks downstream cookie parsing. This builds the header value via
111
+ * parse-mutate-serialize.
112
+ */
113
+ function setRequestCookie(headers, name, value) {
114
+ const cookieMap = /* @__PURE__ */ new Map();
115
+ for (const pair of (headers.get("cookie") || "").split(";")) {
116
+ const trimmed = pair.trim();
117
+ const eq = trimmed.indexOf("=");
118
+ if (eq > 0) cookieMap.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
119
+ }
120
+ cookieMap.set(name, value);
121
+ headers.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${v}`).join("; "));
122
+ }
105
123
  function setCookieToHeader(headers) {
106
124
  return (context) => {
107
125
  const setCookieHeader = context.response.headers.get("set-cookie");
@@ -119,4 +137,4 @@ function setCookieToHeader(headers) {
119
137
  };
120
138
  }
121
139
  //#endregion
122
- export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
140
+ export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -1,5 +1,5 @@
1
1
  import { Session, User } from "../types/models.mjs";
2
- import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
2
+ import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
3
3
  import { createSessionStore, getAccountCookie, getChunkedCookie } from "./session-store.mjs";
4
4
  import { BetterAuthCookie, BetterAuthCookies, BetterAuthOptions, GenericEndpointContext } from "@better-auth/core";
5
5
  import * as better_call0 from "better-call";
@@ -116,4 +116,4 @@ declare const getCookieCache: <S extends {
116
116
  version?: string | ((session: Session & Record<string, any>, user: User & Record<string, any>) => string) | ((session: Session & Record<string, any>, user: User & Record<string, any>) => Promise<string>);
117
117
  } | undefined) => Promise<S | null>;
118
118
  //#endregion
119
- export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
119
+ export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -4,7 +4,7 @@ import { parseUserOutput } from "../db/schema.mjs";
4
4
  import { getDate } from "../utils/date.mjs";
5
5
  import { isPromise } from "../utils/is-promise.mjs";
6
6
  import { sec } from "../utils/time.mjs";
7
- import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
7
+ import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
8
8
  import { createAccountStore, createSessionStore, getAccountCookie, getChunkedCookie, setAccountCookie } from "./session-store.mjs";
9
9
  import { env, isProduction } from "@better-auth/core/env";
10
10
  import { BetterAuthError } from "@better-auth/core/error";
@@ -258,4 +258,4 @@ const getCookieCache = async (request, config) => {
258
258
  return null;
259
259
  };
260
260
  //#endregion
261
- export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
261
+ export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -41,7 +41,8 @@ const createInternalAdapter = (adapter, ctx) => {
41
41
  const createdUser = await createWithHooks({
42
42
  createdAt: /* @__PURE__ */ new Date(),
43
43
  updatedAt: /* @__PURE__ */ new Date(),
44
- ...user
44
+ ...user,
45
+ email: user.email?.toLowerCase()
45
46
  }, "user", void 0);
46
47
  return {
47
48
  user: createdUser,
@@ -364,10 +365,10 @@ const createInternalAdapter = (adapter, ctx) => {
364
365
  value: userId
365
366
  }], "account", void 0);
366
367
  },
367
- deleteAccount: async (accountId) => {
368
+ deleteAccount: async (id) => {
368
369
  await deleteWithHooks([{
369
370
  field: "id",
370
- value: accountId
371
+ value: id
371
372
  }], "account", void 0);
372
373
  },
373
374
  deleteSessions: async (userIdOrSessionTokens) => {
@@ -475,7 +476,10 @@ const createInternalAdapter = (adapter, ctx) => {
475
476
  }, "account", void 0);
476
477
  },
477
478
  updateUser: async (userId, data) => {
478
- const user = await updateWithHooks(data, [{
479
+ const user = await updateWithHooks({
480
+ ...data,
481
+ ...data.email ? { email: data.email.toLowerCase() } : {}
482
+ }, [{
479
483
  field: "id",
480
484
  value: userId
481
485
  }], "user", void 0);
@@ -483,7 +487,10 @@ const createInternalAdapter = (adapter, ctx) => {
483
487
  return user;
484
488
  },
485
489
  updateUserByEmail: async (email, data) => {
486
- const user = await updateWithHooks(data, [{
490
+ const user = await updateWithHooks({
491
+ ...data,
492
+ ...data.email ? { email: data.email.toLowerCase() } : {}
493
+ }, [{
487
494
  field: "email",
488
495
  value: email.toLowerCase()
489
496
  }], "user", void 0);
@@ -634,7 +641,8 @@ const createInternalAdapter = (adapter, ctx) => {
634
641
  value: storedIdentifier
635
642
  }], "verification", void 0);
636
643
  return data;
637
- }
644
+ },
645
+ refreshUserSessions
638
646
  };
639
647
  };
640
648
  //#endregion
@@ -0,0 +1,18 @@
1
+ //#region src/integrations/cookie-plugin-guard.ts
2
+ /**
3
+ * Warns when a cookie integration plugin is not effectively last.
4
+ *
5
+ * A plugin is considered misordered when there is at least one other plugin
6
+ * after it in the `plugins` array that declares `hooks.after`, since those
7
+ * hooks can set cookies that this integration will not see.
8
+ */
9
+ function warnIfCookiePluginNotLast(ctx, pluginId) {
10
+ const plugins = ctx.options.plugins || [];
11
+ if (plugins.length === 0) return;
12
+ const index = plugins.findIndex((p) => p.id === pluginId);
13
+ if (index === -1) return;
14
+ if (!plugins.slice(index + 1).some((p) => p.hooks && Array.isArray(p.hooks.after) && p.hooks.after.length > 0)) return;
15
+ ctx.logger.warn(`[better-auth] Cookie integration plugin "${pluginId}" should be placed last in the plugins array. Plugins with \`hooks.after\` running after it may set cookies that are not forwarded to the framework cookie store. Move your cookie integration plugin to the end of the \`plugins\` array to avoid missing \`Set-Cookie\` headers.`);
16
+ }
17
+ //#endregion
18
+ export { warnIfCookiePluginNotLast };
@@ -1,6 +1,7 @@
1
1
  import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { setShouldSkipSessionRefresh } from "../api/state/should-session-refresh.mjs";
3
3
  import { PACKAGE_VERSION } from "../version.mjs";
4
+ import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
4
5
  import { createAuthMiddleware } from "@better-auth/core/api";
5
6
  //#region src/integrations/next-js.ts
6
7
  function toNextJsHandler(auth) {
@@ -16,6 +17,7 @@ function toNextJsHandler(auth) {
16
17
  };
17
18
  }
18
19
  const nextCookies = () => {
20
+ let hasWarned = false;
19
21
  return {
20
22
  id: "next-cookies",
21
23
  version: PACKAGE_VERSION,
@@ -25,6 +27,10 @@ const nextCookies = () => {
25
27
  return ctx.path === "/get-session";
26
28
  },
27
29
  handler: createAuthMiddleware(async (ctx) => {
30
+ if (!hasWarned) {
31
+ warnIfCookiePluginNotLast(ctx.context, "next-cookies");
32
+ hasWarned = true;
33
+ }
28
34
  if ("_flag" in ctx && ctx._flag === "router") return;
29
35
  let headersStore;
30
36
  try {
@@ -1,5 +1,6 @@
1
1
  import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
+ import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
3
4
  import { createAuthMiddleware } from "@better-auth/core/api";
4
5
  //#region src/integrations/svelte-kit.ts
5
6
  const toSvelteKitHandler = (auth) => {
@@ -20,6 +21,7 @@ function isAuthPath(url, options) {
20
21
  return true;
21
22
  }
22
23
  const sveltekitCookies = (getRequestEvent) => {
24
+ let hasWarned = false;
23
25
  return {
24
26
  id: "sveltekit-cookies",
25
27
  version: PACKAGE_VERSION,
@@ -28,6 +30,10 @@ const sveltekitCookies = (getRequestEvent) => {
28
30
  return true;
29
31
  },
30
32
  handler: createAuthMiddleware(async (ctx) => {
33
+ if (!hasWarned) {
34
+ warnIfCookiePluginNotLast(ctx.context, "sveltekit-cookies");
35
+ hasWarned = true;
36
+ }
31
37
  const returned = ctx.context.responseHeaders;
32
38
  if ("_flag" in ctx && ctx._flag === "router") return;
33
39
  if (returned instanceof Headers) {
@@ -1,5 +1,6 @@
1
1
  import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
+ import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
3
4
  import { createAuthMiddleware } from "@better-auth/core/api";
4
5
  //#region src/integrations/tanstack-start-solid.ts
5
6
  /**
@@ -20,6 +21,7 @@ import { createAuthMiddleware } from "@better-auth/core/api";
20
21
  * ```
21
22
  */
22
23
  const tanstackStartCookies = () => {
24
+ let hasWarned = false;
23
25
  return {
24
26
  id: "tanstack-start-cookies-solid",
25
27
  version: PACKAGE_VERSION,
@@ -28,6 +30,10 @@ const tanstackStartCookies = () => {
28
30
  return true;
29
31
  },
30
32
  handler: createAuthMiddleware(async (ctx) => {
33
+ if (!hasWarned) {
34
+ warnIfCookiePluginNotLast(ctx.context, "tanstack-start-cookies-solid");
35
+ hasWarned = true;
36
+ }
31
37
  const returned = ctx.context.responseHeaders;
32
38
  if ("_flag" in ctx && ctx._flag === "router") return;
33
39
  if (returned instanceof Headers) {
@@ -1,5 +1,6 @@
1
1
  import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
+ import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
3
4
  import { createAuthMiddleware } from "@better-auth/core/api";
4
5
  //#region src/integrations/tanstack-start.ts
5
6
  /**
@@ -20,6 +21,7 @@ import { createAuthMiddleware } from "@better-auth/core/api";
20
21
  * ```
21
22
  */
22
23
  const tanstackStartCookies = () => {
24
+ let hasWarned = false;
23
25
  return {
24
26
  id: "tanstack-start-cookies",
25
27
  version: PACKAGE_VERSION,
@@ -28,6 +30,10 @@ const tanstackStartCookies = () => {
28
30
  return true;
29
31
  },
30
32
  handler: createAuthMiddleware(async (ctx) => {
33
+ if (!hasWarned) {
34
+ warnIfCookiePluginNotLast(ctx.context, "tanstack-start-cookies");
35
+ hasWarned = true;
36
+ }
31
37
  const returned = ctx.context.responseHeaders;
32
38
  if ("_flag" in ctx && ctx._flag === "router") return;
33
39
  if (returned instanceof Headers) {
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.8";
2
+ var version = "1.6.10";
3
3
  //#endregion
4
4
  export { version };
@@ -44,8 +44,13 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
44
44
  };
45
45
  pathMethods: {
46
46
  "/admin/list-users": "GET";
47
+ "/admin/impersonate-user": "POST";
47
48
  "/admin/stop-impersonating": "POST";
48
49
  };
50
+ atomListeners: {
51
+ matcher: (path: string) => path is "/admin/impersonate-user" | "/admin/stop-impersonating";
52
+ signal: "$sessionSignal";
53
+ }[];
49
54
  $ERROR_CODES: {
50
55
  USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: _better_auth_core_utils_error_codes0.RawError<"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL">;
51
56
  FAILED_TO_CREATE_USER: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_CREATE_USER">;
@@ -25,8 +25,13 @@ const adminClient = (options) => {
25
25
  } } }),
26
26
  pathMethods: {
27
27
  "/admin/list-users": "GET",
28
+ "/admin/impersonate-user": "POST",
28
29
  "/admin/stop-impersonating": "POST"
29
30
  },
31
+ atomListeners: [{
32
+ matcher: (path) => path === "/admin/impersonate-user" || path === "/admin/stop-impersonating",
33
+ signal: "$sessionSignal"
34
+ }],
30
35
  $ERROR_CODES: ADMIN_ERROR_CODES
31
36
  };
32
37
  };
@@ -1,4 +1,4 @@
1
- import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, setRequestCookie } from "../../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../../version.mjs";
3
3
  import { serializeSignedCookie } from "better-call";
4
4
  import { createAuthMiddleware } from "@better-auth/core/api";
@@ -48,9 +48,7 @@ const bearer = (options) => {
48
48
  }
49
49
  const existingHeaders = c.request?.headers || c.headers;
50
50
  const headers = new Headers({ ...Object.fromEntries(existingHeaders?.entries()) });
51
- const existingCookie = headers.get("cookie");
52
- const newCookie = `${c.context.authCookies.sessionToken.name}=${signedToken}`;
53
- headers.set("cookie", existingCookie ? `${existingCookie}; ${newCookie}` : newCookie);
51
+ setRequestCookie(headers, c.context.authCookies.sessionToken.name, signedToken);
54
52
  return { context: { headers } };
55
53
  })
56
54
  }],
@@ -14,7 +14,20 @@ const captcha = (options) => ({
14
14
  $ERROR_CODES: EXTERNAL_ERROR_CODES,
15
15
  onRequest: async (request, ctx) => {
16
16
  try {
17
- if (!(options.endpoints?.length ? options.endpoints : defaultEndpoints).some((endpoint) => request.url.includes(endpoint))) return void 0;
17
+ const endpoints = options.endpoints?.length ? options.endpoints : defaultEndpoints;
18
+ const url = new URL(request.url);
19
+ const basePath = ctx.options.basePath ?? "/api/auth";
20
+ let pathname = url.pathname.replace(basePath, "");
21
+ if (pathname.endsWith("//")) pathname = pathname.slice(0, -1);
22
+ if (pathname.startsWith("//")) pathname = pathname.slice(1);
23
+ if (!pathname.startsWith("/")) pathname = "/" + pathname;
24
+ const blockedPaths = ["/sign-in/email-otp"].reduce((acc, curr) => {
25
+ if (options.endpoints?.length && options.endpoints.includes(curr)) return acc;
26
+ return [...acc, curr];
27
+ }, []);
28
+ if (!endpoints.some((endpoint) => {
29
+ return pathname.includes(endpoint) && !blockedPaths.includes(endpoint);
30
+ })) return;
18
31
  if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
19
32
  const captchaResponse = request.headers.get("x-captcha-response");
20
33
  const remoteUserIP = getIp(request, ctx.options) ?? void 0;
@@ -465,7 +465,7 @@ const requestPasswordResetEmailOTP = (opts) => createAuthEndpoint("/email-otp/re
465
465
  } }
466
466
  } }
467
467
  }, async (ctx) => {
468
- const email = ctx.body.email;
468
+ const email = ctx.body.email.toLowerCase();
469
469
  const identifier = toOTPIdentifier("forget-password", email);
470
470
  const otp = await resolveOTP(ctx, opts, email, "forget-password");
471
471
  if (!await ctx.context.internalAdapter.findUserByEmail(email)) {
@@ -517,7 +517,7 @@ const forgetPasswordEmailOTP = (opts) => {
517
517
  } }
518
518
  }, async (ctx) => {
519
519
  warnDeprecation();
520
- const email = ctx.body.email;
520
+ const email = ctx.body.email.toLowerCase();
521
521
  const identifier = toOTPIdentifier("forget-password", email);
522
522
  const otp = await resolveOTP(ctx, opts, email, "forget-password");
523
523
  if (!await ctx.context.internalAdapter.findUserByEmail(email)) {
@@ -567,7 +567,7 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
567
567
  } }
568
568
  } }
569
569
  }, async (ctx) => {
570
- const email = ctx.body.email;
570
+ const email = ctx.body.email.toLowerCase();
571
571
  await atomicVerifyOTP(ctx, opts, toOTPIdentifier("forget-password", email), ctx.body.otp);
572
572
  const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
573
573
  if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
@@ -132,7 +132,7 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
132
132
  }
133
133
  }, async (ctx) => {
134
134
  const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
135
- if (ctx.query.error || !ctx.query.code) throw ctx.redirect(`${defaultErrorURL}?error=${ctx.query.error || "oAuth_code_missing"}&error_description=${ctx.query.error_description}`);
135
+ if (ctx.query.error || !ctx.query.code) throw ctx.redirect(`${defaultErrorURL}?error=${encodeURIComponent(ctx.query.error || "oAuth_code_missing")}&error_description=${encodeURIComponent(ctx.query.error_description || "")}`);
136
136
  const providerId = ctx.params?.providerId;
137
137
  if (!providerId) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED);
138
138
  const providerConfig = options.config.find((p) => p.providerId === providerId);
@@ -143,8 +143,8 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
143
143
  function redirectOnError(error) {
144
144
  const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
145
145
  let url = errorURL || defaultErrorURL;
146
- if (url.includes("?")) url = `${url}&error=${error}`;
147
- else url = `${url}?error=${error}`;
146
+ if (url.includes("?")) url = `${url}&error=${encodeURIComponent(error)}`;
147
+ else url = `${url}?error=${encodeURIComponent(error)}`;
148
148
  throw ctx.redirect(url);
149
149
  }
150
150
  let finalTokenUrl = providerConfig.tokenUrl;
@@ -45,8 +45,9 @@ const oneTap = (options) => ({
45
45
  } catch {
46
46
  throw new APIError("BAD_REQUEST", { message: "invalid id token" });
47
47
  }
48
- const { email, email_verified, name, picture, sub } = payload;
49
- if (!email) return ctx.json({ error: "Email not available in token" });
48
+ const { email: rawEmail, email_verified, name, picture, sub } = payload;
49
+ if (!rawEmail) return ctx.json({ error: "Email not available in token" });
50
+ const email = rawEmail.toLowerCase();
50
51
  const user = await ctx.context.internalAdapter.findUserByEmail(email);
51
52
  if (!user) {
52
53
  if (options?.disableSignup) throw new APIError("BAD_GATEWAY", { message: "User not found" });