@better-auth/core 1.6.12 → 1.6.13

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.
@@ -2,7 +2,7 @@
2
2
  const symbol = Symbol.for("better-auth:global");
3
3
  let bind = null;
4
4
  const __context = {};
5
- const __betterAuthVersion = "1.6.12";
5
+ const __betterAuthVersion = "1.6.13";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -711,7 +711,7 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
711
711
  limit: 1
712
712
  }))[0];
713
713
  if (!target) return null;
714
- return await trx.deleteMany({
714
+ const deleted = await trx.deleteMany({
715
715
  model: unsafeModel,
716
716
  where: [...unsafeWhere, {
717
717
  field: "id",
@@ -720,7 +720,9 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
720
720
  connector: "AND",
721
721
  mode: "sensitive"
722
722
  }]
723
- }) > 0 ? target : null;
723
+ });
724
+ if (typeof deleted !== "number") throw new BetterAuthError(`Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`);
725
+ return deleted > 0 ? target : null;
724
726
  }));
725
727
  resultNeedsOutputTransform = false;
726
728
  }
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
2
2
  import { getOpenTelemetryAPI } from "./api.mjs";
3
3
  //#region src/instrumentation/tracer.ts
4
4
  const INSTRUMENTATION_SCOPE = "better-auth";
5
- const INSTRUMENTATION_VERSION = "1.6.12";
5
+ const INSTRUMENTATION_VERSION = "1.6.13";
6
6
  /**
7
7
  * Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
8
8
  * callbacks). These are APIErrors with 3xx status codes and should not be
@@ -89,7 +89,14 @@ interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions
89
89
  * @param id - The account row's primary key (the `id` column, not the `accountId` column).
90
90
  */
91
91
  deleteAccount(id: string): Promise<void>;
92
- deleteSessions(userIdOrSessionTokens: string | string[]): Promise<void>;
92
+ /**
93
+ * Delete every session belonging to a user.
94
+ */
95
+ deleteUserSessions(userId: string): Promise<void>;
96
+ /**
97
+ * Delete sessions by their session tokens.
98
+ */
99
+ deleteSessions(sessionTokens: string[]): Promise<void>;
93
100
  findOAuthUser(email: string, accountId: string, providerId: string): Promise<{
94
101
  user: User;
95
102
  linkedAccount: Account | null;
@@ -107,7 +114,6 @@ interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions
107
114
  updateUserByEmail<T extends Record<string, any>>(email: string, data: Partial<User & Record<string, any>>): Promise<User & T>;
108
115
  updatePassword(userId: string, password: string): Promise<void>;
109
116
  findAccounts(userId: string): Promise<Account[]>;
110
- findAccount(accountId: string): Promise<Account | null>;
111
117
  findAccountByProviderId(accountId: string, providerId: string): Promise<Account | null>;
112
118
  findAccountByUserId(userId: string): Promise<Account[]>;
113
119
  updateAccount(id: string, data: Partial<Account>): Promise<Account>;
@@ -185,7 +191,7 @@ type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = Plugin
185
191
  * - "cookie": Store state in an encrypted cookie (stateless)
186
192
  * - "database": Store state in the database
187
193
  *
188
- * @default "cookie"
194
+ * @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
189
195
  */
190
196
  storeStateStrategy: "database" | "cookie";
191
197
  };
