@better-auth/core 1.7.0-beta.1 → 1.7.0-beta.10

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 (218) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +69 -4
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +91 -3
  7. package/dist/db/adapter/get-id-field.mjs +1 -1
  8. package/dist/db/adapter/index.d.mts +87 -3
  9. package/dist/db/adapter/types.d.mts +1 -1
  10. package/dist/db/get-tables.mjs +2 -1
  11. package/dist/db/type.d.mts +17 -0
  12. package/dist/env/env-impl.mjs +1 -1
  13. package/dist/error/codes.d.mts +1 -0
  14. package/dist/error/codes.mjs +1 -0
  15. package/dist/error/index.d.mts +7 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/instrumentation/api.mjs +12 -0
  18. package/dist/instrumentation/noop.mjs +42 -0
  19. package/dist/instrumentation/pure.index.d.mts +7 -0
  20. package/dist/instrumentation/pure.index.mjs +7 -0
  21. package/dist/instrumentation/tracer.mjs +6 -3
  22. package/dist/oauth2/authorization-params.d.mts +12 -0
  23. package/dist/oauth2/authorization-params.mjs +12 -0
  24. package/dist/oauth2/basic-credentials.d.mts +30 -0
  25. package/dist/oauth2/basic-credentials.mjs +64 -0
  26. package/dist/oauth2/client-assertion.d.mts +38 -22
  27. package/dist/oauth2/client-assertion.mjs +63 -28
  28. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  29. package/dist/oauth2/client-credentials-token.mjs +20 -31
  30. package/dist/oauth2/create-authorization-url.d.mts +11 -1
  31. package/dist/oauth2/create-authorization-url.mjs +27 -7
  32. package/dist/oauth2/dpop.d.mts +142 -0
  33. package/dist/oauth2/dpop.mjs +246 -0
  34. package/dist/oauth2/index.d.mts +14 -9
  35. package/dist/oauth2/index.mjs +12 -8
  36. package/dist/oauth2/oauth-provider.d.mts +144 -10
  37. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  38. package/dist/oauth2/refresh-access-token.mjs +38 -36
  39. package/dist/oauth2/reject-redirects.mjs +65 -0
  40. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  41. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  42. package/dist/oauth2/utils.d.mts +23 -1
  43. package/dist/oauth2/utils.mjs +48 -2
  44. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  45. package/dist/oauth2/validate-authorization-code.mjs +28 -34
  46. package/dist/oauth2/verify-id-token.d.mts +27 -0
  47. package/dist/oauth2/verify-id-token.mjs +62 -0
  48. package/dist/oauth2/verify.d.mts +88 -15
  49. package/dist/oauth2/verify.mjs +203 -25
  50. package/dist/social-providers/apple.d.mts +16 -4
  51. package/dist/social-providers/apple.mjs +17 -24
  52. package/dist/social-providers/atlassian.d.mts +5 -1
  53. package/dist/social-providers/atlassian.mjs +6 -3
  54. package/dist/social-providers/cognito.d.mts +27 -3
  55. package/dist/social-providers/cognito.mjs +15 -25
  56. package/dist/social-providers/discord.d.mts +7 -3
  57. package/dist/social-providers/discord.mjs +16 -3
  58. package/dist/social-providers/dropbox.d.mts +5 -1
  59. package/dist/social-providers/dropbox.mjs +5 -4
  60. package/dist/social-providers/facebook.d.mts +24 -5
  61. package/dist/social-providers/facebook.mjs +54 -20
  62. package/dist/social-providers/figma.d.mts +5 -1
  63. package/dist/social-providers/figma.mjs +4 -3
  64. package/dist/social-providers/github.d.mts +6 -2
  65. package/dist/social-providers/github.mjs +5 -4
  66. package/dist/social-providers/gitlab.d.mts +5 -1
  67. package/dist/social-providers/gitlab.mjs +3 -2
  68. package/dist/social-providers/google.d.mts +53 -5
  69. package/dist/social-providers/google.mjs +69 -25
  70. package/dist/social-providers/huggingface.d.mts +5 -1
  71. package/dist/social-providers/huggingface.mjs +3 -2
  72. package/dist/social-providers/index.d.mts +221 -45
  73. package/dist/social-providers/index.mjs +2 -2
  74. package/dist/social-providers/kakao.d.mts +5 -1
  75. package/dist/social-providers/kakao.mjs +3 -2
  76. package/dist/social-providers/kick.d.mts +5 -1
  77. package/dist/social-providers/kick.mjs +3 -2
  78. package/dist/social-providers/line.d.mts +8 -2
  79. package/dist/social-providers/line.mjs +5 -6
  80. package/dist/social-providers/linear.d.mts +5 -1
  81. package/dist/social-providers/linear.mjs +3 -2
  82. package/dist/social-providers/linkedin.d.mts +7 -3
  83. package/dist/social-providers/linkedin.mjs +4 -3
  84. package/dist/social-providers/microsoft-entra-id.d.mts +41 -7
  85. package/dist/social-providers/microsoft-entra-id.mjs +37 -28
  86. package/dist/social-providers/naver.d.mts +5 -1
  87. package/dist/social-providers/naver.mjs +3 -2
  88. package/dist/social-providers/notion.d.mts +5 -1
  89. package/dist/social-providers/notion.mjs +5 -2
  90. package/dist/social-providers/paybin.d.mts +5 -1
  91. package/dist/social-providers/paybin.mjs +4 -3
  92. package/dist/social-providers/paypal.d.mts +6 -2
  93. package/dist/social-providers/paypal.mjs +19 -14
  94. package/dist/social-providers/polar.d.mts +5 -1
  95. package/dist/social-providers/polar.mjs +3 -2
  96. package/dist/social-providers/railway.d.mts +5 -1
  97. package/dist/social-providers/railway.mjs +3 -2
  98. package/dist/social-providers/reddit.d.mts +5 -1
  99. package/dist/social-providers/reddit.mjs +7 -5
  100. package/dist/social-providers/roblox.d.mts +6 -2
  101. package/dist/social-providers/roblox.mjs +12 -2
  102. package/dist/social-providers/salesforce.d.mts +5 -1
  103. package/dist/social-providers/salesforce.mjs +4 -3
  104. package/dist/social-providers/slack.d.mts +6 -2
  105. package/dist/social-providers/slack.mjs +11 -8
  106. package/dist/social-providers/spotify.d.mts +5 -1
  107. package/dist/social-providers/spotify.mjs +3 -2
  108. package/dist/social-providers/tiktok.d.mts +5 -1
  109. package/dist/social-providers/tiktok.mjs +14 -2
  110. package/dist/social-providers/twitch.d.mts +5 -1
  111. package/dist/social-providers/twitch.mjs +3 -2
  112. package/dist/social-providers/twitter.d.mts +4 -1
  113. package/dist/social-providers/twitter.mjs +2 -1
  114. package/dist/social-providers/vercel.d.mts +5 -1
  115. package/dist/social-providers/vercel.mjs +3 -2
  116. package/dist/social-providers/vk.d.mts +5 -1
  117. package/dist/social-providers/vk.mjs +3 -2
  118. package/dist/social-providers/wechat.d.mts +5 -1
  119. package/dist/social-providers/wechat.mjs +8 -2
  120. package/dist/social-providers/zoom.d.mts +6 -2
  121. package/dist/social-providers/zoom.mjs +10 -17
  122. package/dist/types/context.d.mts +55 -6
  123. package/dist/types/index.d.mts +1 -1
  124. package/dist/types/init-options.d.mts +182 -12
  125. package/dist/types/plugin-client.d.mts +12 -2
  126. package/dist/utils/async.d.mts +22 -0
  127. package/dist/utils/async.mjs +32 -0
  128. package/dist/utils/host.d.mts +147 -0
  129. package/dist/utils/host.mjs +298 -0
  130. package/dist/utils/ip.d.mts +28 -5
  131. package/dist/utils/ip.mjs +118 -4
  132. package/dist/utils/is-api-error.d.mts +6 -0
  133. package/dist/utils/is-api-error.mjs +8 -0
  134. package/dist/utils/redirect-uri.d.mts +20 -0
  135. package/dist/utils/redirect-uri.mjs +48 -0
  136. package/dist/utils/string.d.mts +5 -1
  137. package/dist/utils/string.mjs +20 -1
  138. package/dist/utils/url.d.mts +18 -1
  139. package/dist/utils/url.mjs +34 -4
  140. package/package.json +26 -16
  141. package/src/api/index.ts +121 -5
  142. package/src/context/transaction.ts +45 -12
  143. package/src/db/adapter/factory.ts +193 -7
  144. package/src/db/adapter/get-id-field.ts +2 -2
  145. package/src/db/adapter/index.ts +85 -2
  146. package/src/db/adapter/types.ts +2 -0
  147. package/src/db/get-tables.ts +9 -1
  148. package/src/db/schema/account.ts +4 -1
  149. package/src/db/schema/user.ts +3 -0
  150. package/src/db/type.ts +17 -0
  151. package/src/env/env-impl.ts +1 -2
  152. package/src/error/codes.ts +1 -0
  153. package/src/error/index.ts +9 -0
  154. package/src/instrumentation/api.ts +17 -0
  155. package/src/instrumentation/noop.ts +74 -0
  156. package/src/instrumentation/pure.index.ts +31 -0
  157. package/src/instrumentation/tracer.ts +8 -3
  158. package/src/oauth2/authorization-params.ts +28 -0
  159. package/src/oauth2/basic-credentials.ts +87 -0
  160. package/src/oauth2/client-assertion.ts +131 -58
  161. package/src/oauth2/client-credentials-token.ts +50 -74
  162. package/src/oauth2/create-authorization-url.ts +33 -7
  163. package/src/oauth2/dpop.ts +568 -0
  164. package/src/oauth2/index.ts +81 -11
  165. package/src/oauth2/oauth-provider.ts +153 -11
  166. package/src/oauth2/refresh-access-token.ts +82 -83
  167. package/src/oauth2/reject-redirects.ts +75 -0
  168. package/src/oauth2/token-endpoint-auth.ts +221 -0
  169. package/src/oauth2/utils.ts +72 -5
  170. package/src/oauth2/validate-authorization-code.ts +69 -89
  171. package/src/oauth2/verify-id-token.ts +115 -0
  172. package/src/oauth2/verify.ts +409 -78
  173. package/src/social-providers/apple.ts +30 -40
  174. package/src/social-providers/atlassian.ts +8 -1
  175. package/src/social-providers/cognito.ts +36 -39
  176. package/src/social-providers/discord.ts +22 -18
  177. package/src/social-providers/dropbox.ts +7 -5
  178. package/src/social-providers/facebook.ts +108 -52
  179. package/src/social-providers/figma.ts +8 -1
  180. package/src/social-providers/github.ts +5 -3
  181. package/src/social-providers/gitlab.ts +2 -0
  182. package/src/social-providers/google.ts +141 -39
  183. package/src/social-providers/huggingface.ts +8 -1
  184. package/src/social-providers/kakao.ts +2 -1
  185. package/src/social-providers/kick.ts +8 -1
  186. package/src/social-providers/line.ts +25 -27
  187. package/src/social-providers/linear.ts +8 -1
  188. package/src/social-providers/linkedin.ts +5 -3
  189. package/src/social-providers/microsoft-entra-id.ts +100 -55
  190. package/src/social-providers/naver.ts +2 -1
  191. package/src/social-providers/notion.ts +8 -1
  192. package/src/social-providers/paybin.ts +2 -0
  193. package/src/social-providers/paypal.ts +28 -17
  194. package/src/social-providers/polar.ts +8 -1
  195. package/src/social-providers/railway.ts +8 -1
  196. package/src/social-providers/reddit.ts +9 -4
  197. package/src/social-providers/roblox.ts +16 -11
  198. package/src/social-providers/salesforce.ts +8 -1
  199. package/src/social-providers/slack.ts +15 -9
  200. package/src/social-providers/spotify.ts +8 -1
  201. package/src/social-providers/tiktok.ts +22 -9
  202. package/src/social-providers/twitch.ts +2 -1
  203. package/src/social-providers/twitter.ts +1 -0
  204. package/src/social-providers/vercel.ts +8 -1
  205. package/src/social-providers/vk.ts +8 -1
  206. package/src/social-providers/wechat.ts +17 -2
  207. package/src/social-providers/zoom.ts +15 -19
  208. package/src/types/context.ts +57 -5
  209. package/src/types/index.ts +7 -0
  210. package/src/types/init-options.ts +204 -14
  211. package/src/types/plugin-client.ts +16 -2
  212. package/src/utils/async.ts +53 -0
  213. package/src/utils/host.ts +425 -0
  214. package/src/utils/ip.ts +197 -13
  215. package/src/utils/is-api-error.ts +10 -0
  216. package/src/utils/redirect-uri.ts +54 -0
  217. package/src/utils/string.ts +37 -0
  218. package/src/utils/url.ts +38 -4
