better-auth 1.7.14 → 1.7.16

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 (70) hide show
  1. package/dist/api/dispatch.mjs +1 -1
  2. package/dist/api/index.d.mts +2 -1
  3. package/dist/api/index.mjs +3 -2
  4. package/dist/api/routes/account.mjs +14 -6
  5. package/dist/api/routes/email-verification.mjs +29 -37
  6. package/dist/api/routes/sign-in.mjs +2 -2
  7. package/dist/api/routes/sign-up.mjs +1 -1
  8. package/dist/api/routes/update-user.mjs +10 -4
  9. package/dist/api/state/session-source.d.mts +10 -0
  10. package/dist/api/state/session-source.mjs +28 -0
  11. package/dist/auth/trusted-origins.mjs +31 -8
  12. package/dist/client/index.d.mts +2 -1
  13. package/dist/client/parser.mjs +1 -11
  14. package/dist/client/vue/index.mjs +13 -7
  15. package/dist/context/secret-utils.mjs +3 -3
  16. package/dist/db/internal-adapter.mjs +24 -6
  17. package/dist/index.d.mts +2 -2
  18. package/dist/index.mjs +2 -2
  19. package/dist/oauth2/link-account.d.mts +3 -5
  20. package/dist/oauth2/link-account.mjs +33 -3
  21. package/dist/oauth2/state.d.mts +6 -5
  22. package/dist/package.mjs +1 -1
  23. package/dist/plugins/admin/access/statement.d.mts +10 -10
  24. package/dist/plugins/admin/access/statement.mjs +2 -0
  25. package/dist/plugins/admin/admin.d.mts +6 -4
  26. package/dist/plugins/admin/admin.mjs +4 -4
  27. package/dist/plugins/admin/client.d.mts +6 -4
  28. package/dist/plugins/admin/error-codes.d.mts +2 -0
  29. package/dist/plugins/admin/error-codes.mjs +3 -1
  30. package/dist/plugins/admin/routes.mjs +9 -0
  31. package/dist/plugins/anonymous/index.mjs +2 -1
  32. package/dist/plugins/device-authorization/routes.mjs +3 -3
  33. package/dist/plugins/email-otp/routes.mjs +33 -50
  34. package/dist/plugins/generic-oauth/index.mjs +54 -30
  35. package/dist/plugins/generic-oauth/providers/hubspot.mjs +5 -2
  36. package/dist/plugins/index.d.mts +2 -1
  37. package/dist/plugins/index.mjs +2 -1
  38. package/dist/plugins/oauth-proxy/index.mjs +1 -1
  39. package/dist/plugins/one-tap/index.mjs +1 -1
  40. package/dist/plugins/organization/adapter.mjs +1 -0
  41. package/dist/plugins/organization/client.d.mts +1 -0
  42. package/dist/plugins/organization/error-codes.d.mts +1 -0
  43. package/dist/plugins/organization/error-codes.mjs +1 -0
  44. package/dist/plugins/organization/has-permission.d.mts +2 -3
  45. package/dist/plugins/organization/has-permission.mjs +62 -23
  46. package/dist/plugins/organization/index.d.mts +2 -1
  47. package/dist/plugins/organization/index.mjs +2 -1
  48. package/dist/plugins/organization/organization.d.mts +2 -2
  49. package/dist/plugins/organization/organization.mjs +1 -3
  50. package/dist/plugins/organization/permission.mjs +3 -3
  51. package/dist/plugins/organization/roles.d.mts +8 -0
  52. package/dist/plugins/organization/roles.mjs +26 -0
  53. package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
  54. package/dist/plugins/organization/routes/crud-invites.mjs +22 -11
  55. package/dist/plugins/organization/routes/crud-members.mjs +10 -14
  56. package/dist/plugins/organization/routes/crud-org.mjs +12 -3
  57. package/dist/plugins/phone-number/client.d.mts +2 -0
  58. package/dist/plugins/phone-number/error-codes.d.mts +2 -0
  59. package/dist/plugins/phone-number/error-codes.mjs +2 -0
  60. package/dist/plugins/phone-number/index.d.mts +25 -1
  61. package/dist/plugins/phone-number/index.mjs +22 -6
  62. package/dist/plugins/phone-number/routes.mjs +34 -35
  63. package/dist/plugins/two-factor/index.d.mts +9 -2
  64. package/dist/plugins/two-factor/index.mjs +73 -26
  65. package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
  66. package/dist/plugins/username/index.mjs +6 -19
  67. package/dist/state.d.mts +6 -0
  68. package/dist/state.mjs +5 -2
  69. package/dist/utils/index.d.mts +2 -2
  70. package/package.json +9 -9