@@ -963,7 +963,11 @@ type BetterAuthOptions = {
963
963
  */
964
964
  allowUnlinkingAll?: boolean;
965
965
  /**
966
- * If enabled (true), this will update the user information based on the newly linked account
966
+ * When enabled, linking an account copies the provider's profile onto
967
+ * the local user, matching the fields persisted on sign-up (`name`,
968
+ * `image`, and any `mapProfileToUser` fields). The local `email` and
969
+ * `emailVerified` are never changed, so a link cannot rebind the
970
+ * account's identity.
967
971
  *
968
972
  * @default false
969
973
  */
@@ -996,7 +1000,7 @@ type BetterAuthOptions = {
996
1000
  * - "cookie": Store state in an encrypted cookie (stateless)
997
1001
  * - "database": Store state in the database
998
1002
  *
999
- * @default "cookie"
1003
+ * @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
1000
1004
  */
1001
1005
  storeStateStrategy?: "database" | "cookie";
1002
1006
  /**
@@ -0,0 +1,19 @@
1
+ import * as z from "zod";
2
+
3
+ //#region src/utils/redirect-uri.d.ts
4
+ /**
5
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
6
+ * server stores and later hands back to a browser.
7
+ *
8
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
9
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
10
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
11
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
12
+ *
13
+ * This is the single source of truth for redirect-URI validation across the
14
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
15
+ * rather than re-implementing the scheme policy per plugin.
16
+ */
17
+ declare const SafeUrlSchema: z.ZodURL;
18
+ //#endregion
19
+ export { SafeUrlSchema };
@@ -0,0 +1,41 @@
1
+ import { isLoopbackHost } from "./host.mjs";
2
+ import { DANGEROUS_URL_SCHEMES } from "./url.mjs";
3
+ import * as z from "zod";
4
+ //#region src/utils/redirect-uri.ts
5
+ /**
6
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
7
+ * server stores and later hands back to a browser.
8
+ *
9
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
10
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
11
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
12
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
13
+ *
14
+ * This is the single source of truth for redirect-URI validation across the
15
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
16
+ * rather than re-implementing the scheme policy per plugin.
17
+ */
18
+ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
19
+ if (!URL.canParse(val)) {
20
+ ctx.addIssue({
21
+ code: "custom",
22
+ message: "URL must be parseable",
23
+ fatal: true
24
+ });
25
+ return z.NEVER;
26
+ }
27
+ const u = new URL(val);
28
+ if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
29
+ ctx.addIssue({
30
+ code: "custom",
31
+ message: "URL cannot use javascript:, data:, or vbscript: scheme"
32
+ });
33
+ return;
34
+ }
35
+ if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
36
+ code: "custom",
37
+ message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
38
+ });
39
+ });
40
+ //#endregion
41
+ export { SafeUrlSchema };
@@ -16,5 +16,22 @@
16
16
  * // Returns: "/sso/saml2/callback/provider1"
17
17
  */
18
18
  declare function normalizePathname(requestUrl: string, basePath: string): string;
19
+ /**
20
+ * Schemes that execute or embed code when navigated to or accepted as a
21
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
22
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
23
+ */
24
+ declare const DANGEROUS_URL_SCHEMES: string[];
25
+ /**
26
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
27
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
28
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
29
+ * `myapp://`) return `true`.
30
+ *
31
+ * Use this to guard browser navigation sinks and any redirect target that may
32
+ * originate from untrusted input. It is intentionally narrow: it blocks code
33
+ * execution schemes without rejecting relative paths or mobile deep links.
34
+ */
35
+ declare function isSafeUrlScheme(value: string): boolean;
19
36
  //#endregion
20
- export { normalizePathname };
37
+ export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
@@ -27,5 +27,29 @@ function normalizePathname(requestUrl, basePath) {
27
27
  if (pathname.startsWith(basePath + "/")) return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
28
28
  return pathname;
29
29
  }
30
+ /**
31
+ * Schemes that execute or embed code when navigated to or accepted as a
32
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
33
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
34
+ */
35
+ const DANGEROUS_URL_SCHEMES = [
36
+ "javascript:",
37
+ "data:",
38
+ "vbscript:"
39
+ ];
40
+ /**
41
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
42
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
43
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
44
+ * `myapp://`) return `true`.
45
+ *
46
+ * Use this to guard browser navigation sinks and any redirect target that may
47
+ * originate from untrusted input. It is intentionally narrow: it blocks code
48
+ * execution schemes without rejecting relative paths or mobile deep links.
49
+ */
50
+ function isSafeUrlScheme(value) {
51
+ if (!URL.canParse(value)) return true;
52
+ return !DANGEROUS_URL_SCHEMES.includes(new URL(value).protocol);
53
+ }
30
54
  //#endregion
31
- export { normalizePathname };
55
+ export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.6.12",
3
+ "version": "1.6.13",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1391,6 +1391,12 @@ export const createAdapterFactory =
1391
1391
  },
1392
1392
  ],
1393
1393
  });
1394
+ // A non-numeric count coerces to a false miss, so fail loud.
1395
+ if (typeof deleted !== "number") {
1396
+ throw new BetterAuthError(
1397
+ `Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`,
1398
+ );
1399
+ }
1394
1400
  return deleted > 0 ? (target as T) : null;
1395
1401
  }),
1396
1402
  );
@@ -158,7 +158,15 @@ export interface InternalAdapter<
158
158
  */
159
159
  deleteAccount(id: string): Promise<void>;
160
160
 
