better-auth 1.6.15 → 1.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/api/middlewares/origin-check.mjs +1 -0
  2. package/dist/api/routes/account.mjs +11 -6
  3. package/dist/api/routes/callback.mjs +1 -1
  4. package/dist/api/routes/session.mjs +3 -1
  5. package/dist/api/routes/update-session.mjs +9 -3
  6. package/dist/client/lynx/index.d.mts +6 -5
  7. package/dist/client/react/index.d.mts +6 -5
  8. package/dist/client/solid/index.d.mts +6 -5
  9. package/dist/client/svelte/index.d.mts +6 -5
  10. package/dist/client/vanilla.d.mts +6 -5
  11. package/dist/client/vue/index.d.mts +6 -5
  12. package/dist/context/create-context.mjs +1 -1
  13. package/dist/cookies/index.mjs +6 -1
  14. package/dist/db/internal-adapter.mjs +5 -0
  15. package/dist/oauth2/link-account.d.mts +13 -0
  16. package/dist/oauth2/link-account.mjs +1 -1
  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 +6 -3
  21. package/dist/plugins/admin/client.d.mts +6 -4
  22. package/dist/plugins/admin/error-codes.d.mts +2 -0
  23. package/dist/plugins/admin/error-codes.mjs +3 -1
  24. package/dist/plugins/admin/routes.mjs +63 -2
  25. package/dist/plugins/admin/schema.d.mts +1 -0
  26. package/dist/plugins/admin/schema.mjs +2 -1
  27. package/dist/plugins/email-otp/routes.mjs +1 -1
  28. package/dist/plugins/generic-oauth/routes.mjs +9 -2
  29. package/dist/plugins/organization/organization.mjs +2 -0
  30. package/dist/plugins/organization/routes/crud-invites.mjs +10 -1
  31. package/dist/plugins/organization/routes/crud-team.mjs +15 -2
  32. package/dist/plugins/organization/schema.d.mts +2 -0
  33. package/dist/plugins/siwe/index.mjs +28 -0
  34. package/dist/plugins/siwe/parse-message.mjs +60 -0
  35. package/dist/plugins/two-factor/index.mjs +9 -1
  36. package/dist/test-utils/test-instance.d.mts +6 -5
  37. package/package.json +10 -10
@@ -141,6 +141,7 @@ async function validateFormCsrf(ctx) {
141
141
  }
142
142
  return await validateOrigin(ctx, true);
143
143
  }
144
+ if (headers.get("origin") || headers.get("referer")) return await validateOrigin(ctx, true);
144
145
  }
145
146
  //#endregion
146
147
  export { formCsrfMiddleware, originCheck, originCheckMiddleware };
