better-auth 1.7.1 → 1.7.3

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 (77) hide show
  1. package/dist/api/index.d.mts +6 -22
  2. package/dist/api/index.mjs +2 -0
  3. package/dist/api/middlewares/origin-check.mjs +9 -5
  4. package/dist/api/routes/account.d.mts +1 -9
  5. package/dist/api/routes/account.mjs +0 -6
  6. package/dist/api/routes/callback.mjs +7 -7
  7. package/dist/api/routes/email-verification.mjs +4 -2
  8. package/dist/api/routes/error.mjs +12 -8
  9. package/dist/api/routes/password.mjs +0 -2
  10. package/dist/api/routes/session.mjs +12 -5
  11. package/dist/api/routes/sign-in.d.mts +2 -2
  12. package/dist/api/routes/sign-in.mjs +1 -4
  13. package/dist/api/routes/sign-up.mjs +0 -2
  14. package/dist/api/routes/update-user.mjs +0 -2
  15. package/dist/api/to-auth-endpoints.mjs +2 -0
  16. package/dist/auth/base.mjs +13 -1
  17. package/dist/auth/trusted-origins.mjs +24 -5
  18. package/dist/client/config.d.mts +2 -0
  19. package/dist/client/config.mjs +1 -0
  20. package/dist/client/lynx/index.d.mts +1 -1
  21. package/dist/client/react/index.d.mts +1 -1
  22. package/dist/client/solid/index.d.mts +1 -1
  23. package/dist/client/svelte/index.d.mts +1 -1
  24. package/dist/client/vanilla.d.mts +1 -1
  25. package/dist/client/vue/index.d.mts +32 -21
  26. package/dist/client/vue/index.mjs +23 -5
  27. package/dist/context/create-context.mjs +2 -0
  28. package/dist/cookies/cache.mjs +10 -2
  29. package/dist/cookies/session-store.d.mts +0 -1
  30. package/dist/cookies/session-store.mjs +16 -5
  31. package/dist/db/adapter-kysely.mjs +1 -2
  32. package/dist/db/get-migration.d.mts +1 -0
  33. package/dist/db/get-migration.mjs +49 -56
  34. package/dist/db/internal-adapter.mjs +17 -20
  35. package/dist/db/schema.d.mts +0 -1
  36. package/dist/db/with-hooks.mjs +7 -7
  37. package/dist/oauth2/account-key.mjs +1 -5
  38. package/dist/oauth2/errors.mjs +4 -3
  39. package/dist/oauth2/link-account.d.mts +0 -1
  40. package/dist/oauth2/link-account.mjs +5 -11
  41. package/dist/oauth2/state.mjs +1 -1
  42. package/dist/package.mjs +1 -1
  43. package/dist/plugins/admin/routes.mjs +1 -4
  44. package/dist/plugins/anonymous/index.mjs +5 -1
  45. package/dist/plugins/anonymous/types.d.mts +2 -3
  46. package/dist/plugins/device-authorization/index.d.mts +11 -11
  47. package/dist/plugins/email-otp/routes.mjs +0 -2
  48. package/dist/plugins/generic-oauth/index.mjs +14 -10
  49. package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
  50. package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
  51. package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
  52. package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +15 -5
  53. package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
  54. package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
  55. package/dist/plugins/generic-oauth/types.d.mts +0 -9
  56. package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
  57. package/dist/plugins/haveibeenpwned/index.mjs +33 -13
  58. package/dist/plugins/index.d.mts +2 -2
  59. package/dist/plugins/index.mjs +2 -2
  60. package/dist/plugins/jwt/verify.mjs +2 -2
  61. package/dist/plugins/last-login-method/index.mjs +1 -0
  62. package/dist/plugins/oauth-proxy/index.d.mts +17 -0
  63. package/dist/plugins/oauth-proxy/index.mjs +145 -99
  64. package/dist/plugins/one-tap/index.mjs +0 -1
  65. package/dist/plugins/open-api/generator.mjs +15 -1
  66. package/dist/plugins/organization/has-permission.mjs +4 -2
  67. package/dist/plugins/phone-number/routes.mjs +0 -2
  68. package/dist/plugins/siwe/index.mjs +5 -6
  69. package/dist/plugins/two-factor/client.d.mts +1 -0
  70. package/dist/plugins/two-factor/error-code.d.mts +1 -0
  71. package/dist/plugins/two-factor/error-code.mjs +1 -0
  72. package/dist/plugins/two-factor/index.d.mts +1 -0
  73. package/dist/plugins/two-factor/index.mjs +3 -2
  74. package/dist/state.d.mts +0 -13
  75. package/dist/test-utils/http-test-instance.d.mts +1 -1
  76. package/dist/test-utils/test-instance.mjs +27 -10
  77. package/package.json +10 -10
