better-auth 1.6.14 → 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 (43) hide show
  1. package/dist/api/dispatch.d.mts +34 -0
  2. package/dist/api/dispatch.mjs +272 -0
  3. package/dist/api/index.d.mts +2 -1
  4. package/dist/api/index.mjs +2 -1
  5. package/dist/api/middlewares/origin-check.mjs +1 -0
  6. package/dist/api/routes/account.mjs +11 -6
  7. package/dist/api/routes/callback.mjs +1 -1
  8. package/dist/api/routes/session.mjs +4 -2
  9. package/dist/api/routes/update-session.mjs +9 -3
  10. package/dist/api/to-auth-endpoints.mjs +14 -265
  11. package/dist/client/lynx/index.d.mts +6 -5
  12. package/dist/client/react/index.d.mts +6 -5
  13. package/dist/client/solid/index.d.mts +6 -5
  14. package/dist/client/svelte/index.d.mts +6 -5
  15. package/dist/client/vanilla.d.mts +6 -5
  16. package/dist/client/vue/index.d.mts +6 -5
  17. package/dist/context/create-context.mjs +1 -1
  18. package/dist/cookies/cookie-utils.mjs +2 -2
  19. package/dist/cookies/index.mjs +6 -1
  20. package/dist/db/internal-adapter.mjs +5 -0
  21. package/dist/oauth2/link-account.d.mts +13 -0
  22. package/dist/oauth2/link-account.mjs +1 -1
  23. package/dist/package.mjs +1 -1
  24. package/dist/plugins/admin/access/statement.d.mts +10 -10
  25. package/dist/plugins/admin/access/statement.mjs +2 -0
  26. package/dist/plugins/admin/admin.d.mts +6 -3
  27. package/dist/plugins/admin/client.d.mts +6 -4
  28. package/dist/plugins/admin/error-codes.d.mts +2 -0
  29. package/dist/plugins/admin/error-codes.mjs +3 -1
  30. package/dist/plugins/admin/routes.mjs +66 -2
  31. package/dist/plugins/admin/schema.d.mts +1 -0
  32. package/dist/plugins/admin/schema.mjs +2 -1
  33. package/dist/plugins/email-otp/routes.mjs +1 -1
  34. package/dist/plugins/generic-oauth/routes.mjs +9 -2
  35. package/dist/plugins/organization/organization.mjs +2 -0
  36. package/dist/plugins/organization/routes/crud-invites.mjs +10 -1
  37. package/dist/plugins/organization/routes/crud-team.mjs +15 -2
  38. package/dist/plugins/organization/schema.d.mts +2 -0
  39. package/dist/plugins/siwe/index.mjs +28 -0
  40. package/dist/plugins/siwe/parse-message.mjs +60 -0
  41. package/dist/plugins/two-factor/index.mjs +9 -1
  42. package/dist/test-utils/test-instance.d.mts +6 -5
  43. package/package.json +10 -10
@@ -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 };
@@ -72,6 +72,7 @@ const setRole = (opts) => createAuthEndpoint("/admin/set-role", {
72
72
  const inputRoles = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role];
73
73
  for (const role of inputRoles) if (!roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
74
74
  }
75
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
75
76
  const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, { role: parseRoles(ctx.body.role) });
76
77
  return ctx.json({ user: parseUserOutput(ctx.context.options, updatedUser) });
77
78
  });
@@ -155,14 +156,43 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
155
156
  permissions: { user: ["create"] }
156
157
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS);
157
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
+ }
158
188
  const email = ctx.body.email.toLowerCase();
159
189
  if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
160
190
  if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
161
191
  const user = await ctx.context.internalAdapter.createUser({
192
+ ...userData,
162
193
  email,
163
194
  name: ctx.body.name,
164
- role: (ctx.body.role && parseRoles(ctx.body.role)) ?? opts?.defaultRole ?? "user",
165
- ...ctx.body.data
195
+ role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
166
196
  });
167
197
  if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
168
198
  if (ctx.body.password) {
@@ -219,6 +249,9 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
219
249
  permissions: { user: ["update"] }
220
250
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS);
221
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);
222
255
  if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
223
256
  if (!hasPermission({
224
257
  userId: ctx.context.session.user.id,
@@ -234,7 +267,37 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
234
267
  }
235
268
  ctx.body.data.role = parseRoles(inputRoles);
236
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
+ }
298
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
237
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);
238
301
  return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
239
302
  });
240
303
  const listUsersQuerySchema = z.object({
@@ -402,6 +465,7 @@ const unbanUser = (opts) => createAuthEndpoint("/admin/unban-user", {
402
465
  options: opts,
403
466
  permissions: { user: ["ban"] }
404
467
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
468
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
405
469
  const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
406
470
  banned: false,
407
471
  banExpires: null,
@@ -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.14",
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.14",
493
- "@better-auth/drizzle-adapter": "1.6.14",
494
- "@better-auth/kysely-adapter": "1.6.14",
495
- "@better-auth/memory-adapter": "1.6.14",
496
- "@better-auth/mongo-adapter": "1.6.14",
497
- "@better-auth/prisma-adapter": "1.6.14",
498
- "@better-auth/telemetry": "1.6.14"
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",