@@ -225,9 +225,15 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
225
225
  * `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
226
226
  * unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
227
227
  * nor a `userId` is available.
228
+ *
229
+ * When a durable store is authoritative, bypasses the cookie cache: these
230
+ * routes mint or refresh provider access tokens, so a server-side session
231
+ * revocation must take effect immediately rather than waiting for the cached
232
+ * cookie to expire. DB-less deployments keep the session in the cookie itself,
233
+ * so the cache is left in place for them.
228
234
  */
229
235
  async function resolveUserId(ctx, userId) {
230
- const session = await getSessionFromCtx(ctx);
236
+ const session = await getSessionFromCtx(ctx, { disableCookieCache: !!ctx.context.options.database || !!ctx.context.options.secondaryStorage });
231
237
  if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
232
238
  const resolvedUserId = session?.user?.id || userId;
233
239
  if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
@@ -382,12 +388,11 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
382
388
  });
383
389
  let account = void 0;
384
390
  const accountData = await getAccountCookie(ctx);
385
- if (accountData && accountData.userId === resolvedUserId && (!providerId || providerId === accountData?.providerId)) account = accountData;
391
+ const usedAccountCookie = !!accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId);
392
+ if (usedAccountCookie) account = accountData;
386
393
  else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
387
394
  if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
388
- let refreshToken = void 0;
389
- if (accountData && providerId === accountData.providerId) refreshToken = accountData.refreshToken ?? void 0;
390
- else refreshToken = account.refreshToken ?? void 0;
395
+ const refreshToken = account.refreshToken ?? void 0;
391
396
  if (!refreshToken) throw APIError.from("BAD_REQUEST", {
392
397
  message: "Refresh token not found",
393
398
  code: "REFRESH_TOKEN_NOT_FOUND"
@@ -409,7 +414,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
409
414
  };
410
415
  await ctx.context.internalAdapter.updateAccount(account.id, updateData);
411
416
  }
412
- if (accountData && providerId === accountData.providerId && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
417
+ if (usedAccountCookie && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
413
418
  ...accountData,
414
419
  accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
415
420
  refreshToken: resolvedRefreshToken,
@@ -81,7 +81,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
81
81
  ...tokens,
82
82
  user: parsedUserData ?? void 0
83
83
  }).then((res) => res?.user);
84
- if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
84
+ if (!userInfo || userInfo.id === void 0 || userInfo.id === null || userInfo.id === "") {
85
85
  c.context.logger.error("Unable to get user info");
86
86
  redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
87
87
  }
@@ -276,7 +276,9 @@ const getSessionFromCtx = async (ctx, config) => {
276
276
  returnStatus: false,
277
277
  query: {
278
278
  ...config,
279
- ...ctx.query
279
+ ...ctx.query,
280
+ disableCookieCache: config?.disableCookieCache || ctx.query?.disableCookieCache,
281
+ disableRefresh: config?.disableRefresh || ctx.query?.disableRefresh
280
282
  }
281
283
  }).catch(() => {
282
284
  return null;
@@ -1,5 +1,5 @@
1
1
  import { parseSessionInput, parseSessionOutput } from "../../db/schema.mjs";
2
- import { setSessionCookie } from "../../cookies/index.mjs";
2
+ import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
3
3
  import { sessionMiddleware } from "./session.mjs";
4
4
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -34,10 +34,16 @@ const updateSession = () => createAuthEndpoint("/update-session", {
34
34
  const session = ctx.context.session;
35
35
  const additionalFields = parseSessionInput(ctx.context.options, body, "update");
36
36
  if (Object.keys(additionalFields).length === 0) throw APIError.fromStatus("BAD_REQUEST", { message: "No fields to update" });
37
- const newSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
37
+ const updatedSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
38
38
  ...additionalFields,
39
39
  updatedAt: /* @__PURE__ */ new Date()
40
- }) ?? {
40
+ });
41
+ const isStateful = !!ctx.context.options.database || !!ctx.context.options.secondaryStorage;
42
+ if (!updatedSession && isStateful) {
43
+ deleteSessionCookie(ctx);
44
+ throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
45
+ }
46
+ const newSession = updatedSession ?? {
41
47
  ...session.session,
42
48
  ...additionalFields,
43
49
  updatedAt: /* @__PURE__ */ new Date()
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
69
69
  priority?: RequestPriority | undefined;
70
70
  cache?: RequestCache | undefined;
71
71
  credentials?: RequestCredentials;
72
- headers?: (HeadersInit & (HeadersInit | {
73
- accept: "application/json" | "text/plain" | "application/octet-stream";
74
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
75
- authorization: "Bearer" | "Basic";
76
- })) | undefined;
77
72
  integrity?: string | undefined;
78
73
  keepalive?: boolean | undefined;
79
74
  method: string;
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
103
98
  prefix: string | (() => string | undefined) | undefined;
104
99
  value: string | (() => string | undefined) | undefined;
105
100
  }) | undefined;
101
+ headers?: {} | {
102
+ [x: string]: string | undefined;
103
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
104
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
105
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
106
+ } | undefined;
106
107
  body?: any;
107
108
  query?: any;
108
109
  params?: any;
@@ -70,11 +70,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
70
70
  priority?: RequestPriority | undefined;
71
71
  cache?: RequestCache | undefined;
72
72
  credentials?: RequestCredentials;
73
- headers?: (HeadersInit & (HeadersInit | {
74
- accept: "application/json" | "text/plain" | "application/octet-stream";
75
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
76
- authorization: "Bearer" | "Basic";
77
- })) | undefined;
78
73
  integrity?: string | undefined;
79
74
  keepalive?: boolean | undefined;
80
75
  method: string;
@@ -104,6 +99,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
104
99
  prefix: string | (() => string | undefined) | undefined;
105
100
  value: string | (() => string | undefined) | undefined;
106
101
  }) | undefined;
102
+ headers?: {} | {
103
+ [x: string]: string | undefined;
104
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
105
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
106
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
107
+ } | undefined;
107
108
  body?: any;
108
109
  query?: any;
109
110
  params?: any;
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
69
69
  priority?: RequestPriority | undefined;
70
70
  cache?: RequestCache | undefined;
71
71
  credentials?: RequestCredentials;
72
- headers?: (HeadersInit & (HeadersInit | {
73
- accept: "application/json" | "text/plain" | "application/octet-stream";
74
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
75
- authorization: "Bearer" | "Basic";
76
- })) | undefined;
77
72
  integrity?: string | undefined;
78
73
  keepalive?: boolean | undefined;
79
74
  method: string;
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
103
98
  prefix: string | (() => string | undefined) | undefined;
104
99
  value: string | (() => string | undefined) | undefined;
105
100
  }) | undefined;
101
+ headers?: {} | {
102
+ [x: string]: string | undefined;
103
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
104
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
105
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
106
+ } | undefined;
106
107
  body?: any;
107
108
  query?: any;
108
109
  params?: any;
@@ -55,11 +55,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
55
55
  priority?: RequestPriority | undefined;
56
56
  cache?: RequestCache | undefined;
57
57
  credentials?: RequestCredentials;
58
- headers?: (HeadersInit & (HeadersInit | {
59
- accept: "application/json" | "text/plain" | "application/octet-stream";
60
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
61
- authorization: "Bearer" | "Basic";
62
- })) | undefined;
63
58
  integrity?: string | undefined;
64
59
  keepalive?: boolean | undefined;
65
60
  method: string;
@@ -89,6 +84,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
89
84
  prefix: string | (() => string | undefined) | undefined;
90
85
  value: string | (() => string | undefined) | undefined;
91
86
  }) | undefined;
87
+ headers?: {} | {
88
+ [x: string]: string | undefined;
89
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
90
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
91
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
92
+ } | undefined;
92
93
  body?: any;
93
94
  query?: any;
94
95
  params?: any;
@@ -53,11 +53,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
53
53
  priority?: RequestPriority | undefined;
54
54
  cache?: RequestCache | undefined;
55
55
  credentials?: RequestCredentials;
56
- headers?: (HeadersInit & (HeadersInit | {
57
- accept: "application/json" | "text/plain" | "application/octet-stream";
58
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
59
- authorization: "Bearer" | "Basic";
60
- })) | undefined;
61
56
  integrity?: string | undefined;
62
57
  keepalive?: boolean | undefined;
63
58
  method: string;
@@ -87,6 +82,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
87
82
  prefix: string | (() => string | undefined) | undefined;
88
83
  value: string | (() => string | undefined) | undefined;
89
84
  }) | undefined;
85
+ headers?: {} | {
86
+ [x: string]: string | undefined;
87
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
88
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
89
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
90
+ } | undefined;
90
91
  body?: any;
91
92
  query?: any;
92
93
  params?: any;
@@ -93,11 +93,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
93
93
  priority?: RequestPriority | undefined;
94
94
  cache?: RequestCache | undefined;
95
95
  credentials?: RequestCredentials;
96
- headers?: (HeadersInit & (HeadersInit | {
97
- accept: "application/json" | "text/plain" | "application/octet-stream";
98
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
99
- authorization: "Bearer" | "Basic";
100
- })) | undefined;
101
96
  integrity?: string | undefined;
102
97
  keepalive?: boolean | undefined;
103
98
  method: string;
@@ -127,6 +122,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
127
122
  prefix: string | (() => string | undefined) | undefined;
128
123
  value: string | (() => string | undefined) | undefined;
129
124
  }) | undefined;
125
+ headers?: {} | {
126
+ [x: string]: string | undefined;
127
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
128
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
129
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
130
+ } | undefined;
130
131
  body?: any;
131
132
  query?: any;
132
133
  params?: any;
@@ -59,7 +59,7 @@ async function createAuthContext(adapter, options, getDatabaseType) {
59
59
  if (!allowedHosts || allowedHosts.length === 0) throw new BetterAuthError("baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"]).");
60
60
  }
61
61
  const baseURL = isDynamicConfig ? void 0 : getBaseURL(typeof options.baseURL === "string" ? options.baseURL : void 0, options.basePath);
62
- if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL could not be determined. Please set a valid base URL using the baseURL config option or the BETTER_AUTH_URL environment variable. Without this, callbacks and redirects may not work correctly.`);
62
+ if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL is not set. Set the baseURL option or BETTER_AUTH_URL env, or use a dynamic baseURL with allowedHosts for multi-host setups. Without it the origin is derived from the incoming request, and callbacks and redirects may not work correctly.`);
63
63
  if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger.error(`[better-auth] Misconfiguration detected.
