@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.
Files changed (188) hide show
  1. package/dist/api/index.d.mts +3 -3
  2. package/dist/context/global.mjs +1 -1
  3. package/dist/db/adapter/factory.mjs +62 -0
  4. package/dist/db/adapter/index.d.mts +35 -1
  5. package/dist/db/adapter/types.d.mts +1 -1
  6. package/dist/db/get-tables.mjs +3 -3
  7. package/dist/db/schema/account.d.mts +1 -1
  8. package/dist/db/schema/account.mjs +1 -1
  9. package/dist/db/type.d.mts +12 -0
  10. package/dist/env/env-impl.mjs +1 -1
  11. package/dist/error/codes.d.mts +6 -0
  12. package/dist/error/codes.mjs +6 -0
  13. package/dist/index.d.mts +2 -2
  14. package/dist/instrumentation/tracer.mjs +1 -1
  15. package/dist/oauth2/authorization-params.d.mts +12 -0
  16. package/dist/oauth2/authorization-params.mjs +12 -0
  17. package/dist/oauth2/basic-credentials.d.mts +30 -0
  18. package/dist/oauth2/basic-credentials.mjs +64 -0
  19. package/dist/oauth2/client-assertion.d.mts +38 -22
  20. package/dist/oauth2/client-assertion.mjs +63 -28
  21. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  22. package/dist/oauth2/client-credentials-token.mjs +18 -29
  23. package/dist/oauth2/create-authorization-url.d.mts +13 -2
  24. package/dist/oauth2/create-authorization-url.mjs +28 -7
  25. package/dist/oauth2/index.d.mts +13 -8
  26. package/dist/oauth2/index.mjs +11 -7
  27. package/dist/oauth2/oauth-provider.d.mts +149 -11
  28. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  29. package/dist/oauth2/refresh-access-token.mjs +20 -33
  30. package/dist/oauth2/scopes.d.mts +76 -0
  31. package/dist/oauth2/scopes.mjs +96 -0
  32. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  33. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  34. package/dist/oauth2/utils.d.mts +9 -1
  35. package/dist/oauth2/utils.mjs +14 -2
  36. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  37. package/dist/oauth2/validate-authorization-code.mjs +17 -30
  38. package/dist/oauth2/verify-id-token.d.mts +26 -0
  39. package/dist/oauth2/verify-id-token.mjs +62 -0
  40. package/dist/oauth2/verify.d.mts +14 -0
  41. package/dist/oauth2/verify.mjs +38 -12
  42. package/dist/social-providers/apple.d.mts +18 -20
  43. package/dist/social-providers/apple.mjs +15 -28
  44. package/dist/social-providers/atlassian.d.mts +8 -2
  45. package/dist/social-providers/atlassian.mjs +9 -6
  46. package/dist/social-providers/cognito.d.mts +29 -3
  47. package/dist/social-providers/cognito.mjs +30 -34
  48. package/dist/social-providers/discord.d.mts +8 -2
  49. package/dist/social-providers/discord.mjs +20 -6
  50. package/dist/social-providers/dropbox.d.mts +8 -2
  51. package/dist/social-providers/dropbox.mjs +10 -9
  52. package/dist/social-providers/facebook.d.mts +24 -3
  53. package/dist/social-providers/facebook.mjs +51 -24
  54. package/dist/social-providers/figma.d.mts +8 -2
  55. package/dist/social-providers/figma.mjs +8 -7
  56. package/dist/social-providers/github.d.mts +8 -2
  57. package/dist/social-providers/github.mjs +9 -8
  58. package/dist/social-providers/gitlab.d.mts +8 -2
  59. package/dist/social-providers/gitlab.mjs +8 -7
  60. package/dist/social-providers/google.d.mts +32 -4
  61. package/dist/social-providers/google.mjs +26 -29
  62. package/dist/social-providers/huggingface.d.mts +8 -2
  63. package/dist/social-providers/huggingface.mjs +11 -10
  64. package/dist/social-providers/index.d.mts +322 -75
  65. package/dist/social-providers/kakao.d.mts +8 -2
  66. package/dist/social-providers/kakao.mjs +11 -10
  67. package/dist/social-providers/kick.d.mts +8 -2
  68. package/dist/social-providers/kick.mjs +7 -6
  69. package/dist/social-providers/line.d.mts +11 -3
  70. package/dist/social-providers/line.mjs +14 -15
  71. package/dist/social-providers/linear.d.mts +8 -2
  72. package/dist/social-providers/linear.mjs +7 -6
  73. package/dist/social-providers/linkedin.d.mts +8 -2
  74. package/dist/social-providers/linkedin.mjs +12 -11
  75. package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
  76. package/dist/social-providers/microsoft-entra-id.mjs +28 -38
  77. package/dist/social-providers/naver.d.mts +8 -2
  78. package/dist/social-providers/naver.mjs +7 -6
  79. package/dist/social-providers/notion.d.mts +8 -2
  80. package/dist/social-providers/notion.mjs +9 -6
  81. package/dist/social-providers/paybin.d.mts +8 -2
  82. package/dist/social-providers/paybin.mjs +12 -11
  83. package/dist/social-providers/paypal.d.mts +8 -3
  84. package/dist/social-providers/paypal.mjs +10 -14
  85. package/dist/social-providers/polar.d.mts +8 -2
  86. package/dist/social-providers/polar.mjs +11 -10
  87. package/dist/social-providers/railway.d.mts +8 -2
  88. package/dist/social-providers/railway.mjs +11 -10
  89. package/dist/social-providers/reddit.d.mts +8 -2
  90. package/dist/social-providers/reddit.mjs +11 -9
  91. package/dist/social-providers/roblox.d.mts +8 -2
  92. package/dist/social-providers/roblox.mjs +15 -5
  93. package/dist/social-providers/salesforce.d.mts +8 -2
  94. package/dist/social-providers/salesforce.mjs +11 -10
  95. package/dist/social-providers/slack.d.mts +8 -2
  96. package/dist/social-providers/slack.mjs +18 -15
  97. package/dist/social-providers/spotify.d.mts +8 -2
  98. package/dist/social-providers/spotify.mjs +7 -6
  99. package/dist/social-providers/tiktok.d.mts +8 -2
  100. package/dist/social-providers/tiktok.mjs +21 -5
  101. package/dist/social-providers/twitch.d.mts +8 -2
  102. package/dist/social-providers/twitch.mjs +7 -6
  103. package/dist/social-providers/twitter.d.mts +7 -2
  104. package/dist/social-providers/twitter.mjs +11 -10
  105. package/dist/social-providers/vercel.d.mts +8 -2
  106. package/dist/social-providers/vercel.mjs +7 -9
  107. package/dist/social-providers/vk.d.mts +8 -2
  108. package/dist/social-providers/vk.mjs +7 -6
  109. package/dist/social-providers/wechat.d.mts +8 -2
  110. package/dist/social-providers/wechat.mjs +16 -6
  111. package/dist/social-providers/zoom.d.mts +10 -3
  112. package/dist/social-providers/zoom.mjs +14 -15
  113. package/dist/types/context.d.mts +33 -11
  114. package/dist/types/index.d.mts +1 -1
  115. package/dist/types/init-options.d.mts +121 -6
  116. package/dist/utils/ip.d.mts +5 -4
  117. package/dist/utils/ip.mjs +3 -3
  118. package/dist/utils/redirect-uri.d.mts +20 -0
  119. package/dist/utils/redirect-uri.mjs +48 -0
  120. package/dist/utils/string.d.mts +5 -1
  121. package/dist/utils/string.mjs +20 -1
  122. package/dist/utils/url.d.mts +18 -1
  123. package/dist/utils/url.mjs +30 -1
  124. package/package.json +13 -12
  125. package/src/db/adapter/factory.ts +126 -0
  126. package/src/db/adapter/index.ts +32 -0
  127. package/src/db/adapter/types.ts +1 -0
  128. package/src/db/get-tables.ts +8 -3
  129. package/src/db/schema/account.ts +14 -2
  130. package/src/db/type.ts +12 -0
  131. package/src/env/env-impl.ts +1 -2
  132. package/src/error/codes.ts +6 -0
  133. package/src/oauth2/authorization-params.ts +28 -0
  134. package/src/oauth2/basic-credentials.ts +87 -0
  135. package/src/oauth2/client-assertion.ts +131 -58
  136. package/src/oauth2/client-credentials-token.ts +48 -72
  137. package/src/oauth2/create-authorization-url.ts +30 -8
  138. package/src/oauth2/index.ts +42 -10
  139. package/src/oauth2/oauth-provider.ts +161 -12
  140. package/src/oauth2/refresh-access-token.ts +52 -78
  141. package/src/oauth2/scopes.ts +118 -0
  142. package/src/oauth2/token-endpoint-auth.ts +221 -0
  143. package/src/oauth2/utils.ts +21 -5
  144. package/src/oauth2/validate-authorization-code.ts +55 -85
  145. package/src/oauth2/verify-id-token.ts +111 -0
  146. package/src/oauth2/verify.ts +82 -15
  147. package/src/social-providers/apple.ts +32 -45
  148. package/src/social-providers/atlassian.ts +20 -9
  149. package/src/social-providers/cognito.ts +51 -48
  150. package/src/social-providers/discord.ts +37 -22
  151. package/src/social-providers/dropbox.ts +20 -12
  152. package/src/social-providers/facebook.ts +108 -57
  153. package/src/social-providers/figma.ts +21 -10
  154. package/src/social-providers/github.ts +16 -10
  155. package/src/social-providers/gitlab.ts +16 -8
  156. package/src/social-providers/google.ts +67 -46
  157. package/src/social-providers/huggingface.ts +20 -9
  158. package/src/social-providers/kakao.ts +18 -9
  159. package/src/social-providers/kick.ts +20 -8
  160. package/src/social-providers/line.ts +39 -37
  161. package/src/social-providers/linear.ts +20 -7
  162. package/src/social-providers/linkedin.ts +16 -10
  163. package/src/social-providers/microsoft-entra-id.ts +66 -64
  164. package/src/social-providers/naver.ts +14 -7
  165. package/src/social-providers/notion.ts +20 -7
  166. package/src/social-providers/paybin.ts +16 -11
  167. package/src/social-providers/paypal.ts +12 -25
  168. package/src/social-providers/polar.ts +20 -9
  169. package/src/social-providers/railway.ts +20 -9
  170. package/src/social-providers/reddit.ts +22 -10
  171. package/src/social-providers/roblox.ts +31 -15
  172. package/src/social-providers/salesforce.ts +21 -10
  173. package/src/social-providers/slack.ts +31 -16
  174. package/src/social-providers/spotify.ts +20 -7
  175. package/src/social-providers/tiktok.ts +32 -13
  176. package/src/social-providers/twitch.ts +14 -9
  177. package/src/social-providers/twitter.ts +18 -8
  178. package/src/social-providers/vercel.ts +24 -11
  179. package/src/social-providers/vk.ts +20 -7
  180. package/src/social-providers/wechat.ts +28 -8
  181. package/src/social-providers/zoom.ts +28 -19
  182. package/src/types/context.ts +33 -12
  183. package/src/types/index.ts +7 -0
  184. package/src/types/init-options.ts +148 -5
  185. package/src/utils/ip.ts +12 -13
  186. package/src/utils/redirect-uri.ts +54 -0
  187. package/src/utils/string.ts +37 -0
  188. 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 subnet prefix length for rate limiting.
211
- * IPv6 addresses will be normalized to this subnet.
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?: 128 | 64 | 48 | 32;
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
- * If enabled (true), this will update the user information based on the newly linked account
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
- * For IPv6 addresses, extract the subnet prefix instead of full address.
16
- * Common values: 32, 48, 64, 128 (default: 128 = full address)
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 128
19
+ * @default 64
19
20
  */
20
- ipv6Subnet?: 128 | 64 | 48 | 32;
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
- // Apply subnet mask
128
- const prefix = subnetPrefix;
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 || 64;
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
+ });
@@ -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
+ }