@@ -44,7 +44,7 @@ export const naver = (options: NaverOptions) => {
44
44
  return {
45
45
  id: "naver",
46
46
  name: "Naver",
47
- createAuthorizationURL({ state, scopes, redirectURI }) {
47
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
48
48
  const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
49
49
  if (options.scope) _scopes.push(...options.scope);
50
50
  if (scopes) _scopes.push(...scopes);
@@ -55,6 +55,7 @@ export const naver = (options: NaverOptions) => {
55
55
  scopes: _scopes,
56
56
  state,
57
57
  redirectURI,
58
+ additionalParams,
58
59
  });
59
60
  },
60
61
  validateAuthorizationCode: async ({ code, redirectURI }) => {
@@ -28,7 +28,13 @@ export const notion = (options: NotionOptions) => {
28
28
  return {
29
29
  id: "notion",
30
30
  name: "Notion",
31
- createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
31
+ createAuthorizationURL({
32
+ state,
33
+ scopes,
34
+ loginHint,
35
+ redirectURI,
36
+ additionalParams,
37
+ }) {
32
38
  const _scopes: string[] = options.disableDefaultScope ? [] : [];
33
39
  if (options.scope) _scopes.push(...options.scope);
34
40
  if (scopes) _scopes.push(...scopes);
@@ -41,6 +47,7 @@ export const notion = (options: NotionOptions) => {
41
47
  redirectURI,
42
48
  loginHint,
43
49
  additionalParams: {
50
+ ...(additionalParams ?? {}),
44
51
  owner: "user",
45
52
  },
46
53
  });
@@ -42,6 +42,7 @@ export const paybin = (options: PaybinOptions) => {
42
42
  codeVerifier,
43
43
  redirectURI,
44
44
  loginHint,
45
+ additionalParams,
45
46
  }) {
46
47
  if (!options.clientId || !options.clientSecret) {
47
48
  logger.error(
@@ -67,6 +68,7 @@ export const paybin = (options: PaybinOptions) => {
67
68
  redirectURI,
68
69
  prompt: options.prompt,
69
70
  loginHint,
71
+ additionalParams,
70
72
  });
71
73
  return url;
72
74
  },
@@ -7,6 +7,7 @@ import type { OAuthProvider, ProviderOptions } from "../oauth2";
7
7
  import { createAuthorizationURL } from "../oauth2";
8
8
 
9
9
  export interface PayPalProfile {
10
+ sub?: string | undefined;
10
11
  user_id: string;
11
12
  name: string;
12
13
  given_name: string;
@@ -78,7 +79,12 @@ export const paypal = (options: PayPalOptions) => {
78
79
  return {
79
80
  id: "paypal",
80
81
  name: "PayPal",
81
- async createAuthorizationURL({ state, codeVerifier, redirectURI }) {
82
+ async createAuthorizationURL({
83
+ state,
84
+ codeVerifier,
85
+ redirectURI,
86
+ additionalParams,
87
+ }) {
82
88
  if (!options.clientId || !options.clientSecret) {
83
89
  logger.error(
84
90
  "Client Id and Client Secret is required for PayPal. Make sure to provide them in the options.",
@@ -103,6 +109,7 @@ export const paypal = (options: PayPalOptions) => {
103
109
  codeVerifier,
104
110
  redirectURI,
105
111
  prompt: options.prompt,
112
+ additionalParams,
106
113
  });
107
114
  return url;
108
115
  },
@@ -194,22 +201,6 @@ export const paypal = (options: PayPalOptions) => {
194
201
  }
195
202
  },
196
203
 
197
- async verifyIdToken(token, nonce) {
198
- if (options.disableIdTokenSignIn) {
199
- return false;
200
- }
201
- if (options.verifyIdToken) {
202
- return options.verifyIdToken(token, nonce);
203
- }
204
- try {
205
- const payload = decodeJwt(token);
206
- return !!payload.sub;
207
- } catch (error) {
208
- logger.error("Failed to verify PayPal ID token:", error);
209
- return false;
210
- }
211
- },
212
-
213
204
  async getUserInfo(token) {
214
205
  if (options.getUserInfo) {
215
206
  return options.getUserInfo(token);
@@ -237,6 +228,26 @@ export const paypal = (options: PayPalOptions) => {
237
228
  }
238
229
 
239
230
  const userInfo = response.data;
231
+ if (token.idToken) {
232
+ let idTokenSubject: string | undefined;
233
+ try {
234
+ idTokenSubject = decodeJwt(token.idToken).sub;
235
+ } catch (error) {
236
+ logger.error("Failed to decode PayPal ID token:", error);
237
+ return null;
238
+ }
239
+
240
+ // OIDC binds UserInfo to the ID Token with `sub`. Keep `user_id`
241
+ // as the account id below for existing PayPal account mappings.
242
+ const userInfoSubject = userInfo.sub ?? userInfo.user_id;
243
+ if (!idTokenSubject || userInfoSubject !== idTokenSubject) {
244
+ logger.error(
245
+ "PayPal user info subject does not match ID token subject",
246
+ );
247
+ return null;
248
+ }
249
+ }
250
+
240
251
  const userMap = await options.mapProfileToUser?.(userInfo);
241
252
 
242
253
  const result = {
@@ -37,7 +37,13 @@ export const polar = (options: PolarOptions) => {
37
37
  return {
38
38
  id: "polar",
39
39
  name: "Polar",
40
- createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
40
+ createAuthorizationURL({
41
+ state,
42
+ scopes,
43
+ codeVerifier,
44
+ redirectURI,
45
+ additionalParams,
46
+ }) {
41
47
  const _scopes = options.disableDefaultScope
42
48
  ? []
43
49
  : ["openid", "profile", "email"];
@@ -52,6 +58,7 @@ export const polar = (options: PolarOptions) => {
52
58
  codeVerifier,
53
59
  redirectURI,
54
60
  prompt: options.prompt,
61
+ additionalParams,
55
62
  });
56
63
  },
57
64
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -29,7 +29,13 @@ export const railway = (options: RailwayOptions) => {
29
29
  return {
30
30
  id: "railway",
31
31
  name: "Railway",
32
- createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
32
+ createAuthorizationURL({
33
+ state,
34
+ scopes,
35
+ codeVerifier,
36
+ redirectURI,
37
+ additionalParams,
38
+ }) {
33
39
  const _scopes = options.disableDefaultScope
34
40
  ? []
35
41
  : ["openid", "email", "profile"];
@@ -43,6 +49,7 @@ export const railway = (options: RailwayOptions) => {
43
49
  state,
44
50
  codeVerifier,
45
51
  redirectURI,
52
+ additionalParams,
46
53
  });
47
54
  },
48
55
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -25,7 +25,7 @@ export const reddit = (options: RedditOptions) => {
25
25
  return {
26
26
  id: "reddit",
27
27
  name: "Reddit",
28
- createAuthorizationURL({ state, scopes, redirectURI }) {
28
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
29
29
  const _scopes = options.disableDefaultScope ? [] : ["identity"];
30
30
  if (options.scope) _scopes.push(...options.scope);
31
31
  if (scopes) _scopes.push(...scopes);
@@ -37,6 +37,7 @@ export const reddit = (options: RedditOptions) => {
37
37
  state,
38
38
  redirectURI,
39
39
  duration: options.duration,
40
+ additionalParams,
40
41
  });
41
42
  },
42
43
  validateAuthorizationCode: async ({ code, redirectURI }) => {
@@ -104,15 +105,19 @@ export const reddit = (options: RedditOptions) => {
104
105
  }
105
106
 
106
107
  const userMap = await options.mapProfileToUser?.(profile);
107
-
108
+ // Reddit's identity scope does not return an email. Synthesize a stable,
109
+ // non-routable placeholder (RFC 2606 `.invalid`) keyed to the user's
110
+ // Reddit id rather than the routable `reddit.com`, which could collide
111
+ // with a real address. Left unverified; `mapProfileToUser` can override.
112
+ const email = userMap?.email || `${profile.id}@reddit.invalid`;
108
113
  return {
109
114
  user: {
110
115
  id: profile.id,
111
116
  name: profile.name,
112
- email: profile.oauth_client_id,
113
- emailVerified: profile.has_verified_email,
114
117
  image: profile.icon_img?.split("?")[0]!,
115
118
  ...userMap,
119
+ email,
120
+ emailVerified: userMap?.emailVerified ?? false,
116
121
  },
117
122
  data: profile,
118
123
  };
@@ -1,6 +1,10 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
- import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ refreshAccessToken,
6
+ validateAuthorizationCode,
7
+ } from "../oauth2";
4
8
 
5
9
  export interface RobloxProfile extends Record<string, any> {
6
10
  /** the user's id */
@@ -37,19 +41,20 @@ export const roblox = (options: RobloxOptions) => {
37
41
  return {
38
42
  id: "roblox",
39
43
  name: "Roblox",
40
- createAuthorizationURL({ state, scopes, redirectURI }) {
44
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
41
45
  const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
42
46
  if (options.scope) _scopes.push(...options.scope);
43
47
  if (scopes) _scopes.push(...scopes);
44
- return new URL(
45
- `https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join(
46
- "+",
47
- )}&response_type=code&client_id=${
48
- options.clientId
49
- }&redirect_uri=${encodeURIComponent(
50
- options.redirectURI || redirectURI,
51
- )}&state=${state}&prompt=${options.prompt || "select_account consent"}`,
52
- );
48
+ return createAuthorizationURL({
49
+ id: "roblox",
50
+ options,
51
+ authorizationEndpoint: "https://apis.roblox.com/oauth/v1/authorize",
52
+ scopes: _scopes,
53
+ state,
54
+ redirectURI,
55
+ prompt: options.prompt || "select_account consent",
56
+ additionalParams,
57
+ });
53
58
  },
54
59
  validateAuthorizationCode: async ({ code, redirectURI }) => {
55
60
  return validateAuthorizationCode({
@@ -64,7 +64,13 @@ export const salesforce = (options: SalesforceOptions) => {
64
64
  id: "salesforce",
65
65
  name: "Salesforce",
66
66
 
67
- async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
67
+ async createAuthorizationURL({
68
+ state,
69
+ scopes,
70
+ codeVerifier,
71
+ redirectURI,
72
+ additionalParams,
73
+ }) {
68
74
  if (!options.clientId || !options.clientSecret) {
69
75
  logger.error(
70
76
  "Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options.",
@@ -89,6 +95,7 @@ export const salesforce = (options: SalesforceOptions) => {
89
95
  state,
90
96
  codeVerifier,
91
97
  redirectURI: options.redirectURI || redirectURI,
98
+ additionalParams,
92
99
  });
93
100
  },
94
101
 
@@ -1,6 +1,10 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
- import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
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
- const url = new URL("https://slack.com/openid/connect/authorize");
52
- url.searchParams.set("scope", _scopes.join(" "));
53
- url.searchParams.set("response_type", "code");
54
- url.searchParams.set("client_id", options.clientId);
55
- url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
56
- url.searchParams.set("state", state);
57
- return url;
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({ state, scopes, codeVerifier, redirectURI }) {
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 { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
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
- return new URL(
139
- `https://www.tiktok.com/v2/auth/authorize?scope=${_scopes.join(
140
- ",",
141
- )}&response_type=code&client_key=${options.clientKey}&redirect_uri=${encodeURIComponent(
142
- options.redirectURI || redirectURI,
143
- )}&state=${state}`,
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({ state, scopes, codeVerifier, redirectURI }) {
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({ state, scopes, codeVerifier, redirectURI }) {
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;
@@ -200,7 +208,14 @@ export const wechat = (options: WeChatOptions) => {
200
208
  user: {
201
209
  id: profile.unionid || profile.openid || openid,
202
210
  name: profile.nickname,
203
- email: profile.email || null,
211
+ // WeChat does not return an email, and the OAuth callback rejects a
212
+ // missing one, so the default sign-in would always fail. Synthesize a
213
+ // stable, non-routable placeholder (RFC 2606 `.invalid`) keyed to the
214
+ // user's WeChat id, left unverified. Applications that collect a real
215
+ // email override it via `mapProfileToUser`.
216
+ email:
217
+ profile.email ||
218
+ `${profile.unionid || profile.openid || openid}@wechat.invalid`,
204
219
  image: profile.headimgurl,
205
220
  emailVerified: false,
206
221
  ...userMap,
@@ -1,7 +1,7 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
3
  import {
4
- generateCodeChallenge,
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 ({ state, redirectURI, codeVerifier }) => {
156
- const params = new URLSearchParams({
157
- response_type: "code",
158
- redirect_uri: options.redirectURI ? options.redirectURI : redirectURI,
159
- client_id: options.clientId,
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
- if (options.pkce) {
164
- const codeChallenge = await generateCodeChallenge(codeVerifier);
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,