@@ -243,7 +243,7 @@ async function dispatchAuthEndpoint(endpoint, input) {
243
243
  });
244
244
  if (result instanceof Response) return result;
245
245
  internalContext.context.returned = result.response;
246
- internalContext.context.responseHeaders = result.headers ?? void 0;
246
+ mergeResponseHeaders(internalContext.context, result.headers);
247
247
  const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
248
248
  if (after.response !== void 0) result.response = after.response;
249
249
  result.headers = after.headers ?? result.headers;
@@ -21,6 +21,7 @@ import { updateSession } from "./routes/update-session.mjs";
21
21
  import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./routes/update-user.mjs";
22
22
  import "./routes/index.mjs";
23
23
  import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
24
+ import { markSessionOutsideStore } from "./state/session-source.mjs";
24
25
  import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
25
26
  import { AuthContext, Awaitable, BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
26
27
  import { InternalLogger } from "@better-auth/core/env";
@@ -4048,4 +4049,4 @@ export declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext
4048
4049
  } extends (infer T_2) ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K]; } : never) & T_1> : never : never : never;
4049
4050
  };
4050
4051
  //#endregion
4051
- export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
4052
+ export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -2,9 +2,9 @@ import { isAPIError } from "../utils/is-api-error.mjs";
2
2
  import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
3
3
  import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
4
4
  import { onRequestRateLimit } from "./rate-limiter/index.mjs";
5
- import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
6
5
  import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
7
6
  import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
7
+ import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
8
8
  import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
9
9
  import { callbackOAuth } from "./routes/callback.mjs";
10
10
  import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
@@ -18,6 +18,7 @@ import { updateSession } from "./routes/update-session.mjs";
18
18
  import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./routes/update-user.mjs";
19
19
  import { dispatchAuthEndpoint } from "./dispatch.mjs";
20
20
  import { toAuthEndpoints } from "./to-auth-endpoints.mjs";
21
+ import { markSessionOutsideStore } from "./state/session-source.mjs";
21
22
  import { logger } from "@better-auth/core/env";
22
23
  import { APIError } from "@better-auth/core/error";
23
24
  import { getIP } from "@better-auth/core/utils/ip";
@@ -222,4 +223,4 @@ const router = (ctx, options) => {
222
223
  });
223
224
  };
224
225
  //#endregion
225
- export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
226
+ export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -1,13 +1,16 @@
1
1
  import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
2
2
  import { parseAccountOutput } from "../../db/schema.mjs";
3
+ import { symmetricEncrypt } from "../../crypto/index.mjs";
3
4
  import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
4
5
  import { getAwaitableValue } from "../../context/helpers.mjs";
6
+ import { derivePurposeKey } from "../../crypto/purpose.mjs";
5
7
  import { resolveOAuthAccountKeyForAPI } from "../../oauth2/account-key.mjs";
6
8
  import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
9
+ import { freshSessionMiddleware, getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
10
+ import { toBoolean } from "../../utils/boolean.mjs";
7
11
  import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
8
12
  import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
9
13
  import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
10
- import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
11
14
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
12
15
  import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
13
16
  import { SocialProviderListEnum } from "@better-auth/core/social-providers";
@@ -129,7 +132,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
129
132
  */
130
133
  additionalData: z.record(z.string(), z.any()).optional()
131
134
  }),
