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
@@ -90,9 +90,20 @@ declare const username: (options?: UsernameOptions | undefined) => {
90
90
  name: string;
91
91
  image?: string | null | undefined;
92
92
  } & Record<string, unknown>, context: _better_auth_core0.GenericEndpointContext | null): Promise<{
93
+ data: {
94
+ username: string;
95
+ displayUsername: string;
96
+ id: string;
97
+ createdAt: Date;
98
+ updatedAt: Date;
99
+ email: string;
100
+ emailVerified: boolean;
101
+ name: string;
102
+ image?: string | null | undefined;
103
+ };
104
+ } | {
93
105
  data: {
94
106
  displayUsername?: string | undefined;
95
- username?: string | undefined;
96
107
  id: string;
97
108
  createdAt: Date;
98
109
  updatedAt: Date;
@@ -115,7 +126,18 @@ declare const username: (options?: UsernameOptions | undefined) => {
115
126
  }> & Record<string, unknown>, context: _better_auth_core0.GenericEndpointContext | null): Promise<{
116
127
  data: {
117
128
  displayUsername?: string | undefined;
118
- username?: string | undefined;
129
+ username: string;
130
+ id?: string | undefined;
131
+ createdAt?: Date | undefined;
132
+ updatedAt?: Date | undefined;
133
+ email?: string | undefined;
134
+ emailVerified?: boolean | undefined;
135
+ name?: string | undefined;
136
+ image?: string | null | undefined;
137
+ };
138
+ } | {
139
+ data: {
140
+ displayUsername?: string | undefined;
119
141
  id?: string | undefined;
120
142
  createdAt?: Date | undefined;
121
143
  updatedAt?: Date | undefined;
@@ -28,6 +28,31 @@ const username = (options) => {
28
28
  const displayUsernameNormalizer = (displayUsername) => {
29
29
  return options?.displayUsernameNormalization ? options.displayUsernameNormalization(displayUsername) : displayUsername;
30
30
  };
31
+ const minUsernameLength = options?.minUsernameLength || 3;
32
+ const maxUsernameLength = options?.maxUsernameLength || 30;
33
+ const validator = options?.usernameValidator || defaultUsernameValidator;
34
+ const pathsWithHttpHookValidation = ["/sign-up/email", "/update-user"];
35
+ async function validateUsername(username, displayUsername, adapter, currentUserId) {
36
+ const usernameToValidate = options?.validationOrder?.username === "post-normalization" ? normalizer(username) : username;
37
+ if (usernameToValidate.length < minUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
38
+ if (usernameToValidate.length > maxUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
39
+ if (!await validator(usernameToValidate)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_USERNAME);
40
+ const normalizedUsername = normalizer(username);
41
+ const existingUser = await adapter.findOne({
42
+ model: "user",
43
+ where: [{
44
+ field: "username",
45
+ value: normalizedUsername
46
+ }]
47
+ });
48
+ if (existingUser) {
49
+ if (!currentUserId || existingUser.id !== currentUserId) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
50
+ }
51
+ if (displayUsername && options?.displayUsernameValidator) {
52
+ const displayUsernameToValidate = options?.validationOrder?.displayUsername === "post-normalization" ? displayUsernameNormalizer(displayUsername) : displayUsername;
53
+ if (!await options.displayUsernameValidator(displayUsernameToValidate)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME);
54
+ }
55
+ }
31
56
  return {
32
57
  id: "username",
33
58
  version: PACKAGE_VERSION,
@@ -36,18 +61,39 @@ const username = (options) => {
36
61
  create: { async before(user, context) {
37
62
  const username = "username" in user ? user.username : null;
38
63
  const displayUsername = "displayUsername" in user ? user.displayUsername : null;
64
+ const currentPath = context?.path;
65
+ const skipValidation = currentPath && pathsWithHttpHookValidation.includes(currentPath);
66
+ if (username) {
67
+ if (!skipValidation) await validateUsername(username, displayUsername, ctx.adapter);
68
+ return { data: {
69
+ ...user,
70
+ username: normalizer(username),
71
+ displayUsername: displayUsername ? displayUsernameNormalizer(displayUsername) : username
72
+ } };
73
+ }
39
74
  return { data: {
40
75
  ...user,
41
- ...username ? { username: normalizer(username) } : {},
42
76
  ...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
43
77
  } };
44
78
  } },
45
79
  update: { async before(user, context) {
46
80
  const username = "username" in user ? user.username : null;
47
81
  const displayUsername = "displayUsername" in user ? user.displayUsername : null;
82
+ const currentPath = context?.path;
83
+ const skipValidation = currentPath && pathsWithHttpHookValidation.includes(currentPath);
84
+ if (username) {
85
+ if (!skipValidation) {
86
+ const currentUserId = context?.context?.session?.user?.id || ("id" in user ? user.id : null);
87
+ await validateUsername(username, displayUsername, ctx.adapter, currentUserId);
88
+ }
89
+ return { data: {
90
+ ...user,
91
+ username: normalizer(username),
92
+ ...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
93
+ } };
94
+ }
48
95
  return { data: {
49
96
  ...user,
50
- ...username ? { username: normalizer(username) } : {},
51
97
  ...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
52
98
  } };
53
99
  } }
@@ -153,7 +199,7 @@ const username = (options) => {
153
199
  if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", USERNAME_ERROR_CODES.EMAIL_NOT_VERIFIED);
154
200
  if (ctx.context.options?.emailVerification?.sendOnSignIn) {
155
201
  const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
156
- const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
202
+ const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
157
203
  await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
158
204
  user,
159
205
  url,
package/dist/state.d.mts CHANGED
@@ -27,8 +27,8 @@ declare function generateGenericState(c: GenericEndpointContext, stateData: Stat
27
27
  state: string;
28
28
  codeVerifier: string;
29
29
  }>;
30
- declare function parseGenericState(c: GenericEndpointContext, state: string, settings?: {
31
- cookieName: string;
30
+ declare function parseGenericState(c: GenericEndpointContext, state: string | undefined, settings?: {
31
+ cookieName?: string;
32
32
  skipStateCookieCheck?: boolean;
33
33
  }): Promise<{
34
34
  [x: string]: unknown;
package/dist/state.mjs CHANGED
@@ -20,10 +20,19 @@ const stateDataSchema = z.looseObject({
20
20
  var StateError = class extends BetterAuthError {
21
21
  code;
22
22
  details;
23
+ /**
24
+ * The per-flow `errorCallbackURL` recovered from the parsed state, when the
25
+ * failure happened after the state was successfully parsed (for example a
26
+ * nonce or state-cookie mismatch). It was origin-validated at sign-in, so
27
+ * the callback can safely redirect there instead of the default error page.
28
+ * Absent when the state could not be parsed at all.
29
+ */
30
+ errorURL;
23
31
  constructor(message, options) {
24
32
  super(message, options);
25
33
  this.code = options.code;
26
34
  this.details = options.details;
35
+ this.errorURL = options.errorURL;
27
36
  }
28
37
  };
29
38
  async function generateGenericState(c, stateData, settings) {
@@ -62,6 +71,7 @@ async function generateGenericState(c, stateData, settings) {
62
71
  };
63
72
  }
64
73
  async function parseGenericState(c, state, settings) {
74
+ if (!state) throw new StateError("State not found in OAuth callback", { code: "state_not_found" });
65
75
  const storeStateStrategy = c.context.oauthConfig.storeStateStrategy;
66
76
  let parsedData;
67
77
  if (storeStateStrategy === "cookie") {
@@ -86,7 +96,8 @@ async function parseGenericState(c, state, settings) {
86
96
  }
87
97
  if (!parsedData.oauthState || parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
88
98
  code: "state_security_mismatch",
89
- details: { state }
99
+ details: { state },
100
+ errorURL: parsedData.errorURL
90
101
  });
91
102
  expireCookie(c, stateCookie);
92
103
  } else {
@@ -98,20 +109,23 @@ async function parseGenericState(c, state, settings) {
98
109
  parsedData = stateDataSchema.parse(JSON.parse(data.value));
99
110
  if (parsedData.oauthState !== void 0 && parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
100
111
  code: "state_security_mismatch",
101
- details: { state }
112
+ details: { state },
113
+ errorURL: parsedData.errorURL
102
114
  });
103
115
  const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state");
104
116
  const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret);
105
117
  if (!(settings?.skipStateCookieCheck ?? c.context.oauthConfig.skipStateCookieCheck) && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", {
106
118
  code: "state_security_mismatch",
107
- details: { state }
119
+ details: { state },
120
+ errorURL: parsedData.errorURL
108
121
  });
109
122
  expireCookie(c, stateCookie);
110
123
  await c.context.internalAdapter.deleteVerificationByIdentifier(state);
111
124
  }
112
125
  if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
113
126
  code: "state_mismatch",
114
- details: { expiresAt: parsedData.expiresAt }
127
+ details: { expiresAt: parsedData.expiresAt },
128
+ errorURL: parsedData.errorURL
115
129
  });
116
130
  return parsedData;
117
131
  }
@@ -1,3 +1,4 @@
1
+ import { applySetCookies } from "../cookies/cookie-utils.mjs";
1
2
  //#region src/test-utils/headers.ts
2
3
  /**
3
4
  * converts set cookie containing headers to
@@ -9,13 +10,7 @@ function convertSetCookieToCookie(headers) {
9
10
  if (name.toLowerCase() === "set-cookie") setCookieHeaders.push(value);
10
11
  });
11
12
  if (setCookieHeaders.length === 0) return headers;
12
- const existingCookies = headers.get("cookie") || "";
13
- const cookies = existingCookies ? existingCookies.split("; ") : [];
14
- setCookieHeaders.forEach((setCookie) => {
15
- const cookiePair = setCookie.split(";")[0];
16
- cookies.push(cookiePair.trim());
17
- });
18
- headers.set("cookie", cookies.join("; "));
13
+ applySetCookies(headers, setCookieHeaders);
19
14
  return headers;
20
15
  }
21
16
  //#endregion
@@ -240,7 +240,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
240
240
  allowedMediaTypes: string[];
241
241
  scope: "server";
242
242
  };
243
- }, void>;
243
+ }, never>;
244
244
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
245
245
  method: ("GET" | "POST")[];
246
246
  operationId: string;
@@ -339,6 +339,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
339
339
  callbackURL: zod.ZodOptional<zod.ZodString>;
340
340
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
341
341
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
342
+ cloneRequest: true;
342
343
  metadata: {
343
344
  allowedMediaTypes: string[];
344
345
  $Infer: {
@@ -505,6 +506,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
505
506
  method: "POST";
506
507
  operationId: string;
507
508
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
509
+ cloneRequest: true;
508
510
  body: zod.ZodObject<{
509
511
  email: zod.ZodString;
510
512
  password: zod.ZodString;
@@ -736,6 +738,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
736
738
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
737
739
  method: "POST";
738
740
  operationId: string;
741
+ cloneRequest: true;
739
742
  body: zod.ZodObject<{
740
743
  email: zod.ZodEmail;
741
744
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -2242,7 +2245,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
2242
2245
  allowedMediaTypes: string[];
2243
2246
  scope: "server";
2244
2247
  };
2245
- }, void>;
2248
+ }, never>;
2246
2249
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
2247
2250
  method: ("GET" | "POST")[];
2248
2251
  operationId: string;
@@ -2341,6 +2344,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
2341
2344
  callbackURL: zod.ZodOptional<zod.ZodString>;
2342
2345
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
2343
2346
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
2347
+ cloneRequest: true;
2344
2348
  metadata: {
2345
2349
  allowedMediaTypes: string[];
2346
2350
  $Infer: {
@@ -2507,6 +2511,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
2507
2511
  method: "POST";
2508
2512
  operationId: string;
2509
2513
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
2514
+ cloneRequest: true;
2510
2515
  body: zod.ZodObject<{
2511
2516
  email: zod.ZodString;
2512
2517
  password: zod.ZodString;
@@ -2738,6 +2743,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
2738
2743
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
2739
2744
  method: "POST";
2740
2745
  operationId: string;
2746
+ cloneRequest: true;
2741
2747
  body: zod.ZodObject<{
2742
2748
  email: zod.ZodEmail;
2743
2749
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -4247,7 +4253,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
4247
4253
  allowedMediaTypes: string[];
4248
4254
  scope: "server";
4249
4255
  };
4250
- }, void>;
4256
+ }, never>;
4251
4257
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
4252
4258
  method: ("GET" | "POST")[];
4253
4259
  operationId: string;
@@ -4346,6 +4352,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
4346
4352
  callbackURL: zod.ZodOptional<zod.ZodString>;
4347
4353
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
4348
4354
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
4355
+ cloneRequest: true;
4349
4356
  metadata: {
4350
4357
  allowedMediaTypes: string[];
4351
4358
  $Infer: {
@@ -4512,6 +4519,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
4512
4519
  method: "POST";
4513
4520
  operationId: string;
4514
4521
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
4522
+ cloneRequest: true;
4515
4523
  body: zod.ZodObject<{
4516
4524
  email: zod.ZodString;
4517
4525
  password: zod.ZodString;
@@ -4743,6 +4751,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
4743
4751
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
4744
4752
  method: "POST";
4745
4753
  operationId: string;
4754
+ cloneRequest: true;
4746
4755
  body: zod.ZodObject<{
4747
4756
  email: zod.ZodEmail;
4748
4757
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -6249,7 +6258,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
6249
6258
  allowedMediaTypes: string[];
6250
6259
  scope: "server";
6251
6260
  };
6252
- }, void>;
6261
+ }, never>;
6253
6262
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
6254
6263
  method: ("GET" | "POST")[];
6255
6264
  operationId: string;
@@ -6348,6 +6357,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
6348
6357
  callbackURL: zod.ZodOptional<zod.ZodString>;
6349
6358
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
6350
6359
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
6360
+ cloneRequest: true;
6351
6361
  metadata: {
6352
6362
  allowedMediaTypes: string[];
6353
6363
  $Infer: {
@@ -6514,6 +6524,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
6514
6524
  method: "POST";
6515
6525
  operationId: string;
6516
6526
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
6527
+ cloneRequest: true;
6517
6528
  body: zod.ZodObject<{
6518
6529
  email: zod.ZodString;
6519
6530
  password: zod.ZodString;
@@ -6745,6 +6756,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
6745
6756
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
6746
6757
  method: "POST";
6747
6758
  operationId: string;
6759
+ cloneRequest: true;
6748
6760
  body: zod.ZodObject<{
6749
6761
  email: zod.ZodEmail;
6750
6762
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -8325,7 +8337,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
8325
8337
  allowedMediaTypes: string[];
8326
8338
  scope: "server";
8327
8339
  };
8328
- }, void>;
8340
+ }, never>;
8329
8341
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
8330
8342
  method: ("GET" | "POST")[];
8331
8343
  operationId: string;
@@ -8424,6 +8436,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
8424
8436
  callbackURL: zod.ZodOptional<zod.ZodString>;
8425
8437
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
8426
8438
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
8439
+ cloneRequest: true;
8427
8440
  metadata: {
8428
8441
  allowedMediaTypes: string[];
8429
8442
  $Infer: {
@@ -8590,6 +8603,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
8590
8603
  method: "POST";
8591
8604
  operationId: string;
8592
8605
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
8606
+ cloneRequest: true;
8593
8607
  body: zod.ZodObject<{
8594
8608
  email: zod.ZodString;
8595
8609
  password: zod.ZodString;
@@ -8821,6 +8835,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
8821
8835
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
8822
8836
  method: "POST";
8823
8837
  operationId: string;
8838
+ cloneRequest: true;
8824
8839
  body: zod.ZodObject<{
8825
8840
  email: zod.ZodEmail;
8826
8841
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -10327,7 +10342,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
10327
10342
  allowedMediaTypes: string[];
10328
10343
  scope: "server";
10329
10344
  };
10330
- }, void>;
10345
+ }, never>;
10331
10346
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
10332
10347
  method: ("GET" | "POST")[];
10333
10348
  operationId: string;
@@ -10426,6 +10441,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
10426
10441
  callbackURL: zod.ZodOptional<zod.ZodString>;
10427
10442
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
10428
10443
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
10444
+ cloneRequest: true;
10429
10445
  metadata: {
10430
10446
  allowedMediaTypes: string[];
10431
10447
  $Infer: {
@@ -10592,6 +10608,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
10592
10608
  method: "POST";
10593
10609
  operationId: string;
10594
10610
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
10611
+ cloneRequest: true;
10595
10612
  body: zod.ZodObject<{
10596
10613
  email: zod.ZodString;
10597
10614
  password: zod.ZodString;
@@ -10823,6 +10840,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
10823
10840
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
10824
10841
  method: "POST";
10825
10842
  operationId: string;
10843
+ cloneRequest: true;
10826
10844
  body: zod.ZodObject<{
10827
10845
  email: zod.ZodEmail;
10828
10846
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -12151,6 +12169,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
12151
12169
  USER_ALREADY_EXISTS: _better_auth_core_utils_error_codes0.RawError<"USER_ALREADY_EXISTS">;
12152
12170
  USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: _better_auth_core_utils_error_codes0.RawError<"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL">;
12153
12171
  EMAIL_CAN_NOT_BE_UPDATED: _better_auth_core_utils_error_codes0.RawError<"EMAIL_CAN_NOT_BE_UPDATED">;
12172
+ CHANGE_EMAIL_DISABLED: _better_auth_core_utils_error_codes0.RawError<"CHANGE_EMAIL_DISABLED">;
12154
12173
  CREDENTIAL_ACCOUNT_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"CREDENTIAL_ACCOUNT_NOT_FOUND">;
12155
12174
  ACCOUNT_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"ACCOUNT_NOT_FOUND">;
12156
12175
  SESSION_EXPIRED: _better_auth_core_utils_error_codes0.RawError<"SESSION_EXPIRED">;
@@ -7,6 +7,7 @@ import { createAuthClient } from "../client/vanilla.mjs";
7
7
  import { bearer } from "../plugins/bearer/index.mjs";
8
8
  import { sql } from "kysely";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { randomUUID } from "node:crypto";
10
11
  import { afterAll } from "vitest";
11
12
  //#region src/test-utils/test-instance.ts
12
13
  const cleanupSet = /* @__PURE__ */ new Set();
@@ -19,10 +20,17 @@ afterAll(async () => {
19
20
  });
20
21
  async function getTestInstance(options, config) {
21
22
  const testWith = config?.testWith || "sqlite";
23
+ const postgresSchema = testWith === "postgres" ? `ba_test_${randomUUID().replaceAll("-", "_")}` : void 0;
24
+ const quotePostgresIdentifier = (identifier) => `"${identifier.replaceAll("\"", "\"\"")}"`;
22
25
  async function getPostgres() {
23
26
  const { Kysely, PostgresDialect } = await import("kysely");
24
27
  const { Pool } = await import("pg");
25
- return new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: "postgres://user:password@localhost:5432/better_auth" }) }) });
28
+ const pool = new Pool({
29
+ connectionString: "postgres://user:password@localhost:5432/better_auth",
30
+ options: postgresSchema ? `-c search_path=${postgresSchema},public` : void 0
31
+ });
32
+ if (postgresSchema) await pool.query(`CREATE SCHEMA IF NOT EXISTS ${quotePostgresIdentifier(postgresSchema)}`);
33
+ return new Kysely({ dialect: new PostgresDialect({ pool }) });
26
34
  }
27
35
  async function getSqlite() {
28
36
  const { DatabaseSync } = await import("node:sqlite");
@@ -103,7 +111,8 @@ async function getTestInstance(options, config) {
103
111
  }
104
112
  if (testWith === "postgres") {
105
113
  const postgres = await getPostgres();
106
- await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(postgres);
114
+ if (postgresSchema) await sql.raw(`DROP SCHEMA IF EXISTS ${quotePostgresIdentifier(postgresSchema)} CASCADE`).execute(postgres);
115
+ else await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(postgres);
107
116
  await postgres.destroy();
108
117
  return;
109
118
  }
@@ -1,4 +1,4 @@
1
1
  import { generateState, parseState } from "../oauth2/state.mjs";
2
2
  import { StateData, generateGenericState, parseGenericState } from "../state.mjs";
3
3
  import { HIDE_METADATA } from "./hide-metadata.mjs";
4
- import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./url.mjs";
4
+ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./url.mjs";
@@ -1,6 +1,7 @@
1
1
  import { BaseURLConfig, DynamicBaseURLConfig } from "@better-auth/core";
2
2
 
3
3
  //#region src/utils/url.d.ts
4
+ declare function trimTrailingSlashes(value: string): string;
4
5
  declare function getBaseURL(url?: string, path?: string, request?: Request, loadEnv?: boolean, trustedProxyHeaders?: boolean | undefined): string | undefined;
5
6
  declare function getOrigin(url: string): string | null;
6
7
  declare function getProtocol(url: string): string | null;
@@ -74,4 +75,4 @@ declare function resolveDynamicBaseURL(config: DynamicBaseURLConfig, source: Req
74
75
  */
75
76
  declare function resolveBaseURL(config: BaseURLConfig | undefined, basePath: string, source?: Request | Headers, loadEnv?: boolean, trustedProxyHeaders?: boolean): string | undefined;
76
77
  //#endregion
77
- export { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL };
78
+ export { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
@@ -2,6 +2,7 @@ import { wildcardMatch } from "./wildcard.mjs";
2
2
  import { env } from "@better-auth/core/env";
3
3
  import { BetterAuthError } from "@better-auth/core/error";
4
4
  //#region src/utils/url.ts
5
+ const SLASH_CHAR_CODE = "/".charCodeAt(0);
5
6
  /**
6
7
  * Minimal loopback check for dev scheme inference only. Reachable from
7
8
  * `client/config.ts` via `getBaseURL`, so we MUST NOT import the full
@@ -17,9 +18,14 @@ function isLoopbackForDevScheme(host) {
17
18
  const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
18
19
  return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname.startsWith("127.");
19
20
  }
21
+ function trimTrailingSlashes(value) {
22
+ let end = value.length;
23
+ while (end > 0 && value.charCodeAt(end - 1) === SLASH_CHAR_CODE) end--;
24
+ return end === value.length ? value : value.slice(0, end);
25
+ }
20
26
  function checkHasPath(url) {
21
27
  try {
22
- return (new URL(url).pathname.replace(/\/+$/, "") || "/") !== "/";
28
+ return (trimTrailingSlashes(new URL(url).pathname) || "/") !== "/";
23
29
  } catch {
24
30
  throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`);
25
31
  }
@@ -36,7 +42,7 @@ function assertHasProtocol(url) {
36
42
  function withPath(url, path = "/api/auth") {
37
43
  assertHasProtocol(url);
38
44
  if (checkHasPath(url)) return url;
39
- const trimmedUrl = url.replace(/\/+$/, "");
45
+ const trimmedUrl = trimTrailingSlashes(url);
40
46
  if (!path || path === "/") return trimmedUrl;
41
47
  path = path.startsWith("/") ? path : `/${path}`;
42
48
  return `${trimmedUrl}${path}`;
@@ -229,4 +235,4 @@ function resolveBaseURL(config, basePath, source, loadEnv, trustedProxyHeaders)
229
235
  return getBaseURL(void 0, basePath, request, loadEnv, trustedProxyHeaders);
230
236
  }
231
237
  //#endregion
232
- export { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL };
238
+ export { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.6.10",
3
+ "version": "1.6.12",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -479,29 +479,29 @@
479
479
  }
480
480
  },
481
481
  "dependencies": {
482
- "@better-auth/utils": "0.4.0",
482
+ "@better-auth/utils": "0.4.1",
483
483
  "@better-fetch/fetch": "1.1.21",
484
484
  "@noble/ciphers": "^2.1.1",
485
485
  "@noble/hashes": "^2.0.1",
486
486
  "better-call": "1.3.5",
487
487
  "defu": "^6.1.4",
488
488
  "jose": "^6.1.3",
489
- "kysely": "^0.28.14",
489
+ "kysely": "^0.28.17 || ^0.29.0",
490
490
  "nanostores": "^1.1.1",
491
491
  "zod": "^4.3.6",
492
- "@better-auth/drizzle-adapter": "1.6.10",
493
- "@better-auth/core": "1.6.10",
494
- "@better-auth/kysely-adapter": "1.6.10",
495
- "@better-auth/memory-adapter": "1.6.10",
496
- "@better-auth/mongo-adapter": "1.6.10",
497
- "@better-auth/prisma-adapter": "1.6.10",
498
- "@better-auth/telemetry": "1.6.10"
492
+ "@better-auth/core": "1.6.12",
493
+ "@better-auth/drizzle-adapter": "1.6.12",
494
+ "@better-auth/memory-adapter": "1.6.12",
495
+ "@better-auth/mongo-adapter": "1.6.12",
496
+ "@better-auth/kysely-adapter": "1.6.12",
497
+ "@better-auth/prisma-adapter": "1.6.12",
498
+ "@better-auth/telemetry": "1.6.12"
499
499
  },
500
500
  "devDependencies": {
501
501
  "@lynx-js/react": "^0.116.3",
502
- "@sveltejs/kit": "^2.57.1",
503
- "@tanstack/react-start": "^1.163.2",
504
- "@tanstack/solid-start": "^1.163.2",
502
+ "@sveltejs/kit": "^2.60.1",
503
+ "@tanstack/react-start": "^1.168.4",
504
+ "@tanstack/solid-start": "^1.168.4",
505
505
  "@types/bun": "^1.3.9",
506
506
  "@types/google.accounts": "^0.0.18",
507
507
  "@types/pg": "^8.16.0",
@@ -512,7 +512,7 @@
512
512
  "happy-dom": "^20.8.9",
513
513
  "listhen": "^1.9.0",
514
514
  "msw": "^2.12.10",
515
- "next": "^16.2.3",
515
+ "next": "^16.2.6",
516
516
  "oauth2-mock-server": "^8.2.2",
517
517
  "react": "^19.2.4",
518
518
  "react-dom": "^19.2.4",
@@ -520,6 +520,7 @@
520
520
  "tsdown": "0.21.1",
521
521
  "type-fest": "^5.4.4",
522
522
  "typescript": "^5.9.3",
523
+ "vite": "^7.3.2",
523
524
  "vitest": "^4.1.5",
524
525
  "vue": "^3.5.29"
525
526
  },