better-auth 1.6.10 → 1.6.12

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 (90) hide show
  1. package/dist/api/index.d.mts +8 -2
  2. package/dist/api/routes/callback.d.mts +1 -1
  3. package/dist/api/routes/callback.mjs +36 -40
  4. package/dist/api/routes/email-verification.d.mts +1 -0
  5. package/dist/api/routes/email-verification.mjs +4 -3
  6. package/dist/api/routes/session.mjs +14 -9
  7. package/dist/api/routes/sign-in.d.mts +1 -0
  8. package/dist/api/routes/sign-in.mjs +2 -1
  9. package/dist/api/routes/sign-up.d.mts +1 -0
  10. package/dist/api/routes/sign-up.mjs +9 -7
  11. package/dist/api/routes/update-user.mjs +5 -5
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/parser.mjs +0 -1
  14. package/dist/client/plugins/index.d.mts +3 -3
  15. package/dist/client/proxy.mjs +2 -1
  16. package/dist/context/helpers.mjs +3 -2
  17. package/dist/cookies/cookie-utils.d.mts +24 -1
  18. package/dist/cookies/cookie-utils.mjs +85 -22
  19. package/dist/cookies/index.d.mts +2 -3
  20. package/dist/cookies/index.mjs +39 -11
  21. package/dist/cookies/session-store.mjs +4 -23
  22. package/dist/db/get-migration.mjs +4 -4
  23. package/dist/db/index.d.mts +2 -2
  24. package/dist/db/index.mjs +3 -2
  25. package/dist/db/internal-adapter.mjs +96 -1
  26. package/dist/db/schema.d.mts +15 -2
  27. package/dist/db/schema.mjs +26 -1
  28. package/dist/db/with-hooks.d.mts +1 -0
  29. package/dist/db/with-hooks.mjs +58 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/oauth2/errors.mjs +16 -1
  33. package/dist/oauth2/link-account.mjs +6 -4
  34. package/dist/oauth2/state.mjs +8 -2
  35. package/dist/package.mjs +1 -1
  36. package/dist/plugins/access/access.d.mts +3 -15
  37. package/dist/plugins/access/access.mjs +11 -6
  38. package/dist/plugins/access/index.d.mts +2 -2
  39. package/dist/plugins/access/types.d.mts +11 -4
  40. package/dist/plugins/admin/access/statement.d.mts +29 -93
  41. package/dist/plugins/admin/admin.mjs +0 -4
  42. package/dist/plugins/admin/client.d.mts +1 -1
  43. package/dist/plugins/admin/routes.mjs +1 -0
  44. package/dist/plugins/anonymous/client.d.mts +1 -0
  45. package/dist/plugins/anonymous/error-codes.d.mts +1 -0
  46. package/dist/plugins/anonymous/error-codes.mjs +1 -0
  47. package/dist/plugins/anonymous/index.d.mts +1 -0
  48. package/dist/plugins/anonymous/index.mjs +16 -2
  49. package/dist/plugins/bearer/index.mjs +4 -9
  50. package/dist/plugins/captcha/index.mjs +2 -2
  51. package/dist/plugins/device-authorization/error-codes.mjs +1 -0
  52. package/dist/plugins/device-authorization/index.d.mts +1 -0
  53. package/dist/plugins/device-authorization/routes.mjs +34 -3
  54. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  55. package/dist/plugins/generic-oauth/index.mjs +6 -6
  56. package/dist/plugins/generic-oauth/routes.mjs +34 -32
  57. package/dist/plugins/generic-oauth/types.d.mts +7 -0
  58. package/dist/plugins/index.d.mts +2 -2
  59. package/dist/plugins/last-login-method/client.mjs +2 -2
  60. package/dist/plugins/magic-link/index.d.mts +8 -1
  61. package/dist/plugins/magic-link/index.mjs +4 -17
  62. package/dist/plugins/mcp/authorize.mjs +8 -2
  63. package/dist/plugins/mcp/index.mjs +73 -34
  64. package/dist/plugins/multi-session/index.mjs +2 -2
  65. package/dist/plugins/oauth-proxy/index.mjs +44 -31
  66. package/dist/plugins/oauth-proxy/utils.mjs +3 -10
  67. package/dist/plugins/oidc-provider/authorize.mjs +8 -2
  68. package/dist/plugins/oidc-provider/index.mjs +63 -37
  69. package/dist/plugins/one-tap/index.mjs +13 -8
  70. package/dist/plugins/open-api/generator.mjs +16 -5
  71. package/dist/plugins/organization/access/statement.d.mts +68 -201
  72. package/dist/plugins/organization/adapter.mjs +61 -56
  73. package/dist/plugins/organization/client.d.mts +3 -1
  74. package/dist/plugins/organization/error-codes.d.mts +2 -0
  75. package/dist/plugins/organization/error-codes.mjs +3 -1
  76. package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
  77. package/dist/plugins/organization/routes/crud-invites.mjs +7 -2
  78. package/dist/plugins/organization/types.d.mts +12 -2
  79. package/dist/plugins/two-factor/index.mjs +3 -2
  80. package/dist/plugins/username/index.d.mts +24 -2
  81. package/dist/plugins/username/index.mjs +49 -3
  82. package/dist/state.d.mts +2 -2
  83. package/dist/state.mjs +18 -4
  84. package/dist/test-utils/headers.mjs +2 -7
  85. package/dist/test-utils/test-instance.d.mts +25 -6
  86. package/dist/test-utils/test-instance.mjs +11 -2
  87. package/dist/utils/index.d.mts +1 -1
  88. package/dist/utils/url.d.mts +2 -1
  89. package/dist/utils/url.mjs +9 -3
  90. package/package.json +15 -14
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./utils/url.mjs";
1
+ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
2
2
  import { generateGenericState, parseGenericState } from "./state.mjs";