132
- use: [sessionMiddleware],
135
+ use: [sensitiveSessionMiddleware],
133
136
  metadata: { openapi: {
134
137
  description: "Link a social account to the user",
135
138
  operationId: "linkSocialAccount",
@@ -169,11 +172,12 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
169
172
  c.context.logger.warn("Invalid id token", { provider: c.body.provider });
170
173
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
171
174
  }
172
- const linkingUserInfo = await provider.getUserInfo({
175
+ const oauthTokens = {
173
176
  idToken: token,
174
177
  accessToken: c.body.idToken.accessToken,
175
178
  refreshToken: c.body.idToken.refreshToken
176
- });
179
+ };
180
+ const linkingUserInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
177
181
  if (!linkingUserInfo || !linkingUserInfo?.user) {
178
182
  c.context.logger.error("Failed to get user info", { provider: c.body.provider });
179
183
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
@@ -204,7 +208,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
204
208
  });
205
209
  }
206
210
  if (linkedAccount) throw APIError.from("CONFLICT", BASE_ERROR_CODES.SOCIAL_ACCOUNT_ALREADY_LINKED);
207
- if (!c.context.trustedProviders.includes(provider.id) && !linkingUserInfo.user.emailVerified || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
211
+ if (!c.context.trustedProviders.includes(provider.id) && !toBoolean(linkingUserInfo.user.emailVerified) || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
208
212
  message: "Account not linked - linking not allowed",
209
213
  code: "LINKING_NOT_ALLOWED"
210
214
  });
@@ -237,7 +241,11 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
237
241
  const state = await generateState(c, {
238
242
  link: {
239
243
  userId: session.user.id,
240
- email: session.user.email
244
+ email: session.user.email,
245
+ encryptedSessionToken: await symmetricEncrypt({
246
+ key: derivePurposeKey(c.context.secretConfig, "oauth-link-session-token"),
247
+ data: session.session.token
248
+ })
241
249
  },
242
250
  additionalData: c.body.additionalData,
243
251
  idTokenNonce
@@ -26,7 +26,7 @@ async function sendVerificationEmailFn(ctx, user) {
26
26
  ctx.context.logger.error("Verification email isn't enabled.");
27
27
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.VERIFICATION_EMAIL_NOT_ENABLED);
28
28
  }
29
- const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
29
+ const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
30
30
  const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
31
31
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
32
32
  await ctx.context.options.emailVerification.sendVerificationEmail({
@@ -183,24 +183,30 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
183
183
  const parsed = z.object({
184
184
  email: z.email(),
185
185
  updateTo: z.string().optional(),
186
- requestType: z.string().optional()
186
+ requestType: z.string().optional(),
187
+ userId: z.string().optional()
187
188
  }).parse(jwt.payload);
188
- const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
189
- if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
190
189
  if (parsed.updateTo) {
190
+ if (!parsed.userId) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
191
+ const boundUser = await ctx.context.internalAdapter.findUserById(parsed.userId);
192
+ if (!boundUser) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
193
+ if (boundUser.email.toLowerCase() !== parsed.email) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
191
194
  const session = await getSessionFromCtx(ctx);
192
- if (session && session.user.email !== parsed.email) return redirectOnError(BASE_ERROR_CODES.INVALID_USER);
195
+ if (session && session.user.id !== boundUser.id) return redirectOnError(BASE_ERROR_CODES.INVALID_USER);
193
196
  switch (parsed.requestType) {
194
197
  /**
195
198
  * User clicks confirmation -> sends verification to new email
196
199
  */
197
200
  case "change-email-confirmation": {
198
- const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-verification" });
201
+ const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, {
202
+ requestType: "change-email-verification",
203
+ userId: boundUser.id
204
+ });
199
205
  const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
200
206
  const url = `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`;
201
207
  if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
202
208
  user: {
203
- ...user.user,
209
+ ...boundUser,
204
210
  email: parsed.updateTo
205
211
  },
206
212
  url,
@@ -213,26 +219,18 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
213
219
  * User clicks verification -> updates email
214
220
  */
215
221
  case "change-email-verification": {
216
- let activeSession = session;
217
- if (!activeSession) {
218
- const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
219
- if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
220
- activeSession = {
221
- session: newSession,
222
- user: user.user
223
- };
224
- }
225
- const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
222
+ const emailVerified = boundUser.emailVerified;
223
+ const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
226
224
  email: parsed.updateTo,
227
- emailVerified: true
225
+ emailVerified
228
226
  });
229
- if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
230
- await setSessionCookie(ctx, {
231
- session: activeSession.session,
227
+ if (emailVerified && ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
228
+ if (session) await setSessionCookie(ctx, {
229
+ session: session.session,
232
230
  user: {
233
- ...activeSession.user,
231
+ ...session.user,
234
232
  email: parsed.updateTo,
235
- emailVerified: true
233
+ emailVerified
236
234
  }
237
235
  });
238
236
  if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
@@ -248,30 +246,21 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
248
246
  * - updates email immediately
249
247
  */
250
248
  default: {
251
- let activeSession = session;
252
- if (!activeSession) {
253
- const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
254
- if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
255
- activeSession = {
256
- session: newSession,
257
- user: user.user
258
- };
259
- }
260
- const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
249
+ const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
261
250
  email: parsed.updateTo,
262
251
  emailVerified: false
263
252
  });
264
- const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo);
253
+ const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo, void 0, void 0, { userId: boundUser.id });
265
254
  const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
266
255
  if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
267
256
  user: updatedUser,
268
257
  url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
269
258
  token: newToken
270
259
  }, safeCloneRequest(ctx.request)));
271
- await setSessionCookie(ctx, {
272
- session: activeSession.session,
260
+ if (session) await setSessionCookie(ctx, {
261
+ session: session.session,
273
262
  user: {
274
- ...activeSession.user,
263
+ ...session.user,
275
264
  email: parsed.updateTo,
276
265
  emailVerified: false
277
266
  }
@@ -284,6 +273,9 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
284
273
  }
285
274
  }
286
275
  }
276
+ const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
277
+ if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
278
+ if (parsed.userId && parsed.userId !== user.user.id) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
287
279
  if (user.user.emailVerified) {
288
280
  if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
289
281
  return ctx.json({
@@ -167,7 +167,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
167
167
  refreshToken: c.body.idToken.refreshToken,
168
168
  user: c.body.idToken.user
169
169
  };
170
- const userInfo = await provider.getUserInfo(oauthTokens);
170
+ const userInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
171
171
  if (!userInfo || !userInfo?.user) {
172
172
  c.context.logger.error("Failed to get user info", { provider: c.body.provider });
173
173
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
@@ -338,7 +338,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
338
338
  if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.emailVerified) {
339
339
  if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
340
340
  if (ctx.context.options?.emailVerification?.sendOnSignIn) {
341
- const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
341
+ const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
342
342
  const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
343
343
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
344
344
  await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
@@ -239,7 +239,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
239
239
  password: hash
240
240
  });
241
241
  if (ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification) {
242
- const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn);
242
+ const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: createdUser.id });
243
243
  const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
244
244
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
245
245
  if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
@@ -450,7 +450,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
450
450
  throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
451
451
  }
452
452
  if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
453
- await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn);
453
+ await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
454
454
  ctx.context.logger.info("Change email attempt for existing email");
