@better-auth/core 1.6.12 → 1.6.14
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.
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +4 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/types/context.d.mts +9 -3
- package/dist/types/init-options.d.mts +6 -2
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +30 -1
- package/package.json +1 -1
- package/src/db/adapter/factory.ts +6 -0
- package/src/types/context.ts +10 -4
- package/src/types/init-options.ts +6 -2
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/url.ts +28 -0
package/dist/context/global.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
})
|
|
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.
|
|
5
|
+
const INSTRUMENTATION_VERSION = "1.6.14";
|
|
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
|
package/dist/types/context.d.mts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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,20 @@
|
|
|
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
|
+
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
|
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
|
+
declare const SafeUrlSchema: z.ZodURL;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SafeUrlSchema };
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
|
11
|
+
* - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
|
|
12
|
+
* `*.localhost` per RFC 6761), where HTTP is allowed for local development.
|
|
13
|
+
* - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
|
|
14
|
+
*
|
|
15
|
+
* This is the single source of truth for redirect-URI validation across the
|
|
16
|
+
* OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
|
|
17
|
+
* rather than re-implementing the scheme policy per plugin.
|
|
18
|
+
*/
|
|
19
|
+
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
20
|
+
let u;
|
|
21
|
+
try {
|
|
22
|
+
u = new URL(val);
|
|
23
|
+
} catch {
|
|
24
|
+
ctx.addIssue({
|
|
25
|
+
code: "custom",
|
|
26
|
+
message: "URL must be parseable",
|
|
27
|
+
fatal: true
|
|
28
|
+
});
|
|
29
|
+
return z.NEVER;
|
|
30
|
+
}
|
|
31
|
+
if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "URL cannot use javascript:, data:, or vbscript: scheme"
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (val.includes("#")) ctx.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "Redirect URI must not contain a fragment component"
|
|
41
|
+
});
|
|
42
|
+
if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
|
|
43
|
+
code: "custom",
|
|
44
|
+
message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { SafeUrlSchema };
|
package/dist/utils/url.d.mts
CHANGED
|
@@ -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 };
|
package/dist/utils/url.mjs
CHANGED
|
@@ -27,5 +27,34 @@ 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
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
|
|
58
|
+
}
|
|
30
59
|
//#endregion
|
|
31
|
-
export { normalizePathname };
|
|
60
|
+
export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
|
package/package.json
CHANGED
|
@@ -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
|
);
|
package/src/types/context.ts
CHANGED
|
@@ -158,7 +158,15 @@ export interface InternalAdapter<
|
|
|
158
158
|
*/
|
|
159
159
|
deleteAccount(id: string): Promise<void>;
|
|
160
160
|
|
|
161
|
-
|
|
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
|
-
*
|
|
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,54 @@
|
|
|
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
|
+
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
|
11
|
+
* - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
|
|
12
|
+
* `*.localhost` per RFC 6761), where HTTP is allowed for local development.
|
|
13
|
+
* - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
|
|
14
|
+
*
|
|
15
|
+
* This is the single source of truth for redirect-URI validation across the
|
|
16
|
+
* OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
|
|
17
|
+
* rather than re-implementing the scheme policy per plugin.
|
|
18
|
+
*/
|
|
19
|
+
export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
20
|
+
let u: URL;
|
|
21
|
+
try {
|
|
22
|
+
u = new URL(val);
|
|
23
|
+
} catch {
|
|
24
|
+
ctx.addIssue({
|
|
25
|
+
code: "custom",
|
|
26
|
+
message: "URL must be parseable",
|
|
27
|
+
fatal: true,
|
|
28
|
+
});
|
|
29
|
+
return z.NEVER;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
|
|
33
|
+
ctx.addIssue({
|
|
34
|
+
code: "custom",
|
|
35
|
+
message: "URL cannot use javascript:, data:, or vbscript: scheme",
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (val.includes("#")) {
|
|
41
|
+
ctx.addIssue({
|
|
42
|
+
code: "custom",
|
|
43
|
+
message: "Redirect URI must not contain a fragment component",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (u.protocol === "http:" && !isLoopbackHost(u.host)) {
|
|
48
|
+
ctx.addIssue({
|
|
49
|
+
code: "custom",
|
|
50
|
+
message:
|
|
51
|
+
"Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
package/src/utils/url.ts
CHANGED
|
@@ -41,3 +41,31 @@ 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
|
+
let parsed: URL;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(value);
|
|
66
|
+
} catch {
|
|
67
|
+
// Relative URLs carry no scheme to abuse.
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
|
|
71
|
+
}
|