better-auth 1.7.2 → 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 (63) hide show
  1. package/dist/api/index.d.mts +6 -22
  2. package/dist/api/index.mjs +2 -0
  3. package/dist/api/routes/account.d.mts +1 -9
  4. package/dist/api/routes/account.mjs +0 -6
  5. package/dist/api/routes/callback.mjs +0 -2
  6. package/dist/api/routes/password.mjs +0 -2
  7. package/dist/api/routes/session.mjs +12 -5
  8. package/dist/api/routes/sign-in.d.mts +2 -2
  9. package/dist/api/routes/sign-in.mjs +1 -5
  10. package/dist/api/routes/sign-up.mjs +0 -2
  11. package/dist/api/routes/update-user.mjs +0 -2
  12. package/dist/api/to-auth-endpoints.mjs +2 -0
  13. package/dist/auth/base.mjs +13 -1
  14. package/dist/auth/trusted-origins.mjs +6 -4
  15. package/dist/client/config.d.mts +2 -0
  16. package/dist/client/config.mjs +1 -0
  17. package/dist/client/vue/index.d.mts +31 -20
  18. package/dist/client/vue/index.mjs +23 -5
  19. package/dist/context/create-context.mjs +2 -0
  20. package/dist/cookies/session-store.d.mts +0 -1
  21. package/dist/cookies/session-store.mjs +16 -5
  22. package/dist/db/adapter-kysely.mjs +1 -2
  23. package/dist/db/get-migration.d.mts +1 -0
  24. package/dist/db/get-migration.mjs +10 -32
  25. package/dist/db/internal-adapter.mjs +14 -17
  26. package/dist/db/schema.d.mts +0 -1
  27. package/dist/index.mjs +0 -1
  28. package/dist/oauth2/account-key.mjs +1 -5
  29. package/dist/oauth2/link-account.d.mts +0 -1
  30. package/dist/oauth2/link-account.mjs +5 -11
  31. package/dist/package.mjs +1 -1
  32. package/dist/plugins/admin/routes.mjs +0 -3
  33. package/dist/plugins/device-authorization/index.d.mts +11 -11
  34. package/dist/plugins/email-otp/routes.mjs +0 -2
  35. package/dist/plugins/generic-oauth/index.mjs +14 -10
  36. package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
  37. package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
  38. package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
  39. package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
  40. package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
  41. package/dist/plugins/generic-oauth/types.d.mts +0 -9
  42. package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
  43. package/dist/plugins/haveibeenpwned/index.mjs +31 -11
  44. package/dist/plugins/index.d.mts +2 -2
  45. package/dist/plugins/index.mjs +2 -2
  46. package/dist/plugins/last-login-method/index.mjs +1 -0
  47. package/dist/plugins/oauth-proxy/index.d.mts +17 -0
  48. package/dist/plugins/oauth-proxy/index.mjs +145 -99
  49. package/dist/plugins/one-tap/index.mjs +0 -1
  50. package/dist/plugins/open-api/generator.mjs +15 -1
  51. package/dist/plugins/open-api/index.mjs +0 -1
  52. package/dist/plugins/organization/has-permission.mjs +4 -2
  53. package/dist/plugins/phone-number/routes.mjs +0 -3
  54. package/dist/plugins/siwe/index.mjs +0 -3
  55. package/dist/plugins/two-factor/client.d.mts +1 -0
  56. package/dist/plugins/two-factor/error-code.d.mts +1 -0
  57. package/dist/plugins/two-factor/error-code.mjs +1 -0
  58. package/dist/plugins/two-factor/index.d.mts +1 -0
  59. package/dist/plugins/two-factor/index.mjs +3 -2
  60. package/dist/state.d.mts +0 -13
  61. package/dist/test-utils/test-instance.mjs +10 -9
  62. package/package.json +9 -9
  63. package/dist/utils/index.mjs +0 -5
@@ -6,18 +6,36 @@ import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
6
6
  function getAtomKey(str) {
7
7
  return `use${capitalizeFirstLetter(str)}`;
8
8
  }