455
455
  return ctx.json({ status: true });
456
456
  }
@@ -467,7 +467,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
467
467
  }
468
468
  });
469
469
  if (canSendVerification) {
470
- const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn);
470
+ const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
471
471
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
472
472
  await ctx.context.runInBackgroundOrAwait(canSendVerification({
473
473
  user: {
@@ -484,7 +484,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
484
484
  * If the email is verified, we need to send a verification email
485
485
  */
486
486
  if (canSendConfirmation) {
487
- const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-confirmation" });
487
+ const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
488
+ requestType: "change-email-confirmation",
489
+ userId: ctx.context.session.user.id
490
+ });
488
491
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
489
492
  await ctx.context.runInBackgroundOrAwait(canSendConfirmation({
490
493
  user: ctx.context.session.user,
@@ -498,7 +501,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
498
501
  ctx.context.logger.error("Verification email isn't enabled.");
499
502
  throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
500
503
  }
501
- const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-verification" });
504
+ const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
505
+ requestType: "change-email-verification",
506
+ userId: ctx.context.session.user.id
507
+ });
502
508
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
503
509
  await ctx.context.runInBackgroundOrAwait(canSendVerification({
504
510
  user: {
@@ -0,0 +1,10 @@
1
+ //#region src/api/state/session-source.d.ts
2
+ /**
3
+ * Marks the request session with `token` as built without a session-store
4
+ * row. Checks that re-read the store to catch a concurrent revocation skip
5
+ * such sessions.
6
+ *
7
+ * @internal Shared by Better Auth packages; not a supported public API.
8
+ */
9
+ export declare function markSessionOutsideStore(token: string): Promise<void>;
10
+ //#endregion
@@ -0,0 +1,28 @@
1
+ import { defineRequestState } from "@better-auth/core/context";
2
+ //#region src/api/state/session-source.ts
3
+ /**
4
+ * Tokens of request sessions that a plugin built without a session-store
5
+ * row, such as one authenticated by an API key on every request.
6
+ */
7
+ const { get: getTokensOutsideStore } = defineRequestState(() => /* @__PURE__ */ new Set());
8
+ /**
9
+ * Marks the request session with `token` as built without a session-store
10
+ * row. Checks that re-read the store to catch a concurrent revocation skip
11
+ * such sessions.
12
+ *
13
+ * @internal Shared by Better Auth packages; not a supported public API.
14
+ */
15
+ async function markSessionOutsideStore(token) {
16
+ (await getTokensOutsideStore()).add(token);
17
+ }
18
+ /**
19
+ * Whether the request session with `token` was marked by
20
+ * `markSessionOutsideStore`. Outside a request, nothing is marked.
21
+ *
22
+ * @internal Shared by Better Auth packages; not a supported public API.
23
+ */
24
+ async function isSessionOutsideStore(token) {
25
+ return (await getTokensOutsideStore().catch(() => null))?.has(token) ?? false;
26
+ }
27
+ //#endregion
28
+ export { isSessionOutsideStore, markSessionOutsideStore };
@@ -4,6 +4,19 @@ import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
4
4
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
5
5
  const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
6
6
  const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
7
+ const IP_WILDCARD_HOST_PATTERN = /^(?=.*\d)[\d*?.]+$/;
8
+ const IPV4_HOST_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
9
+ /**
10
+ * A wildcard host made only of digits, dots and wildcards (`192.168.*.*`,
11
+ * `10.*`) names an address range, so it admits only an IPv4 address. A plain
12
+ * `*` would otherwise cross `.` and let `192.168.attacker.com` match. Other
13
+ * hosts keep the documented subdomain matching.
14
+ */
15
+ const isAllowedIPv4Host = (patternAuthority, authority) => {
16
+ const patternHost = patternAuthority.split(":")[0];
17
+ if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
18
+ return IPV4_HOST_PATTERN.test(authority.split(":")[0]);
19
+ };
7
20
  /**
8
21
  * Resolves `.` and `..` segments in a path after percent-decoding so a
9
22
  * path-pinned pattern cannot be bypassed with traversal: e.g.
@@ -33,7 +46,7 @@ const normalizePath = (path) => {
33
46
  * host); the path is percent-decoded and resolved so traversal cannot bypass
34
47
  * a path-pinned pattern.
35
48
  */
36
- const parseCustomSchemeOrigin = (value) => {
49
+ const parseCustomSchemeOrigin = (value, wildcardPattern = false) => {
37
50
  if (CONTROL_CHARACTER_PATTERN.test(value)) return null;
38
51
  const schemeEnd = value.indexOf(":");
39
52
  if (schemeEnd <= 0) return null;
@@ -42,7 +55,7 @@ const parseCustomSchemeOrigin = (value) => {
42
55
  let authority = "";
43
56
  if (rest.startsWith("//")) {
44
57
  rest = rest.slice(2);
45
- const authorityEnd = rest.search(/[/?#]/);
58
+ const authorityEnd = rest.search(wildcardPattern ? /[/#]/ : /[/?#]/);
46
59
  if (authorityEnd === -1) {
47
60
  authority = rest;
48
61
  rest = "";
@@ -51,7 +64,7 @@ const parseCustomSchemeOrigin = (value) => {
51
64
  rest = rest.slice(authorityEnd);
52
65
  }
53
66
  }
54
- const pathEnd = rest.search(/[?#]/);
67
+ const pathEnd = rest.search(wildcardPattern ? /#/ : /[?#]/);
55
68
  const path = normalizePath(pathEnd === -1 ? rest : rest.slice(0, pathEnd));
56
69
  return {
57
70
  scheme,
@@ -87,17 +100,27 @@ const isSafeRelativeURL = (value) => {
87
100
  */
88
101
  const matchesOriginPattern = (url, pattern, settings) => {
89
102
  if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
90
- if (pattern.includes("*") || pattern.includes("?")) {
91
- if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
103
+ const hasWildcard = pattern.includes("*") || pattern.includes("?");
104
+ if (hasWildcard && !pattern.includes("://")) {
92
105
  const host = getHost(url);
93
106
  if (!host) return false;
94
- return wildcardMatch(pattern)(host);
107
+ return wildcardMatch(pattern)(host) && isAllowedIPv4Host(pattern, host);
95
108
  }
96
109
  const protocol = getProtocol(url);
97
- if (protocol === "http:" || protocol === "https:" || !protocol) return pattern === getOrigin(url);
110
+ if (protocol === "http:" || protocol === "https:" || !protocol) {
111
+ if (!hasWildcard) return pattern === getOrigin(url);
112
+ const origin = getOrigin(url);
113
+ if (origin === null || !wildcardMatch(pattern)(origin)) return false;
114
+ return isAllowedIPv4Host(pattern.slice(pattern.indexOf("://") + 3), origin.slice(origin.indexOf("://") + 3));
115
+ }
98
116
  const parsed = parseCustomSchemeOrigin(url);
99
- const parsedPattern = parseCustomSchemeOrigin(pattern);
117
+ const parsedPattern = parseCustomSchemeOrigin(pattern, hasWildcard);
100
118
  if (!parsed || !parsedPattern || parsed.scheme !== parsedPattern.scheme) return false;
119
+ if (hasWildcard) {
120
+ if (parsed.authority.includes("@")) return false;
121
+ if (parsedPattern.authority && !(wildcardMatch(parsedPattern.authority)(parsed.authority) && isAllowedIPv4Host(parsedPattern.authority, parsed.authority))) return false;
122
+ return !parsedPattern.path || wildcardMatch(parsedPattern.path)(parsed.path || "/");
123
+ }
101
124
  if (parsedPattern.authority && parsed.authority !== parsedPattern.authority) return false;
102
125
  if (!parsedPattern.path) return true;
103
126
  return parsed.path === parsedPattern.path || parsed.path.startsWith(`${parsedPattern.path}/`);
@@ -16,7 +16,8 @@ import { OrganizationOptions } from "../plugins/organization/types.mjs";
16
16
  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";
17
17
  import { getOrgAdapter } from "../plugins/organization/adapter.mjs";
18
18
  import { hasPermission } from "../plugins/organization/has-permission.mjs";
19
- import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "../plugins/organization/organization.mjs";
19
+ import { parseRoles } from "../plugins/organization/roles.mjs";
20
+ import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization } from "../plugins/organization/organization.mjs";
20
21
  import "../plugins/organization/index.mjs";
21
22
  import { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
22
23
  import { DBPrimitive } from "@better-auth/core/db";
@@ -1,10 +1,4 @@
1
1
  //#region src/client/parser.ts
2
- const PROTO_POLLUTION_PATTERNS = {
3
- proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,
4
- constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,
5
- protoShort: /"__proto__"\s*:/,
6
- constructorShort: /"constructor"\s*:/
7
- };
8
2
  const JSON_SIGNATURE = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
9
3
  const SPECIAL_VALUES = {
10
4
  true: true,
@@ -40,14 +34,10 @@ function betterJSONParse(value, options = {}) {
40
34
  if (strict) throw new SyntaxError("[better-json] Invalid JSON");
41
35
  return value;
42
36
  }
43
- if (Object.entries(PROTO_POLLUTION_PATTERNS).some(([key, pattern]) => {
44
- const matches = pattern.test(trimmed);
45
- if (matches && warnings) console.warn(`[better-json] Detected potential prototype pollution attempt using ${key} pattern`);
46
- return matches;
47
- }) && strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
48
37
  try {
49
38
  const secureReviver = (key, value) => {
50
39
  if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
40
+ if (strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
51
41
  if (warnings) console.warn(`[better-json] Dropping "${key}" key to prevent prototype pollution`);
52
42
  return;
53
43
  }
@@ -1,3 +1,4 @@
1
+ import { trimTrailingSlashes } from "../../utils/url.mjs";
1
2
  import { getClientConfig } from "../config.mjs";
2
3
  import { createDynamicPathProxy } from "../proxy.mjs";
3
4
  import { useStore } from "./vue-store.mjs";
@@ -16,18 +17,23 @@ function toHeadersInit(headers) {
16
17
  }
17
18
  function createAuthClient(options) {
18
19
  const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
19
- const sessionCacheKey = [
20
- "better-auth",
21
- "session",
22
- options?.baseURL || "inferred",
23
- options?.basePath ?? "/api/auth"
24
- ].join(":");
20
+ const baseURLKey = options?.baseURL || "inferred";
21
+ const basePath = options?.basePath ?? "/api/auth";
22
+ const sessionCacheKey = `better-auth:session:${baseURLKey}:${basePath}`;
23
+ const sessionPath = trimTrailingSlashes(basePath.startsWith("/") ? basePath : `/${basePath}`);
24
+ /**
25
+ * Stays relative without an explicit `baseURL` so the SSR session fetch
26
+ * forwards the incoming cookies.
27
+ *
28
+ * @see https://nuxt.com/docs/4.x/getting-started/data-fetching
29
+ */
30
+ const sessionURL = `${options?.baseURL ? baseURL : sessionPath}/get-session`;
25
31
  const resolvedHooks = {};
26
32
  for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
27
33
  function useSession(useFetch) {
28
34
  if (useFetch) {
29
35
  const sessionSignal = useStore($sessionSignal);
30
- return useFetch(`${baseURL}/get-session`, {
36
+ return useFetch(sessionURL, {
31
37
  headers: toHeadersInit(options?.fetchOptions?.headers),
32
38
  key: sessionCacheKey,
33
39
  watch: [sessionSignal]
@@ -12,12 +12,12 @@ function estimateEntropy(str) {
12
12
  }
13
13
  function parseSecretsEnv(envValue) {
14
14
  if (!envValue) return null;
15
- return envValue.split(",").map((entry) => {
15
+ return envValue.split(",").map((entry, index) => {
16
16
  entry = entry.trim();
17
17
  const colonIdx = entry.indexOf(":");
18
- if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry: "${entry}". Expected format: "<version>:<secret>"`);
18
+ if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry at position ${index + 1}. Expected format: "<version>:<secret>"`);
19
19
  const version = parseInt(entry.slice(0, colonIdx), 10);
20
- if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS: "${entry.slice(0, colonIdx)}". Version must be a non-negative integer.`);
20
+ if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS entry at position ${index + 1}. Version must be a non-negative integer.`);
21
21
  const value = entry.slice(colonIdx + 1).trim();
22
22
  if (!value) throw new BetterAuthError(`Empty secret value for version ${version} in BETTER_AUTH_SECRETS.`);
23
23
  return {
@@ -78,8 +78,18 @@ const createInternalAdapter = (adapter, ctx) => {
78
78
  const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
79
79
  return activeSessions ? safeJSONParse(activeSessions) || [] : [];
80
80
  }
81
- async function deleteCachedUserSessions(userId, sessionReferences) {
81
+ const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
82
+ const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
83
+ async function isRevokedSession(session, revokedBeforeByUser) {
84
+ if (!secondaryStorage) return false;
85
+ const revokedBefore = revokedBeforeByUser?.get(session.userId) ?? Number(await secondaryStorage.get(revokedBeforeKey(session.userId)));
86
+ revokedBeforeByUser?.set(session.userId, revokedBefore);
87
+ return new Date(session.createdAt).getTime() < revokedBefore;
88
+ }
89
+ async function deleteCachedUserSessions(userId, sessionReferences, revokedAt) {
82
90
  if (!secondaryStorage) return;
91
+ const previousRevokedBefore = Number(await secondaryStorage.get(revokedBeforeKey(userId)));
92
+ await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
83
93
  const deletedTokens = new Set(sessionReferences.map((session) => session.token));
84
94
  for (const { token } of sessionReferences) await secondaryStorage.delete(token);
85
95
  const activeSessionsKey = `active-sessions-${userId}`;
@@ -97,7 +107,8 @@ const createInternalAdapter = (adapter, ctx) => {
97
107
  async function queueCachedUserSessionDeletion(userId, sessionReferences) {
98
108
  if (!secondaryStorage) return;
99
109
  const references = sessionReferences ?? await getActiveSessionReferences(userId);
100
- await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references), { onError(error) {
110
+ const revokedAt = Date.now();
111
+ await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references, revokedAt), { onError(error) {
101
112
  logger.error("Failed to delete committed user sessions from secondary storage", error);
102
113
  } });
103
114
  }
@@ -194,6 +205,7 @@ const createInternalAdapter = (adapter, ctx) => {
194
205
  const now = Date.now();
195
206
  const seenTokens = /* @__PURE__ */ new Set();
196
207
  const sessions = [];
208
+ const revokedBeforeByUser = /* @__PURE__ */ new Map();
197
209
  for (const { token, expiresAt } of list) {
198
210
  if (expiresAt <= now || seenTokens.has(token)) continue;
199
211
  seenTokens.add(token);
@@ -201,7 +213,7 @@ const createInternalAdapter = (adapter, ctx) => {
201
213
  if (!data) continue;
202
214
  try {
203
215
  const parsed = typeof data === "string" ? JSON.parse(data) : data;
204
- if (!parsed?.session) continue;
216
+ if (!parsed?.session || await isRevokedSession(parsed.session, revokedBeforeByUser)) continue;
205
217
  sessions.push(parseSessionOutput(ctx.options, {
206
218
  ...parsed.session,
207
219
  expiresAt: new Date(parsed.session.expiresAt)
@@ -312,7 +324,10 @@ const createInternalAdapter = (adapter, ctx) => {
312
324
  });
313
325
  const sessionTTL = getTTLSeconds(data.expiresAt, now);
314
326
  if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({
315
- session: sessionData,
327
+ session: {
328
+ ...sessionData,
329
+ createdAt: data.createdAt
330
+ },
316
331
  user
317
332
  }), sessionTTL);
318
333
  return sessionData;
@@ -336,7 +351,7 @@ const createInternalAdapter = (adapter, ctx) => {
336
351
  if (!sessionStringified && (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase)) return null;
337
352
  if (sessionStringified) {
338
353
  const s = safeJSONParse(sessionStringified);
339
- if (!s) return null;
354
+ if (!s || await isRevokedSession(s.session)) return null;
340
355
  return {
341
356
  session: parseSessionOutput(ctx.options, {
342
357
  ...s.session,
@@ -372,11 +387,12 @@ const createInternalAdapter = (adapter, ctx) => {
372
387
  const uniqueTokens = [...new Set(sessionTokens)];
373
388
  const sessionsByToken = /* @__PURE__ */ new Map();
374
389
  const missingTokens = [];
390
+ const revokedBeforeByUser = /* @__PURE__ */ new Map();
375
391
  if (secondaryStorage) for (const sessionToken of uniqueTokens) {
376
392
  const sessionStringified = await secondaryStorage.get(sessionToken);
377
393
  if (sessionStringified) try {
378
394
  const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified;
379
- if (!s) continue;
395
+ if (!s || await isRevokedSession(s.session, revokedBeforeByUser)) continue;
380
396
  const expiresAt = new Date(s.session.expiresAt);
381
397
  if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) continue;
382
398
  const session = {
@@ -590,6 +606,8 @@ const createInternalAdapter = (adapter, ctx) => {
590
606
  join: { ...options?.includeAccounts ? { account: true } : {} }
591
607
  });
592
608
  if (!result) return null;
609
+ const normalize = (value) => value.normalize("NFC").toLowerCase();
610
+ if (normalize(result.email) !== normalize(email)) return null;
593
611
  const { account: accounts, ...user } = result;
594
612
  return {
595
613
  user,