better-auth 1.6.9 → 1.6.11

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 (71) hide show
  1. package/dist/api/index.d.mts +0 -2
  2. package/dist/api/routes/callback.mjs +6 -5
  3. package/dist/api/routes/email-verification.mjs +2 -2
  4. package/dist/api/routes/error.mjs +1 -1
  5. package/dist/api/routes/sign-in.d.mts +0 -1
  6. package/dist/api/routes/sign-in.mjs +4 -11
  7. package/dist/api/routes/sign-up.mjs +1 -1
  8. package/dist/api/routes/update-user.mjs +1 -1
  9. package/dist/api/to-auth-endpoints.mjs +7 -1
  10. package/dist/client/index.d.mts +2 -2
  11. package/dist/client/plugins/index.d.mts +2 -1
  12. package/dist/cookies/cookie-utils.d.mts +10 -1
  13. package/dist/cookies/cookie-utils.mjs +19 -1
  14. package/dist/cookies/index.d.mts +2 -2
  15. package/dist/cookies/index.mjs +2 -2
  16. package/dist/db/internal-adapter.mjs +103 -7
  17. package/dist/db/with-hooks.d.mts +1 -0
  18. package/dist/db/with-hooks.mjs +58 -1
  19. package/dist/integrations/cookie-plugin-guard.mjs +18 -0
  20. package/dist/integrations/next-js.mjs +6 -0
  21. package/dist/integrations/svelte-kit.mjs +6 -0
  22. package/dist/integrations/tanstack-start-solid.mjs +6 -0
  23. package/dist/integrations/tanstack-start.mjs +6 -0
  24. package/dist/oauth2/link-account.mjs +3 -1
  25. package/dist/package.mjs +1 -1
  26. package/dist/plugins/access/access.d.mts +3 -15
  27. package/dist/plugins/access/index.d.mts +2 -2
  28. package/dist/plugins/access/types.d.mts +11 -4
  29. package/dist/plugins/admin/access/statement.d.mts +29 -93
  30. package/dist/plugins/admin/client.d.mts +5 -0
  31. package/dist/plugins/admin/client.mjs +5 -0
  32. package/dist/plugins/admin/routes.mjs +1 -0
  33. package/dist/plugins/anonymous/client.d.mts +1 -0
  34. package/dist/plugins/anonymous/error-codes.d.mts +1 -0
  35. package/dist/plugins/anonymous/error-codes.mjs +1 -0
  36. package/dist/plugins/anonymous/index.d.mts +1 -0
  37. package/dist/plugins/anonymous/index.mjs +16 -2
  38. package/dist/plugins/bearer/index.mjs +2 -4
  39. package/dist/plugins/captcha/index.mjs +14 -1
  40. package/dist/plugins/device-authorization/error-codes.mjs +1 -0
  41. package/dist/plugins/device-authorization/index.d.mts +1 -0
  42. package/dist/plugins/device-authorization/routes.mjs +34 -3
  43. package/dist/plugins/email-otp/routes.mjs +3 -3
  44. package/dist/plugins/generic-oauth/routes.mjs +3 -3
  45. package/dist/plugins/index.d.mts +2 -2
  46. package/dist/plugins/magic-link/index.d.mts +8 -1
  47. package/dist/plugins/magic-link/index.mjs +5 -17
  48. package/dist/plugins/mcp/authorize.mjs +8 -2
  49. package/dist/plugins/mcp/index.mjs +73 -30
  50. package/dist/plugins/oidc-provider/authorize.mjs +8 -2
  51. package/dist/plugins/oidc-provider/index.mjs +63 -33
  52. package/dist/plugins/one-tap/index.mjs +16 -10
  53. package/dist/plugins/organization/access/statement.d.mts +68 -201
  54. package/dist/plugins/organization/client.d.mts +1 -0
  55. package/dist/plugins/organization/client.mjs +1 -1
  56. package/dist/plugins/organization/error-codes.d.mts +1 -0
  57. package/dist/plugins/organization/error-codes.mjs +1 -0
  58. package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
  59. package/dist/plugins/organization/routes/crud-invites.d.mts +8 -1
  60. package/dist/plugins/organization/routes/crud-invites.mjs +5 -3
  61. package/dist/plugins/organization/routes/crud-team.mjs +7 -2
  62. package/dist/plugins/organization/types.d.mts +12 -2
  63. package/dist/plugins/siwe/client.d.mts +4 -0
  64. package/dist/plugins/siwe/client.mjs +5 -1
  65. package/dist/plugins/siwe/index.d.mts +13 -2
  66. package/dist/plugins/siwe/index.mjs +179 -165
  67. package/dist/plugins/username/index.d.mts +11 -0
  68. package/dist/plugins/username/index.mjs +18 -2
  69. package/dist/test-utils/test-instance.d.mts +1 -6
  70. package/dist/test-utils/test-instance.mjs +11 -2
  71. package/package.json +10 -10
