better-auth 1.6.15 → 1.6.17

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 (105) hide show
  1. package/dist/api/index.d.mts +2 -2
  2. package/dist/api/index.mjs +3 -4
  3. package/dist/api/middlewares/origin-check.mjs +6 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +31 -11
  6. package/dist/api/routes/callback.mjs +3 -3
  7. package/dist/api/routes/index.d.mts +1 -1
  8. package/dist/api/routes/password.mjs +3 -4
  9. package/dist/api/routes/session.d.mts +12 -1
  10. package/dist/api/routes/session.mjs +16 -2
  11. package/dist/api/routes/sign-in.mjs +5 -5
  12. package/dist/api/routes/sign-up.mjs +2 -2
  13. package/dist/api/routes/update-session.mjs +9 -4
  14. package/dist/api/routes/update-user.mjs +10 -12
  15. package/dist/auth/base.mjs +11 -7
  16. package/dist/client/equality.d.mts +19 -0
  17. package/dist/client/equality.mjs +42 -0
  18. package/dist/client/index.d.mts +5 -4
  19. package/dist/client/index.mjs +2 -1
  20. package/dist/client/lynx/index.d.mts +6 -5
  21. package/dist/client/path-to-object.d.mts +5 -2
  22. package/dist/client/plugins/index.d.mts +4 -1
  23. package/dist/client/plugins/index.mjs +4 -1
  24. package/dist/client/query.d.mts +4 -3
  25. package/dist/client/query.mjs +27 -17
  26. package/dist/client/react/index.d.mts +6 -5
  27. package/dist/client/session-atom.mjs +129 -4
  28. package/dist/client/session-refresh.d.mts +3 -18
  29. package/dist/client/session-refresh.mjs +38 -49
  30. package/dist/client/solid/index.d.mts +6 -5
  31. package/dist/client/svelte/index.d.mts +6 -5
  32. package/dist/client/types.d.mts +2 -2
  33. package/dist/client/vanilla.d.mts +6 -5
  34. package/dist/client/vue/index.d.mts +6 -5
  35. package/dist/context/create-context.mjs +3 -2
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +30 -2
  38. package/dist/db/internal-adapter.mjs +56 -0
  39. package/dist/oauth2/link-account.d.mts +13 -0
  40. package/dist/oauth2/link-account.mjs +1 -1
  41. package/dist/package.mjs +1 -1
  42. package/dist/plugins/access/access.mjs +49 -19
  43. package/dist/plugins/admin/access/statement.d.mts +10 -10
  44. package/dist/plugins/admin/access/statement.mjs +2 -0
  45. package/dist/plugins/admin/admin.d.mts +6 -3
  46. package/dist/plugins/admin/client.d.mts +6 -4
  47. package/dist/plugins/admin/error-codes.d.mts +2 -0
  48. package/dist/plugins/admin/error-codes.mjs +3 -1
  49. package/dist/plugins/admin/routes.mjs +73 -5
  50. package/dist/plugins/admin/schema.d.mts +1 -0
  51. package/dist/plugins/admin/schema.mjs +2 -1
  52. package/dist/plugins/captcha/constants.mjs +8 -1
  53. package/dist/plugins/captcha/index.mjs +8 -2
  54. package/dist/plugins/captcha/types.d.mts +21 -0
  55. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  56. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  57. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  58. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  59. package/dist/plugins/device-authorization/routes.mjs +16 -9
  60. package/dist/plugins/email-otp/routes.mjs +23 -53
  61. package/dist/plugins/generic-oauth/index.mjs +7 -2
  62. package/dist/plugins/generic-oauth/routes.mjs +20 -9
  63. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  64. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  65. package/dist/plugins/index.d.mts +5 -1
  66. package/dist/plugins/index.mjs +4 -1
  67. package/dist/plugins/jwt/index.mjs +2 -2
  68. package/dist/plugins/mcp/client/index.mjs +1 -0
  69. package/dist/plugins/mcp/index.mjs +8 -0
  70. package/dist/plugins/multi-session/index.mjs +7 -5
  71. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  72. package/dist/plugins/oauth-popup/client.mjs +203 -0
  73. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  74. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  75. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  76. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  77. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  78. package/dist/plugins/oauth-popup/index.mjs +227 -0
  79. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  80. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  81. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  82. package/dist/plugins/oidc-provider/index.mjs +10 -0
  83. package/dist/plugins/one-tap/client.mjs +12 -6
  84. package/dist/plugins/one-tap/index.d.mts +1 -0
  85. package/dist/plugins/one-tap/index.mjs +9 -5
  86. package/dist/plugins/one-time-token/index.mjs +1 -3
  87. package/dist/plugins/open-api/generator.mjs +7 -4
  88. package/dist/plugins/organization/adapter.d.mts +29 -1
  89. package/dist/plugins/organization/adapter.mjs +66 -6
  90. package/dist/plugins/organization/organization.mjs +2 -0
  91. package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
  92. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  93. package/dist/plugins/organization/routes/crud-team.mjs +51 -5
  94. package/dist/plugins/organization/schema.d.mts +2 -0
  95. package/dist/plugins/phone-number/routes.mjs +41 -36
  96. package/dist/plugins/siwe/index.mjs +30 -3
  97. package/dist/plugins/siwe/parse-message.mjs +60 -0
  98. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  99. package/dist/plugins/two-factor/index.mjs +9 -1
  100. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  101. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  102. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  103. package/dist/plugins/username/index.mjs +6 -6
  104. package/dist/test-utils/test-instance.d.mts +6 -5
  105. package/package.json +10 -10
