better-auth 1.7.13 → 1.7.15

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.
@@ -243,7 +243,7 @@ async function dispatchAuthEndpoint(endpoint, input) {
243
243
  });
244
244
  if (result instanceof Response) return result;
245
245
  internalContext.context.returned = result.response;
246
- internalContext.context.responseHeaders = result.headers ?? void 0;
246
+ mergeResponseHeaders(internalContext.context, result.headers);
247
247
  const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
248
248
  if (after.response !== void 0) result.response = after.response;
249
249
  result.headers = after.headers ?? result.headers;
@@ -1,10 +1,4 @@
1
1
  //#region src/client/parser.ts
2
- const PROTO_POLLUTION_PATTERNS = {
3
- proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,
4
- constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,
5
- protoShort: /"__proto__"\s*:/,
6
- constructorShort: /"constructor"\s*:/
7
- };
8
2
  const JSON_SIGNATURE = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
9
3
  const SPECIAL_VALUES = {
10
4
  true: true,
@@ -40,14 +34,10 @@ function betterJSONParse(value, options = {}) {
40
34
  if (strict) throw new SyntaxError("[better-json] Invalid JSON");
41
35
  return value;
42
36
  }
43
- if (Object.entries(PROTO_POLLUTION_PATTERNS).some(([key, pattern]) => {
44
- const matches = pattern.test(trimmed);
45
- if (matches && warnings) console.warn(`[better-json] Detected potential prototype pollution attempt using ${key} pattern`);
46
- return matches;
47
- }) && strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
48
37
  try {
49
38
  const secureReviver = (key, value) => {
50
39
  if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
40
+ if (strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
51
41
  if (warnings) console.warn(`[better-json] Dropping "${key}" key to prevent prototype pollution`);
52
42
  return;
53
43
  }
@@ -1,3 +1,4 @@
1
+ import { trimTrailingSlashes } from "../../utils/url.mjs";
1
2
  import { getClientConfig } from "../config.mjs";
2
3
  import { createDynamicPathProxy } from "../proxy.mjs";
3
4
  import { useStore } from "./vue-store.mjs";
@@ -16,18 +17,23 @@ function toHeadersInit(headers) {
16
17
  }
17
18
  function createAuthClient(options) {
18
19
  const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
19
- const sessionCacheKey = [
20
- "better-auth",
21
- "session",
22
- options?.baseURL || "inferred",
23
- options?.basePath ?? "/api/auth"
24
- ].join(":");
20
+ const baseURLKey = options?.baseURL || "inferred";
21
+ const basePath = options?.basePath ?? "/api/auth";
22
+ const sessionCacheKey = `better-auth:session:${baseURLKey}:${basePath}`;
23
+ const sessionPath = trimTrailingSlashes(basePath.startsWith("/") ? basePath : `/${basePath}`);
24
+ /**
25
+ * Stays relative without an explicit `baseURL` so the SSR session fetch
26
+ * forwards the incoming cookies.
27
+ *
28
+ * @see https://nuxt.com/docs/4.x/getting-started/data-fetching
29
+ */
30
+ const sessionURL = `${options?.baseURL ? baseURL : sessionPath}/get-session`;
25
31
  const resolvedHooks = {};
26
32
  for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
27
33
  function useSession(useFetch) {
28
34
  if (useFetch) {
29
35
  const sessionSignal = useStore($sessionSignal);
30
- return useFetch(`${baseURL}/get-session`, {
36
+ return useFetch(sessionURL, {
31
37
  headers: toHeadersInit(options?.fetchOptions?.headers),
32
38
  key: sessionCacheKey,
33
39
  watch: [sessionSignal]
@@ -12,12 +12,12 @@ function estimateEntropy(str) {
12
12
  }
13
13
  function parseSecretsEnv(envValue) {
14
14
  if (!envValue) return null;
15
- return envValue.split(",").map((entry) => {
15
+ return envValue.split(",").map((entry, index) => {
16
16
  entry = entry.trim();
17
17
  const colonIdx = entry.indexOf(":");
18
- if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry: "${entry}". Expected format: "<version>:<secret>"`);
18
+ if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry at position ${index + 1}. Expected format: "<version>:<secret>"`);
19
19
  const version = parseInt(entry.slice(0, colonIdx), 10);
20
- if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS: "${entry.slice(0, colonIdx)}". Version must be a non-negative integer.`);
20
+ if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS entry at position ${index + 1}. Version must be a non-negative integer.`);
21
21
  const value = entry.slice(colonIdx + 1).trim();
22
22
  if (!value) throw new BetterAuthError(`Empty secret value for version ${version} in BETTER_AUTH_SECRETS.`);
23
23
  return {
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.13";
2
+ var version = "1.7.15";
3
3
  //#endregion
4
4
  export { version };
@@ -12,7 +12,7 @@ declare module "@better-auth/core" {
12
12
  export declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
13
13
  id: "admin";
14
14
  version: string;
15
- init(): {
15
+ init(authContext: import("@better-auth/core").AuthContext): {
16
16
  options: {
17
17
  databaseHooks: {
18
18
  user: {
@@ -863,6 +863,7 @@ export declare const admin: <O extends AdminOptions>(options?: O | undefined) =>
863
863
  INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
864
864
  YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
865
865
  PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
866
+ USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
866
867
  };
867
868
  schema: {
868
869
  user: {
@@ -22,7 +22,7 @@ const admin = (options) => {
22
22
  return {
23
23
  id: "admin",
24
24
  version: PACKAGE_VERSION,
25
- init() {
25
+ init(authContext) {
26
26
  return { options: { databaseHooks: {
27
27
  user: { create: { async before(user) {
28
28
  return { data: {
@@ -31,11 +31,11 @@ const admin = (options) => {
31
31
  } };
32
32
  } } },
33
33
  session: { create: { async before(session, ctx) {
34
- if (!ctx) return;
35
- const user = await ctx.context.internalAdapter.findUserById(session.userId);
34
+ const internalAdapter = ctx?.context.internalAdapter ?? authContext.internalAdapter;
35
+ const user = await internalAdapter.findUserById(session.userId);
36
36
  if (user?.banned) {
37
37
  if (user.banExpires && new Date(user.banExpires).getTime() < Date.now()) {
38
- await ctx.context.internalAdapter.updateUser(session.userId, {
38
+ await internalAdapter.updateUser(session.userId, {
39
39
  banned: false,
40
40
  banReason: null,
41
41
  banExpires: null
@@ -75,6 +75,7 @@ export declare const adminClient: <O extends AdminClientOptions>(options?: O | u
75
75
  INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
76
76
  YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
77
77
  PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
78
+ USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
78
79
  };
79
80
  };
80
81
  //#endregion
@@ -23,5 +23,6 @@ export declare const ADMIN_ERROR_CODES: {
23
23
  INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
24
24
  YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
25
25
  PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
26
+ USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
26
27
  };
27
28
  //#endregion
@@ -23,7 +23,8 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
23
23
  YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
24
24
  INVALID_ROLE_TYPE: "Invalid role type",
25
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"
26
+ PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead",
27
+ USER_ID_CANNOT_BE_UPDATED: "User id cannot be updated"
27
28
  });
28
29
  //#endregion
29
30
  export { ADMIN_ERROR_CODES };
@@ -257,6 +257,7 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
257
257
  const updateData = ctx.body.data;
258
258
  const hasDataKey = (key) => Object.prototype.hasOwnProperty.call(updateData, key);
259
259
  if (hasDataKey("password")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER);
260
+ if (hasDataKey("id")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ID_CANNOT_BE_UPDATED);
260
261
  if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
261
262
  if (!hasPermission({
262
263
  userId: ctx.context.session.user.id,
@@ -25,8 +25,11 @@ import { betterFetch } from "@better-fetch/fetch";
25
25
  function hubspot(options) {
26
26
  const defaultScopes = ["oauth"];
27
27
  const getUserInfo = async (tokens) => {
28
- const tokenInfoUrl = `https://api.hubapi.com/oauth/v1/access-tokens/${tokens.accessToken}`;
29
- const { data: profile, error } = await betterFetch(tokenInfoUrl, { headers: { "Content-Type": "application/json" } });
28
+ if (!tokens.accessToken) return null;
29
+ const { data: profile, error } = await betterFetch("https://api.hubapi.com/oauth/v1/access-tokens/:token", {
30
+ params: { token: tokens.accessToken },
31
+ headers: { "Content-Type": "application/json" }
32
+ });
30
33
  if (error || !profile) return null;
31
34
  const id = profile.user_id ?? profile.signed_access_token?.userId;
32
35
  if (!id) return null;
@@ -9,7 +9,18 @@ import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
9
9
  import { createAuthEndpoint } from "@better-auth/core/api";
10
10
  import * as z from "zod";
11
11
  import { createOTP } from "@better-auth/utils/otp";
12
+ import { base32 } from "@better-auth/utils/base32";
12
13
  //#region src/plugins/two-factor/totp/index.ts
14
+ const BASE32_PREFIX = "base32:";
15
+ /**
16
+ * Secrets imported from other providers are stored as `base32:<secret>`.
17
+ * Generated secrets never contain `:`, so they are used as-is.
18
+ */
19
+ async function decryptTOTPSecret(options) {
20
+ const secret = await symmetricDecrypt(options);
21
+ if (!secret.startsWith(BASE32_PREFIX)) return secret;
22
+ return base32.decode(secret.slice(7).toUpperCase());
23
+ }
13
24
  const generateTOTPBodySchema = z.object({ secret: z.string().meta({ description: "The secret to generate the TOTP code" }) });
14
25
  const verifyTOTPBodySchema = z.object({
15
26
  code: z.string().meta({ description: "The otp code to verify. Eg: \"012345\"" }),
@@ -88,7 +99,7 @@ const totp2fa = (options) => {
88
99
  }]
89
100
  });
90
101
  if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
91
- const secret = await symmetricDecrypt({
102
+ const secret = await decryptTOTPSecret({
92
103
  key: ctx.context.secretConfig,
93
104
  data: twoFactor.secret
94
105
  });
@@ -139,7 +150,7 @@ const totp2fa = (options) => {
139
150
  const attempt = isSignIn ? await beginAttempt(5) : null;
140
151
  let status;
141
152
  try {
142
- const decrypted = await symmetricDecrypt({
153
+ const decrypted = await decryptTOTPSecret({
143
154
  key: ctx.context.secretConfig,
144
155
  data: twoFactor.secret
145
156
  });
@@ -155,24 +155,14 @@ const usernameImpl = (options, includeDisplayUsername) => {
155
155
  ctx.context.logger.warn("Username or password not found");
156
156
  throw APIError.from("UNAUTHORIZED", USERNAME_ERROR_CODES.INVALID_USERNAME_OR_PASSWORD);
157
157
  }
158
- const username = options?.validationOrder?.username === "pre-normalization" ? normalizer(ctx.body.username) : ctx.body.username;
159
- const minUsernameLength = options?.minUsernameLength || 3;
160
- const maxUsernameLength = options?.maxUsernameLength || 30;
161
- if (username.length < minUsernameLength) {
162
- ctx.context.logger.warn("Username too short");
163
- throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
164
- }
165
- if (username.length > maxUsernameLength) {
166
- ctx.context.logger.warn("Username too long");
167
- throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
168
- }
169
- if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
158
+ const validationError = await validateUsernameValue(ctx.body.username);
159
+ if (validationError) throw APIError.from("UNPROCESSABLE_ENTITY", validationError);
170
160
  assertPasswordNotTooLong(ctx, ctx.body.password);
171
161
  const user = await ctx.context.adapter.findOne({
172
162
  model: "user",
173
163
  where: [{
174
164
  field: "username",
175
- value: normalizer(username)
165
+ value: normalizer(ctx.body.username)
176
166
  }]
177
167
  });
178
168
  if (!user) {
@@ -227,11 +217,8 @@ const usernameImpl = (options, includeDisplayUsername) => {
227
217
  }, async (ctx) => {
228
218
  const username = ctx.body.username;
229
219
  if (!username) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
230
- const minUsernameLength = options?.minUsernameLength || 3;
231
- const maxUsernameLength = options?.maxUsernameLength || 30;
232
- if (username.length < minUsernameLength) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
233
- if (username.length > maxUsernameLength) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
234
- if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
220
+ const validationError = await validateUsernameValue(username);
221
+ if (validationError) throw APIError.from("UNPROCESSABLE_ENTITY", validationError);
235
222
  if (await ctx.context.adapter.findOne({
236
223
  model: "user",
237
224
  where: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.13",
3
+ "version": "1.7.15",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -503,13 +503,13 @@
503
503
  "kysely": "^0.28.17 || ^0.29.0",
504
504
  "nanostores": "^1.3.0",
505
505
  "zod": "^4.5.4",
506
- "@better-auth/core": "1.7.13",
507
- "@better-auth/drizzle-adapter": "1.7.13",
508
- "@better-auth/kysely-adapter": "1.7.13",
509
- "@better-auth/memory-adapter": "1.7.13",
510
- "@better-auth/mongo-adapter": "1.7.13",
511
- "@better-auth/prisma-adapter": "1.7.13",
512
- "@better-auth/telemetry": "1.7.13"
506
+ "@better-auth/core": "1.7.15",
507
+ "@better-auth/drizzle-adapter": "1.7.15",
508
+ "@better-auth/kysely-adapter": "1.7.15",
509
+ "@better-auth/memory-adapter": "1.7.15",
510
+ "@better-auth/mongo-adapter": "1.7.15",
511
+ "@better-auth/prisma-adapter": "1.7.15",
512
+ "@better-auth/telemetry": "1.7.15"
513
513
  },
514
514
  "devDependencies": {
515
515
  "@lynx-js/react": "^0.121.2",