64
64
  You are using the memory DB with generateId: false.
65
65
  This will cause no id to be generated for any model.
@@ -116,7 +116,12 @@ async function setCookieCache(ctx, session, dontRememberMe) {
116
116
  }
117
117
  if (ctx.context.options.account?.storeAccountCookie) {
118
118
  const accountData = await getAccountCookie(ctx);
119
- if (accountData) await setAccountCookie(ctx, accountData);
119
+ if (accountData) if (accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
120
+ else {
121
+ expireCookie(ctx, ctx.context.authCookies.accountData);
122
+ const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx);
123
+ accountStore.setCookies(accountStore.clean());
124
+ }
120
125
  }
121
126
  }
122
127
  async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
@@ -16,6 +16,7 @@ const createInternalAdapter = (adapter, ctx) => {
16
16
  const options = ctx.options;
17
17
  const secondaryStorage = options.secondaryStorage;
18
18
  const verificationConsumeLocks = /* @__PURE__ */ new Map();
19
+ let warnedNonAtomicConsume = false;
19
20
  const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
20
21
  const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
21
22
  async function refreshUserSessions(user) {
@@ -653,6 +654,10 @@ const createInternalAdapter = (adapter, ctx) => {
653
654
  if (secondaryStorage && !options.verification?.storeInDatabase) {
654
655
  const consumeCacheKey = async (key) => {
655
656
  if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
657
+ if (!warnedNonAtomicConsume) {
658
+ warnedNonAtomicConsume = true;
659
+ logger.warn("Secondary storage does not implement `getAndDelete`, so single-use verification values cannot be consumed atomically across processes. Implement `getAndDelete` or use database-backed verification storage to guarantee single use.");
660
+ }
656
661
  return withVerificationConsumeLock(key, async () => {
657
662
  const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
658
663
  if (!parsed) return null;
@@ -9,6 +9,19 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
9
9
  disableSignUp?: boolean | undefined;
10
10
  overrideUserInfo?: boolean | undefined;
11
11
  isTrustedProvider?: boolean | undefined;
12
+ /**
13
+ * Whether `account.providerId` may be matched against the globally
14
+ * configured `accountLinking.trustedProviders` list to infer trust.
15
+ *
16
+ * Defaults to `true` for built-in social/OAuth providers, whose
17
+ * `providerId` namespace is controlled by the developer's config. Callers
18
+ * whose `providerId` is user-controlled (e.g. the SSO plugin, where any
19
+ * authenticated user can register a provider with an arbitrary id) must
20
+ * pass `false` so a provider named after a trusted social provider can't
21
+ * launder that trust. Such callers should supply their own
22
+ * `isTrustedProvider` signal instead.
23
+ */
24
+ trustProviderByName?: boolean | undefined;
12
25
  }): Promise<{
13
26
  error: string;
14
27
  data: null;
@@ -17,7 +17,7 @@ async function handleOAuthUserInfo(c, opts) {
17
17
  const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
18
18
  if (!linkedAccount) {
19
19
  const accountLinking = c.context.options.account?.accountLinking;
20
- const isTrustedProvider = opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId);
20
+ const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
21
21
  const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
22
22
  if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
23
23
  if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`);
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.15";
2
+ var version = "1.6.16";
3
3
  //#endregion
4
4
  export { version };
@@ -1,49 +1,49 @@
1
1
  import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/types.mjs";
2
2
  //#region src/plugins/admin/access/statement.d.ts
3
3
  declare const defaultStatements: {
4
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
4
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
5
5
  readonly session: readonly ["list", "revoke", "delete"];
6
6
  };
7
7
  declare const defaultAc: {
8
8
  newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
9
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
9
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
10
10
  readonly session: readonly ["list", "revoke", "delete"];
11
11
  }, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
12
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
12
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
13
13
  readonly session: readonly ["list", "revoke", "delete"];
14
14
  }>;
15
15
  statements: {
16
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
16
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
17
17
  readonly session: readonly ["list", "revoke", "delete"];
18
18
  };
19
19
  };
20
20
  declare const adminAc: Role<ExactRoleStatements<{
21
- readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
21
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
22
22
  readonly session: ["list", "revoke", "delete"];
23
23
  }>, {
24
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
24
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
25
25
  readonly session: readonly ["list", "revoke", "delete"];
26
26
  }>;
27
27
  declare const userAc: Role<ExactRoleStatements<{
28
28
  readonly user: [];
29
29
  readonly session: [];
30
30
  }>, {
31
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
31
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
32
32
  readonly session: readonly ["list", "revoke", "delete"];
33
33
  }>;
34
34
  declare const defaultRoles: {
35
35
  admin: Role<ExactRoleStatements<{
36
- readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
36
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
37
37
  readonly session: ["list", "revoke", "delete"];
38
38
  }>, {
39
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
39
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
40
40
  readonly session: readonly ["list", "revoke", "delete"];
41
41
  }>;
42
42
  user: Role<ExactRoleStatements<{
43
43
  readonly user: [];
44
44
  readonly session: [];
45
45
  }>, {
46
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
46
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
47
47
  readonly session: readonly ["list", "revoke", "delete"];
48
48
  }>;
49
49
  };
@@ -10,6 +10,7 @@ const defaultStatements = {
10
10
  "impersonate-admins",
11
11
  "delete",
12
12
  "set-password",
13
+ "set-email",
13
14
  "get",
14
15
  "update"
15
16
  ],
@@ -29,6 +30,7 @@ const adminAc = defaultAc.newRole({
29
30
  "impersonate",
30
31
  "delete",
31
32
  "set-password",
33
+ "set-email",
32
34
  "get",
33
35
  "update"
34
36
  ],
@@ -824,13 +824,13 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
824
824
  $Infer: {
825
825
  body: {
826
826
  permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
827
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
827
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
828
828
  readonly session: readonly ["list", "revoke", "delete"];
829
829
  })]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
830
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
830
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
831
831
  readonly session: readonly ["list", "revoke", "delete"];
832
832
  })[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
833
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
833
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
834
834
  readonly session: readonly ["list", "revoke", "delete"];
835
835
  })[key]> : never)[] | undefined };
836
836
  } & {
@@ -866,6 +866,8 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
866
866
  YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
867
867
  YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
868
868
  INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
869
+ YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
870
+ PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
869
871
  };
870
872
  schema: {
871
873
  user: {
@@ -898,6 +900,7 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
898
900
  impersonatedBy: {
899
901
  type: "string";
900
902
  required: false;
903
+ input: false;
901
904
  };
902
905
  };
903
906
  };
@@ -14,7 +14,7 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
14
14
  version: string;
15
15
  $InferServerPlugin: ReturnType<typeof admin<{
16
16
  ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<{
17
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
17
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
18
18
  readonly session: readonly ["list", "revoke", "delete"];
19
19
  }>;
20
20
  roles: O["roles"] extends Record<string, Role> ? O["roles"] : {
@@ -28,13 +28,13 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
28
28
  roles: any;
29
29
  } ? keyof O["roles"] : "admin" | "user")>(data: {
30
30
  permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
31
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
31
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
32
32
  readonly session: readonly ["list", "revoke", "delete"];
33
33
  })]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
34
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
34
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
35
35
  readonly session: readonly ["list", "revoke", "delete"];
36
36
  })[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
37
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
37
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
38
38
  readonly session: readonly ["list", "revoke", "delete"];
39
39
  })[key]> : never)[] | undefined };
40
40
  } & {
@@ -73,6 +73,8 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
73
73
  YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
74
74
  YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
75
75
  INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
76
+ YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
77
+ PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
76
78
  };
77
79
  };
78
80
  //#endregion
@@ -23,6 +23,8 @@ declare const ADMIN_ERROR_CODES: {
23
23
  YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
24
24
  YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
25
25
  INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
26
+ YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
27
+ PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
26
28
  };
27
29
  //#endregion
28
30
  export { ADMIN_ERROR_CODES };
@@ -21,7 +21,9 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
21
21
  YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself",
22
22
  YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value",
23
23
  YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
24
- INVALID_ROLE_TYPE: "Invalid role type"
24
+ INVALID_ROLE_TYPE: "Invalid role type",
25
+ YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: "You are not allowed to update users email",
26
+ PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead"
25
27
  });
26
28
  //#endregion
27
29
  export { ADMIN_ERROR_CODES };
@@ -156,14 +156,43 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
156
156
  permissions: { user: ["create"] }
157
157
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS);
158
158
  }
159
+ const { role: dataRole, ...userData } = ctx.body.data ?? {};
160
+ const requestedRole = ctx.body.role ?? dataRole;
161
+ if (requestedRole !== void 0) {
162
+ if (session) {
163
+ if (!hasPermission({
164
+ userId: session.user.id,
165
+ role: session.user.role,
166
+ options: opts,
167
+ permissions: { user: ["set-role"] }
168
+ })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE);
169
+ }
170
+ const inputRoles = Array.isArray(requestedRole) ? requestedRole : [requestedRole];
171
+ for (const role of inputRoles) {
172
+ if (typeof role !== "string") throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.INVALID_ROLE_TYPE);
173
+ if (opts.roles && !opts.roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
174
+ }
175
+ }
176
+ if (session && [
177
+ "banned",
178
+ "banReason",
179
+ "banExpires"
180
+ ].some((key) => Object.prototype.hasOwnProperty.call(userData, key))) {
181
+ if (!hasPermission({
182
+ userId: session.user.id,
183
+ role: session.user.role,
184
+ options: opts,
185
+ permissions: { user: ["ban"] }
186
+ })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
187
+ }
159
188
  const email = ctx.body.email.toLowerCase();
160
189
  if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
161
190
  if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
162
191
  const user = await ctx.context.internalAdapter.createUser({
192
+ ...userData,
163
193
  email,
164
194
  name: ctx.body.name,
165
- role: (ctx.body.role && parseRoles(ctx.body.role)) ?? opts?.defaultRole ?? "user",
166
- ...ctx.body.data
195
+ role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
167
196
  });
168
197
  if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
169
198
  if (ctx.body.password) {
@@ -220,6 +249,9 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
220
249
  permissions: { user: ["update"] }
221
250
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS);
222
251
  if (Object.keys(ctx.body.data).length === 0) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE);
252
+ const updateData = ctx.body.data;
253
+ const hasDataKey = (key) => Object.prototype.hasOwnProperty.call(updateData, key);
254
+ if (hasDataKey("password")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER);
223
255
  if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
224
256
  if (!hasPermission({
225
257
  userId: ctx.context.session.user.id,
@@ -235,8 +267,37 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
235
267
  }
236
268
  ctx.body.data.role = parseRoles(inputRoles);
237
269
  }
270
+ if ([
271
+ "banned",
272
+ "banReason",
273
+ "banExpires"
274
+ ].some(hasDataKey)) {
275
+ if (!hasPermission({
276
+ userId: ctx.context.session.user.id,
277
+ role: ctx.context.session.user.role,
278
+ options: opts,
279
+ permissions: { user: ["ban"] }
280
+ })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
281
+ if (updateData.banned === true && ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF);
282
+ }
283
+ if (hasDataKey("email") || hasDataKey("emailVerified")) {
284
+ if (!hasPermission({
285
+ userId: ctx.context.session.user.id,
286
+ role: ctx.context.session.user.role,
287
+ options: opts,
288
+ permissions: { user: ["set-email"] }
289
+ })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL);
290
+ if (hasDataKey("email")) {
291
+ const email = String(updateData.email).toLowerCase();
292
+ if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
293
+ const existUser = await ctx.context.internalAdapter.findUserByEmail(email);
294
+ if (existUser && existUser.user.id !== ctx.body.userId) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
295
+ updateData.email = email;
296
+ }
297
+ }
238
298
  if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
239
299
  const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
300
+ if (updateData.banned === true) await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
240
301
  return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
241
302
  });
242
303
  const listUsersQuerySchema = z.object({
@@ -30,6 +30,7 @@ declare const schema: {
30
30
  impersonatedBy: {
31
31
  type: "string";
32
32
  required: false;
33
+ input: false;
33
34
  };
34
35
  };
35
36
  };
@@ -25,7 +25,8 @@ const schema = {
25
25
  } },
26
26
  session: { fields: { impersonatedBy: {
27
27
  type: "string",
28
- required: false
28
+ required: false,
29
+ input: false
29
30
  } } }
30
31
  };
31
32
  //#endregion
@@ -334,7 +334,7 @@ const verifyEmailOTP = (opts) => createAuthEndpoint("/email-otp/verify-email", {
334
334
  });
335
335
  }
336
336
  const currentSession = await getSessionFromCtx(ctx);
337
- if (currentSession && updatedUser.emailVerified) {
337
+ if (currentSession && updatedUser.emailVerified && currentSession.user.id === updatedUser.id) {
338
338
  const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
339
339
  await setCookieCache(ctx, {
340
340
  session: currentSession.session,
@@ -209,7 +209,12 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
209
209
  ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
210
210
  redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
211
211
  }
212
- const id = mapUser.id ? String(mapUser.id) : String(userInfo.id);
212
+ const rawId = mapUser.id !== void 0 && mapUser.id !== null && mapUser.id !== "" ? mapUser.id : userInfo.id;
213
+ const id = rawId !== void 0 && rawId !== null ? String(rawId) : "";
214
+ if (!id) {
215
+ ctx.context.logger.error("Provider did not return an account id (e.g. `sub`). Unable to sign in.", userInfo);
216
+ redirectOnError(ctx, resolvedErrorURL, "id_is_missing");
217
+ }
213
218
  const name = mapUser.name ? mapUser.name : userInfo.name;
214
219
  if (!name) {
215
220
  ctx.context.logger.error("Unable to get user info", userInfo);
@@ -398,8 +403,10 @@ async function getUserInfo(tokens, finalUserInfoUrl) {
398
403
  method: "GET",
399
404
  headers: { Authorization: `Bearer ${tokens.accessToken}` }
400
405
  });
406
+ const subjectId = userInfo.data?.sub ?? userInfo.data?.id;
407
+ if (subjectId === void 0 || subjectId === null || subjectId === "") return null;
401
408
  return {
402
- id: userInfo.data?.sub ?? "",
409
+ id: subjectId,
403
410
  emailVerified: userInfo.data?.email_verified ?? false,
404
411
  email: userInfo.data?.email,
405
412
  image: userInfo.data?.picture,
@@ -394,11 +394,13 @@ function organization(options) {
394
394
  activeOrganizationId: {
395
395
  type: "string",
396
396
  required: false,
397
+ input: false,
397
398
  fieldName: opts.schema?.session?.fields?.activeOrganizationId
398
399
  },
399
400
  ...teamSupport ? { activeTeamId: {
400
401
  type: "string",
401
402
  required: false,
403
+ input: false,
402
404
  fieldName: opts.schema?.session?.fields?.activeTeamId
403
405
  } } : {}
404
406
  } }
@@ -170,7 +170,12 @@ const createInvitation = (option) => {
170
170
  }, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
171
171
  if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
172
172
  if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
173
- if ((typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId).some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
173
+ const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
174
+ if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
175
+ for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({
176
+ teamId,
177
+ organizationId
178
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
174
179
  }
175
180
  if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
176
181
  const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
@@ -285,6 +290,10 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
285
290
  const teamIds = acceptedI.teamId.split(",");
286
291
  const onlyOne = teamIds.length === 1;
287
292
  for (const teamId of teamIds) {
293
+ if (!await adapter.findTeamById({
294
+ teamId,
295
+ organizationId: invitation.organizationId
296
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
288
297
  await adapter.findOrCreateTeamMember({
289
298
  teamId,
290
299
  userId: session.user.id
@@ -441,8 +441,15 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
441
441
  use: [orgMiddleware, orgSessionMiddleware]
442
442
  }, async (ctx) => {
443
443
  const session = ctx.context.session;
444
- const teams = await getOrgAdapter(ctx.context, ctx.context.orgOptions).listTeamsByUser({ userId: session.user.id });
445
- return ctx.json(teams);
444
+ const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
445
+ const teams = await adapter.listTeamsByUser({ userId: session.user.id });
446
+ const orgIds = [...new Set(teams.map((team) => team.organizationId))];
447
+ const memberships = await Promise.all(orgIds.map((organizationId) => adapter.checkMembership({
448
+ userId: session.user.id,
449
+ organizationId
450
+ })));
451
+ const memberOrgIds = new Set(orgIds.filter((_, index) => memberships[index]));
452
+ return ctx.json(teams.filter((team) => memberOrgIds.has(team.organizationId)));
446
453
  });
447
454
  const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
448
455
  const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", {
@@ -494,6 +501,12 @@ const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team
494
501
  const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
495
502
  const teamId = ctx.query?.teamId || session?.session.activeTeamId;
496
503
  if (!teamId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM);
504
+ const team = await adapter.findTeamById({ teamId });
505
+ if (!team) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
506
+ if (!await adapter.checkMembership({
507
+ userId: session.user.id,
508
+ organizationId: team.organizationId
509
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM);
497
510
  if (!await adapter.findTeamMember({
498
511
  userId: session.user.id,
499
512
  teamId
@@ -183,6 +183,7 @@ interface SessionDefaultFields {
183
183
  activeOrganizationId: {
184
184
  type: "string";
185
185
  required: false;
186
+ input: false;
186
187
  };
187
188
  }
188
189
  type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessControl"] extends {
@@ -222,6 +223,7 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
222
223
  activeTeamId: {
223
224
  type: "string";
224
225
  required: false;
226
+ input: false;
225
227
  };
226
228
  } : {});
227
229
  };
@@ -5,6 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { APIError } from "../../api/index.mjs";
6
6
  import { PACKAGE_VERSION } from "../../version.mjs";
7
7
  import { toChecksumAddress } from "../../utils/hashing.mjs";
8
+ import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
8
9
  import { schema } from "./schema.mjs";
9
10
  import { createAuthEndpoint } from "@better-auth/core/api";
10
11
  import * as z from "zod";
@@ -74,6 +75,33 @@ const siwe = (options) => {
74
75
  code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE"
75
76
  });
76
77
  const { value: nonce } = verification;
78
+ const parsedMessage = parseSiweMessage(message);
79
+ const nonceMatches = parsedMessage.nonce === nonce;
80
+ const addressMatches = !!parsedMessage.address && parsedMessage.address.toLowerCase() === walletAddress.toLowerCase();
81
+ const chainMatches = parsedMessage.chainId === chainId;
82
+ const domainMatches = !!parsedMessage.domain && normalizeSiweDomain(parsedMessage.domain) === normalizeSiweDomain(options.domain);
83
+ if (!nonceMatches || !addressMatches || !chainMatches || !domainMatches) throw APIError.fromStatus("UNAUTHORIZED", {
84
+ message: "Unauthorized: SIWE message does not match the expected nonce, domain, address, or chain ID",
85
+ status: 401,
86
+ code: "UNAUTHORIZED_SIWE_MESSAGE_MISMATCH"
87
+ });
88
+ const now = Date.now();
89
+ if (parsedMessage.expirationTime) {
90
+ const expiresAt = Date.parse(parsedMessage.expirationTime);
91
+ if (!Number.isNaN(expiresAt) && now >= expiresAt) throw APIError.fromStatus("UNAUTHORIZED", {
92
+ message: "Unauthorized: SIWE message has expired",
93
+ status: 401,
94
+ code: "UNAUTHORIZED_SIWE_MESSAGE_EXPIRED"
95
+ });
96
+ }
97
+ if (parsedMessage.notBefore) {
98
+ const notBefore = Date.parse(parsedMessage.notBefore);
99
+ if (!Number.isNaN(notBefore) && now < notBefore) throw APIError.fromStatus("UNAUTHORIZED", {
100
+ message: "Unauthorized: SIWE message is not yet valid",
101
+ status: 401,
102
+ code: "UNAUTHORIZED_SIWE_MESSAGE_NOT_YET_VALID"
103
+ });
104
+ }
77
105
  if (!await options.verifyMessage({
78
106
  message,
79
107
  signature,
@@ -0,0 +1,60 @@
1
+ //#region src/plugins/siwe/parse-message.ts
2
+ const HEADER_REGEX = /^(?:([a-zA-Z][a-zA-Z0-9+.-]*):\/\/)?(\S+) wants you to sign in with your Ethereum account:$/;
3
+ const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
4
+ const FIELD_REGEX = /^([A-Za-z ]+): (.*)$/;
5
+ function parseSiweMessage(message) {
6
+ const result = {};
7
+ const lines = message.split(/\r?\n/);
8
+ const headerMatch = lines[0]?.match(HEADER_REGEX);
9
+ if (headerMatch) {
10
+ if (headerMatch[1]) result.scheme = headerMatch[1];
11
+ result.domain = headerMatch[2];
12
+ }
13
+ const addressLine = lines[1]?.trim();
14
+ if (addressLine && ADDRESS_REGEX.test(addressLine)) result.address = addressLine;
15
+ for (const line of lines) {
16
+ const match = line.match(FIELD_REGEX);
17
+ if (!match) continue;
18
+ const [, key, value] = match;
19
+ switch (key) {
20
+ case "URI":
21
+ result.uri = value;
22
+ break;
23
+ case "Version":
24
+ result.version = value;
25
+ break;
26
+ case "Chain ID": {
27
+ const parsed = Number(value);
28
+ if (Number.isInteger(parsed)) result.chainId = parsed;
29
+ break;
30
+ }
31
+ case "Nonce":
32
+ result.nonce = value;
33
+ break;
34
+ case "Issued At":
35
+ result.issuedAt = value;
36
+ break;
37
+ case "Expiration Time":
38
+ result.expirationTime = value;
39
+ break;
40
+ case "Not Before":
41
+ result.notBefore = value;
42
+ break;
43
+ case "Request ID":
44
+ result.requestId = value;
45
+ break;
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ /**
51
+ * Normalizes a SIWE `domain` (RFC 3986 authority) for comparison: strips any
52
+ * scheme and path, lowercases, leaving `host[:port]`.
53
+ */
54
+ function normalizeSiweDomain(domain) {
55
+ const withoutScheme = domain.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
56
+ const pathStart = withoutScheme.indexOf("/");
57
+ return pathStart === -1 ? withoutScheme : withoutScheme.slice(0, pathStart);
58
+ }
59
+ //#endregion
60
+ export { normalizeSiweDomain, parseSiweMessage };
@@ -220,7 +220,15 @@ const twoFactor = (options) => {
220
220
  expireCookie(ctx, trustDeviceCookieAttrs);
221
221
  }
222
222
  /**
223
- * remove the session cookie. It's set by the sign in credential
223
+ * Remove the session cookie set by the credential sign-in.
224
+ *
225
+ * The credential handler already created a session and set
226
+ * `ctx.context.newSession`. Since 2FA is still pending, that
227
+ * session is deleted here and `newSession` is reset to `null`
228
+ * so downstream hooks don't observe a session that no longer
229
+ * exists. Hooks that read `ctx.context.newSession` after a
230
+ * sign-in must therefore null-check it: it is `null` while a
231
+ * 2FA challenge is in flight (no authenticated session yet).
224
232
  */
225
233
  deleteSessionCookie(ctx, true);
226
234
  await ctx.context.internalAdapter.deleteSession(data.session.token);
@@ -7999,11 +7999,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
7999
7999
  priority?: RequestPriority | undefined;
8000
8000
  cache?: RequestCache | undefined;
8001
8001
  credentials?: RequestCredentials;
8002
- headers?: (HeadersInit & (HeadersInit | {
8003
- accept: "application/json" | "text/plain" | "application/octet-stream";
8004
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
8005
- authorization: "Bearer" | "Basic";
8006
- })) | undefined;
8007
8002
  integrity?: string | undefined;
8008
8003
  keepalive?: boolean | undefined;
8009
8004
  method: string;
@@ -8033,6 +8028,12 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
8033
8028
  prefix: string | (() => string | undefined) | undefined;
8034
8029
  value: string | (() => string | undefined) | undefined;
8035
8030
  }) | undefined;
8031
+ headers?: {} | {
8032
+ [x: string]: string | undefined;
8033
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
8034
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
8035
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
8036
+ } | undefined;
8036
8037
  body?: any;
8037
8038
  query?: any;
8038
8039
  params?: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.6.15",
3
+ "version": "1.6.16",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -480,22 +480,22 @@
480
480
  },
481
481
  "dependencies": {
482
482
  "@better-auth/utils": "0.4.1",
483
- "@better-fetch/fetch": "1.1.21",
483
+ "@better-fetch/fetch": "1.2.2",
484
484
  "@noble/ciphers": "^2.1.1",
485
485
  "@noble/hashes": "^2.0.1",
486
- "better-call": "1.3.5",
486
+ "better-call": "1.3.6",
487
487
  "defu": "^6.1.4",
488
488
  "jose": "^6.1.3",
489
489
  "kysely": "^0.28.17 || ^0.29.0",
490
490
  "nanostores": "^1.1.1",
491
491
  "zod": "^4.3.6",
492
- "@better-auth/core": "1.6.15",
493
- "@better-auth/drizzle-adapter": "1.6.15",
494
- "@better-auth/kysely-adapter": "1.6.15",
495
- "@better-auth/memory-adapter": "1.6.15",
496
- "@better-auth/mongo-adapter": "1.6.15",
497
- "@better-auth/prisma-adapter": "1.6.15",
498
- "@better-auth/telemetry": "1.6.15"
492
+ "@better-auth/core": "1.6.16",
493
+ "@better-auth/drizzle-adapter": "1.6.16",
494
+ "@better-auth/kysely-adapter": "1.6.16",
495
+ "@better-auth/memory-adapter": "1.6.16",
496
+ "@better-auth/mongo-adapter": "1.6.16",
497
+ "@better-auth/prisma-adapter": "1.6.16",
498
+ "@better-auth/telemetry": "1.6.16"
499
499
  },
500
500
  "devDependencies": {
501
501
  "@lynx-js/react": "^0.116.3",