@@ -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.17";
3
3
  //#endregion
4
4
  export { version };
@@ -1,33 +1,63 @@
1
1
  import { BetterAuthError } from "@better-auth/core/error";
2
2
  //#region src/plugins/access/access.ts
3
+ function unknownResourceResponse(requestedResource) {
4
+ return {
5
+ success: false,
6
+ error: `You are not allowed to access resource: ${requestedResource}`
7
+ };
8
+ }
9
+ function unauthorizedResourceResponse(requestedResource) {
10
+ return {
11
+ success: false,
12
+ error: `unauthorized to access resource "${requestedResource}"`
13
+ };
14
+ }
15
+ function normalizeConnector(connector) {
16
+ return connector === "OR" ? "OR" : "AND";
17
+ }
18
+ function isActionList(actions) {
19
+ return Array.isArray(actions);
20
+ }
21
+ function normalizeActionRequest(requestedActions) {
22
+ if (isActionList(requestedActions)) return {
23
+ actions: requestedActions,
24
+ connector: "AND"
25
+ };
26
+ if (!requestedActions || typeof requestedActions !== "object") throw new BetterAuthError("Invalid access control request");
27
+ const { actions, connector } = requestedActions;
28
+ if (!isActionList(actions)) return {
29
+ actions: [],
30
+ connector: normalizeConnector(connector)
31
+ };
32
+ return {
33
+ actions,
34
+ connector: normalizeConnector(connector)
35
+ };
36
+ }
37
+ function hasAllowedAction(allowedActions, requestedAction) {
38
+ return typeof requestedAction === "string" && allowedActions.includes(requestedAction);
39
+ }
40
+ function isResourceAuthorized(allowedActions, { actions, connector }) {
41
+ if (actions.length === 0) return false;
42
+ if (connector === "OR") return actions.some((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
43
+ return actions.every((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
44
+ }
3
45
  function role(statements) {
4
46
  return {
5
47
  authorize(request, connector = "AND") {
6
- let success = false;
48
+ let hasAuthorizedResource = false;
7
49
  for (const [requestedResource, requestedActions] of Object.entries(request)) {
8
50
  const allowedActions = statements[requestedResource];
9
51
  if (!allowedActions) {
10
- if (connector === "AND") return {
11
- success: false,
12
- error: `You are not allowed to access resource: ${requestedResource}`
13
- };
14
- success = false;
52
+ if (connector === "AND") return unknownResourceResponse(requestedResource);
15
53
  continue;
16
54
  }
17
- if (Array.isArray(requestedActions)) success = requestedActions.length > 0 && requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
18
- else if (typeof requestedActions === "object") {
19
- const actions = requestedActions;
20
- if (!Array.isArray(actions.actions) || actions.actions.length === 0) success = false;
21
- else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
22
- else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
23
- } else throw new BetterAuthError("Invalid access control request");
24
- if (success && connector === "OR") return { success };
25
- if (!success && connector === "AND") return {
26
- success: false,
27
- error: `unauthorized to access resource "${requestedResource}"`
28
- };
55
+ const isAuthorized = isResourceAuthorized(allowedActions, normalizeActionRequest(requestedActions));
56
+ if (isAuthorized) hasAuthorizedResource = true;
57
+ if (isAuthorized && connector === "OR") return { success: true };
58
+ if (!isAuthorized && connector === "AND") return unauthorizedResourceResponse(requestedResource);
29
59
  }
30
- if (success) return { success };
60
+ if (hasAuthorizedResource) return { success: true };
31
61
  return {
32
62
  success: false,
33
63
  error: "Not authorized"
@@ -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({
@@ -755,16 +816,23 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
755
816
  const { newPassword, userId } = ctx.body;
756
817
  const minPasswordLength = ctx.context.password.config.minPasswordLength;
757
818
  if (newPassword.length < minPasswordLength) {
758
- ctx.context.logger.error("Password is too short");
819
+ ctx.context.logger.warn("Password is too short");
759
820
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
760
821
  }
761
822
  const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
762
823
  if (newPassword.length > maxPasswordLength) {
763
- ctx.context.logger.error("Password is too long");
824
+ ctx.context.logger.warn("Password is too long");
764
825
  throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
765
826
  }
827
+ if (!await ctx.context.internalAdapter.findUserById(userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
766
828
  const hashedPassword = await ctx.context.password.hash(newPassword);
767
- await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
829
+ if ((await ctx.context.internalAdapter.findAccounts(userId)).find((account) => account.providerId === "credential")) await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
830
+ else await ctx.context.internalAdapter.createAccount({
831
+ userId,
832
+ providerId: "credential",
833
+ accountId: userId,
834
+ password: hashedPassword
835
+ });
768
836
  return ctx.json({ status: true });
769
837
  });
770
838
  const userHasPermissionBodySchema = 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
@@ -1,4 +1,11 @@
1
1
  //#region src/plugins/captcha/constants.ts
2
+ /**
3
+ * Upper bound (in milliseconds) for a single provider verification request.
4
+ * Without it, a hanging provider would tie up the request indefinitely before
5
+ * any rate limiting applies, so every verify handler aborts at this deadline
6
+ * and fails closed.
7
+ */
8
+ const CAPTCHA_VERIFY_TIMEOUT_MS = 1e4;
2
9
  const defaultEndpoints = [
3
10
  "/sign-up/email",
4
11
  "/sign-in/email",
@@ -17,4 +24,4 @@ const siteVerifyMap = {
17
24
  [Providers.CAPTCHAFOX]: "https://api.captchafox.com/siteverify"
18
25
  };
19
26
  //#endregion
20
- export { Providers, defaultEndpoints, siteVerifyMap };
27
+ export { CAPTCHA_VERIFY_TIMEOUT_MS, Providers, defaultEndpoints, siteVerifyMap };
@@ -42,10 +42,16 @@ const captcha = (options) => ({
42
42
  secretKey: options.secretKey,
43
43
  remoteIP: remoteUserIP
44
44
  };
45
- if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile(handlerParams);
45
+ if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile({
46
+ ...handlerParams,
47
+ expectedAction: options.expectedAction,
48
+ allowedHostnames: options.allowedHostnames
49
+ });
46
50
  if (options.provider === Providers.GOOGLE_RECAPTCHA) return await googleRecaptcha({
47
51
  ...handlerParams,
48
- minScore: options.minScore
52
+ minScore: options.minScore,
53
+ expectedAction: options.expectedAction,
54
+ allowedHostnames: options.allowedHostnames
49
55
  });
50
56
  if (options.provider === Providers.HCAPTCHA) return await hCaptcha({
51
57
  ...handlerParams,
@@ -10,9 +10,30 @@ interface BaseCaptchaOptions {
10
10
  interface GoogleRecaptchaOptions extends BaseCaptchaOptions {
11
11
  provider: typeof Providers.GOOGLE_RECAPTCHA;
12
12
  minScore?: number | undefined;
13
+ /**
14
+ * Expected reCAPTCHA v3 `action`. When set, a verification whose action does
15
+ * not match is rejected, preventing a token minted for another action on the
16
+ * same site key from being replayed against this endpoint.
17
+ */
18
+ expectedAction?: string | undefined;
19
+ /**
20
+ * Allow-list of hostnames the token must have been issued for. When set, a
21
+ * verification reporting a different hostname is rejected.
22
+ */
23
+ allowedHostnames?: string[] | undefined;
13
24
  }
14
25
  interface CloudflareTurnstileOptions extends BaseCaptchaOptions {
15
26
  provider: typeof Providers.CLOUDFLARE_TURNSTILE;
27
+ /**
28
+ * Expected Turnstile `action`. When set, a verification whose action does
29
+ * not match is rejected, preventing cross-context token reuse.
30
+ */
31
+ expectedAction?: string | undefined;
32
+ /**
33
+ * Allow-list of hostnames the token must have been issued for. When set, a
34
+ * verification reporting a different or missing hostname is rejected.
35
+ */
36
+ allowedHostnames?: string[] | undefined;
16
37
  }
17
38
  interface HCaptchaOptions extends BaseCaptchaOptions {
18
39
  provider: typeof Providers.HCAPTCHA;
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const captchaFox = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
7
8
  const response = await betterFetch(siteVerifyURL, {
8
9
  method: "POST",
10
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
9
11
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
10
12
  body: encodeToURLParams({
11
13
  secret: secretKey,
@@ -1,10 +1,12 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { betterFetch } from "@better-fetch/fetch";
4
5
  //#region src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
5
- const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP }) => {
6
+ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, expectedAction, allowedHostnames }) => {
6
7
  const response = await betterFetch(siteVerifyURL, {
7
8
  method: "POST",
9
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
8
10
  headers: { "Content-Type": "application/json" },
9
11
  body: JSON.stringify({
10
12
  secret: secretKey,
@@ -13,11 +15,14 @@ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey,
13
15
  })
14
16
  });
15
17
  if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
16
- if (!response.data.success) return middlewareResponse({
18
+ const verificationFailed = () => middlewareResponse({
17
19
  message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
18
20
  code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
19
21
  status: 403
20
22
  });
23
+ if (!response.data.success) return verificationFailed();
24
+ if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
25
+ if (allowedHostnames && allowedHostnames.length > 0 && !(response.data.hostname && allowedHostnames.includes(response.data.hostname))) return verificationFailed();
21
26
  };
22
27
  //#endregion
23
28
  export { cloudflareTurnstile };
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,9 +7,10 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const isV3 = (response) => {
7
8
  return "score" in response && typeof response.score === "number";
8
9
  };
9
- const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP }) => {
10
+ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP, expectedAction, allowedHostnames }) => {
10
11
  const response = await betterFetch(siteVerifyURL, {
11
12
  method: "POST",
13
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
12
14
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
13
15
  body: encodeToURLParams({
14
16
  secret: secretKey,
@@ -17,11 +19,14 @@ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minS
17
19
  })
18
20
  });
19
21
  if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
20
- if (!response.data.success || isV3(response.data) && response.data.score < minScore) return middlewareResponse({
22
+ const verificationFailed = () => middlewareResponse({
21
23
  message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
22
24
  code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
23
25
  status: 403
24
26
  });
27
+ if (!response.data.success || isV3(response.data) && response.data.score < minScore) return verificationFailed();
28
+ if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
29
+ if (allowedHostnames && allowedHostnames.length > 0 && !allowedHostnames.includes(response.data.hostname)) return verificationFailed();
25
30
  };
26
31
  //#endregion
27
32
  export { googleRecaptcha };
@@ -1,4 +1,5 @@
1
1
  import { middlewareResponse } from "../../../utils/middleware-response.mjs";
2
+ import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
2
3
  import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
3
4
  import { encodeToURLParams } from "../utils.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
6
7
  const hCaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
7
8
  const response = await betterFetch(siteVerifyURL, {
8
9
  method: "POST",
10
+ timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
9
11
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
10
12
  body: encodeToURLParams({
11
13
  secret: secretKey,