better-auth 1.8.0-beta.4 → 1.8.0-beta.5

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 (64) hide show
  1. package/dist/api/dispatch.d.mts +1 -0
  2. package/dist/api/dispatch.mjs +30 -7
  3. package/dist/api/index.mjs +1 -1
  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/auth/trusted-origins.mjs +31 -8
  10. package/dist/client/index.d.mts +2 -1
  11. package/dist/db/internal-adapter.mjs +24 -6
  12. package/dist/index.d.mts +2 -2
  13. package/dist/index.mjs +2 -2
  14. package/dist/oauth2/link-account.d.mts +3 -5
  15. package/dist/oauth2/link-account.mjs +33 -3
  16. package/dist/oauth2/state.d.mts +3 -4
  17. package/dist/package.mjs +1 -1
  18. package/dist/plugins/admin/access/statement.d.mts +10 -10
  19. package/dist/plugins/admin/access/statement.mjs +2 -0
  20. package/dist/plugins/admin/admin.d.mts +4 -3
  21. package/dist/plugins/admin/client.d.mts +5 -4
  22. package/dist/plugins/admin/error-codes.d.mts +1 -0
  23. package/dist/plugins/admin/error-codes.mjs +1 -0
  24. package/dist/plugins/admin/routes.mjs +8 -0
  25. package/dist/plugins/anonymous/index.mjs +1 -1
  26. package/dist/plugins/device-authorization/routes.mjs +3 -3
  27. package/dist/plugins/email-otp/routes.mjs +33 -50
  28. package/dist/plugins/generic-oauth/index.mjs +54 -30
  29. package/dist/plugins/index.d.mts +2 -1
  30. package/dist/plugins/index.mjs +2 -1
  31. package/dist/plugins/oauth-proxy/index.mjs +1 -1
  32. package/dist/plugins/one-tap/index.mjs +1 -1
  33. package/dist/plugins/organization/adapter.mjs +1 -0
  34. package/dist/plugins/organization/client.d.mts +1 -0
  35. package/dist/plugins/organization/error-codes.d.mts +1 -0
  36. package/dist/plugins/organization/error-codes.mjs +1 -0
  37. package/dist/plugins/organization/has-permission.d.mts +2 -3
  38. package/dist/plugins/organization/has-permission.mjs +62 -23
  39. package/dist/plugins/organization/index.d.mts +2 -1
  40. package/dist/plugins/organization/index.mjs +2 -1
  41. package/dist/plugins/organization/organization.d.mts +2 -2
  42. package/dist/plugins/organization/organization.mjs +1 -3
  43. package/dist/plugins/organization/permission.mjs +3 -3
  44. package/dist/plugins/organization/roles.d.mts +8 -0
  45. package/dist/plugins/organization/roles.mjs +26 -0
  46. package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
  47. package/dist/plugins/organization/routes/crud-invites.d.mts +22 -0
  48. package/dist/plugins/organization/routes/crud-invites.mjs +28 -13
  49. package/dist/plugins/organization/routes/crud-members.mjs +10 -14
  50. package/dist/plugins/organization/routes/crud-org.mjs +12 -3
  51. package/dist/plugins/phone-number/client.d.mts +2 -0
  52. package/dist/plugins/phone-number/error-codes.d.mts +2 -0
  53. package/dist/plugins/phone-number/error-codes.mjs +2 -0
  54. package/dist/plugins/phone-number/index.d.mts +25 -1
  55. package/dist/plugins/phone-number/index.mjs +22 -6
  56. package/dist/plugins/phone-number/routes.mjs +128 -62
  57. package/dist/plugins/phone-number/types.d.mts +11 -5
  58. package/dist/plugins/two-factor/index.mjs +7 -4
  59. package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
  60. package/dist/plugins/username/index.mjs +1 -1
  61. package/dist/state.d.mts +6 -0
  62. package/dist/state.mjs +8 -2
  63. package/dist/utils/index.d.mts +2 -2
  64. package/package.json +9 -9
@@ -1,5 +1,6 @@
1
1
  import { AuthContext } from "@better-auth/core";
2
2
  import { Endpoint, EndpointContext, InputContext } from "better-call";
3
+ import "@better-auth/core/api";
3
4
  //#region src/api/dispatch.d.ts
4
5
  /**
5
6
  * Input accepted by {@link dispatchAuthEndpoint}. `context` must already be a
@@ -6,6 +6,7 @@ import { APIError } from "@better-auth/core/error";
6
6
  import { createDefu } from "defu";
7
7
  import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_ROUTE, ATTR_OPERATION_ID, createWithSpan } from "@better-auth/core/instrumentation";
8
8
  import { kAPIErrorHeaderSymbol, toResponse } from "better-call";
9
+ import { getInternalAuthCompletion } from "@better-auth/core/utils/auth-completion";
9
10
  //#region src/api/dispatch.ts
10
11
  const defuReplaceArrays = createDefu((obj, key, value) => {
11
12
  if (Array.isArray(obj[key]) && Array.isArray(value)) {
@@ -14,6 +15,18 @@ const defuReplaceArrays = createDefu((obj, key, value) => {
14
15
  }
15
16
  });
16
17
  const hooksSourceWeakMap = /* @__PURE__ */ new WeakMap();
