@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.4
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 +63 -1
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/error/codes.d.mts +1 -0
- package/dist/error/codes.mjs +1 -0
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +18 -29
- package/dist/oauth2/create-authorization-url.d.mts +9 -1
- package/dist/oauth2/create-authorization-url.mjs +23 -5
- package/dist/oauth2/index.d.mts +10 -7
- package/dist/oauth2/index.mjs +9 -7
- package/dist/oauth2/oauth-provider.d.mts +21 -2
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +19 -32
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +9 -1
- package/dist/oauth2/utils.mjs +12 -1
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify.mjs +15 -5
- package/dist/social-providers/apple.d.mts +4 -18
- package/dist/social-providers/apple.mjs +14 -3
- package/dist/social-providers/atlassian.d.mts +3 -1
- package/dist/social-providers/atlassian.mjs +5 -2
- package/dist/social-providers/cognito.d.mts +16 -1
- package/dist/social-providers/cognito.mjs +6 -2
- package/dist/social-providers/discord.d.mts +4 -2
- package/dist/social-providers/discord.mjs +16 -3
- package/dist/social-providers/dropbox.d.mts +3 -1
- package/dist/social-providers/dropbox.mjs +5 -4
- package/dist/social-providers/facebook.d.mts +3 -1
- package/dist/social-providers/facebook.mjs +5 -2
- package/dist/social-providers/figma.d.mts +3 -1
- package/dist/social-providers/figma.mjs +3 -2
- package/dist/social-providers/github.d.mts +3 -1
- package/dist/social-providers/github.mjs +5 -4
- package/dist/social-providers/gitlab.d.mts +3 -1
- package/dist/social-providers/gitlab.mjs +3 -2
- package/dist/social-providers/google.d.mts +3 -1
- package/dist/social-providers/google.mjs +5 -2
- package/dist/social-providers/huggingface.d.mts +3 -1
- package/dist/social-providers/huggingface.mjs +3 -2
- package/dist/social-providers/index.d.mts +104 -36
- package/dist/social-providers/kakao.d.mts +3 -1
- package/dist/social-providers/kakao.mjs +3 -2
- package/dist/social-providers/kick.d.mts +3 -1
- package/dist/social-providers/kick.mjs +3 -2
- package/dist/social-providers/line.d.mts +3 -1
- package/dist/social-providers/line.mjs +3 -2
- package/dist/social-providers/linear.d.mts +3 -1
- package/dist/social-providers/linear.mjs +3 -2
- package/dist/social-providers/linkedin.d.mts +3 -1
- package/dist/social-providers/linkedin.mjs +3 -2
- package/dist/social-providers/microsoft-entra-id.d.mts +2 -1
- package/dist/social-providers/microsoft-entra-id.mjs +3 -2
- package/dist/social-providers/naver.d.mts +3 -1
- package/dist/social-providers/naver.mjs +3 -2
- package/dist/social-providers/notion.d.mts +3 -1
- package/dist/social-providers/notion.mjs +5 -2
- package/dist/social-providers/paybin.d.mts +3 -1
- package/dist/social-providers/paybin.mjs +3 -2
- package/dist/social-providers/paypal.d.mts +3 -1
- package/dist/social-providers/paypal.mjs +4 -3
- package/dist/social-providers/polar.d.mts +3 -1
- package/dist/social-providers/polar.mjs +3 -2
- package/dist/social-providers/railway.d.mts +3 -1
- package/dist/social-providers/railway.mjs +3 -2
- package/dist/social-providers/reddit.d.mts +3 -1
- package/dist/social-providers/reddit.mjs +3 -2
- package/dist/social-providers/roblox.d.mts +4 -2
- package/dist/social-providers/roblox.mjs +12 -2
- package/dist/social-providers/salesforce.d.mts +3 -1
- package/dist/social-providers/salesforce.mjs +3 -2
- package/dist/social-providers/slack.d.mts +4 -2
- package/dist/social-providers/slack.mjs +11 -8
- package/dist/social-providers/spotify.d.mts +3 -1
- package/dist/social-providers/spotify.mjs +3 -2
- package/dist/social-providers/tiktok.d.mts +3 -1
- package/dist/social-providers/tiktok.mjs +14 -2
- package/dist/social-providers/twitch.d.mts +3 -1
- package/dist/social-providers/twitch.mjs +3 -2
- package/dist/social-providers/twitter.d.mts +5 -2
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/vercel.d.mts +3 -1
- package/dist/social-providers/vercel.mjs +3 -2
- package/dist/social-providers/vk.d.mts +3 -1
- package/dist/social-providers/vk.mjs +3 -2
- package/dist/social-providers/wechat.d.mts +3 -1
- package/dist/social-providers/wechat.mjs +7 -1
- package/dist/social-providers/zoom.d.mts +4 -2
- package/dist/social-providers/zoom.mjs +10 -17
- package/dist/types/context.d.mts +23 -3
- package/dist/types/init-options.d.mts +29 -5
- package/dist/utils/ip.d.mts +5 -4
- package/dist/utils/ip.mjs +3 -3
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +30 -1
- package/package.json +9 -8
- package/src/db/adapter/factory.ts +118 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/type.ts +12 -0
- package/src/error/codes.ts +1 -0
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +48 -72
- package/src/oauth2/create-authorization-url.ts +28 -6
- package/src/oauth2/index.ts +25 -9
- package/src/oauth2/oauth-provider.ts +21 -2
- package/src/oauth2/refresh-access-token.ts +50 -76
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +19 -0
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify.ts +20 -4
- package/src/social-providers/apple.ts +26 -2
- package/src/social-providers/atlassian.ts +8 -1
- package/src/social-providers/cognito.ts +26 -1
- package/src/social-providers/discord.ts +21 -17
- package/src/social-providers/dropbox.ts +7 -5
- package/src/social-providers/facebook.ts +11 -6
- package/src/social-providers/figma.ts +8 -1
- package/src/social-providers/github.ts +4 -2
- package/src/social-providers/gitlab.ts +2 -0
- package/src/social-providers/google.ts +2 -0
- package/src/social-providers/huggingface.ts +8 -1
- package/src/social-providers/kakao.ts +2 -1
- package/src/social-providers/kick.ts +8 -1
- package/src/social-providers/line.ts +2 -0
- package/src/social-providers/linear.ts +8 -1
- package/src/social-providers/linkedin.ts +2 -0
- package/src/social-providers/microsoft-entra-id.ts +1 -0
- package/src/social-providers/naver.ts +2 -1
- package/src/social-providers/notion.ts +8 -1
- package/src/social-providers/paybin.ts +2 -0
- package/src/social-providers/paypal.ts +7 -1
- package/src/social-providers/polar.ts +8 -1
- package/src/social-providers/railway.ts +8 -1
- package/src/social-providers/reddit.ts +2 -1
- package/src/social-providers/roblox.ts +16 -11
- package/src/social-providers/salesforce.ts +8 -1
- package/src/social-providers/slack.ts +15 -9
- package/src/social-providers/spotify.ts +8 -1
- package/src/social-providers/tiktok.ts +22 -9
- package/src/social-providers/twitch.ts +2 -1
- package/src/social-providers/twitter.ts +1 -0
- package/src/social-providers/vercel.ts +8 -1
- package/src/social-providers/vk.ts +8 -1
- package/src/social-providers/wechat.ts +9 -1
- package/src/social-providers/zoom.ts +15 -19
- package/src/types/context.ts +25 -4
- package/src/types/init-options.ts +29 -5
- package/src/utils/ip.ts +12 -13
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +28 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createAuthorizationURL,
|
|
5
|
+
refreshAccessToken,
|
|
6
|
+
validateAuthorizationCode,
|
|
7
|
+
} from "../oauth2";
|
|
4
8
|
|
|
5
9
|
export interface SlackProfile extends Record<string, any> {
|
|
6
10
|
ok: boolean;
|
|
@@ -42,19 +46,21 @@ export const slack = (options: SlackOptions) => {
|
|
|
42
46
|
return {
|
|
43
47
|
id: "slack",
|
|
44
48
|
name: "Slack",
|
|
45
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
49
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
46
50
|
const _scopes = options.disableDefaultScope
|
|
47
51
|
? []
|
|
48
52
|
: ["openid", "profile", "email"];
|
|
49
53
|
if (scopes) _scopes.push(...scopes);
|
|
50
54
|
if (options.scope) _scopes.push(...options.scope);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
return createAuthorizationURL({
|
|
56
|
+
id: "slack",
|
|
57
|
+
options,
|
|
58
|
+
authorizationEndpoint: "https://slack.com/openid/connect/authorize",
|
|
59
|
+
scopes: _scopes,
|
|
60
|
+
state,
|
|
61
|
+
redirectURI,
|
|
62
|
+
additionalParams,
|
|
63
|
+
});
|
|
58
64
|
},
|
|
59
65
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
60
66
|
return validateAuthorizationCode({
|
|
@@ -24,7 +24,13 @@ export const spotify = (options: SpotifyOptions) => {
|
|
|
24
24
|
return {
|
|
25
25
|
id: "spotify",
|
|
26
26
|
name: "Spotify",
|
|
27
|
-
createAuthorizationURL({
|
|
27
|
+
createAuthorizationURL({
|
|
28
|
+
state,
|
|
29
|
+
scopes,
|
|
30
|
+
codeVerifier,
|
|
31
|
+
redirectURI,
|
|
32
|
+
additionalParams,
|
|
33
|
+
}) {
|
|
28
34
|
const _scopes = options.disableDefaultScope ? [] : ["user-read-email"];
|
|
29
35
|
if (options.scope) _scopes.push(...options.scope);
|
|
30
36
|
if (scopes) _scopes.push(...scopes);
|
|
@@ -36,6 +42,7 @@ export const spotify = (options: SpotifyOptions) => {
|
|
|
36
42
|
state,
|
|
37
43
|
codeVerifier,
|
|
38
44
|
redirectURI,
|
|
45
|
+
additionalParams,
|
|
39
46
|
});
|
|
40
47
|
},
|
|
41
48
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
RESERVED_AUTHORIZATION_PARAMS_SET,
|
|
5
|
+
refreshAccessToken,
|
|
6
|
+
validateAuthorizationCode,
|
|
7
|
+
} from "../oauth2";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)
|
|
@@ -131,17 +135,26 @@ export const tiktok = (options: TiktokOptions) => {
|
|
|
131
135
|
return {
|
|
132
136
|
id: "tiktok",
|
|
133
137
|
name: "TikTok",
|
|
134
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
138
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
135
139
|
const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
|
|
136
140
|
if (options.scope) _scopes.push(...options.scope);
|
|
137
141
|
if (scopes) _scopes.push(...scopes);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
142
|
+
// TikTok uses `client_key` instead of the standard `client_id`, so the
|
|
143
|
+
// shared createAuthorizationURL helper cannot be used directly.
|
|
144
|
+
const url = new URL("https://www.tiktok.com/v2/auth/authorize");
|
|
145
|
+
url.searchParams.set("scope", _scopes.join(","));
|
|
146
|
+
url.searchParams.set("response_type", "code");
|
|
147
|
+
url.searchParams.set("client_key", options.clientKey);
|
|
148
|
+
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
|
|
149
|
+
url.searchParams.set("state", state);
|
|
150
|
+
if (additionalParams) {
|
|
151
|
+
for (const [key, value] of Object.entries(additionalParams)) {
|
|
152
|
+
if (RESERVED_AUTHORIZATION_PARAMS_SET.has(key)) continue;
|
|
153
|
+
if (key === "client_key") continue;
|
|
154
|
+
url.searchParams.set(key, value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return url;
|
|
145
158
|
},
|
|
146
159
|
|
|
147
160
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -42,7 +42,7 @@ export const twitch = (options: TwitchOptions) => {
|
|
|
42
42
|
return {
|
|
43
43
|
id: "twitch",
|
|
44
44
|
name: "Twitch",
|
|
45
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
45
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
46
46
|
const _scopes = options.disableDefaultScope
|
|
47
47
|
? []
|
|
48
48
|
: ["user:read:email", "openid"];
|
|
@@ -61,6 +61,7 @@ export const twitch = (options: TwitchOptions) => {
|
|
|
61
61
|
"preferred_username",
|
|
62
62
|
"picture",
|
|
63
63
|
],
|
|
64
|
+
additionalParams,
|
|
64
65
|
});
|
|
65
66
|
},
|
|
66
67
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -122,6 +122,7 @@ export const twitter = (options: TwitterOption) => {
|
|
|
122
122
|
state: data.state,
|
|
123
123
|
codeVerifier: data.codeVerifier,
|
|
124
124
|
redirectURI: data.redirectURI,
|
|
125
|
+
additionalParams: data.additionalParams,
|
|
125
126
|
});
|
|
126
127
|
},
|
|
127
128
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -20,7 +20,13 @@ export const vercel = (options: VercelOptions) => {
|
|
|
20
20
|
return {
|
|
21
21
|
id: "vercel",
|
|
22
22
|
name: "Vercel",
|
|
23
|
-
createAuthorizationURL({
|
|
23
|
+
createAuthorizationURL({
|
|
24
|
+
state,
|
|
25
|
+
scopes,
|
|
26
|
+
codeVerifier,
|
|
27
|
+
redirectURI,
|
|
28
|
+
additionalParams,
|
|
29
|
+
}) {
|
|
24
30
|
if (!codeVerifier) {
|
|
25
31
|
throw new BetterAuthError("codeVerifier is required for Vercel");
|
|
26
32
|
}
|
|
@@ -40,6 +46,7 @@ export const vercel = (options: VercelOptions) => {
|
|
|
40
46
|
state,
|
|
41
47
|
codeVerifier,
|
|
42
48
|
redirectURI,
|
|
49
|
+
additionalParams,
|
|
43
50
|
});
|
|
44
51
|
},
|
|
45
52
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -30,7 +30,13 @@ export const vk = (options: VkOption) => {
|
|
|
30
30
|
return {
|
|
31
31
|
id: "vk",
|
|
32
32
|
name: "VK",
|
|
33
|
-
async createAuthorizationURL({
|
|
33
|
+
async createAuthorizationURL({
|
|
34
|
+
state,
|
|
35
|
+
scopes,
|
|
36
|
+
codeVerifier,
|
|
37
|
+
redirectURI,
|
|
38
|
+
additionalParams,
|
|
39
|
+
}) {
|
|
34
40
|
const _scopes = options.disableDefaultScope ? [] : ["email", "phone"];
|
|
35
41
|
if (options.scope) _scopes.push(...options.scope);
|
|
36
42
|
if (scopes) _scopes.push(...scopes);
|
|
@@ -44,6 +50,7 @@ export const vk = (options: VkOption) => {
|
|
|
44
50
|
state,
|
|
45
51
|
redirectURI,
|
|
46
52
|
codeVerifier,
|
|
53
|
+
additionalParams,
|
|
47
54
|
});
|
|
48
55
|
},
|
|
49
56
|
validateAuthorizationCode: async ({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import type { OAuth2Tokens, OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
|
+
import { RESERVED_AUTHORIZATION_PARAMS_SET } from "../oauth2";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* WeChat user profile information
|
|
@@ -58,7 +59,7 @@ export const wechat = (options: WeChatOptions) => {
|
|
|
58
59
|
return {
|
|
59
60
|
id: "wechat",
|
|
60
61
|
name: "WeChat",
|
|
61
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
62
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
62
63
|
const _scopes = options.disableDefaultScope ? [] : ["snsapi_login"];
|
|
63
64
|
options.scope && _scopes.push(...options.scope);
|
|
64
65
|
scopes && _scopes.push(...scopes);
|
|
@@ -72,6 +73,13 @@ export const wechat = (options: WeChatOptions) => {
|
|
|
72
73
|
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
|
|
73
74
|
url.searchParams.set("state", state);
|
|
74
75
|
url.searchParams.set("lang", options.lang || "cn");
|
|
76
|
+
if (additionalParams) {
|
|
77
|
+
for (const [key, value] of Object.entries(additionalParams)) {
|
|
78
|
+
if (RESERVED_AUTHORIZATION_PARAMS_SET.has(key)) continue;
|
|
79
|
+
if (key === "appid") continue;
|
|
80
|
+
url.searchParams.set(key, value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
75
83
|
url.hash = "wechat_redirect";
|
|
76
84
|
|
|
77
85
|
return url;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
6
|
validateAuthorizationCode,
|
|
7
7
|
} from "../oauth2";
|
|
@@ -152,25 +152,21 @@ export const zoom = (userOptions: ZoomOptions) => {
|
|
|
152
152
|
return {
|
|
153
153
|
id: "zoom",
|
|
154
154
|
name: "Zoom",
|
|
155
|
-
createAuthorizationURL: async ({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
createAuthorizationURL: async ({
|
|
156
|
+
state,
|
|
157
|
+
redirectURI,
|
|
158
|
+
codeVerifier,
|
|
159
|
+
additionalParams,
|
|
160
|
+
}) =>
|
|
161
|
+
createAuthorizationURL({
|
|
162
|
+
id: "zoom",
|
|
163
|
+
options,
|
|
164
|
+
authorizationEndpoint: "https://zoom.us/oauth/authorize",
|
|
160
165
|
state,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
params.set("code_challenge_method", "S256");
|
|
166
|
-
params.set("code_challenge", codeChallenge);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const url = new URL("https://zoom.us/oauth/authorize");
|
|
170
|
-
url.search = params.toString();
|
|
171
|
-
|
|
172
|
-
return url;
|
|
173
|
-
},
|
|
166
|
+
redirectURI,
|
|
167
|
+
codeVerifier: options.pkce ? codeVerifier : undefined,
|
|
168
|
+
additionalParams,
|
|
169
|
+
}),
|
|
174
170
|
validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
|
|
175
171
|
return validateAuthorizationCode({
|
|
176
172
|
code,
|
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,
|
|
@@ -216,6 +222,21 @@ export interface InternalAdapter<
|
|
|
216
222
|
|
|
217
223
|
deleteVerificationByIdentifier(identifier: string): Promise<void>;
|
|
218
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Atomically consume a single-use verification row by `identifier` and
|
|
227
|
+
* return it. Only the first concurrent caller receives the latest row;
|
|
228
|
+
* subsequent callers receive `null`. Consuming one row invalidates the
|
|
229
|
+
* whole identifier so stale rows cannot be replayed. Rows past their
|
|
230
|
+
* `expiresAt` are treated as already invalid: the row is deleted but
|
|
231
|
+
* `null` is returned, so callers do not need to gate on `expiresAt`
|
|
232
|
+
* themselves. Callers MUST gate any state change (issue session, mint
|
|
233
|
+
* token, change password) on a non-null result.
|
|
234
|
+
*
|
|
235
|
+
* Replaces the racy `findVerificationValue` + `deleteVerificationByIdentifier`
|
|
236
|
+
* pair at single-use credential consumption sites.
|
|
237
|
+
*/
|
|
238
|
+
consumeVerificationValue(identifier: string): Promise<Verification | null>;
|
|
239
|
+
|
|
219
240
|
updateVerificationByIdentifier(
|
|
220
241
|
identifier: string,
|
|
221
242
|
data: Partial<Verification>,
|
|
@@ -306,7 +327,7 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
|
|
|
306
327
|
* - "cookie": Store state in an encrypted cookie (stateless)
|
|
307
328
|
* - "database": Store state in the database
|
|
308
329
|
*
|
|
309
|
-
* @default "cookie"
|
|
330
|
+
* @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
|
|
310
331
|
*/
|
|
311
332
|
storeStateStrategy: "database" | "cookie";
|
|
312
333
|
};
|
|
@@ -207,12 +207,13 @@ export type BetterAuthAdvancedOptions = {
|
|
|
207
207
|
*/
|
|
208
208
|
disableIpTracking?: boolean;
|
|
209
209
|
/**
|
|
210
|
-
* IPv6
|
|
211
|
-
*
|
|
210
|
+
* IPv6 prefix length used to collapse addresses before rate-limit keying.
|
|
211
|
+
* Any integer from 0 to 128 is accepted; common values are 32, 48, 56, 64, 128.
|
|
212
|
+
* Out-of-range values fall back to safe behavior (negative -> mask all, > 128 -> no mask).
|
|
212
213
|
*
|
|
213
214
|
* @default 64
|
|
214
215
|
*/
|
|
215
|
-
ipv6Subnet?:
|
|
216
|
+
ipv6Subnet?: number;
|
|
216
217
|
}
|
|
217
218
|
| undefined;
|
|
218
219
|
/**
|
|
@@ -1020,6 +1021,25 @@ export type BetterAuthOptions = {
|
|
|
1020
1021
|
* @default false
|
|
1021
1022
|
*/
|
|
1022
1023
|
disableImplicitLinking?: boolean;
|
|
1024
|
+
/**
|
|
1025
|
+
* Require the existing local user row to have
|
|
1026
|
+
* `emailVerified: true` before implicit account linking
|
|
1027
|
+
* uses the IdP's `email_verified` claim as ownership
|
|
1028
|
+
* proof. Defaults to `true` so an attacker who
|
|
1029
|
+
* pre-registers an unverified account at a victim's
|
|
1030
|
+
* email cannot have the victim's OAuth identity linked
|
|
1031
|
+
* into the attacker-owned row on first sign-in. Set to
|
|
1032
|
+
* `false` for backward compatibility on apps whose
|
|
1033
|
+
* users sign up via OAuth without verifying their email
|
|
1034
|
+
* locally; understand the takeover risk before doing
|
|
1035
|
+
* so.
|
|
1036
|
+
*
|
|
1037
|
+
* @default true
|
|
1038
|
+
*
|
|
1039
|
+
* @deprecated The option will be removed on the next
|
|
1040
|
+
* minor; the gate will become unconditional.
|
|
1041
|
+
*/
|
|
1042
|
+
requireLocalEmailVerified?: boolean;
|
|
1023
1043
|
/**
|
|
1024
1044
|
* List of trusted providers. Can be a static array or a function
|
|
1025
1045
|
* that returns providers dynamically. The function is called
|
|
@@ -1073,7 +1093,11 @@ export type BetterAuthOptions = {
|
|
|
1073
1093
|
*/
|
|
1074
1094
|
allowUnlinkingAll?: boolean;
|
|
1075
1095
|
/**
|
|
1076
|
-
*
|
|
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.
|
|
1077
1101
|
*
|
|
1078
1102
|
* @default false
|
|
1079
1103
|
*/
|
|
@@ -1106,7 +1130,7 @@ export type BetterAuthOptions = {
|
|
|
1106
1130
|
* - "cookie": Store state in an encrypted cookie (stateless)
|
|
1107
1131
|
* - "database": Store state in the database
|
|
1108
1132
|
*
|
|
1109
|
-
* @default "cookie"
|
|
1133
|
+
* @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
|
|
1110
1134
|
*/
|
|
1111
1135
|
storeStateStrategy?: "database" | "cookie";
|
|
1112
1136
|
/**
|
package/src/utils/ip.ts
CHANGED
|
@@ -12,12 +12,13 @@ import * as z from "zod";
|
|
|
12
12
|
|
|
13
13
|
interface NormalizeIPOptions {
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* Common values: 32, 48, 64, 128
|
|
15
|
+
* Prefix length used to collapse IPv6 addresses before keying.
|
|
16
|
+
* Any integer from 0 to 128 is accepted. Common values: 32, 48, 56, 64, 128.
|
|
17
|
+
* Values outside 0-128 are clamped.
|
|
17
18
|
*
|
|
18
|
-
* @default
|
|
19
|
+
* @default 64
|
|
19
20
|
*/
|
|
20
|
-
ipv6Subnet?:
|
|
21
|
+
ipv6Subnet?: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -117,15 +118,13 @@ function expandIPv6(ipv6: string): string[] {
|
|
|
117
118
|
* Normalizes an IPv6 address to canonical form
|
|
118
119
|
* e.g., "2001:DB8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
|
|
119
120
|
*/
|
|
120
|
-
function normalizeIPv6(
|
|
121
|
-
ipv6: string,
|
|
122
|
-
subnetPrefix?: 128 | 32 | 48 | 64,
|
|
123
|
-
): string {
|
|
121
|
+
function normalizeIPv6(ipv6: string, subnetPrefix?: number): string {
|
|
124
122
|
const groups = expandIPv6(ipv6);
|
|
125
123
|
|
|
126
|
-
if (subnetPrefix && subnetPrefix < 128) {
|
|
127
|
-
//
|
|
128
|
-
|
|
124
|
+
if (subnetPrefix !== undefined && subnetPrefix < 128) {
|
|
125
|
+
// Clamp to a valid bit range so out-of-spec inputs degrade safely:
|
|
126
|
+
// negative or fractional values would otherwise produce malformed masks.
|
|
127
|
+
const prefix = Math.max(0, Math.floor(subnetPrefix));
|
|
129
128
|
let bitsRemaining: number = prefix;
|
|
130
129
|
|
|
131
130
|
const maskedGroups = groups.map((group) => {
|
|
@@ -191,8 +190,8 @@ export function normalizeIP(
|
|
|
191
190
|
return ipv4.toLowerCase();
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
// Normalize IPv6
|
|
195
|
-
const subnetPrefix = options.ipv6Subnet
|
|
193
|
+
// Normalize IPv6. Use ?? so an explicit 0 (mask-all) is honoured.
|
|
194
|
+
const subnetPrefix = options.ipv6Subnet ?? 64;
|
|
196
195
|
return normalizeIPv6(ip, subnetPrefix);
|
|
197
196
|
}
|
|
198
197
|
|
|
@@ -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/string.ts
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
1
|
export function capitalizeFirstLetter(str: string) {
|
|
2
2
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
const WORD_PATTERN =
|
|
6
|
+
/[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
|
|
7
|
+
const APOSTROPHE_PATTERN = /['\u2019]/g;
|
|
8
|
+
|
|
9
|
+
function splitWords(input: string): string[] {
|
|
10
|
+
return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toSnakeCase(input: string): string {
|
|
14
|
+
return splitWords(input)
|
|
15
|
+
.map((word) => word.toLowerCase())
|
|
16
|
+
.join("_");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toKebabCase(input: string): string {
|
|
20
|
+
return splitWords(input)
|
|
21
|
+
.map((word) => word.toLowerCase())
|
|
22
|
+
.join("-");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toCamelCase(input: string): string {
|
|
26
|
+
return splitWords(input).reduce((acc, word, i) => {
|
|
27
|
+
return (
|
|
28
|
+
acc +
|
|
29
|
+
(i === 0
|
|
30
|
+
? word.toLowerCase()
|
|
31
|
+
: `${word[0]!.toUpperCase()}${word.slice(1)}`)
|
|
32
|
+
);
|
|
33
|
+
}, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toPascalCase(input: string): string {
|
|
37
|
+
return splitWords(input)
|
|
38
|
+
.map((word) => `${word[0]!.toUpperCase()}${word.slice(1).toLowerCase()}`)
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
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
|
+
}
|