@@ -3,8 +3,7 @@ import { getDate } from "../utils/date.mjs";
3
3
  import { assertValidUserInfo, assertValidUserInfoSource } from "../utils/validate-user-info.mjs";
4
4
  import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs";
5
5
  import { getWithHooks } from "./with-hooks.mjs";
6
- import { getCurrentAdapter, getCurrentAuthContext, queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
7
- import { createLocalAccountIssuer } from "@better-auth/core/db";
6
+ import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransactionHook, runWithTransaction, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
8
7
  import { APIError, BetterAuthError } from "@better-auth/core/error";
9
8
  import { generateId } from "@better-auth/core/utils/id";
10
9
  import { safeJSONParse } from "@better-auth/core/utils/json";
@@ -153,7 +152,7 @@ const createInternalAdapter = (adapter, ctx) => {
153
152
  assertValidUserInfoSource(validationSource);
154
153
  let endpointContext;
155
154
  try {
156
- endpointContext = await getCurrentAuthContext();
155
+ endpointContext = getCurrentAuthEndpointContext();
157
156
  } catch (error) {
158
157
  logger.error("Unable to run validateUserInfo: missing endpoint context", error);
159
158
  throw new APIError("FORBIDDEN", {
@@ -247,7 +246,7 @@ const createInternalAdapter = (adapter, ctx) => {
247
246
  },
248
247
  createSession: async (userId, dontRememberMe, override, overrideAll, storageOptions) => {
249
248
  const headers = await (async () => {
250
- const ctx = await getCurrentAuthContext().catch(() => null);
249
+ const ctx = tryGetCurrentAuthEndpointContext();
251
250
  return ctx?.headers || ctx?.request?.headers;
252
251
  })();
253
252
  const storeInDb = options.session?.storeSessionInDatabase;
@@ -540,18 +539,21 @@ const createInternalAdapter = (adapter, ctx) => {
540
539
  operator: "in"
541
540
  }], "session", void 0);
542
541
  },
543
- findAccountOwnerByKey: async ({ issuer, accountId }) => {
544
- const accountWithUser = await (await getCurrentAdapter(adapter)).findOne({
542
+ findAccountOwnerByKey: async ({ providerId, accountId }) => {
543
+ const accountsWithUsers = await (await getCurrentAdapter(adapter)).findMany({
545
544
  model: "account",
546
545
  where: [{
547
- field: "issuer",
548
- value: issuer
546
+ field: "providerId",
547
+ value: providerId
549
548
  }, {
550
549
  field: "accountId",
551
550
  value: accountId
552
551
  }],
552
+ limit: 2,
553
553
  join: { user: true }
554
554
  });
555
+ if (accountsWithUsers.length > 1) throw new BetterAuthError(`Multiple accounts match the same accountId for provider ${JSON.stringify(providerId)}. Resolve duplicate account identities before continuing.`);
556
+ const accountWithUser = accountsWithUsers[0];
555
557
  if (!accountWithUser) return null;
556
558
  const { user, ...account } = accountWithUser;
557
559
  return user ? {
@@ -632,10 +634,6 @@ const createInternalAdapter = (adapter, ctx) => {
632
634
  field: "providerId",
633
635
  value: "credential"
634
636
  },
635
- {
636
- field: "issuer",
637
- value: createLocalAccountIssuer("credential")
638
- },
639
637
  {
640
638
  field: "accountId",
641
639
  value: userId
@@ -663,10 +661,6 @@ const createInternalAdapter = (adapter, ctx) => {
663
661
  field: "providerId",
664
662
  value: "credential"
665
663
  },
666
- {
667
- field: "issuer",
668
- value: createLocalAccountIssuer("credential")
669
- },
670
664
  {
671
665
  field: "accountId",
672
666
  value: userId
@@ -674,17 +668,20 @@ const createInternalAdapter = (adapter, ctx) => {
674
668
  ]
675
669
  });
676
670
  },
677
- findAccountByKey: async ({ issuer, accountId }) => {
678
- return await (await getCurrentAdapter(adapter)).findOne({
671
+ findAccountByKey: async ({ providerId, accountId }) => {
672
+ const accounts = await (await getCurrentAdapter(adapter)).findMany({
679
673
  model: "account",
674
+ limit: 2,
680
675
  where: [{
681
- field: "issuer",
682
- value: issuer
676
+ field: "providerId",
677
+ value: providerId
683
678
  }, {
684
679
  field: "accountId",
685
680
  value: accountId
686
681
  }]
687
682
  });
683
+ if (accounts.length > 1) throw new BetterAuthError(`Multiple accounts match the same accountId for provider ${JSON.stringify(providerId)}. Resolve duplicate account identities before continuing.`);
684
+ return accounts[0] ?? null;
688
685
  },
689
686
  findAccountByUserId: async (userId) => {
690
687
  return await (await getCurrentAdapter(adapter)).findMany({
@@ -31,7 +31,6 @@ declare function parseAccountInput(options: BetterAuthOptions, account: Partial<
31
31
  createdAt: Date;
32
32
  updatedAt: Date;
33
33
  providerId: string;
34
- issuer: string;
35
34
  accountId: string;
36
35
  userId: string;
37
36
  accessToken?: string | null | undefined;
@@ -1,10 +1,10 @@
1
- import { getCurrentAdapter, getCurrentAuthContext, queueAfterTransactionHook } from "@better-auth/core/context";
1
+ import { getCurrentAdapter, queueAfterTransactionHook, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
2
2
  import { ATTR_CONTEXT, ATTR_DB_COLLECTION_NAME, ATTR_HOOK_TYPE, withSpan } from "@better-auth/core/instrumentation";
3
3
  //#region src/db/with-hooks.ts
4
4
  function getWithHooks(adapter, ctx) {
5
5
  const hooksEntries = ctx.hooks;
6
6
  async function createWithHooks(data, model, customCreateFn) {
7
- const context = await getCurrentAuthContext().catch(() => null);
7
+ const context = tryGetCurrentAuthEndpointContext();
8
8
  let actualData = data;
9
9
  for (const { source, hooks } of hooksEntries) {
10
10
  const toRun = hooks[model]?.create?.before;
@@ -41,7 +41,7 @@ function getWithHooks(adapter, ctx) {
41
41
  return created;
42
42
  }
43
43
  async function updateWithHooks(data, where, model, customUpdateFn) {
44
- const context = await getCurrentAuthContext().catch(() => null);
44
+ const context = tryGetCurrentAuthEndpointContext();
45
45
  let actualData = data;
46
46
  for (const { source, hooks } of hooksEntries) {
47
47
  const toRun = hooks[model]?.update?.before;
@@ -77,7 +77,7 @@ function getWithHooks(adapter, ctx) {
77
77
  return updated;
78
78
  }
79
79
  async function updateManyWithHooks(data, where, model, customUpdateFn) {
80
- const context = await getCurrentAuthContext().catch(() => null);
80
+ const context = tryGetCurrentAuthEndpointContext();
81
81
  let actualData = data;
82
82
  for (const { source, hooks } of hooksEntries) {
83
83
  const toRun = hooks[model]?.update?.before;
@@ -113,7 +113,7 @@ function getWithHooks(adapter, ctx) {
113
113
  return updated;
114
114
  }
115
115
  async function deleteWithHooks(where, model, customDeleteFn) {
116
- const context = await getCurrentAuthContext().catch(() => null);
116
+ const context = tryGetCurrentAuthEndpointContext();
117
117
  let entityToDelete = null;
118
118
  try {
119
119
  entityToDelete = (await (await getCurrentAdapter(adapter)).findMany({
@@ -150,7 +150,7 @@ function getWithHooks(adapter, ctx) {
150
150
  return deleted;
151
151
  }
152
152
  async function deleteManyWithHooks(where, model, customDeleteFn) {
153
- const context = await getCurrentAuthContext().catch(() => null);
153
+ const context = tryGetCurrentAuthEndpointContext();
154
154
  let entitiesToDelete = [];
155
155
  try {
156
156
  entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany({
@@ -202,7 +202,7 @@ function getWithHooks(adapter, ctx) {
202
202
  * the helper resolves to `null` (no `consumeFn` call, no after hooks).
203
203
  */
204
204
  async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) {
205
- const context = await getCurrentAuthContext().catch(() => null);
205
+ const context = tryGetCurrentAuthEndpointContext();
206
206
  const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => {
207
207
  const fn = hooks[model]?.delete?.before;
208
208
  return fn ? [{
@@ -1,4 +1,3 @@
1
- import { createOAuthAccountIssuer } from "@better-auth/core/db";
2
1
  import { APIError, BASE_ERROR_CODES, BetterAuthError } from "@better-auth/core/error";
3
2
  //#region src/oauth2/account-key.ts
4
3
  /**
@@ -21,11 +20,8 @@ async function resolveOAuthAccountKey(provider, tokens, profile) {
21
20
  const resolvedSubject = await accountSubject(accountKeyContext);
22
21
  const accountId = String(resolvedSubject);
23
22
  if (typeof resolvedSubject === "number" && !Number.isFinite(resolvedSubject) || accountId.trim().length === 0 || accountId === "undefined" || accountId === "null") throw new BetterAuthError("OAUTH_ACCOUNT_SUBJECT_INVALID");
24
- const accountIssuer = provider.accountIssuer;
25
- const issuer = accountIssuer === void 0 ? createOAuthAccountIssuer(provider.id) : typeof accountIssuer === "function" ? await accountIssuer(accountKeyContext) : accountIssuer;
26
- if (typeof issuer !== "string" || issuer.trim().length === 0 || issuer === "undefined" || issuer === "null") throw new BetterAuthError("OAUTH_ACCOUNT_ISSUER_INVALID");
27
23
  return {
28
- issuer,
24
+ providerId: provider.id,
29
25
  accountId
30
26
  };
31
27
  }
@@ -1,3 +1,4 @@
1
+ import { appendQueryParams } from "@better-auth/core/utils/url";
1
2
  //#region src/oauth2/errors.ts
2
3
  /**
3
4
  * Error codes used in OAuth callback redirects (`?error=<code>`). These are
@@ -33,8 +34,8 @@ const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handl
33
34
  function redirectOnError(ctx, errorURL, error, description) {
34
35
  const params = new URLSearchParams({ error });
35
36
  if (description) params.set("error_description", description);
36
- const sep = errorURL.includes("?") ? "&" : "?";
37
- throw ctx.redirect(`${errorURL}${sep}${params.toString()}`);
37
+ const redirectURL = appendQueryParams(errorURL, params);
38
+ throw ctx.redirect(redirectURL);
38
39
  }
39
40
  /**
40
41
  * Build the logger message shown when an OAuth provider does not return an
@@ -42,7 +43,7 @@ function redirectOnError(ctx, errorURL, error, description) {
42
43
  * the same workaround docs.
43
44
  */
44
45
  function missingEmailLogMessage(providerId, options) {
45
- return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or synthesize one via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
46
+ return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or create a placeholder via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
46
47
  }
47
48
  //#endregion
48
49
  export { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage, redirectOnError };
@@ -56,7 +56,6 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
56
56
  createdAt: Date;
57
57
  updatedAt: Date;
58
58
  providerId: string;
59
- issuer: string;
60
59
  accountId: string;
61
60
  userId: string;
62
61
  accessToken?: string | null | undefined;
@@ -18,7 +18,7 @@ async function handleOAuthUserInfo(c, opts) {
18
18
  const requireExactAccountBinding = !!opts.selectedUser || opts.requireExactAccountBinding === true;
19
19
  let pendingAccountCookie = null;
20
20
  const accountOwner = await c.context.internalAdapter.findAccountOwnerByKey({
21
- issuer: account.issuer,
21
+ providerId: account.providerId,
22
22
  accountId: account.accountId
23
23
  }).catch((e) => {
24
24
  c.context.logger.error("Better auth was unable to query your database.\nError: ", e);
@@ -38,10 +38,6 @@ async function handleOAuthUserInfo(c, opts) {
38
38
  code: "account_ownership_conflict",
39
39
  message: "Account is already linked to another user"
40
40
  });
41
- if (requireExactAccountBinding && accountOwner.account.providerId !== account.providerId) throw new APIError("CONFLICT", {
42
- code: "account_provider_conflict",
43
- message: "Account is already linked through another provider"
44
- });
45
41
  return {
46
42
  user: accountOwner.user,
47
43
  linkedAccount: accountOwner.account,
@@ -75,7 +71,7 @@ async function handleOAuthUserInfo(c, opts) {
75
71
  let user = dbUser?.user;
76
72
  const isRegister = !user;
77
73
  if (dbUser) {
78
- const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.issuer === account.issuer && acc.accountId === account.accountId);
74
+ const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
79
75
  if (!linkedAccount) {
80
76
  const accountLinking = c.context.options.account?.accountLinking;
81
77
  const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
@@ -102,7 +98,6 @@ async function handleOAuthUserInfo(c, opts) {
102
98
  });
103
99
  const createdAccount = await c.context.internalAdapter.linkAccount({
104
100
  providerId: account.providerId,
105
- issuer: account.issuer,
106
101
  accountId: account.accountId,
107
102
  userId: dbUser.user.id,
108
103
  accessToken: await setTokenUtil(account.accessToken, c.context),
@@ -116,7 +111,7 @@ async function handleOAuthUserInfo(c, opts) {
116
111
  error: "unable to link account",
117
112
  data: null
118
113
  };
119
- if (requireExactAccountBinding && (createdAccount.issuer !== account.issuer || createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
114
+ if (requireExactAccountBinding && (createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
120
115
  code: "account_hook_binding_conflict",
121
116
  message: "Account hook changed the selected authentication binding"
122
117
  });
@@ -172,7 +167,7 @@ async function handleOAuthUserInfo(c, opts) {
172
167
  error: "unable to update account",
173
168
  data: null
174
169
  };
175
- if (requireExactAccountBinding && (updatedAccount.issuer !== account.issuer || updatedAccount.accountId !== account.accountId || updatedAccount.providerId !== account.providerId || updatedAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
170
+ if (requireExactAccountBinding && (updatedAccount.accountId !== account.accountId || updatedAccount.providerId !== account.providerId || updatedAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
176
171
  code: "account_hook_binding_conflict",
177
172
  message: "Account hook changed the selected authentication binding"
178
173
  });
@@ -214,7 +209,6 @@ async function handleOAuthUserInfo(c, opts) {
214
209
  refreshTokenExpiresAt: account.refreshTokenExpiresAt,
215
210
  scope: account.scope,
216
211
  providerId: account.providerId,
217
- issuer: account.issuer,
218
212
  accountId: account.accountId
219
213
  };
220
214
  const { createdUser, createdAccount } = await runWithTransaction(c.context.adapter, async () => {
@@ -233,7 +227,7 @@ async function handleOAuthUserInfo(c, opts) {
233
227
  })
234
228
  };
235
229
  });
236
- if (requireExactAccountBinding && (createdAccount.issuer !== account.issuer || createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== createdUser.id)) throw new APIError("CONFLICT", {
230
+ if (requireExactAccountBinding && (createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== createdUser.id)) throw new APIError("CONFLICT", {
237
231
  code: "account_hook_binding_conflict",
238
232
  message: "Account hook changed the selected authentication binding"
239
233
  });
@@ -55,7 +55,7 @@ async function parseState(c) {
55
55
  let redirectErrorURL = errorURL;
56
56
  if (error instanceof StateError) {
57
57
  code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
58
- redirectErrorURL = error.errorURL ?? errorURL;
58
+ redirectErrorURL = error.errorURL || errorURL;
59
59
  }
60
60
  redirectOnError(c, redirectErrorURL, code);
61
61
  }
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.1";
2
+ var version = "1.7.3";
3
3
  //#endregion
4
4
  export { version };
@@ -4,7 +4,6 @@ import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cooki
4
4
  import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
5
5
  import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
6
6
  import { hasPermission } from "./has-permission.mjs";
7
- import { createLocalAccountIssuer } from "@better-auth/core/db";
8
7
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
9
8
  import { whereOperators } from "@better-auth/core/db/adapter";
10
9
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
@@ -203,7 +202,6 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
203
202
  const hashedPassword = await ctx.context.password.hash(ctx.body.password);
204
203
  await ctx.context.internalAdapter.linkAccount({
205
204
  providerId: "credential",
206
- issuer: createLocalAccountIssuer("credential"),
207
205
  accountId: user.id,
208
206
  password: hashedPassword,
209
207
  userId: user.id
@@ -534,7 +532,7 @@ const banUser = (opts) => createAuthEndpoint("/admin/ban-user", {
534
532
  const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
535
533
  banned: true,
536
534
  banReason: ctx.body.banReason || opts?.defaultBanReason || "No reason",
537
- banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : void 0,
535
+ banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : null,
538
536
  updatedAt: /* @__PURE__ */ new Date()
539
537
  });
540
538
  await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
@@ -842,7 +840,6 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
842
840
  else await ctx.context.internalAdapter.createAccount({
843
841
  userId,
844
842
  providerId: "credential",
845
- issuer: createLocalAccountIssuer("credential"),
846
843
  accountId: user.id,
847
844
  password: hashedPassword
848
845
  });
@@ -10,6 +10,7 @@ import { schema } from "./schema.mjs";
10
10
  import { generateId } from "@better-auth/core/utils/id";
11
11
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
12
12
  import * as z from "zod";
13
+ import { createPlaceholderEmail } from "@better-auth/core/utils/email";
13
14
  //#region src/plugins/anonymous/index.ts
14
15
  /**
15
16
  * Resolves the anonymous session being upgraded during an account-link callback.
@@ -51,7 +52,10 @@ async function getAnonUserEmail(options) {
51
52
  }
52
53
  const id = generateId();
53
54
  if (options?.emailDomainName) return `temp-${id}@${options.emailDomainName}`;
54
- return `temp@${id}.com`;
55
+ return createPlaceholderEmail({
56
+ identifier: id,
57
+ namespace: "anonymous"
58
+ });
55
59
  }
56
60
  const anonymous = (options) => {
57
61
  return {
@@ -18,9 +18,8 @@ interface UserWithAnonymous extends User {
18
18
  }
19
19
  interface AnonymousOptions {
20
20
  /**
21
- * Configure the domain name of the temporary email
22
- * address for anonymous users in the database.
23
- * @default "baseURL"
21
+ * Configure a custom domain for anonymous user email addresses.
22
+ * @default "anonymous.placeholder.invalid"
24
23
  */
25
24
  emailDomainName?: string | undefined;
26
25
  /**
@@ -162,37 +162,37 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
162
162
  client_id: z.ZodString;
163
163
  user_id: z.ZodOptional<z.ZodString>;
164
164
  scope: z.ZodOptional<z.ZodString>;
165
- } & (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
165
+ } & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
166
166
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
167
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) : ({
167
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_1 ? { -readonly [P in keyof T_1]: T_1[P] } : never) : ({
168
168
  client_id: z.ZodString;
169
169
  user_id: z.ZodOptional<z.ZodString>;
170
170
  scope: z.ZodOptional<z.ZodString>;
171
- } extends infer T_1 extends z.core.util.SomeObject ? { [K in keyof T_1 as K extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
171
+ } extends infer T_2 extends z.core.util.SomeObject ? { [K in keyof T_2 as K extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
172
172
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
173
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K]: T_1[K] } : never) & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
173
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K]: T_2[K] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
174
174
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
175
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_2 extends z.core.util.SomeObject ? { [K_1 in keyof T_2]: T_2[K_1] } : never)) extends infer T ? { [k in keyof T]: T[k] } : never, z.core.$strip> | z.ZodObject<(("scope" | "user_id" | "client_id") & keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
175
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_4 ? { -readonly [P in keyof T_4]: T_4[P] } : never) extends infer T_3 extends z.core.util.SomeObject ? { [K_1 in keyof T_3]: T_3[K_1] } : never)) extends infer T ? { [k in keyof T]: T[k] } : never, z.core.$strip> | z.ZodObject<(("scope" | "user_id" | "client_id") & keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
176
176
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
177
177
  }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends never ? {
178
178
  user_id: z.ZodOptional<z.ZodString>;
179
179
  scope: z.ZodOptional<z.ZodString>;
180
180
  client_id: z.ZodOptional<z.ZodString>;
181
- } & (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
181
+ } & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
182
182
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
183
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) : ({
183
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_6 ? { -readonly [P in keyof T_6]: T_6[P] } : never) : ({
184
184
  user_id: z.ZodOptional<z.ZodString>;
185
185
  scope: z.ZodOptional<z.ZodString>;
186
186
  client_id: z.ZodOptional<z.ZodString>;
187
- } extends infer T_4 extends z.core.util.SomeObject ? { [K_2 in keyof T_4 as K_2 extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
187
+ } extends infer T_7 extends z.core.util.SomeObject ? { [K_2 in keyof T_7 as K_2 extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
188
188
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
189
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K_2]: T_4[K_2] } : never) & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
189
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K_2]: T_7[K_2] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
190
190
  [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
191
- }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_5 extends z.core.util.SomeObject ? { [K_1 in keyof T_5]: T_5[K_1] } : never)) extends infer T_3 ? { [k_1 in keyof T_3]: T_3[k_1] } : never, z.core.$strip>;
191
+ }>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_9 ? { -readonly [P in keyof T_9]: T_9[P] } : never) extends infer T_8 extends z.core.util.SomeObject ? { [K_1 in keyof T_8]: T_8[K_1] } : never)) extends infer T_5 ? { [k_1 in keyof T_5]: T_5[k_1] } : never, z.core.$strip>;
192
192
  error: z.ZodObject<{
193
193
  error: z.ZodEnum<{ [k_3 in (readonly ["invalid_request", "invalid_client", "unauthorized_client", "invalid_scope", ...Grant extends {
194
194
  requestErrorCodes: infer ErrorCodes extends readonly string[];
195
- } ? ErrorCodes : readonly [], "server_error"])[number]]: k_3 } extends infer T_6 ? { [k_2 in keyof T_6]: T_6[k_2] } : never>;
195
+ } ? ErrorCodes : readonly [], "server_error"])[number]]: k_3 } extends infer T_10 ? { [k_2 in keyof T_10]: T_10[k_2] } : never>;
196
196
  error_description: z.ZodString;
197
197
  }, z.core.$strip>;
198
198
  onValidationError: ({
@@ -10,7 +10,6 @@ import { APIError as APIError$1 } from "../../api/index.mjs";
10
10
  import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
11
11
  import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
12
12
  import { storeOTP, tryReuseOTP, verifyStoredOTP } from "./otp-token.mjs";
13
- import { createLocalAccountIssuer } from "@better-auth/core/db";
14
13
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
15
14
  import { createAuthEndpoint } from "@better-auth/core/api";
16
15
  import { deprecate } from "@better-auth/core/utils/deprecate";
@@ -593,7 +592,6 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
593
592
  if (!await ctx.context.internalAdapter.findCredentialAccount(user.user.id)) await ctx.context.internalAdapter.createAccount({
594
593
  userId: user.user.id,
595
594
  providerId: "credential",
596
- issuer: createLocalAccountIssuer("credential"),
597
595
  accountId: user.user.id,
598
596
  password: passwordHash
599
597
  });
@@ -93,7 +93,6 @@ const genericOAuth = (options) => {
93
93
  return null;
94
94
  });
95
95
  if (discovered) {
96
- if (!discovered.issuer && !c.accountIssuer) throw new Error(`Provider "${c.providerId}": discovery did not return an issuer. Configure accountIssuer explicitly to establish a stable account namespace.`);
97
96
  authorizationUrl ??= discovered.authorization_endpoint;
98
97
  tokenUrl ??= discovered.token_endpoint;
99
98
  userInfoUrl ??= discovered.userinfo_endpoint;
@@ -106,7 +105,8 @@ const genericOAuth = (options) => {
106
105
  try {
107
106
  jwksUrl = new URL(discovered.jwks_uri, c.discoveryUrl);
108
107
  } catch {
109
- throw new Error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document.`);
108
+ ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
109
+ continue;
110
110
  }
111
111
  idTokenConfig = {
112
112
  jwks: createRemoteJWKSet(jwksUrl),
@@ -115,16 +115,24 @@ const genericOAuth = (options) => {
115
115
  algorithms: isOidc ? signingAlgs : void 0
116
116
  };
117
117
  }
118
- } else if (!c.accountIssuer) throw new Error(`Provider "${c.providerId}": discovery returned no valid data. Provider initialization stopped to keep its account issuer stable.`);
119
- else if (!authorizationUrl || !tokenUrl) ctx.logger.error(`Provider "${c.providerId}": discovery returned no data and no explicit endpoints configured. OAuth sign-in will fail for this provider.`);
118
+ }
119
+ if (!authorizationUrl || !tokenUrl && !c.getToken) {
120
+ ctx.logger.error(`Provider "${c.providerId}": discovery left no usable authorization endpoint or token exchange. Provider skipped.`);
121
+ continue;
122
+ }
123
+ }
124
+ if (c.requireIdTokenVerification && !idTokenConfig) {
125
+ if (c.discoveryUrl) {
126
+ ctx.logger.error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri. Provider skipped.`);
127
+ continue;
128
+ }
129
+ throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
120
130
  }
121
- if (c.requireIdTokenVerification && !idTokenConfig) throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
122
131
  const tokenEndpointAuth = c.tokenEndpointAuth;
123
132
  if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
124
133
  if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
125
134
  if (!c.clientSecret && !tokenEndpointAuth && c.authentication === "basic") throw new Error(`Provider "${c.providerId}": authentication "basic" requires clientSecret`);
126
135
  const accountSubject = c.accountSubject;
127
- const accountIssuer = c.accountIssuer;
128
136
  const provider = {
129
137
  id: c.providerId,
130
138
  name: c.name ?? c.providerId,
@@ -137,10 +145,6 @@ const genericOAuth = (options) => {
137
145
  });
138
146
  return isOidc ? genericProfile.sub ?? "" : genericProfile.id ?? "";
139
147
  },
140
- accountIssuer: typeof accountIssuer === "function" ? ({ tokens, profile }) => accountIssuer({
141
- tokens,
142
- profile
143
- }) : accountIssuer ?? issuer,
144
148
  idToken: idTokenConfig,
145
149
  requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
146
150
  allowIdpInitiated: c.allowIdpInitiated,
@@ -27,12 +27,10 @@ function auth0(options) {
27
27
  "profile",
28
28
  "email"
29
29
  ];
30
- const domain = options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
31
- const discoveryUrl = `https://${domain}/.well-known/openid-configuration`;
30
+ const domainUrl = options.domain.startsWith("http://") || options.domain.startsWith("https://") ? options.domain : `https://${options.domain}`;
32
31
  return {
33
32
  providerId: "auth0",
34
- accountIssuer: `https://${domain}/`,
35
- discoveryUrl,
33
+ discoveryUrl: `https://${new URL(domainUrl).host}/.well-known/openid-configuration`,
36
34
  clientId: options.clientId,
37
35
  clientSecret: options.clientSecret,
38
36
  tokenEndpointAuth: options.tokenEndpointAuth,
@@ -22,20 +22,17 @@
22
22
  * ```
23
23
  */
24
24
  function keycloak(options) {
25
- const defaultScopes = [
26
- "openid",
27
- "profile",
28
- "email"
29
- ];
30
- const issuer = options.issuer.replace(/\/$/, "");
31
25
  return {
32
26
  providerId: "keycloak",
33
- accountIssuer: issuer,
34
- discoveryUrl: `${issuer}/.well-known/openid-configuration`,
27
+ discoveryUrl: `${options.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
35
28
  clientId: options.clientId,
36
29
  clientSecret: options.clientSecret,
37
30
  tokenEndpointAuth: options.tokenEndpointAuth,
38
- scopes: options.scopes ?? defaultScopes,
31
+ scopes: options.scopes ?? [
32
+ "openid",
33
+ "profile",
34
+ "email"
35
+ ],
39
36
  redirectURI: options.redirectURI,
40
37
  endSessionEndpoint: options.endSessionEndpoint,
41
38
  postLogoutRedirectURI: options.postLogoutRedirectURI,
@@ -71,7 +71,6 @@ function line(options) {
71
71
  return {
72
72
  providerId: options.providerId ?? "line",
73
73
  accountSubject: ({ profile }) => profile.sub ?? "",
74
- accountIssuer: "https://access.line.me",
75
74
  authorizationUrl,
76
75
  tokenUrl,
77
76
  userInfoUrl,
@@ -1,5 +1,6 @@
1
1
  import { decodeJwt } from "jose";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
+ import { createPlaceholderEmail } from "@better-auth/core/utils/email";
3
4
  //#region src/plugins/generic-oauth/providers/microsoft-entra-id.ts
4
5
  function getMicrosoftProfileName(profile) {
5
6
  return profile.name ?? (`${profile.given_name ?? profile.givenname ?? ""} ${profile.family_name ?? profile.familyname ?? ""}`.trim() || void 0);
@@ -46,25 +47,34 @@ function microsoftEntraId(options) {
46
47
  } catch {
47
48
  return null;
48
49
  }
49
- if (!(typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0)) return null;
50
+ const oid = typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0;
51
+ if (!oid) return null;
52
+ const tokenEmail = tokenProfile.email;
50
53
  const tokenUserInfo = {
51
54
  ...tokenProfile,
52
55
  name: getMicrosoftProfileName(tokenProfile),
53
- email: tokenProfile.email ?? tokenProfile.preferred_username ?? void 0,
56
+ email: tokenEmail ?? createPlaceholderEmail({
57
+ identifier: oid,
58
+ namespace: "microsoft-entra-id"
59
+ }),
54
60
  image: tokenProfile.picture,
55
- emailVerified: tokenProfile.email_verified ?? false
61
+ emailVerified: tokenEmail ? tokenProfile.email_verified ?? false : false
56
62
  };
57
63
  if (!tokens.accessToken) return tokenUserInfo;
58
64
  const { data: profile, error } = await betterFetch(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.accessToken}` } });
59
65
  if (error || !profile) return tokenUserInfo;
60
66
  if (typeof tokenProfile.sub !== "string" || profile.sub !== tokenProfile.sub) return tokenUserInfo;
67
+ const emailClaim = tokenProfile.email ?? profile.email;
61
68
  return {
62
69
  ...profile,
63
70
  ...tokenProfile,
64
71
  name: getMicrosoftProfileName(tokenProfile) ?? getMicrosoftProfileName(profile),
65
- email: tokenProfile.email ?? profile.email ?? tokenProfile.preferred_username ?? profile.preferred_username ?? void 0,
72
+ email: emailClaim ?? createPlaceholderEmail({
73
+ identifier: oid,
74
+ namespace: "microsoft-entra-id"
75
+ }),
66
76
  image: tokenProfile.picture ?? profile.picture,
67
- emailVerified: tokenProfile.email_verified ?? profile.email_verified ?? false
77
+ emailVerified: emailClaim != null ? tokenProfile.email_verified ?? profile.email_verified ?? false : false
68
78
  };
69
79
  };
70
80
  return {
@@ -22,20 +22,17 @@
22
22
  * ```
23
23
  */
24
24
  function okta(options) {
25
- const defaultScopes = [
26
- "openid",
27
- "profile",
28
- "email"
29
- ];
30
- const issuer = options.issuer.replace(/\/$/, "");
31
25
  return {
32
26
  providerId: "okta",
33
- accountIssuer: issuer,
34
- discoveryUrl: `${issuer}/.well-known/openid-configuration`,
27
+ discoveryUrl: `${options.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
35
28
  clientId: options.clientId,
36
29
  clientSecret: options.clientSecret,
37
30
  tokenEndpointAuth: options.tokenEndpointAuth,
38
- scopes: options.scopes ?? defaultScopes,
31
+ scopes: options.scopes ?? [
32
+ "openid",
33
+ "profile",
34
+ "email"
35
+ ],
39
36
  redirectURI: options.redirectURI,
40
37
  endSessionEndpoint: options.endSessionEndpoint,
41
38
  postLogoutRedirectURI: options.postLogoutRedirectURI,
@@ -41,7 +41,6 @@ function slack(options) {
41
41
  return {
42
42
  providerId: "slack",
43
43
  accountSubject: ({ profile }) => profile.sub ?? "",
44
- accountIssuer: "https://slack.com",
45
44
  authorizationUrl: "https://slack.com/openid/connect/authorize",
46
45
  tokenUrl: "https://slack.com/api/openid.connect.token",
47
46
  userInfoUrl: "https://slack.com/api/openid.connect.userInfo",