161
- deleteSessions(userIdOrSessionTokens: string | string[]): Promise<void>;
161
+ /**
162
+ * Delete every session belonging to a user.
163
+ */
164
+ deleteUserSessions(userId: string): Promise<void>;
165
+
166
+ /**
167
+ * Delete sessions by their session tokens.
168
+ */
169
+ deleteSessions(sessionTokens: string[]): Promise<void>;
162
170
 
163
171
  findOAuthUser(
164
172
  email: string,
@@ -196,8 +204,6 @@ export interface InternalAdapter<
196
204
 
197
205
  findAccounts(userId: string): Promise<Account[]>;
198
206
 
199
- findAccount(accountId: string): Promise<Account | null>;
200
-
201
207
  findAccountByProviderId(
202
208
  accountId: string,
203
209
  providerId: string,
@@ -321,7 +327,7 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
321
327
  * - "cookie": Store state in an encrypted cookie (stateless)
322
328
  * - "database": Store state in the database
323
329
  *
324
- * @default "cookie"
330
+ * @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
325
331
  */
326
332
  storeStateStrategy: "database" | "cookie";
327
333
  };
@@ -1093,7 +1093,11 @@ export type BetterAuthOptions = {
1093
1093
  */
1094
1094
  allowUnlinkingAll?: boolean;
1095
1095
  /**
1096
- * If enabled (true), this will update the user information based on the newly linked account
1096
+ * When enabled, linking an account copies the provider's profile onto
1097
+ * the local user, matching the fields persisted on sign-up (`name`,
1098
+ * `image`, and any `mapProfileToUser` fields). The local `email` and
1099
+ * `emailVerified` are never changed, so a link cannot rebind the
1100
+ * account's identity.
1097
1101
  *
1098
1102
  * @default false
1099
1103
  */
@@ -1126,7 +1130,7 @@ export type BetterAuthOptions = {
1126
1130
  * - "cookie": Store state in an encrypted cookie (stateless)
1127
1131
  * - "database": Store state in the database
1128
1132
  *
1129
- * @default "cookie"
1133
+ * @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
1130
1134
  */
1131
1135
  storeStateStrategy?: "database" | "cookie";
1132
1136
  /**
@@ -0,0 +1,45 @@
1
+ import * as z from "zod";
2
+ import { isLoopbackHost } from "./host";
3
+ import { DANGEROUS_URL_SCHEMES } from "./url";
4
+
5
+ /**
6
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
7
+ * server stores and later hands back to a browser.
8
+ *
9
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
10
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
11
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
12
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
13
+ *
14
+ * This is the single source of truth for redirect-URI validation across the
15
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
16
+ * rather than re-implementing the scheme policy per plugin.
17
+ */
18
+ export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
19
+ if (!URL.canParse(val)) {
20
+ ctx.addIssue({
21
+ code: "custom",
22
+ message: "URL must be parseable",
23
+ fatal: true,
24
+ });
25
+ return z.NEVER;
26
+ }
27
+
28
+ const u = new URL(val);
29
+
30
+ if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
31
+ ctx.addIssue({
32
+ code: "custom",
33
+ message: "URL cannot use javascript:, data:, or vbscript: scheme",
34
+ });
35
+ return;
36
+ }
37
+
38
+ if (u.protocol === "http:" && !isLoopbackHost(u.host)) {
39
+ ctx.addIssue({
40
+ code: "custom",
41
+ message:
42
+ "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)",
43
+ });
44
+ }
45
+ });
package/src/utils/url.ts CHANGED
@@ -41,3 +41,28 @@ export function normalizePathname(
41
41
 
42
42
  return pathname;
43
43
  }
44
+
45
+ /**
46
+ * Schemes that execute or embed code when navigated to or accepted as a
47
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
48
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
49
+ */
50
+ export const DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];
51
+
52
+ /**
53
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
54
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
55
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
56
+ * `myapp://`) return `true`.
57
+ *
58
+ * Use this to guard browser navigation sinks and any redirect target that may
59
+ * originate from untrusted input. It is intentionally narrow: it blocks code
60
+ * execution schemes without rejecting relative paths or mobile deep links.
61
+ */
62
+ export function isSafeUrlScheme(value: string): boolean {
63
+ if (!URL.canParse(value)) {
64
+ // Relative URLs carry no scheme to abuse.
65
+ return true;
66
+ }
67
+ return !DANGEROUS_URL_SCHEMES.includes(new URL(value).protocol);
68
+ }