@@ -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();
@@ -410,7 +410,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
410
410
  }, async (ctx) => {
411
411
  if (!ctx.context.options.user?.changeEmail?.enabled) {
412
412
  ctx.context.logger.error("Change email is disabled.");
413
- throw APIError.fromStatus("BAD_REQUEST", { message: "Change email is disabled" });
413
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CHANGE_EMAIL_DISABLED);
414
414
  }
415
415
  const newEmail = ctx.body.newEmail.toLowerCase();
416
416
  if (newEmail === ctx.context.session.user.email) {
@@ -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();
@@ -8,7 +8,7 @@ import { parseJSON } from "./parser.mjs";
8
8
  import { AuthQueryAtom, useAuthQuery } from "./query.mjs";
9
9
  import { SessionRefreshOptions, SessionResponse, createSessionRefreshManager } from "./session-refresh.mjs";
10
10
  import { AuthClient, createAuthClient } from "./vanilla.mjs";
11
- import { AccessControl, ArrayElement, Role, Statements, SubArray, Subset } from "../plugins/access/types.mjs";
11
+ import { AccessControl, ArrayElement, ExactRoleStatements, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, Statements, SubArray, Subset } from "../plugins/access/types.mjs";
12
12
  import { AuthorizeResponse, createAccessControl, role } from "../plugins/access/access.mjs";
13
13
  import { OrganizationOptions } from "../plugins/organization/types.mjs";
14
14
  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";
@@ -31,4 +31,4 @@ declare function InferAuth<O extends {
31
31
  options: BetterAuthOptions;
32
32
  }>(): O["options"];
33
33
  //#endregion
34
- export { AccessControl, ArrayElement, AuthClient, AuthQueryAtom, AuthorizeResponse, BetterAuthClientOptions, BetterAuthClientPlugin, BroadcastChannel, BroadcastListener, BroadcastMessage, CamelCase, ClientAtomListener, ClientStore, type DBPrimitive, DefaultOrganizationPlugin, DynamicAccessControlEndpoints, ExtractPluginField, type FocusListener, type FocusManager, HasRequiredKeys, InferActions, InferAdditionalFromClient, InferAuth, InferClientAPI, InferCtx, InferErrorCodes, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPlugin, InferPluginFieldFromTuple, InferRoute, InferRoutes, InferSessionFromClient, InferSignUpEmailCtx, InferTeam, InferUserFromClient, InferUserUpdateCtx, Invitation, InvitationInput, InvitationStatus, IsAny, IsSignal, Member, MemberInput, MergeRoutes, type OnlineListener, type OnlineManager, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, OverrideMerge, PathToObject, Prettify, PrettifyDeep, ProxyRequest, RequiredKeysOf, Role, SessionQueryParams, SessionRefreshOptions, SessionResponse, Statements, StripEmptyObjects, SubArray, Subset, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, type UnionToIntersection, createAccessControl, createAuthClient, createSessionRefreshManager, defaultRolesSchema, getGlobalBroadcastChannel, getOrgAdapter, hasPermission, invitationSchema, invitationStatus, kBroadcastChannel, kFocusManager, kOnlineManager, memberSchema, organization, organizationRoleSchema, organizationSchema, parseJSON, parseRoles, role, roleSchema, teamMemberSchema, teamSchema, useAuthQuery };
34
+ export { AccessControl, ArrayElement, AuthClient, AuthQueryAtom, AuthorizeResponse, BetterAuthClientOptions, BetterAuthClientPlugin, BroadcastChannel, BroadcastListener, BroadcastMessage, CamelCase, ClientAtomListener, ClientStore, type DBPrimitive, DefaultOrganizationPlugin, DynamicAccessControlEndpoints, ExactRoleStatements, ExtractPluginField, type FocusListener, type FocusManager, HasRequiredKeys, InferActions, InferAdditionalFromClient, InferAuth, InferClientAPI, InferCtx, InferErrorCodes, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPlugin, InferPluginFieldFromTuple, InferRoute, InferRoutes, InferSessionFromClient, InferSignUpEmailCtx, InferTeam, InferUserFromClient, InferUserUpdateCtx, Invitation, InvitationInput, InvitationStatus, IsAny, IsSignal, Member, MemberInput, MergeRoutes, type OnlineListener, type OnlineManager, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, OverrideMerge, PathToObject, Prettify, PrettifyDeep, ProxyRequest, RequiredKeysOf, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SessionQueryParams, SessionRefreshOptions, SessionResponse, Statements, StripEmptyObjects, SubArray, Subset, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, type UnionToIntersection, createAccessControl, createAuthClient, createSessionRefreshManager, defaultRolesSchema, getGlobalBroadcastChannel, getOrgAdapter, hasPermission, invitationSchema, invitationStatus, kBroadcastChannel, kFocusManager, kOnlineManager, memberSchema, organization, organizationRoleSchema, organizationSchema, parseJSON, parseRoles, role, roleSchema, teamMemberSchema, teamSchema, useAuthQuery };
@@ -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 };
@@ -15,8 +15,9 @@ const createInternalAdapter = (adapter, ctx) => {
15
15
  const logger = ctx.logger;
16
16
  const options = ctx.options;
17
17
  const secondaryStorage = options.secondaryStorage;
18
+ const verificationConsumeLocks = /* @__PURE__ */ new Map();
18
19
  const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
19
- const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks } = getWithHooks(adapter, ctx);
20
+ const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
20
21
  async function refreshUserSessions(user) {
21
22
  if (!secondaryStorage) return;
22
23
  const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`);
@@ -35,13 +36,30 @@ const createInternalAdapter = (adapter, ctx) => {
35
36
  }), Math.floor(sessionTTL));
36
37
  }));
37
38
  }
39
+ async function withVerificationConsumeLock(key, fn) {
40
+ const previous = verificationConsumeLocks.get(key) ?? Promise.resolve();
41
+ let release;
42
+ const current = new Promise((resolve) => {
43
+ release = resolve;
44
+ });
45
+ const next = previous.catch(() => {}).then(() => current);
46
+ verificationConsumeLocks.set(key, next);
47
+ await previous.catch(() => {});
48
+ try {
49
+ return await fn();
50
+ } finally {
51
+ release();
52
+ if (verificationConsumeLocks.get(key) === next) verificationConsumeLocks.delete(key);
53
+ }
54
+ }
38
55
  return {
39
56
  createOAuthUser: async (user, account) => {
40
57
  return runWithTransaction(adapter, async () => {
41
58
  const createdUser = await createWithHooks({
42
59
  createdAt: /* @__PURE__ */ new Date(),
43
60
  updatedAt: /* @__PURE__ */ new Date(),
44
- ...user
61
+ ...user,
62
+ email: user.email?.toLowerCase()
45
63
  }, "user", void 0);
46
64
  return {
47
65
  user: createdUser,
@@ -364,10 +382,10 @@ const createInternalAdapter = (adapter, ctx) => {
364
382
  value: userId
365
383
  }], "account", void 0);
366
384
  },
367
- deleteAccount: async (accountId) => {
385
+ deleteAccount: async (id) => {
368
386
  await deleteWithHooks([{
369
387
  field: "id",
370
- value: accountId
388
+ value: id
371
389
  }], "account", void 0);
372
390
  },
373
391
  deleteSessions: async (userIdOrSessionTokens) => {
@@ -475,7 +493,10 @@ const createInternalAdapter = (adapter, ctx) => {
475
493
  }, "account", void 0);
476
494
  },
477
495
  updateUser: async (userId, data) => {
478
- const user = await updateWithHooks(data, [{
496
+ const user = await updateWithHooks({
497
+ ...data,
498
+ ...data.email ? { email: data.email.toLowerCase() } : {}
499
+ }, [{
479
500
  field: "id",
480
501
  value: userId
481
502
  }], "user", void 0);
@@ -483,7 +504,10 @@ const createInternalAdapter = (adapter, ctx) => {
483
504
  return user;
484
505
  },
485
506
  updateUserByEmail: async (email, data) => {
486
- const user = await updateWithHooks(data, [{
507
+ const user = await updateWithHooks({
508
+ ...data,
509
+ ...data.email ? { email: data.email.toLowerCase() } : {}
510
+ }, [{
487
511
  field: "email",
488
512
  value: email.toLowerCase()
489
513
  }], "user", void 0);
@@ -611,6 +635,77 @@ const createInternalAdapter = (adapter, ctx) => {
611
635
  value: storedIdentifier
612
636
  }], "verification", void 0);
613
637
  },
638
+ consumeVerificationValue: async (identifier) => {
639
+ const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier);
640
+ const storedIdentifier = await processIdentifier(identifier, storageOption);
641
+ const identifiersToTry = storageOption && storageOption !== "plain" ? [storedIdentifier, identifier] : [storedIdentifier];
642
+ if (secondaryStorage && !options.verification?.storeInDatabase) {
643
+ const parseCachedVerification = (raw) => {
644
+ if (!raw) return null;
645
+ if (typeof raw === "string") return safeJSONParse(raw);
646
+ if (typeof raw === "object") return raw;
647
+ return null;
648
+ };
649
+ const consumeCacheKey = async (key) => {
650
+ if (secondaryStorage.getAndDelete) return parseCachedVerification(await secondaryStorage.getAndDelete(key));
651
+ return withVerificationConsumeLock(key, async () => {
652
+ const parsed = parseCachedVerification(await secondaryStorage.get(key));
653
+ if (!parsed) return null;
654
+ await secondaryStorage.delete(key);
655
+ return parsed;
656
+ });
657
+ };
658
+ for (const stored of identifiersToTry) {
659
+ const cached = await consumeCacheKey(`verification:${stored}`);
660
+ if (!cached) continue;
661
+ await Promise.all(identifiersToTry.filter((candidate) => candidate !== stored).map((candidate) => secondaryStorage.delete(`verification:${candidate}`)));
662
+ return cached;
663
+ }
664
+ return null;
665
+ }
666
+ async function consumeByIdentifier(id) {
667
+ const where = [{
668
+ field: "identifier",
669
+ value: id
670
+ }];
671
+ return withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
672
+ const txAdapter = await getCurrentAdapter(adapter);
673
+ const latest = (await txAdapter.findMany({
674
+ model: "verification",
675
+ where,
676
+ sortBy: {
677
+ field: "createdAt",
678
+ direction: "desc"
679
+ },
680
+ limit: 1
681
+ }))[0] ?? null;
682
+ if (!latest) return null;
683
+ const hookWhere = [{
684
+ field: "id",
685
+ value: latest.id
686
+ }];
687
+ return consumeOneWithHooks("verification", hookWhere, async () => {
688
+ const consumed = await txAdapter.consumeOne({
689
+ model: "verification",
690
+ where: hookWhere
691
+ });
692
+ if (!consumed) return null;
693
+ await txAdapter.deleteMany({
694
+ model: "verification",
695
+ where
696
+ });
697
+ return consumed;
698
+ }, latest);
699
+ }));
700
+ }
701
+ let consumed = null;
702
+ for (const stored of identifiersToTry) {
703
+ consumed = await consumeByIdentifier(stored);
704
+ if (consumed) break;
705
+ }
706
+ if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
707
+ return consumed;
708
+ },
614
709
  updateVerificationByIdentifier: async (identifier, data) => {
615
710
  const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier));
616
711
  if (secondaryStorage) {
@@ -634,7 +729,8 @@ const createInternalAdapter = (adapter, ctx) => {
634
729
  value: storedIdentifier
635
730
  }], "verification", void 0);
636
731
  return data;
637
- }
732
+ },
733
+ refreshUserSessions
638
734
  };
639
735
  };
640
736
  //#endregion
@@ -31,6 +31,7 @@ declare function getWithHooks(adapter: DBAdapter<BetterAuthOptions>, ctx: {
31
31
  fn: (where: Where[]) => void | Promise<any>;
32
32
  executeMainFn?: boolean;
33
33
  } | undefined) => Promise<any>;
34
+ consumeOneWithHooks: <T extends Record<string, any>>(model: BaseModelNames, hookWhere: Where[], consumeFn: () => Promise<T | null>, preSnapshot?: T | null) => Promise<T | null>;
34
35
  };
35
36
  //#endregion
36
37
  export { DatabaseHooksEntry, getWithHooks };
@@ -185,12 +185,69 @@ function getWithHooks(adapter, ctx) {
185
185
  }
186
186
  return deleted;
187
187
  }
188
+ /**
189
+ * Wraps an atomic consume operation in the plugin `delete.before` and
190
+ * `delete.after` hook lifecycle. The caller supplies a `consumeFn` that
191
+ * performs the actual single-row delete-and-return (typically the
192
+ * adapter's `consumeOne`). The first concurrent caller wins, subsequent
193
+ * racers resolve to `null` without firing `delete.after` hooks.
194
+ *
195
+ * `preSnapshot` lets the caller hand in a row it already fetched so
196
+ * `delete.before` hooks don't trigger a second read. Without it, the
197
+ * helper falls back to a best-effort `findMany` against `hookWhere`.
198
+ * The snapshot only feeds `delete.before`; the `consumeFn` return value
199
+ * is the race gate.
200
+ *
201
+ * Returning `false` from a `delete.before` hook aborts the consume and
202
+ * the helper resolves to `null` (no `consumeFn` call, no after hooks).
203
+ */
204
+ async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) {
205
+ const context = await getCurrentAuthContext().catch(() => null);
206
+ const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => {
207
+ const fn = hooks[model]?.delete?.before;
208
+ return fn ? [{
209
+ source,
210
+ fn
211
+ }] : [];
212
+ });
213
+ let snapshot = preSnapshot ?? null;
214
+ if (beforeHooks.length) {
215
+ if (!snapshot) try {
216
+ snapshot = (await (await getCurrentAdapter(adapter)).findMany({
217
+ model,
218
+ where: hookWhere,
219
+ limit: 1
220
+ }))[0] || null;
221
+ } catch {}
222
+ if (snapshot) {
223
+ for (const { source, fn } of beforeHooks) if (await withSpan(`db delete.before ${model}`, {
224
+ [ATTR_HOOK_TYPE]: "delete.before",
225
+ [ATTR_DB_COLLECTION_NAME]: model,
226
+ [ATTR_CONTEXT]: source
227
+ }, () => fn(snapshot, context)) === false) return null;
228
+ }
229
+ }
230
+ const consumed = await consumeFn();
231
+ if (!consumed) return null;
232
+ for (const { source, hooks } of hooksEntries) {
233
+ const toRun = hooks[model]?.delete?.after;
234
+ if (toRun) await queueAfterTransactionHook(async () => {
235
+ await withSpan(`db delete.after ${model}`, {
236
+ [ATTR_HOOK_TYPE]: "delete.after",
237
+ [ATTR_DB_COLLECTION_NAME]: model,
238
+ [ATTR_CONTEXT]: source
239
+ }, () => toRun(consumed, context));
240
+ });
241
+ }
242
+ return consumed;
243
+ }
188
244
  return {
189
245
  createWithHooks,
190
246
  updateWithHooks,
191
247
  updateManyWithHooks,
192
248
  deleteWithHooks,
193
- deleteManyWithHooks
249
+ deleteManyWithHooks,
250
+ consumeOneWithHooks
194
251
  };
195
252
  }
196
253
  //#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) {