3
3
  import { generateState, parseState } from "./oauth2/state.mjs";
4
4
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
14
14
  export * from "@better-auth/core/utils/error-codes";
15
15
  export * from "@better-auth/core/utils/id";
16
16
  export * from "@better-auth/core/utils/json";
17
- export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL };
17
+ export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
@@ -1,6 +1,21 @@
1
1
  //#region src/oauth2/errors.ts
2
2
  const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handling-providers-without-email";
3
3
  /**
4
+ * Redirect the user to the OAuth error page with a machine-readable `error`
5
+ * code (and optional `error_description`).
6
+ *
7
+ * Every OAuth callback path routes its failures through this helper so the
8
+ * query parameter name, the `?`/`&` separator, and URL encoding are decided in
9
+ * one place. The error page reads the `error` query parameter, so callers must
10
+ * never hand-build the redirect with a different parameter name.
11
+ */
12
+ function redirectOnError(ctx, errorURL, error, description) {
13
+ const params = new URLSearchParams({ error });
14
+ if (description) params.set("error_description", description);
15
+ const sep = errorURL.includes("?") ? "&" : "?";
16
+ throw ctx.redirect(`${errorURL}${sep}${params.toString()}`);
17
+ }
18
+ /**
4
19
  * Build the logger message shown when an OAuth provider does not return an
5
20
  * email address. Kept in one place so every rejection site points users at
6
21
  * the same workaround docs.
@@ -9,4 +24,4 @@ function missingEmailLogMessage(providerId, options) {
9
24
  return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or synthesize one via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
10
25
  }
11
26
  //#endregion
12
- export { missingEmailLogMessage };
27
+ export { missingEmailLogMessage, redirectOnError };
@@ -1,5 +1,6 @@
1
1
  import { isAPIError } from "../utils/is-api-error.mjs";
2
2
  import { setAccountCookie } from "../cookies/session-store.mjs";
3
+ import { redirectOnError } from "./errors.mjs";
3
4
  import { setTokenUtil } from "./utils.mjs";
4
5
  import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
5
6
  import { isDevelopment, logger } from "@better-auth/core/env";
@@ -8,8 +9,7 @@ async function handleOAuthUserInfo(c, opts) {
8
9
  const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
9
10
  const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
10
11
  logger.error("Better auth was unable to query your database.\nError: ", e);
11
- const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
12
- throw c.redirect(`${errorURL}?error=internal_server_error`);
12
+ redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
13
13
  });
14
14
  let user = dbUser?.user;
15
15
  const isRegister = !user;
@@ -17,7 +17,9 @@ 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
- if (!(opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId)) && !userInfo.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
20
+ const isTrustedProvider = opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId);
21
+ const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
22
+ if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
21
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.`);
22
24
  return {
23
25
  error: "account not linked",
@@ -94,7 +96,7 @@ async function handleOAuthUserInfo(c, opts) {
94
96
  if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
95
97
  if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp && c.context.options.emailVerification?.sendVerificationEmail) {
96
98
  const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
97
- const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
99
+ const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
98
100
  await c.context.runInBackgroundOrAwait(c.context.options.emailVerification.sendVerificationEmail({
99
101
  user,
100
102
  url,
@@ -1,4 +1,5 @@
1
1
  import { generateRandomString } from "../crypto/random.mjs";
2
+ import { redirectOnError } from "./errors.mjs";
2
3
  import { setOAuthState } from "../api/state/oauth.mjs";
3
4
  import { StateError, generateGenericState, parseGenericState } from "../state.mjs";
4
5
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -36,8 +37,13 @@ async function parseState(c) {
36
37
  parsedData = await parseGenericState(c, state);
37
38
  } catch (error) {
38
39
  c.context.logger.error("Failed to parse state", error);
39
- if (error instanceof StateError && error.code === "state_security_mismatch") throw c.redirect(`${errorURL}?error=state_mismatch`);
40
- throw c.redirect(`${errorURL}?error=please_restart_the_process`);
40
+ let code = "internal_server_error";
41
+ let redirectErrorURL = errorURL;
42
+ if (error instanceof StateError) {
43
+ code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
44
+ redirectErrorURL = error.errorURL ?? errorURL;
45
+ }
46
+ redirectOnError(c, redirectErrorURL, code);
41
47
  }
42
48
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
43
49
  if (parsedData) await setOAuthState(parsedData);
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.10";
2
+ var version = "1.6.12";
3
3
  //#endregion
4
4
  export { version };
@@ -1,4 +1,4 @@
1
- import { Statements, Subset } from "./types.mjs";
1
+ import { ExactRoleStatements, Role, RoleInput, Statements } from "./types.mjs";
2
2
 
3
3
  //#region src/plugins/access/access.d.ts
4
4
  type AuthorizeResponse = {
@@ -8,21 +8,9 @@ type AuthorizeResponse = {
8
8
  success: true;
9
9
  error?: never | undefined;
10
10
  };
11
- declare function role<TStatements extends Statements>(statements: TStatements): {
12
- authorize<K extends keyof TStatements>(request: { [key in K]?: TStatements[key] | {
13
- actions: TStatements[key];
14
- connector: "OR" | "AND";
15
- } }, connector?: "OR" | "AND"): AuthorizeResponse;
16
- statements: TStatements;
17
- };
11
+ declare function role<const TRoleStatements extends Statements, TAuthorizeStatements extends Statements = TRoleStatements>(statements: TRoleStatements): Role<ExactRoleStatements<TRoleStatements>, TAuthorizeStatements>;
18
12
  declare function createAccessControl<const TStatements extends Statements>(s: TStatements): {
19
- newRole<K extends keyof TStatements>(statements: Subset<K, TStatements>): {
20
- authorize<K_1 extends K>(request: K_1 extends infer T extends keyof Subset<K, TStatements> ? { [key in T]?: Subset<K, TStatements>[key] | {
21
- actions: Subset<K, TStatements>[key];
22
- connector: "OR" | "AND";
23
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
24
- statements: Subset<K, TStatements>;
25
- };
13
+ newRole<const TRoleStatements extends Statements>(statements: RoleInput<TStatements, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, TStatements>;
26
14
  statements: TStatements;
27
15
  };
28
16
  //#endregion
@@ -6,14 +6,19 @@ function role(statements) {
6
6
  let success = false;
7
7
  for (const [requestedResource, requestedActions] of Object.entries(request)) {
8
8
  const allowedActions = statements[requestedResource];
9
- if (!allowedActions) return {
10
- success: false,
11
- error: `You are not allowed to access resource: ${requestedResource}`
12
- };
13
- if (Array.isArray(requestedActions)) success = requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
9
+ if (!allowedActions) {
10
+ if (connector === "AND") return {
11
+ success: false,
12
+ error: `You are not allowed to access resource: ${requestedResource}`
13
+ };
14
+ success = false;
15
+ continue;
16
+ }
17
+ if (Array.isArray(requestedActions)) success = requestedActions.length > 0 && requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
14
18
  else if (typeof requestedActions === "object") {
15
19
  const actions = requestedActions;
16
- if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
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));
17
22
  else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
18
23
  } else throw new BetterAuthError("Invalid access control request");
19
24
  if (success && connector === "OR") return { success };
@@ -1,3 +1,3 @@
1
- import { AccessControl, ArrayElement, Role, Statements, SubArray, Subset } from "./types.mjs";
1
+ import { AccessControl, ArrayElement, ExactRoleStatements, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, Statements, SubArray, Subset } from "./types.mjs";
2
2
  import { AuthorizeResponse, createAccessControl, role } from "./access.mjs";
3
- export { AccessControl, ArrayElement, AuthorizeResponse, Role, Statements, SubArray, Subset, createAccessControl, role };
3
+ export { AccessControl, ArrayElement, AuthorizeResponse, ExactRoleStatements, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, Statements, SubArray, Subset, createAccessControl, role };
@@ -8,10 +8,17 @@ type Subset<K extends keyof R, R extends Record<string | LiteralString, readonly
8
8
  type Statements = {
9
9
  readonly [resource: string]: readonly LiteralString[];
10
10
  };
11
+ type RoleStatements<TStatements extends Statements> = { readonly [P in keyof TStatements]?: SubArray<TStatements[P]> };
12
+ type RoleInput<TStatements extends Statements, TRoleStatements extends Statements> = TRoleStatements & (string extends keyof TRoleStatements ? {} : RoleStatements<TStatements> & Record<Exclude<keyof TRoleStatements, keyof TStatements>, never>);
13
+ type ExactRoleStatements<TStatements extends Statements> = { readonly [P in keyof TStatements]: readonly [...TStatements[P]] };
11
14
  type AccessControl<TStatements extends Statements = Statements> = ReturnType<typeof createAccessControl<TStatements>>;
12
- type Role<TStatements extends Statements = Record<string, any>> = {
13
- authorize: (request: any, connector?: ("OR" | "AND") | undefined) => AuthorizeResponse;
14
- statements: TStatements;
15
+ type RoleAuthorizeRequest<TStatements extends Statements> = { [P in keyof TStatements]?: SubArray<TStatements[P]> | {
16
+ actions: SubArray<TStatements[P]>;
17
+ connector: "OR" | "AND";
18
+ } };
19
+ type Role<TRoleStatements extends Statements = Record<string, any>, TAuthorizeStatements extends Statements = TRoleStatements> = {
20
+ authorize: (request: RoleAuthorizeRequest<TAuthorizeStatements>, connector?: ("OR" | "AND") | undefined) => AuthorizeResponse;
21
+ statements: TRoleStatements;
15
22
  };
16
23
  //#endregion
17
- export { AccessControl, ArrayElement, Role, Statements, SubArray, Subset };
24
+ export { AccessControl, ArrayElement, ExactRoleStatements, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, Statements, SubArray, Subset };
@@ -1,115 +1,51 @@
1
- import { Subset } from "../../access/types.mjs";
2
- import { AuthorizeResponse } from "../../access/access.mjs";
1
+ import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/types.mjs";
3
2
  //#region src/plugins/admin/access/statement.d.ts
4
3
  declare const defaultStatements: {
5
4
  readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
6
5
  readonly session: readonly ["list", "revoke", "delete"];
7
6
  };
8
7
  declare const defaultAc: {
9
- newRole<K extends "user" | "session">(statements: Subset<K, {
8
+ newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
10
9
  readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
11
10
  readonly session: readonly ["list", "revoke", "delete"];
12
- }>): {
13
- authorize<K_1 extends K>(request: K_1 extends infer T extends keyof Subset<K, {
14
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
15
- readonly session: readonly ["list", "revoke", "delete"];
16
- }> ? { [key in T]?: Subset<K, {
17
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
18
- readonly session: readonly ["list", "revoke", "delete"];
19
- }>[key] | {
20
- actions: Subset<K, {
21
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
22
- readonly session: readonly ["list", "revoke", "delete"];
23
- }>[key];
24
- connector: "OR" | "AND";
25
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
26
- statements: Subset<K, {
27
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
28
- readonly session: readonly ["list", "revoke", "delete"];
29
- }>;
30
- };
11
+ }, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
12
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
13
+ readonly session: readonly ["list", "revoke", "delete"];
14
+ }>;
31
15
  statements: {
32
16
  readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
33
17
  readonly session: readonly ["list", "revoke", "delete"];
34
18
  };
35
19
  };
36
- declare const adminAc: {
37
- authorize<K extends "user" | "session">(request: K extends infer T extends keyof Subset<"user" | "session", {
38
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
39
- readonly session: readonly ["list", "revoke", "delete"];
40
- }> ? { [key in T]?: Subset<"user" | "session", {
41
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
42
- readonly session: readonly ["list", "revoke", "delete"];
43
- }>[key] | {
44
- actions: Subset<"user" | "session", {
45
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
46
- readonly session: readonly ["list", "revoke", "delete"];
47
- }>[key];
48
- connector: "OR" | "AND";
49
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
50
- statements: Subset<"user" | "session", {
20
+ declare const adminAc: Role<ExactRoleStatements<{
21
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
22
+ readonly session: ["list", "revoke", "delete"];
23
+ }>, {
24
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
25
+ readonly session: readonly ["list", "revoke", "delete"];
26
+ }>;
27
+ declare const userAc: Role<ExactRoleStatements<{
28
+ readonly user: [];
29
+ readonly session: [];
30
+ }>, {
31
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
32
+ readonly session: readonly ["list", "revoke", "delete"];
33
+ }>;
34
+ declare const defaultRoles: {
35
+ admin: Role<ExactRoleStatements<{
36
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
37
+ readonly session: ["list", "revoke", "delete"];
38
+ }>, {
51
39
  readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
52
40
  readonly session: readonly ["list", "revoke", "delete"];
53
41
  }>;
54
- };
55
- declare const userAc: {
56
- authorize<K extends "user" | "session">(request: K extends infer T extends keyof Subset<"user" | "session", {
57
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
58
- readonly session: readonly ["list", "revoke", "delete"];
59
- }> ? { [key in T]?: Subset<"user" | "session", {
60
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
61
- readonly session: readonly ["list", "revoke", "delete"];
62
- }>[key] | {
63
- actions: Subset<"user" | "session", {
64
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
65
- readonly session: readonly ["list", "revoke", "delete"];
66
- }>[key];
67
- connector: "OR" | "AND";
68
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
69
- statements: Subset<"user" | "session", {
42
+ user: Role<ExactRoleStatements<{
43
+ readonly user: [];
44
+ readonly session: [];
45
+ }>, {
70
46
  readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
71
47
  readonly session: readonly ["list", "revoke", "delete"];
72
48
  }>;
73
49
  };
74
- declare const defaultRoles: {
75
- admin: {
76
- authorize<K extends "user" | "session">(request: K extends infer T extends keyof Subset<"user" | "session", {
77
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
78
- readonly session: readonly ["list", "revoke", "delete"];
79
- }> ? { [key in T]?: Subset<"user" | "session", {
80
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
81
- readonly session: readonly ["list", "revoke", "delete"];
82
- }>[key] | {
83
- actions: Subset<"user" | "session", {
84
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
85
- readonly session: readonly ["list", "revoke", "delete"];
86
- }>[key];
87
- connector: "OR" | "AND";
88
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
89
- statements: Subset<"user" | "session", {
90
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
91
- readonly session: readonly ["list", "revoke", "delete"];
92
- }>;
93
- };
94
- user: {
95
- authorize<K extends "user" | "session">(request: K extends infer T extends keyof Subset<"user" | "session", {
96
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
97
- readonly session: readonly ["list", "revoke", "delete"];
98
- }> ? { [key in T]?: Subset<"user" | "session", {
99
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
100
- readonly session: readonly ["list", "revoke", "delete"];
101
- }>[key] | {
102
- actions: Subset<"user" | "session", {
103
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
104
- readonly session: readonly ["list", "revoke", "delete"];
105
- }>[key];
106
- connector: "OR" | "AND";
107
- } | undefined } : never, connector?: "OR" | "AND"): AuthorizeResponse;
108
- statements: Subset<"user" | "session", {
109
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
110
- readonly session: readonly ["list", "revoke", "delete"];
111
- }>;
112
- };
113
- };
114
50
  //#endregion
115
51
  export { adminAc, defaultAc, defaultRoles, defaultStatements, userAc };
@@ -42,10 +42,6 @@ const admin = (options) => {
42
42
  });
43
43
  return;
44
44
  }
45
- if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) {
46
- const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
47
- throw ctx.redirect(`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`);
48
- }
49
45
  throw APIError.from("FORBIDDEN", {
50
46
  message: opts.bannedUserMessage,
51
47
  code: "BANNED_USER"
@@ -76,4 +76,4 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
76
76
  };
77
77
  };
78
78
  //#endregion
79
- export { adminClient };
79
+ export { AdminClientOptions, adminClient };
@@ -703,6 +703,7 @@ const removeUser = (opts) => createAuthEndpoint("/admin/remove-user", {
703
703
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS);
704
704
  if (ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF);
705
705
  if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
706
+ await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
706
707
  await ctx.context.internalAdapter.deleteUser(ctx.body.userId);
707
708
  return ctx.json({ success: true });
708
709
  });
@@ -23,6 +23,7 @@ declare const anonymousClient: () => {
23
23
  COULD_NOT_CREATE_SESSION: _better_auth_core_utils_error_codes0.RawError<"COULD_NOT_CREATE_SESSION">;
24
24
  ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: _better_auth_core_utils_error_codes0.RawError<"ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY">;
25
25
  FAILED_TO_DELETE_ANONYMOUS_USER: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER">;
26
+ FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS">;
26
27
  USER_IS_NOT_ANONYMOUS: _better_auth_core_utils_error_codes0.RawError<"USER_IS_NOT_ANONYMOUS">;
27
28
  DELETE_ANONYMOUS_USER_DISABLED: _better_auth_core_utils_error_codes0.RawError<"DELETE_ANONYMOUS_USER_DISABLED">;
28
29
  };
@@ -7,6 +7,7 @@ declare const ANONYMOUS_ERROR_CODES: {
7
7
  COULD_NOT_CREATE_SESSION: _better_auth_core_utils_error_codes0.RawError<"COULD_NOT_CREATE_SESSION">;
8
8
  ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: _better_auth_core_utils_error_codes0.RawError<"ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY">;
9
9
  FAILED_TO_DELETE_ANONYMOUS_USER: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER">;
10
+ FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS">;
10
11
  USER_IS_NOT_ANONYMOUS: _better_auth_core_utils_error_codes0.RawError<"USER_IS_NOT_ANONYMOUS">;
11
12
  DELETE_ANONYMOUS_USER_DISABLED: _better_auth_core_utils_error_codes0.RawError<"DELETE_ANONYMOUS_USER_DISABLED">;
12
13
  };
@@ -6,6 +6,7 @@ const ANONYMOUS_ERROR_CODES = defineErrorCodes({
6
6
  COULD_NOT_CREATE_SESSION: "Could not create session",
7
7
  ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: "Anonymous users cannot sign in again anonymously",
8
8
  FAILED_TO_DELETE_ANONYMOUS_USER: "Failed to delete anonymous user",
9
+ FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS: "Failed to delete anonymous user sessions",
9
10
  USER_IS_NOT_ANONYMOUS: "User is not anonymous",
10
11
  DELETE_ANONYMOUS_USER_DISABLED: "Deleting anonymous users is disabled"
11
12
  });
@@ -162,6 +162,7 @@ declare const anonymous: (options?: AnonymousOptions | undefined) => {
162
162
  COULD_NOT_CREATE_SESSION: _better_auth_core_utils_error_codes0.RawError<"COULD_NOT_CREATE_SESSION">;
163
163
  ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: _better_auth_core_utils_error_codes0.RawError<"ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY">;
164
164
  FAILED_TO_DELETE_ANONYMOUS_USER: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER">;
165
+ FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS: _better_auth_core_utils_error_codes0.RawError<"FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS">;
165
166
  USER_IS_NOT_ANONYMOUS: _better_auth_core_utils_error_codes0.RawError<"USER_IS_NOT_ANONYMOUS">;
166
167
  DELETE_ANONYMOUS_USER_DISABLED: _better_auth_core_utils_error_codes0.RawError<"DELETE_ANONYMOUS_USER_DISABLED">;
167
168
  };
@@ -101,6 +101,12 @@ const anonymous = (options) => {
101
101
  const session = ctx.context.session;
102
102
  if (options?.disableDeleteAnonymousUser) throw APIError.from("BAD_REQUEST", ANONYMOUS_ERROR_CODES.DELETE_ANONYMOUS_USER_DISABLED);
103
103
  if (!session.user.isAnonymous) throw APIError.from("FORBIDDEN", ANONYMOUS_ERROR_CODES.USER_IS_NOT_ANONYMOUS);
104
+ try {
105
+ await ctx.context.internalAdapter.deleteSessions(session.user.id);
106
+ } catch (error) {
107
+ ctx.context.logger.error("Failed to delete anonymous user sessions", error);
108
+ throw APIError.from("INTERNAL_SERVER_ERROR", ANONYMOUS_ERROR_CODES.FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS);
109
+ }
104
110
  try {
105
111
  await ctx.context.internalAdapter.deleteUser(session.user.id);
106
112
  } catch (error) {
@@ -113,7 +119,7 @@ const anonymous = (options) => {
113
119
  },
114
120
  hooks: { after: [{
115
121
  matcher(ctx) {
116
- return ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/oauth2/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify") || false;
122
+ return ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/oauth2/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify") || ctx.path?.startsWith("/verify-email") || false;
117
123
  },
118
124
  handler: createAuthMiddleware(async (ctx) => {
119
125
  const setCookie = ctx.context.responseHeaders?.get("set-cookie");
@@ -147,7 +153,15 @@ const anonymous = (options) => {
147
153
  const isSameUser = newSessionUser?.id === session.user.id;
148
154
  const newSessionIsAnonymous = Boolean(newSessionUser?.isAnonymous);
149
155
  if (options?.disableDeleteAnonymousUser || isSameUser || newSessionIsAnonymous) return;
150
- await ctx.context.internalAdapter.deleteUser(session.user.id);
156
+ try {
157
+ await ctx.context.internalAdapter.deleteSessions(session.user.id);
158
+ await ctx.context.internalAdapter.deleteUser(session.user.id);
159
+ } catch (error) {
160
+ ctx.context.logger.error("Failed to clean up anonymous user during post-link cleanup", {
161
+ anonymousUserId: session.user.id,
162
+ error
163
+ });
164
+ }
151
165
  })
152
166
  }] },
153
167
  options,
@@ -30,16 +30,11 @@ const bearer = (options) => {
30
30
  if (authHeader.slice(0, 7).toLowerCase() !== BEARER_SCHEME) return;
31
31
  const token = authHeader.slice(7).trim();
32
32
  if (!token) return;
33
- let signedToken;
34
33
  let decodedToken;
35
- if (token.includes(".")) {
36
- const isEncoded = token.includes("%");
37
- signedToken = isEncoded ? token : encodeURIComponent(token);
38
- decodedToken = isEncoded ? tryDecode(token) : token;
39
- } else {
34
+ if (token.includes(".")) decodedToken = token.includes("%") ? tryDecode(token) : token;
35
+ else {
40
36
  if (options?.requireSignature) return;
41
- signedToken = (await serializeSignedCookie("", token, c.context.secret)).replace("=", "");
42
- decodedToken = tryDecode(signedToken);
37
+ decodedToken = tryDecode((await serializeSignedCookie("", token, c.context.secret)).replace("=", ""));
43
38
  }
44
39
  try {
45
40
  if (!await createHMAC("SHA-256", "base64urlnopad").verify(c.context.secret, decodedToken.split(".")[0], decodedToken.split(".")[1])) return;
@@ -48,7 +43,7 @@ const bearer = (options) => {
48
43
  }
49
44
  const existingHeaders = c.request?.headers || c.headers;
50
45
  const headers = new Headers({ ...Object.fromEntries(existingHeaders?.entries()) });
51
- setRequestCookie(headers, c.context.authCookies.sessionToken.name, signedToken);
46
+ setRequestCookie(headers, c.context.authCookies.sessionToken.name, decodedToken);
52
47
  return { context: { headers } };
53
48
  })
54
49
  }],
@@ -21,12 +21,12 @@ const captcha = (options) => ({
21
21
  if (pathname.endsWith("//")) pathname = pathname.slice(0, -1);
22
22
  if (pathname.startsWith("//")) pathname = pathname.slice(1);
23
23
  if (!pathname.startsWith("/")) pathname = "/" + pathname;
24
- const blockedPaths = ["/sign-in/email-otp"].reduce((acc, curr) => {
24
+ const exemptPaths = ["/sign-in/email-otp"].reduce((acc, curr) => {
25
25
  if (options.endpoints?.length && options.endpoints.includes(curr)) return acc;
26
26
  return [...acc, curr];
27
27
  }, []);
28
28
  if (!endpoints.some((endpoint) => {
29
- return pathname.includes(endpoint) && !blockedPaths.includes(endpoint);
29
+ return pathname.includes(endpoint) && !exemptPaths.some((p) => pathname.includes(p));
30
30
  })) return;
31
31
  if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
32
32
  const captchaResponse = request.headers.get("x-captcha-response");
@@ -8,6 +8,7 @@ const DEVICE_AUTHORIZATION_ERROR_CODES = defineErrorCodes({
8
8
  ACCESS_DENIED: "Access denied",
9
9
  INVALID_USER_CODE: "Invalid user code",
10
10
  DEVICE_CODE_ALREADY_PROCESSED: "Device code already processed",
11
+ DEVICE_CODE_NOT_CLAIMED: "Device code has not been claimed by a verifying session; call `GET /device` with the `user_code` while signed in before approving or denying",
11
12
  POLLING_TOO_FREQUENTLY: "Polling too frequently",
12
13
  USER_NOT_FOUND: "User not found",
13
14
  FAILED_TO_CREATE_SESSION: "Failed to create session",
@@ -388,6 +388,7 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
388
388
  ACCESS_DENIED: _better_auth_core_utils_error_codes0.RawError<"ACCESS_DENIED">;
389
389
  INVALID_USER_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_USER_CODE">;
390
390
  DEVICE_CODE_ALREADY_PROCESSED: _better_auth_core_utils_error_codes0.RawError<"DEVICE_CODE_ALREADY_PROCESSED">;
391
+ DEVICE_CODE_NOT_CLAIMED: _better_auth_core_utils_error_codes0.RawError<"DEVICE_CODE_NOT_CLAIMED">;
391
392
  POLLING_TOO_FREQUENTLY: _better_auth_core_utils_error_codes0.RawError<"POLLING_TOO_FREQUENTLY">;
392
393
  INVALID_DEVICE_CODE_STATUS: _better_auth_core_utils_error_codes0.RawError<"INVALID_DEVICE_CODE_STATUS">;
393
394
  AUTHENTICATION_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"AUTHENTICATION_REQUIRED">;
@@ -99,6 +99,7 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
99
99
  data: {
100
100
  deviceCode,
101
101
  userCode,
102
+ userId: null,
102
103
  expiresAt,
103
104
  status: "pending",
104
105
  pollingInterval: ms(opts.interval),
@@ -331,6 +332,28 @@ const deviceVerify = createAuthEndpoint("/device", {
331
332
  error: "expired_token",
332
333
  error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
333
334
  });
335
+ const session = await getSessionFromCtx(ctx);
336
+ if (session?.user?.id && !deviceCodeRecord.userId && deviceCodeRecord.status === "pending") {
337
+ if (await ctx.context.adapter.update({
338
+ model: "deviceCode",
339
+ where: [
340
+ {
341
+ field: "id",
342
+ value: deviceCodeRecord.id
343
+ },
344
+ {
345
+ field: "status",
346
+ value: "pending"
347
+ },
348
+ {
349
+ field: "userId",
350
+ operator: "eq",
351
+ value: null
352
+ }
353
+ ],
354
+ update: { userId: session.user.id }
355
+ })) deviceCodeRecord.userId = session.user.id;
356
+ }
334
357
  return ctx.json({
335
358
  user_code,
336
359
  status: deviceCodeRecord.status
@@ -387,7 +410,11 @@ const deviceApprove = createAuthEndpoint("/device/approve", {
387
410
  error: "invalid_request",
388
411
  error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED.message
389
412
  });
390
- if (deviceCodeRecord.userId && deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
413
+ if (!deviceCodeRecord.userId) throw new APIError("BAD_REQUEST", {
414
+ error: "invalid_request",
415
+ error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_NOT_CLAIMED.message
416
+ });
417
+ if (deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
391
418
  error: "access_denied",
392
419
  error_description: "You are not authorized to approve this device authorization"
393
420
  });
@@ -454,7 +481,11 @@ const deviceDeny = createAuthEndpoint("/device/deny", {
454
481
  error: "invalid_request",
455
482
  error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED.message
456
483
  });
457
- if (deviceCodeRecord.userId && deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
484
+ if (!deviceCodeRecord.userId) throw new APIError("BAD_REQUEST", {
485
+ error: "invalid_request",
486
+ error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_NOT_CLAIMED.message
487
+ });
488
+ if (deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
458
489
  error: "access_denied",
459
490
  error_description: "You are not authorized to deny this device authorization"
460
491
  });
@@ -466,7 +497,7 @@ const deviceDeny = createAuthEndpoint("/device/deny", {
466
497
  }],
467
498
  update: {
468
499
  status: "denied",
469
- userId: deviceCodeRecord.userId || session.user.id
500
+ userId: session.user.id
470
501
  }
471
502
  });
472
503
  return ctx.json({ success: true });
@@ -117,7 +117,7 @@ declare const genericOAuth: (options: GenericOAuthOptions) => {
117
117
  };
118
118
  scope: "server";
119
119
  };
120
- }, void>;
120
+ }, never>;
121
121
  oAuth2LinkAccount: better_call0.StrictEndpoint<"/oauth2/link", {
122
122
  method: "POST";
123
123
  body: zod.ZodObject<{