18
+ const signInGates = /* @__PURE__ */ new WeakSet();
19
+ /**
20
+ * Runs a plugin after hook ahead of every other plugin after hook, so a sign-in
21
+ * it withholds (such as a pending two-factor challenge) is never observed as
22
+ * complete, regardless of plugin registration order.
23
+ *
24
+ * @internal
25
+ */
26
+ function asSignInGate(hook) {
27
+ signInGates.add(hook.handler);
28
+ return hook;
29
+ }
17
30
  /**
18
31
  * Resolves the operation id used for spans, preferring an explicit
19
32
  * `operationId`, then the OpenAPI one, then the caller's `fallback` (the
@@ -163,12 +176,19 @@ function getHooks(authContext) {
163
176
  const pluginAfterHooks = plugins.flatMap((plugin) => (plugin.hooks?.after ?? []).map((h) => {
164
177
  hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
165
178
  return h;
166
- }));
179
+ })).sort((a, b) => Number(signInGates.has(b.handler)) - Number(signInGates.has(a.handler)));
180
+ const completionHooks = plugins.flatMap((plugin) => {
181
+ const completion = getInternalAuthCompletion(plugin);
182
+ if (!completion) return [];
183
+ hooksSourceWeakMap.set(completion.handler, `plugin:${plugin.id}`);
184
+ return [completion];
185
+ });
167
186
  if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks);
168
187
  if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks);
169
188
  return {
170
189
  beforeHooks,
171
- afterHooks
190
+ afterHooks,
191
+ completionHooks
172
192
  };
173
193
  }
174
194
  /**
@@ -208,7 +228,7 @@ async function dispatchAuthEndpoint(endpoint, input) {
208
228
  [ATTR_HTTP_ROUTE]: route,
209
229
  [ATTR_OPERATION_ID]: operationId
210
230
  }, async () => runWithEndpointContext(internalContext, async () => {
211
- const { beforeHooks, afterHooks } = getHooks(internalContext.context);
231
+ const { beforeHooks, afterHooks, completionHooks } = getHooks(internalContext.context);
212
232
  const before = await runBeforeHooks(internalContext, beforeHooks, endpoint, operationId);
213
233
  if ("context" in before && before.context && typeof before.context === "object") {
214
234
  const { headers, ...rest } = before.context;
@@ -244,9 +264,12 @@ async function dispatchAuthEndpoint(endpoint, input) {
244
264
  if (result instanceof Response) return result;
245
265
  internalContext.context.returned = result.response;
246
266
  mergeResponseHeaders(internalContext.context, result.headers);
247
- const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
248
- if (after.response !== void 0) result.response = after.response;
249
- result.headers = after.headers ?? result.headers;
267
+ await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
268
+ const matchingCompletions = completionHooks.filter((hook) => hook.matcher(internalContext));
269
+ if (matchingCompletions.length > 1) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Multiple authentication completion handlers matched." });
270
+ const completed = await runAfterHooks(internalContext, matchingCompletions, endpoint, operationId);
271
+ if (completed.response !== void 0) result.response = completed.response;
272
+ result.headers = completed.headers ?? result.headers;
250
273
  if (isAPIError(result.response) && shouldPublishLog(internalContext.context.logger.level, "debug")) result.response.stack = result.response.errorStack;
251
274
  if (isAPIError(result.response) && !shouldReturnResponse) {
252
275
  if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
@@ -274,4 +297,4 @@ async function dispatchAuthEndpoint(endpoint, input) {
274
297
  }));
275
298
  }
276
299
  //#endregion
277
- export { dispatchAuthEndpoint, getOperationId };
300
+ export { asSignInGate, dispatchAuthEndpoint, getOperationId };
@@ -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";
@@ -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: isStateful(c) ? await symmetricEncrypt({
246
+ key: derivePurposeKey(c.context.secretConfig, "oauth-link-session-token"),
247
+ data: session.session.token
248
+ }) : void 0
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({
@@ -251,7 +251,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
251
251
  const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
252
252
  const request = safeCloneRequest(ctx.request);
253
253
  await queueAfterTransactionHook(async () => {
254
- const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
254
+ const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
255
255
  const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
256
256
  await ctx.context.runInBackgroundOrAwait(sendVerificationEmail({
257
257
  user,
@@ -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: {
@@ -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";
@@ -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,
package/dist/index.d.mts CHANGED
@@ -7,8 +7,8 @@ import { InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPlug
7
7
  import { Auth } from "./types/auth.mjs";
8
8
  import { BetterAuthAdvancedOptions, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource } from "./types/index.mjs";
9
9
  import { betterAuth } from "./auth/full.mjs";
10
+ import { StateData, generateGenericState, getAuthStateVerificationIdentifier, parseGenericState } from "./state.mjs";
10
11
  import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
11
- import { StateData, generateGenericState, parseGenericState } from "./state.mjs";
12
12
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
13
13
  import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
14
14
  import "./utils/index.mjs";
@@ -28,4 +28,4 @@ export * from "@better-auth/core/utils/json";
28
28
  export * from "@better-auth/core/social-providers";
29
29
  export type * from "better-call";
30
30
  export type * from "zod";
31
- export { APIError, type Account, type AdditionalSessionFieldsInput, type AdditionalUserFieldsInput, type Auth, type BetterAuthAdvancedOptions, type BetterAuthClientOptions, type BetterAuthClientPlugin, type BetterAuthCookies, type BetterAuthOptions, type BetterAuthPlugin, type BetterAuthRateLimitOptions, type ClientAtomListener, type ClientStore, type DBAdapter, type DBAdapterInstance, type DBAdapterSchemaCreation, type DBTransactionAdapter, type ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, type HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, type InferOptionSchema, type InferPluginContext, type InferPluginErrorCodes, type InferPluginFieldFromTuple, type InferPluginIDs, type InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, type IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, type JoinConfig, type JoinOption, type OverrideMerge, type Prettify, type PrettifyDeep, type RateLimit, type RequiredKeysOf, type Session, SessionQueryParams, type StandardSchemaV1, type StateData, type StoreIdentifierOption, type StripEmptyObjects, type TelemetryEvent, type UnionToIntersection, type User, type UserProvisioningSource, type ValidateUserInfoAction, type ValidateUserInfoMethod, type ValidateUserInfoOAuthInfo, type ValidateUserInfoResult, type ValidateUserInfoSSOInfo, type ValidateUserInfoSource, type Verification, type Where, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
31
+ export { APIError, type Account, type AdditionalSessionFieldsInput, type AdditionalUserFieldsInput, type Auth, type BetterAuthAdvancedOptions, type BetterAuthClientOptions, type BetterAuthClientPlugin, type BetterAuthCookies, type BetterAuthOptions, type BetterAuthPlugin, type BetterAuthRateLimitOptions, type ClientAtomListener, type ClientStore, type DBAdapter, type DBAdapterInstance, type DBAdapterSchemaCreation, type DBTransactionAdapter, type ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, type HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, type InferOptionSchema, type InferPluginContext, type InferPluginErrorCodes, type InferPluginFieldFromTuple, type InferPluginIDs, type InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, type IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, type JoinConfig, type JoinOption, type OverrideMerge, type Prettify, type PrettifyDeep, type RateLimit, type RequiredKeysOf, type Session, SessionQueryParams, type StandardSchemaV1, type StateData, type StoreIdentifierOption, type StripEmptyObjects, type TelemetryEvent, type UnionToIntersection, type User, type UserProvisioningSource, type ValidateUserInfoAction, type ValidateUserInfoMethod, type ValidateUserInfoOAuthInfo, type ValidateUserInfoResult, type ValidateUserInfoSSOInfo, type ValidateUserInfoSource, type Verification, type Where, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getAuthStateVerificationIdentifier, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
2
- import { generateGenericState, parseGenericState } from "./state.mjs";
2
+ import { generateGenericState, getAuthStateVerificationIdentifier, parseGenericState } from "./state.mjs";
3
3
  import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
4
4
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
5
5
  import { APIError } from "./api/index.mjs";
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
14
14
  export * from "@better-auth/core/utils/error-codes";
15
15
  export * from "@better-auth/core/utils/id";
16
16
  export * from "@better-auth/core/utils/json";
17
- export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
17
+ export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getAuthStateVerificationIdentifier, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
@@ -1,5 +1,6 @@
1
1
  import { Account, User } from "../types/models.mjs";
2
2
  import "../types/index.mjs";
3
+ import { StateData } from "../state.mjs";
3
4
  import { GenericEndpointContext, UserProvisioningSource } from "@better-auth/core";
4
5
  //#region src/oauth2/link-account.d.ts
5
6
  type OAuthAccountData = Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
@@ -16,10 +17,7 @@ type LinkedProviderProfile = {
16
17
  image?: string | null | undefined;
17
18
  };
18
19
  interface LinkOAuthAccountOptions {
19
- link: {
20
- userId: string;
21
- email: string;
22
- };
20
+ link: NonNullable<StateData["link"]>;
23
21
  userInfo: LinkedProviderProfile;
24
22
  account: OAuthAccountData;
25
23
  profile: Record<string, unknown>;
@@ -35,7 +33,7 @@ interface LinkOAuthAccountFailure {
35
33
  type LinkOAuthAccountResult = {
36
34
  linked: true;
37
35
  } | LinkOAuthAccountFailure;
38
- export declare function linkOAuthAccount(c: GenericEndpointContext, { link, userInfo, account, profile, scopes }: LinkOAuthAccountOptions): Promise<LinkOAuthAccountResult>;
36
+ export declare function linkOAuthAccount(c: GenericEndpointContext, opts: LinkOAuthAccountOptions): Promise<LinkOAuthAccountResult>;
39
37
  export declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
40
38
  userInfo: Omit<User, "createdAt" | "updatedAt">;
41
39
  account: OAuthAccountData;