@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5
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/api/index.d.mts +3 -3
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +62 -0
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +1 -1
- package/dist/db/schema/account.mjs +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +6 -0
- package/dist/error/codes.mjs +6 -0
- package/dist/index.d.mts +2 -2
- 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 +13 -2
- package/dist/oauth2/create-authorization-url.mjs +28 -7
- package/dist/oauth2/index.d.mts +13 -8
- package/dist/oauth2/index.mjs +11 -7
- package/dist/oauth2/oauth-provider.d.mts +149 -11
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +20 -33
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -0
- 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 +14 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify-id-token.d.mts +26 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +14 -0
- package/dist/oauth2/verify.mjs +38 -12
- package/dist/social-providers/apple.d.mts +18 -20
- package/dist/social-providers/apple.mjs +15 -28
- package/dist/social-providers/atlassian.d.mts +8 -2
- package/dist/social-providers/atlassian.mjs +9 -6
- package/dist/social-providers/cognito.d.mts +29 -3
- package/dist/social-providers/cognito.mjs +30 -34
- package/dist/social-providers/discord.d.mts +8 -2
- package/dist/social-providers/discord.mjs +20 -6
- package/dist/social-providers/dropbox.d.mts +8 -2
- package/dist/social-providers/dropbox.mjs +10 -9
- package/dist/social-providers/facebook.d.mts +24 -3
- package/dist/social-providers/facebook.mjs +51 -24
- package/dist/social-providers/figma.d.mts +8 -2
- package/dist/social-providers/figma.mjs +8 -7
- package/dist/social-providers/github.d.mts +8 -2
- package/dist/social-providers/github.mjs +9 -8
- package/dist/social-providers/gitlab.d.mts +8 -2
- package/dist/social-providers/gitlab.mjs +8 -7
- package/dist/social-providers/google.d.mts +32 -4
- package/dist/social-providers/google.mjs +26 -29
- package/dist/social-providers/huggingface.d.mts +8 -2
- package/dist/social-providers/huggingface.mjs +11 -10
- package/dist/social-providers/index.d.mts +322 -75
- package/dist/social-providers/kakao.d.mts +8 -2
- package/dist/social-providers/kakao.mjs +11 -10
- package/dist/social-providers/kick.d.mts +8 -2
- package/dist/social-providers/kick.mjs +7 -6
- package/dist/social-providers/line.d.mts +11 -3
- package/dist/social-providers/line.mjs +14 -15
- package/dist/social-providers/linear.d.mts +8 -2
- package/dist/social-providers/linear.mjs +7 -6
- package/dist/social-providers/linkedin.d.mts +8 -2
- package/dist/social-providers/linkedin.mjs +12 -11
- package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
- package/dist/social-providers/microsoft-entra-id.mjs +28 -38
- package/dist/social-providers/naver.d.mts +8 -2
- package/dist/social-providers/naver.mjs +7 -6
- package/dist/social-providers/notion.d.mts +8 -2
- package/dist/social-providers/notion.mjs +9 -6
- package/dist/social-providers/paybin.d.mts +8 -2
- package/dist/social-providers/paybin.mjs +12 -11
- package/dist/social-providers/paypal.d.mts +8 -3
- package/dist/social-providers/paypal.mjs +10 -14
- package/dist/social-providers/polar.d.mts +8 -2
- package/dist/social-providers/polar.mjs +11 -10
- package/dist/social-providers/railway.d.mts +8 -2
- package/dist/social-providers/railway.mjs +11 -10
- package/dist/social-providers/reddit.d.mts +8 -2
- package/dist/social-providers/reddit.mjs +11 -9
- package/dist/social-providers/roblox.d.mts +8 -2
- package/dist/social-providers/roblox.mjs +15 -5
- package/dist/social-providers/salesforce.d.mts +8 -2
- package/dist/social-providers/salesforce.mjs +11 -10
- package/dist/social-providers/slack.d.mts +8 -2
- package/dist/social-providers/slack.mjs +18 -15
- package/dist/social-providers/spotify.d.mts +8 -2
- package/dist/social-providers/spotify.mjs +7 -6
- package/dist/social-providers/tiktok.d.mts +8 -2
- package/dist/social-providers/tiktok.mjs +21 -5
- package/dist/social-providers/twitch.d.mts +8 -2
- package/dist/social-providers/twitch.mjs +7 -6
- package/dist/social-providers/twitter.d.mts +7 -2
- package/dist/social-providers/twitter.mjs +11 -10
- package/dist/social-providers/vercel.d.mts +8 -2
- package/dist/social-providers/vercel.mjs +7 -9
- package/dist/social-providers/vk.d.mts +8 -2
- package/dist/social-providers/vk.mjs +7 -6
- package/dist/social-providers/wechat.d.mts +8 -2
- package/dist/social-providers/wechat.mjs +16 -6
- package/dist/social-providers/zoom.d.mts +10 -3
- package/dist/social-providers/zoom.mjs +14 -15
- package/dist/types/context.d.mts +33 -11
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +121 -6
- 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 +13 -12
- package/src/db/adapter/factory.ts +126 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/db/type.ts +12 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +6 -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 +30 -8
- package/src/oauth2/index.ts +42 -10
- package/src/oauth2/oauth-provider.ts +161 -12
- package/src/oauth2/refresh-access-token.ts +52 -78
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +21 -5
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +82 -15
- package/src/social-providers/apple.ts +32 -45
- package/src/social-providers/atlassian.ts +20 -9
- package/src/social-providers/cognito.ts +51 -48
- package/src/social-providers/discord.ts +37 -22
- package/src/social-providers/dropbox.ts +20 -12
- package/src/social-providers/facebook.ts +108 -57
- package/src/social-providers/figma.ts +21 -10
- package/src/social-providers/github.ts +16 -10
- package/src/social-providers/gitlab.ts +16 -8
- package/src/social-providers/google.ts +67 -46
- package/src/social-providers/huggingface.ts +20 -9
- package/src/social-providers/kakao.ts +18 -9
- package/src/social-providers/kick.ts +20 -8
- package/src/social-providers/line.ts +39 -37
- package/src/social-providers/linear.ts +20 -7
- package/src/social-providers/linkedin.ts +16 -10
- package/src/social-providers/microsoft-entra-id.ts +66 -64
- package/src/social-providers/naver.ts +14 -7
- package/src/social-providers/notion.ts +20 -7
- package/src/social-providers/paybin.ts +16 -11
- package/src/social-providers/paypal.ts +12 -25
- package/src/social-providers/polar.ts +20 -9
- package/src/social-providers/railway.ts +20 -9
- package/src/social-providers/reddit.ts +22 -10
- package/src/social-providers/roblox.ts +31 -15
- package/src/social-providers/salesforce.ts +21 -10
- package/src/social-providers/slack.ts +31 -16
- package/src/social-providers/spotify.ts +20 -7
- package/src/social-providers/tiktok.ts +32 -13
- package/src/social-providers/twitch.ts +14 -9
- package/src/social-providers/twitter.ts +18 -8
- package/src/social-providers/vercel.ts +24 -11
- package/src/social-providers/vk.ts +20 -7
- package/src/social-providers/wechat.ts +28 -8
- package/src/social-providers/zoom.ts +28 -19
- package/src/types/context.ts +33 -12
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +148 -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
|
@@ -47,6 +47,98 @@ export type GenerateIdFn = (options: {
|
|
|
47
47
|
size?: number | undefined;
|
|
48
48
|
}) => string | false;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* What Better Auth is about to do with an incoming identity when
|
|
52
|
+
* {@link BetterAuthOptions.user}'s `validateUserInfo` runs.
|
|
53
|
+
*
|
|
54
|
+
* - `create-user`: a brand-new user record is about to be created.
|
|
55
|
+
* - `link-account`: a new provider account is about to be linked to an
|
|
56
|
+
* already-existing user.
|
|
57
|
+
* - `sign-in`: an existing OAuth or SSO user is signing in again. This is the
|
|
58
|
+
* one case where the provider can assert *changed* data, so the hook receives
|
|
59
|
+
* the fresh provider email and profile (not the stored row), letting a domain
|
|
60
|
+
* or org policy reject a user whose provider identity moved out of bounds.
|
|
61
|
+
*
|
|
62
|
+
* Non-provider returning sign-ins are not re-validated: they carry only the
|
|
63
|
+
* stored row, which has not changed since `create-user` gated it. Use the admin
|
|
64
|
+
* plugin's ban controls or a `databaseHooks.session.create.before` hook to
|
|
65
|
+
* block those.
|
|
66
|
+
*/
|
|
67
|
+
export type ValidateUserInfoAction = "create-user" | "link-account" | "sign-in";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The authentication method that produced the incoming user info. The named
|
|
71
|
+
* methods cover Better Auth's built-ins; the open `string` keeps it extensible
|
|
72
|
+
* for plugins (for example `"scim"`).
|
|
73
|
+
*/
|
|
74
|
+
export type ValidateUserInfoMethod =
|
|
75
|
+
| "oauth"
|
|
76
|
+
| "sso-oidc"
|
|
77
|
+
| "sso-saml"
|
|
78
|
+
| "email-password"
|
|
79
|
+
| "magic-link"
|
|
80
|
+
| "email-otp"
|
|
81
|
+
| "anonymous"
|
|
82
|
+
| "siwe"
|
|
83
|
+
| "phone-number"
|
|
84
|
+
| "admin"
|
|
85
|
+
| (string & {});
|
|
86
|
+
|
|
87
|
+
/** OAuth-specific provisioning context; present only when `method` is `"oauth"`. */
|
|
88
|
+
export type ValidateUserInfoOAuthInfo = {
|
|
89
|
+
/** The social or generic OAuth provider id (e.g. `"google"`). */
|
|
90
|
+
providerId: string;
|
|
91
|
+
/** The raw provider profile (userinfo or id-token claims), unmapped. */
|
|
92
|
+
profile?: Record<string, unknown> | undefined;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** SSO-specific provisioning context; present for OIDC and SAML SSO methods. */
|
|
96
|
+
export type ValidateUserInfoSSOInfo = {
|
|
97
|
+
/** The configured SSO provider id. */
|
|
98
|
+
providerId: string;
|
|
99
|
+
/** The raw OIDC claims or SAML assertion attributes, unmapped. */
|
|
100
|
+
profile?: Record<string, unknown> | undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Provisioning origin passed to `createUser`; the creation seam adds `action: "create-user"` to build {@link ValidateUserInfoSource}. */
|
|
104
|
+
export type UserProvisioningSource = {
|
|
105
|
+
method: ValidateUserInfoMethod;
|
|
106
|
+
/** Provider id and raw profile; present iff `method` is `"oauth"`. */
|
|
107
|
+
oauth?: ValidateUserInfoOAuthInfo | undefined;
|
|
108
|
+
/** Provider id and raw profile; present iff `method` is `"sso-oidc"` or `"sso-saml"`. */
|
|
109
|
+
sso?: ValidateUserInfoSSOInfo | undefined;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The context passed to `validateUserInfo`: the lifecycle
|
|
114
|
+
* {@link ValidateUserInfoAction}, the {@link ValidateUserInfoMethod}, and (for
|
|
115
|
+
* OAuth/SSO provider methods) protocol-specific provider metadata.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* // Scope to one OAuth provider:
|
|
119
|
+
* if (source.oauth?.providerId !== "google") return;
|
|
120
|
+
* // Branch on the method:
|
|
121
|
+
* if (source.method === "anonymous") return { error: "no_anonymous" };
|
|
122
|
+
* // Inspect SSO claims:
|
|
123
|
+
* if (source.method === "sso-saml" && source.sso?.profile?.department !== "eng") {
|
|
124
|
+
* return { error: "invalid_department" };
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export type ValidateUserInfoSource = UserProvisioningSource & {
|
|
129
|
+
action: ValidateUserInfoAction;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type ValidateUserInfoResult = {
|
|
133
|
+
/** A short, machine-readable rejection code, surfaced to the client. */
|
|
134
|
+
error: string;
|
|
135
|
+
/**
|
|
136
|
+
* A human-readable reason, surfaced to the client. Do not put sensitive
|
|
137
|
+
* details here.
|
|
138
|
+
*/
|
|
139
|
+
errorDescription?: string | undefined;
|
|
140
|
+
};
|
|
141
|
+
|
|
50
142
|
/**
|
|
51
143
|
* Configuration for dynamic base URL resolution.
|
|
52
144
|
* Allows Better Auth to work with multiple domains (e.g., Vercel preview deployments).
|
|
@@ -207,12 +299,13 @@ export type BetterAuthAdvancedOptions = {
|
|
|
207
299
|
*/
|
|
208
300
|
disableIpTracking?: boolean;
|
|
209
301
|
/**
|
|
210
|
-
* IPv6
|
|
211
|
-
*
|
|
302
|
+
* IPv6 prefix length used to collapse addresses before rate-limit keying.
|
|
303
|
+
* Any integer from 0 to 128 is accepted; common values are 32, 48, 56, 64, 128.
|
|
304
|
+
* Out-of-range values fall back to safe behavior (negative -> mask all, > 128 -> no mask).
|
|
212
305
|
*
|
|
213
306
|
* @default 64
|
|
214
307
|
*/
|
|
215
|
-
ipv6Subnet?:
|
|
308
|
+
ipv6Subnet?: number;
|
|
216
309
|
}
|
|
217
310
|
| undefined;
|
|
218
311
|
/**
|
|
@@ -776,6 +869,33 @@ export type BetterAuthOptions = {
|
|
|
776
869
|
*/
|
|
777
870
|
user?:
|
|
778
871
|
| (BetterAuthDBOptions<"user", keyof BaseUser> & {
|
|
872
|
+
/**
|
|
873
|
+
* Gate which identities Better Auth admits. Called just before
|
|
874
|
+
* `create-user`, `link-account`, and (for OAuth) `sign-in`, across
|
|
875
|
+
* every authentication method, including stateless setups with no
|
|
876
|
+
* persistent database. On `sign-in` the hook receives the *fresh*
|
|
877
|
+
* provider email and profile, so a domain policy can reject a user
|
|
878
|
+
* whose provider identity moved out of bounds.
|
|
879
|
+
*
|
|
880
|
+
* Non-provider returning sign-ins are not re-validated; use the admin
|
|
881
|
+
* plugin's ban controls or a `databaseHooks.session.create.before`
|
|
882
|
+
* hook for those.
|
|
883
|
+
*
|
|
884
|
+
* Return nothing to allow; return `{ error }` to reject. Browser flows
|
|
885
|
+
* redirect to the configured error URL; programmatic flows surface a
|
|
886
|
+
* `403`.
|
|
887
|
+
*
|
|
888
|
+
* TODO: rename to `validateUser` (and the `ValidateUserInfo*` types).
|
|
889
|
+
* "UserInfo" is the OIDC term and misleads for the email/password,
|
|
890
|
+
* SIWE, phone, and admin methods.
|
|
891
|
+
*/
|
|
892
|
+
validateUserInfo?: (
|
|
893
|
+
data: {
|
|
894
|
+
user: Partial<User> & Record<string, unknown>;
|
|
895
|
+
source: ValidateUserInfoSource;
|
|
896
|
+
},
|
|
897
|
+
context: GenericEndpointContext,
|
|
898
|
+
) => Awaitable<void | ValidateUserInfoResult>;
|
|
779
899
|
/**
|
|
780
900
|
* Changing email configuration
|
|
781
901
|
*/
|
|
@@ -1020,6 +1140,25 @@ export type BetterAuthOptions = {
|
|
|
1020
1140
|
* @default false
|
|
1021
1141
|
*/
|
|
1022
1142
|
disableImplicitLinking?: boolean;
|
|
1143
|
+
/**
|
|
1144
|
+
* Require the existing local user row to have
|
|
1145
|
+
* `emailVerified: true` before implicit account linking
|
|
1146
|
+
* uses the IdP's `email_verified` claim as ownership
|
|
1147
|
+
* proof. Defaults to `true` so an attacker who
|
|
1148
|
+
* pre-registers an unverified account at a victim's
|
|
1149
|
+
* email cannot have the victim's OAuth identity linked
|
|
1150
|
+
* into the attacker-owned row on first sign-in. Set to
|
|
1151
|
+
* `false` for backward compatibility on apps whose
|
|
1152
|
+
* users sign up via OAuth without verifying their email
|
|
1153
|
+
* locally; understand the takeover risk before doing
|
|
1154
|
+
* so.
|
|
1155
|
+
*
|
|
1156
|
+
* @default true
|
|
1157
|
+
*
|
|
1158
|
+
* @deprecated The option will be removed on the next
|
|
1159
|
+
* minor; the gate will become unconditional.
|
|
1160
|
+
*/
|
|
1161
|
+
requireLocalEmailVerified?: boolean;
|
|
1023
1162
|
/**
|
|
1024
1163
|
* List of trusted providers. Can be a static array or a function
|
|
1025
1164
|
* that returns providers dynamically. The function is called
|
|
@@ -1073,7 +1212,11 @@ export type BetterAuthOptions = {
|
|
|
1073
1212
|
*/
|
|
1074
1213
|
allowUnlinkingAll?: boolean;
|
|
1075
1214
|
/**
|
|
1076
|
-
*
|
|
1215
|
+
* When enabled, linking an account copies the provider's profile onto
|
|
1216
|
+
* the local user, matching the fields persisted on sign-up (`name`,
|
|
1217
|
+
* `image`, and any `mapProfileToUser` fields). The local `email` and
|
|
1218
|
+
* `emailVerified` are never changed, so a link cannot rebind the
|
|
1219
|
+
* account's identity.
|
|
1077
1220
|
*
|
|
1078
1221
|
* @default false
|
|
1079
1222
|
*/
|
|
@@ -1106,7 +1249,7 @@ export type BetterAuthOptions = {
|
|
|
1106
1249
|
* - "cookie": Store state in an encrypted cookie (stateless)
|
|
1107
1250
|
* - "database": Store state in the database
|
|
1108
1251
|
*
|
|
1109
|
-
* @default "cookie"
|
|
1252
|
+
* @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
|
|
1110
1253
|
*/
|
|
1111
1254
|
storeStateStrategy?: "database" | "cookie";
|
|
1112
1255
|
/**
|
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
|
+
}
|