@better-auth/core 1.7.0-beta.2 → 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 +64 -3
- 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 +5 -12
- 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 +5 -3
- 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 +5 -3
- package/dist/social-providers/facebook.mjs +6 -3
- package/dist/social-providers/figma.d.mts +3 -1
- package/dist/social-providers/figma.mjs +3 -2
- package/dist/social-providers/github.d.mts +4 -2
- 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 +5 -3
- package/dist/social-providers/linkedin.mjs +4 -3
- package/dist/social-providers/microsoft-entra-id.d.mts +3 -2
- 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 +30 -4
- 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 +121 -3
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +2 -0
- package/src/db/schema/user.ts +3 -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 +27 -3
- package/src/social-providers/atlassian.ts +8 -1
- package/src/social-providers/cognito.ts +26 -1
- package/src/social-providers/discord.ts +22 -18
- package/src/social-providers/dropbox.ts +7 -5
- package/src/social-providers/facebook.ts +14 -9
- package/src/social-providers/figma.ts +8 -1
- package/src/social-providers/github.ts +5 -3
- 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 +5 -3
- package/src/social-providers/microsoft-entra-id.ts +2 -1
- 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 +33 -5
- 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
|
@@ -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
|
@@ -151,9 +151,22 @@ export interface InternalAdapter<
|
|
|
151
151
|
|
|
152
152
|
deleteAccounts(userId: string): Promise<void>;
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Delete an account by its primary key.
|
|
156
|
+
*
|
|
157
|
+
* @param id - The account row's primary key (the `id` column, not the `accountId` column).
|
|
158
|
+
*/
|
|
159
|
+
deleteAccount(id: string): Promise<void>;
|
|
155
160
|
|
|
156
|
-
|
|
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>;
|
|
157
170
|
|
|
158
171
|
findOAuthUser(
|
|
159
172
|
email: string,
|
|
@@ -191,8 +204,6 @@ export interface InternalAdapter<
|
|
|
191
204
|
|
|
192
205
|
findAccounts(userId: string): Promise<Account[]>;
|
|
193
206
|
|
|
194
|
-
findAccount(accountId: string): Promise<Account | null>;
|
|
195
|
-
|
|
196
207
|
findAccountByProviderId(
|
|
197
208
|
accountId: string,
|
|
198
209
|
providerId: string,
|
|
@@ -211,10 +222,27 @@ export interface InternalAdapter<
|
|
|
211
222
|
|
|
212
223
|
deleteVerificationByIdentifier(identifier: string): Promise<void>;
|
|
213
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
|
+
|
|
214
240
|
updateVerificationByIdentifier(
|
|
215
241
|
identifier: string,
|
|
216
242
|
data: Partial<Verification>,
|
|
217
243
|
): Promise<Verification>;
|
|
244
|
+
|
|
245
|
+
refreshUserSessions(user: User): Promise<void>;
|
|
218
246
|
}
|
|
219
247
|
|
|
220
248
|
type CreateCookieGetterFn = (
|
|
@@ -299,7 +327,7 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
|
|
|
299
327
|
* - "cookie": Store state in an encrypted cookie (stateless)
|
|
300
328
|
* - "database": Store state in the database
|
|
301
329
|
*
|
|
302
|
-
* @default "cookie"
|
|
330
|
+
* @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
|
|
303
331
|
*/
|
|
304
332
|
storeStateStrategy: "database" | "cookie";
|
|
305
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
|
+
}
|