9
+ /** Preserves standard `HeadersInit` values and removes undefined record entries. */
10
+ function toHeadersInit(headers) {
11
+ if (!headers) return void 0;
12
+ if (headers instanceof Headers || Array.isArray(headers)) return headers;
13
+ const normalizedHeaders = {};
14
+ for (const [name, value] of Object.entries(headers)) if (value !== void 0) normalizedHeaders[name] = value;
15
+ return normalizedHeaders;
16
+ }
9
17
  function createAuthClient(options) {
10
- const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $fetch, $store, atomListeners } = getClientConfig(options, false);
18
+ 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(":");
11
25
  const resolvedHooks = {};
12
26
  for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
13
27
  function useSession(useFetch) {
14
28
  if (useFetch) {
15
- const ref = useStore(pluginsAtoms.$sessionSignal);
16
- return useFetch(`${baseURL}/get-session`, { ref }).then((res) => {
29
+ const sessionSignal = useStore($sessionSignal);
30
+ return useFetch(`${baseURL}/get-session`, {
31
+ headers: toHeadersInit(options?.fetchOptions?.headers),
32
+ key: sessionCacheKey,
33
+ watch: [sessionSignal]
34
+ }).then((result) => {
17
35
  return {
18
- data: res.data,
36
+ data: result.data,
19
37
  isPending: false,
20
- error: res.error
38
+ error: result.error
21
39
  };
22
40
  });
23
41
  }
@@ -15,6 +15,7 @@ import { getAuthTables } from "@better-auth/core/db";
15
15
  import { createLogger, env, isProduction, isTest } from "@better-auth/core/env";
16
16
  import { BetterAuthError } from "@better-auth/core/error";
17
17
  import { generateId } from "@better-auth/core/utils/id";
18
+ import { schemaCheckFor } from "@better-auth/core/db/internal";
18
19
  import { socialProviders } from "@better-auth/core/social-providers";
19
20
  import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
20
21
  import { createTelemetry } from "@better-auth/telemetry";
@@ -227,6 +228,7 @@ Most of the features of Better Auth will not work correctly.`);
227
228
  };
228
229
  const initOrPromise = runPluginInit(ctx);
229
230
  if (isPromise(initOrPromise)) await initOrPromise;
231
+ ctx.checkSchema = schemaCheckFor(ctx.adapter);
230
232
  return ctx;
231
233
  }
232
234
  //#endregion
@@ -18,7 +18,6 @@ declare function getAccountCookie(c: GenericEndpointContext): Promise<{
18
18
  createdAt: Date;
19
19
  updatedAt: Date;
20
20
  providerId: string;
21
- issuer: string;
22
21
  accountId: string;
23
22
  userId: string;
24
23
  accessToken?: string | null | undefined;
@@ -26,13 +26,24 @@ const MAX_COOKIE_CHUNKS = 100;
26
26
  function getMaxCookieValueSize(name, options) {
27
27
  return MAX_COOKIE_SIZE - serializeCookie(name, "", { ...options }).length;
28
28
  }
29
+ function parseCookieChunkIndex(cookieName, name) {
30
+ const prefix = `${cookieName}.`;
31
+ if (!name.startsWith(prefix)) return null;
32
+ const suffix = name.slice(prefix.length);
33
+ const index = Number(suffix);
34
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== suffix) return null;
35
+ return index;
36
+ }
29
37
  /**
30
38
  * Read all existing chunks from cookies
31
39
  */
32
40
  function readExistingChunks(cookieName, ctx) {
33
41
  const chunks = {};
34
42
  const cookies = parseCookies(ctx.headers?.get("cookie") || "");
35
- for (const [name, value] of cookies) if (name.startsWith(cookieName)) chunks[name] = value;
43
+ for (const [name, value] of cookies) {
44
+ if (name !== cookieName && parseCookieChunkIndex(cookieName, name) === null) continue;
45
+ chunks[name] = value;
46
+ }
36
47
  return chunks;
37
48
  }
38
49
  /**
@@ -128,10 +139,10 @@ function getChunkedCookie(ctx, cookieName) {
128
139
  const chunks = [];
129
140
  const cookieHeader = ctx.headers?.get("cookie");
130
141
  if (!cookieHeader) return null;
131
- for (const [name, val] of parseCookies(cookieHeader)) if (name.startsWith(cookieName + ".")) {
132
- const indexStr = name.split(".").at(-1);
133
- const index = parseInt(indexStr || "0", 10);
134
- if (!isNaN(index)) chunks.push({
142
+ for (const [name, val] of parseCookies(cookieHeader)) {
143
+ const index = parseCookieChunkIndex(cookieName, name);
144
+ if (index === null) continue;
145
+ chunks.push({
135
146
  index,
136
147
  value: val
137
148
  });
@@ -3,10 +3,9 @@ import { BetterAuthError } from "@better-auth/core/error";
3
3
  //#region src/db/adapter-kysely.ts
4
4
  async function getAdapter(options) {
5
5
  return getBaseAdapter(options, async (opts) => {
6
- const { createKyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
6
+ const { createKyselyAdapter, kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
7
7
  const { kysely, databaseType, transaction } = await createKyselyAdapter(opts);
8
8
  if (!kysely) throw new BetterAuthError("Failed to initialize database adapter");
9
- const { kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
10
9
  return kyselyAdapter(kysely, {
11
10
  type: databaseType || "sqlite",
12
11
  debugLogs: opts.database && "debugLogs" in opts.database ? opts.database.debugLogs : false,
@@ -48,6 +48,7 @@ declare function getMigrations(config: BetterAuthOptions, {
48
48
  name: string;
49
49
  }[];
50
50
  unsafeChanges: string[];
51
+ schemaProblems: string[];
51
52
  runMigrations: () => Promise<void>;
52
53
  compileMigrations: () => Promise<string>;
53
54
  }>;
@@ -2,9 +2,9 @@ import { getSchema } from "./get-schema.mjs";
2
2
  import { getAuthTables } from "@better-auth/core/db";
3
3
  import { createLogger } from "@better-auth/core/env";
4
4
  import { BetterAuthError } from "@better-auth/core/error";
5
- import { createKyselyAdapter } from "@better-auth/kysely-adapter";
5
+ import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
6
6
  import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
7
- import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey } from "@better-auth/core/db/internal";
7
+ import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, invalidateSchemaChecks } from "@better-auth/core/db/internal";
8
8
  import { sql } from "kysely";
9
9
  //#region src/db/get-migration.ts
10
10
  const map = {
@@ -293,7 +293,6 @@ function assertExistingTableIndexFits({ columnBounds, dbType, existingColumns, f
293
293
  }
294
294
  if (requiredBytes > byteBudget) throw new BetterAuthError(`Cannot create database index "${index.name}" on existing table "${table}" because its columns can exceed ${dbType === "mysql" ? "MySQL" : "SQL Server"}'s ${byteBudget}-byte index-key limit. Bound the indexed string columns to the generated schema lengths, resolve oversized values, then run the migration again.`);
295
295
  }
296
- const columnBackfillGuideUrl = "https://better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-is-scoped-by-issuer";
297
296
  /**
298
297
  * Thrown when {@link getMigrations} refuses to add a required column with no
299
298
  * default value to a populated table. Distinct from the plain
@@ -320,27 +319,6 @@ function matchType(columnDataType, fieldType, dbType) {
320
319
  return (Array.isArray(fieldType) ? types["string"].map((t) => t.toLowerCase()) : types[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType));
321
320
  }
322
321
  /**
323
- * Get the current PostgreSQL schema (search_path) for the database connection
324
- * Returns the first schema in the search_path, defaulting to 'public' if not found
325
- */
326
- async function getPostgresSchema(db) {
327
- try {
328
- const result = await sql`SHOW search_path`.execute(db);
329
- const searchPath = result.rows[0]?.search_path ?? result.rows[0]?.searchPath;
330
- if (searchPath) return searchPath.split(",").map((s) => s.trim()).map((s) => s.replace(/^["']|["']$/g, "")).filter((s) => !s.startsWith("$") && !s.startsWith("\\$"))[0] || "public";
331
- } catch {}
332
- return "public";
333
- }
334
- async function getMssqlSchema(db) {
335
- try {
336
- return (await sql`
337
- SELECT SCHEMA_NAME() AS "schemaName"
338
- `.execute(db)).rows[0]?.schemaName || "dbo";
339
- } catch {
340
- return "dbo";
341
- }
342
- }
343
- /**
344
322
  * Build the migration plan that `auth migrate` executes and `auth generate`
345
323
  * prints for the Kysely adapter.
346
324
  *
@@ -357,11 +335,6 @@ async function getMssqlSchema(db) {
357
335
  async function getMigrations(config, { throwOnUnsafe = true } = {}) {
358
336
  const betterAuthSchema = getSchema(config);
359
337
  const authTables = getAuthTables(config);
360
- const accountIssuer = authTables.account && {
361
- table: authTables.account.modelName,
362
- column: authTables.account.fields.issuer?.fieldName || "issuer"
363
- };
364
- const isAccountIssuerColumn = (table, column) => table === accountIssuer?.table && column === accountIssuer.column;
365
338
  const logger = createLogger(config.logger);
366
339
  const unsafeChanges = [];
367
340
  const reportUnsafeChange = (message) => {
@@ -410,6 +383,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
410
383
  logger.warn(`Could not filter tables by schema. Using all discovered tables. Error: ${error instanceof Error ? error.message : String(error)}`);
411
384
  }
412
385
  else if (dbType === "mssql") tableMetadata = allTableMetadata.filter((table) => table.schema === currentSchema);
386
+ const schemaProblems = diffSchema(toPhysicalSchema(db, betterAuthSchema), toIntrospectedTables(tableMetadata)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "database"));
413
387
  const toBeCreated = [];
414
388
  const toBeAdded = [];
415
389
  const toBeAddedIndexes = [];
@@ -602,8 +576,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
602
576
  }
603
577
  if (populated) {
604
578
  const textDetail = field.type === "string" ? " For a text column, every existing row ends up with the same empty string." : "";
605
- const guideLink = isAccountIssuerColumn(table.table, fieldName) ? ` See ${columnBackfillGuideUrl}` : "";
606
- reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.${guideLink}`);
579
+ reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.`);
607
580
  }
608
581
  }
609
582
  const type = getType(field, fieldName, getTableIndexStringLength(table.table, fieldName));
@@ -668,7 +641,11 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
668
641
  }
669
642
  for (const index of deferredIndexes) migrations.push(index);
670
643
  async function runMigrations() {
671
- for (const migration of migrations) await migration.execute();
644
+ try {
645
+ for (const migration of migrations) await migration.execute();
646
+ } finally {
647
+ if (migrations.length && config.database) invalidateSchemaChecks(config.database);
648
+ }
672
649
  }
673
650
  async function compileMigrations() {
674
651
  return migrations.map((m) => m.compile().sql).join(";\n\n") + ";";
@@ -678,6 +655,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
678
655
  toBeAdded,
679
656
  toBeAddedIndexes,
680
657
  unsafeChanges,
658
+ schemaProblems,
681
659
  runMigrations,
682
660
  compileMigrations
683
661
  };
@@ -4,7 +4,6 @@ import { assertValidUserInfo, assertValidUserInfoSource } from "../utils/validat
4
4
  import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs";
5
5
  import { getWithHooks } from "./with-hooks.mjs";
6
6
  import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransactionHook, runWithTransaction, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
7
- import { createLocalAccountIssuer } from "@better-auth/core/db";
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";
@@ -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;
package/dist/index.mjs CHANGED
@@ -2,7 +2,6 @@ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProt
2
2
  import { generateGenericState, 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
- import "./utils/index.mjs";
6
5
  import { APIError } from "./api/index.mjs";
7
6
  import { betterAuth } from "./auth/full.mjs";
8
7
  import { getCurrentAdapter } from "@better-auth/core/context";
@@ -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
  }
@@ -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
  });
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.2";
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
@@ -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
  });
@@ -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,