@better-auth/core 1.3.27 → 1.3.29

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 (121) hide show
  1. package/.turbo/turbo-build.log +54 -4
  2. package/build.config.ts +6 -0
  3. package/dist/db/adapter/index.d.cts +13 -23
  4. package/dist/db/adapter/index.d.mts +13 -23
  5. package/dist/db/adapter/index.d.ts +13 -23
  6. package/dist/db/index.cjs +16 -0
  7. package/dist/db/index.d.cts +6 -83
  8. package/dist/db/index.d.mts +6 -83
  9. package/dist/db/index.d.ts +6 -83
  10. package/dist/db/index.mjs +16 -1
  11. package/dist/env/index.cjs +312 -0
  12. package/dist/env/index.d.cts +36 -0
  13. package/dist/env/index.d.mts +36 -0
  14. package/dist/env/index.d.ts +36 -0
  15. package/dist/env/index.mjs +297 -0
  16. package/dist/error/index.cjs +44 -0
  17. package/dist/error/index.d.cts +33 -0
  18. package/dist/error/index.d.mts +33 -0
  19. package/dist/error/index.d.ts +33 -0
  20. package/dist/error/index.mjs +41 -0
  21. package/dist/index.d.cts +156 -101
  22. package/dist/index.d.mts +156 -101
  23. package/dist/index.d.ts +156 -101
  24. package/dist/middleware/index.cjs +25 -0
  25. package/dist/middleware/index.d.cts +13 -0
  26. package/dist/middleware/index.d.mts +13 -0
  27. package/dist/middleware/index.d.ts +13 -0
  28. package/dist/middleware/index.mjs +21 -0
  29. package/dist/oauth2/index.cjs +368 -0
  30. package/dist/oauth2/index.d.cts +100 -0
  31. package/dist/oauth2/index.d.mts +100 -0
  32. package/dist/oauth2/index.d.ts +100 -0
  33. package/dist/oauth2/index.mjs +357 -0
  34. package/dist/shared/core.Bl6TpxyD.d.mts +181 -0
  35. package/dist/shared/core.Bqe5IGAi.d.ts +13 -0
  36. package/dist/shared/core.Bshk2o_x.d.ts +1721 -0
  37. package/dist/shared/core.BwoNUcJQ.d.cts +53 -0
  38. package/dist/shared/core.BwoNUcJQ.d.mts +53 -0
  39. package/dist/shared/core.BwoNUcJQ.d.ts +53 -0
  40. package/dist/shared/core.C6_2xGyf.d.mts +1721 -0
  41. package/dist/shared/{core.CnvFgghY.d.cts → core.CajxAutx.d.cts} +27 -1
  42. package/dist/shared/{core.CnvFgghY.d.mts → core.CajxAutx.d.mts} +27 -1
  43. package/dist/shared/{core.CnvFgghY.d.ts → core.CajxAutx.d.ts} +27 -1
  44. package/dist/shared/core.CfqdiZTu.d.cts +1721 -0
  45. package/dist/shared/core.DkdZ1o38.d.ts +181 -0
  46. package/dist/shared/core.Dl-70uns.d.cts +84 -0
  47. package/dist/shared/core.Dl-70uns.d.mts +84 -0
  48. package/dist/shared/core.Dl-70uns.d.ts +84 -0
  49. package/dist/shared/core.DyEdx0m7.d.cts +181 -0
  50. package/dist/shared/core.E9DfzGLz.d.mts +13 -0
  51. package/dist/shared/core.HqYn20Fi.d.cts +13 -0
  52. package/dist/social-providers/index.cjs +2793 -0
  53. package/dist/social-providers/index.d.cts +3903 -0
  54. package/dist/social-providers/index.d.mts +3903 -0
  55. package/dist/social-providers/index.d.ts +3903 -0
  56. package/dist/social-providers/index.mjs +2743 -0
  57. package/dist/utils/index.cjs +7 -0
  58. package/dist/utils/index.d.cts +10 -0
  59. package/dist/utils/index.d.mts +10 -0
  60. package/dist/utils/index.d.ts +10 -0
  61. package/dist/utils/index.mjs +5 -0
  62. package/package.json +98 -2
  63. package/src/db/adapter/index.ts +424 -0
  64. package/src/db/index.ts +2 -0
  65. package/src/db/schema/rate-limit.ts +21 -0
  66. package/src/db/type.ts +28 -0
  67. package/src/env/color-depth.ts +172 -0
  68. package/src/env/env-impl.ts +124 -0
  69. package/src/env/index.ts +23 -0
  70. package/src/env/logger.test.ts +33 -0
  71. package/src/env/logger.ts +145 -0
  72. package/src/error/codes.ts +31 -0
  73. package/src/error/index.ts +11 -0
  74. package/src/index.ts +0 -2
  75. package/src/middleware/index.ts +33 -0
  76. package/src/oauth2/client-credentials-token.ts +102 -0
  77. package/src/oauth2/create-authorization-url.ts +85 -0
  78. package/src/oauth2/index.ts +22 -0
  79. package/src/oauth2/oauth-provider.ts +194 -0
  80. package/src/oauth2/refresh-access-token.ts +124 -0
  81. package/src/oauth2/utils.ts +36 -0
  82. package/src/oauth2/validate-authorization-code.ts +156 -0
  83. package/src/social-providers/apple.ts +213 -0
  84. package/src/social-providers/atlassian.ts +130 -0
  85. package/src/social-providers/cognito.ts +269 -0
  86. package/src/social-providers/discord.ts +172 -0
  87. package/src/social-providers/dropbox.ts +112 -0
  88. package/src/social-providers/facebook.ts +204 -0
  89. package/src/social-providers/figma.ts +115 -0
  90. package/src/social-providers/github.ts +154 -0
  91. package/src/social-providers/gitlab.ts +152 -0
  92. package/src/social-providers/google.ts +171 -0
  93. package/src/social-providers/huggingface.ts +116 -0
  94. package/src/social-providers/index.ts +118 -0
  95. package/src/social-providers/kakao.ts +178 -0
  96. package/src/social-providers/kick.ts +95 -0
  97. package/src/social-providers/line.ts +169 -0
  98. package/src/social-providers/linear.ts +120 -0
  99. package/src/social-providers/linkedin.ts +110 -0
  100. package/src/social-providers/microsoft-entra-id.ts +243 -0
  101. package/src/social-providers/naver.ts +112 -0
  102. package/src/social-providers/notion.ts +106 -0
  103. package/src/social-providers/paypal.ts +261 -0
  104. package/src/social-providers/reddit.ts +122 -0
  105. package/src/social-providers/roblox.ts +110 -0
  106. package/src/social-providers/salesforce.ts +157 -0
  107. package/src/social-providers/slack.ts +114 -0
  108. package/src/social-providers/spotify.ts +93 -0
  109. package/src/social-providers/tiktok.ts +211 -0
  110. package/src/social-providers/twitch.ts +111 -0
  111. package/src/social-providers/twitter.ts +194 -0
  112. package/src/social-providers/vk.ts +128 -0
  113. package/src/social-providers/zoom.ts +218 -0
  114. package/src/types/context.ts +334 -0
  115. package/src/types/cookie.ts +7 -0
  116. package/src/types/index.ts +19 -1
  117. package/src/types/init-options.ts +1048 -2
  118. package/src/types/plugin-client.ts +69 -0
  119. package/src/types/plugin.ts +134 -0
  120. package/src/utils/error-codes.ts +51 -0
  121. package/src/utils/index.ts +1 -0
@@ -0,0 +1,261 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import { BetterAuthError } from "../error";
3
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
4
+ import { createAuthorizationURL } from "@better-auth/core/oauth2";
5
+ import { logger } from "@better-auth/core/env";
6
+ import { decodeJwt } from "jose";
7
+ import { base64 } from "@better-auth/utils/base64";
8
+
9
+ export interface PayPalProfile {
10
+ user_id: string;
11
+ name: string;
12
+ given_name: string;
13
+ family_name: string;
14
+ middle_name?: string;
15
+ picture?: string;
16
+ email: string;
17
+ email_verified: boolean;
18
+ gender?: string;
19
+ birthdate?: string;
20
+ zoneinfo?: string;
21
+ locale?: string;
22
+ phone_number?: string;
23
+ address?: {
24
+ street_address?: string;
25
+ locality?: string;
26
+ region?: string;
27
+ postal_code?: string;
28
+ country?: string;
29
+ };
30
+ verified_account?: boolean;
31
+ account_type?: string;
32
+ age_range?: string;
33
+ payer_id?: string;
34
+ }
35
+
36
+ export interface PayPalTokenResponse {
37
+ scope?: string;
38
+ access_token: string;
39
+ refresh_token?: string;
40
+ token_type: "Bearer";
41
+ id_token?: string;
42
+ expires_in: number;
43
+ nonce?: string;
44
+ }
45
+
46
+ export interface PayPalOptions extends ProviderOptions<PayPalProfile> {
47
+ clientId: string;
48
+ /**
49
+ * PayPal environment - 'sandbox' for testing, 'live' for production
50
+ * @default 'sandbox'
51
+ */
52
+ environment?: "sandbox" | "live";
53
+ /**
54
+ * Whether to request shipping address information
55
+ * @default false
56
+ */
57
+ requestShippingAddress?: boolean;
58
+ }
59
+
60
+ export const paypal = (options: PayPalOptions) => {
61
+ const environment = options.environment || "sandbox";
62
+ const isSandbox = environment === "sandbox";
63
+
64
+ const authorizationEndpoint = isSandbox
65
+ ? "https://www.sandbox.paypal.com/signin/authorize"
66
+ : "https://www.paypal.com/signin/authorize";
67
+
68
+ const tokenEndpoint = isSandbox
69
+ ? "https://api-m.sandbox.paypal.com/v1/oauth2/token"
70
+ : "https://api-m.paypal.com/v1/oauth2/token";
71
+
72
+ const userInfoEndpoint = isSandbox
73
+ ? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo"
74
+ : "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
75
+
76
+ return {
77
+ id: "paypal",
78
+ name: "PayPal",
79
+ async createAuthorizationURL({ state, codeVerifier, redirectURI }) {
80
+ if (!options.clientId || !options.clientSecret) {
81
+ logger.error(
82
+ "Client Id and Client Secret is required for PayPal. Make sure to provide them in the options.",
83
+ );
84
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
85
+ }
86
+
87
+ /**
88
+ * Log in with PayPal doesn't use traditional OAuth2 scopes
89
+ * Instead, permissions are configured in the PayPal Developer Dashboard
90
+ * We don't pass any scopes to avoid "invalid scope" errors
91
+ **/
92
+
93
+ const _scopes: string[] = [];
94
+
95
+ const url = await createAuthorizationURL({
96
+ id: "paypal",
97
+ options,
98
+ authorizationEndpoint,
99
+ scopes: _scopes,
100
+ state,
101
+ codeVerifier,
102
+ redirectURI,
103
+ prompt: options.prompt,
104
+ });
105
+ return url;
106
+ },
107
+
108
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
109
+ /**
110
+ * PayPal requires Basic Auth for token exchange
111
+ **/
112
+
113
+ const credentials = base64.encode(
114
+ `${options.clientId}:${options.clientSecret}`,
115
+ );
116
+
117
+ try {
118
+ const response = await betterFetch(tokenEndpoint, {
119
+ method: "POST",
120
+ headers: {
121
+ Authorization: `Basic ${credentials}`,
122
+ Accept: "application/json",
123
+ "Accept-Language": "en_US",
124
+ "Content-Type": "application/x-www-form-urlencoded",
125
+ },
126
+ body: new URLSearchParams({
127
+ grant_type: "authorization_code",
128
+ code: code,
129
+ redirect_uri: redirectURI,
130
+ }).toString(),
131
+ });
132
+
133
+ if (!response.data) {
134
+ throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
135
+ }
136
+
137
+ const data = response.data as PayPalTokenResponse;
138
+
139
+ const result = {
140
+ accessToken: data.access_token,
141
+ refreshToken: data.refresh_token,
142
+ accessTokenExpiresAt: data.expires_in
143
+ ? new Date(Date.now() + data.expires_in * 1000)
144
+ : undefined,
145
+ idToken: data.id_token,
146
+ };
147
+
148
+ return result;
149
+ } catch (error) {
150
+ logger.error("PayPal token exchange failed:", error);
151
+ throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
152
+ }
153
+ },
154
+
155
+ refreshAccessToken: options.refreshAccessToken
156
+ ? options.refreshAccessToken
157
+ : async (refreshToken) => {
158
+ const credentials = base64.encode(
159
+ `${options.clientId}:${options.clientSecret}`,
160
+ );
161
+
162
+ try {
163
+ const response = await betterFetch(tokenEndpoint, {
164
+ method: "POST",
165
+ headers: {
166
+ Authorization: `Basic ${credentials}`,
167
+ Accept: "application/json",
168
+ "Accept-Language": "en_US",
169
+ "Content-Type": "application/x-www-form-urlencoded",
170
+ },
171
+ body: new URLSearchParams({
172
+ grant_type: "refresh_token",
173
+ refresh_token: refreshToken,
174
+ }).toString(),
175
+ });
176
+
177
+ if (!response.data) {
178
+ throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
179
+ }
180
+
181
+ const data = response.data as any;
182
+ return {
183
+ accessToken: data.access_token,
184
+ refreshToken: data.refresh_token,
185
+ accessTokenExpiresAt: data.expires_in
186
+ ? new Date(Date.now() + data.expires_in * 1000)
187
+ : undefined,
188
+ };
189
+ } catch (error) {
190
+ logger.error("PayPal token refresh failed:", error);
191
+ throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
192
+ }
193
+ },
194
+
195
+ async verifyIdToken(token, nonce) {
196
+ if (options.disableIdTokenSignIn) {
197
+ return false;
198
+ }
199
+ if (options.verifyIdToken) {
200
+ return options.verifyIdToken(token, nonce);
201
+ }
202
+ try {
203
+ const payload = decodeJwt(token);
204
+ return !!payload.sub;
205
+ } catch (error) {
206
+ logger.error("Failed to verify PayPal ID token:", error);
207
+ return false;
208
+ }
209
+ },
210
+
211
+ async getUserInfo(token) {
212
+ if (options.getUserInfo) {
213
+ return options.getUserInfo(token);
214
+ }
215
+
216
+ if (!token.accessToken) {
217
+ logger.error("Access token is required to fetch PayPal user info");
218
+ return null;
219
+ }
220
+
221
+ try {
222
+ const response = await betterFetch<PayPalProfile>(
223
+ `${userInfoEndpoint}?schema=paypalv1.1`,
224
+ {
225
+ headers: {
226
+ Authorization: `Bearer ${token.accessToken}`,
227
+ Accept: "application/json",
228
+ },
229
+ },
230
+ );
231
+
232
+ if (!response.data) {
233
+ logger.error("Failed to fetch user info from PayPal");
234
+ return null;
235
+ }
236
+
237
+ const userInfo = response.data;
238
+ const userMap = await options.mapProfileToUser?.(userInfo);
239
+
240
+ const result = {
241
+ user: {
242
+ id: userInfo.user_id,
243
+ name: userInfo.name,
244
+ email: userInfo.email,
245
+ image: userInfo.picture,
246
+ emailVerified: userInfo.email_verified,
247
+ ...userMap,
248
+ },
249
+ data: userInfo,
250
+ };
251
+
252
+ return result;
253
+ } catch (error) {
254
+ logger.error("Failed to fetch user info from PayPal:", error);
255
+ return null;
256
+ }
257
+ },
258
+
259
+ options,
260
+ } satisfies OAuthProvider<PayPalProfile>;
261
+ };
@@ -0,0 +1,122 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ getOAuth2Tokens,
6
+ refreshAccessToken,
7
+ } from "@better-auth/core/oauth2";
8
+ import { base64 } from "@better-auth/utils/base64";
9
+
10
+ export interface RedditProfile {
11
+ id: string;
12
+ name: string;
13
+ icon_img: string | null;
14
+ has_verified_email: boolean;
15
+ oauth_client_id: string;
16
+ verified: boolean;
17
+ }
18
+
19
+ export interface RedditOptions extends ProviderOptions<RedditProfile> {
20
+ clientId: string;
21
+ duration?: string;
22
+ }
23
+
24
+ export const reddit = (options: RedditOptions) => {
25
+ return {
26
+ id: "reddit",
27
+ name: "Reddit",
28
+ createAuthorizationURL({ state, scopes, redirectURI }) {
29
+ const _scopes = options.disableDefaultScope ? [] : ["identity"];
30
+ options.scope && _scopes.push(...options.scope);
31
+ scopes && _scopes.push(...scopes);
32
+ return createAuthorizationURL({
33
+ id: "reddit",
34
+ options,
35
+ authorizationEndpoint: "https://www.reddit.com/api/v1/authorize",
36
+ scopes: _scopes,
37
+ state,
38
+ redirectURI,
39
+ duration: options.duration,
40
+ });
41
+ },
42
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
43
+ const body = new URLSearchParams({
44
+ grant_type: "authorization_code",
45
+ code,
46
+ redirect_uri: options.redirectURI || redirectURI,
47
+ });
48
+ const headers = {
49
+ "content-type": "application/x-www-form-urlencoded",
50
+ accept: "text/plain",
51
+ "user-agent": "better-auth",
52
+ Authorization: `Basic ${base64.encode(
53
+ `${options.clientId}:${options.clientSecret}`,
54
+ )}`,
55
+ };
56
+
57
+ const { data, error } = await betterFetch<object>(
58
+ "https://www.reddit.com/api/v1/access_token",
59
+ {
60
+ method: "POST",
61
+ headers,
62
+ body: body.toString(),
63
+ },
64
+ );
65
+
66
+ if (error) {
67
+ throw error;
68
+ }
69
+
70
+ return getOAuth2Tokens(data);
71
+ },
72
+
73
+ refreshAccessToken: options.refreshAccessToken
74
+ ? options.refreshAccessToken
75
+ : async (refreshToken) => {
76
+ return refreshAccessToken({
77
+ refreshToken,
78
+ options: {
79
+ clientId: options.clientId,
80
+ clientKey: options.clientKey,
81
+ clientSecret: options.clientSecret,
82
+ },
83
+ authentication: "basic",
84
+ tokenEndpoint: "https://www.reddit.com/api/v1/access_token",
85
+ });
86
+ },
87
+ async getUserInfo(token) {
88
+ if (options.getUserInfo) {
89
+ return options.getUserInfo(token);
90
+ }
91
+
92
+ const { data: profile, error } = await betterFetch<RedditProfile>(
93
+ "https://oauth.reddit.com/api/v1/me",
94
+ {
95
+ headers: {
96
+ Authorization: `Bearer ${token.accessToken}`,
97
+ "User-Agent": "better-auth",
98
+ },
99
+ },
100
+ );
101
+
102
+ if (error) {
103
+ return null;
104
+ }
105
+
106
+ const userMap = await options.mapProfileToUser?.(profile);
107
+
108
+ return {
109
+ user: {
110
+ id: profile.id,
111
+ name: profile.name,
112
+ email: profile.oauth_client_id,
113
+ emailVerified: profile.has_verified_email,
114
+ image: profile.icon_img?.split("?")[0]!,
115
+ ...userMap,
116
+ },
117
+ data: profile,
118
+ };
119
+ },
120
+ options,
121
+ } satisfies OAuthProvider<RedditProfile>;
122
+ };
@@ -0,0 +1,110 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ validateAuthorizationCode,
5
+ refreshAccessToken,
6
+ } from "@better-auth/core/oauth2";
7
+
8
+ export interface RobloxProfile extends Record<string, any> {
9
+ /** the user's id */
10
+ sub: string;
11
+ /** the user's username */
12
+ preferred_username: string;
13
+ /** the user's display name, will return the same value as the preferred_username if not set */
14
+ nickname: string;
15
+ /** the user's display name, again, will return the same value as the preferred_username if not set */
16
+ name: string;
17
+ /** the account creation date as a unix timestamp in seconds */
18
+ created_at: number;
19
+ /** the user's profile URL */
20
+ profile: string;
21
+ /** the user's avatar URL */
22
+ picture: string;
23
+ }
24
+
25
+ export interface RobloxOptions extends ProviderOptions<RobloxProfile> {
26
+ clientId: string;
27
+ prompt?:
28
+ | "none"
29
+ | "consent"
30
+ | "login"
31
+ | "select_account"
32
+ | "select_account consent";
33
+ }
34
+
35
+ export const roblox = (options: RobloxOptions) => {
36
+ return {
37
+ id: "roblox",
38
+ name: "Roblox",
39
+ createAuthorizationURL({ state, scopes, redirectURI }) {
40
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
41
+ options.scope && _scopes.push(...options.scope);
42
+ scopes && _scopes.push(...scopes);
43
+ return new URL(
44
+ `https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join(
45
+ "+",
46
+ )}&response_type=code&client_id=${
47
+ options.clientId
48
+ }&redirect_uri=${encodeURIComponent(
49
+ options.redirectURI || redirectURI,
50
+ )}&state=${state}&prompt=${options.prompt || "select_account consent"}`,
51
+ );
52
+ },
53
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
54
+ return validateAuthorizationCode({
55
+ code,
56
+ redirectURI: options.redirectURI || redirectURI,
57
+ options,
58
+ tokenEndpoint: "https://apis.roblox.com/oauth/v1/token",
59
+ authentication: "post",
60
+ });
61
+ },
62
+ refreshAccessToken: options.refreshAccessToken
63
+ ? options.refreshAccessToken
64
+ : async (refreshToken) => {
65
+ return refreshAccessToken({
66
+ refreshToken,
67
+ options: {
68
+ clientId: options.clientId,
69
+ clientKey: options.clientKey,
70
+ clientSecret: options.clientSecret,
71
+ },
72
+ tokenEndpoint: "https://apis.roblox.com/oauth/v1/token",
73
+ });
74
+ },
75
+ async getUserInfo(token) {
76
+ if (options.getUserInfo) {
77
+ return options.getUserInfo(token);
78
+ }
79
+ const { data: profile, error } = await betterFetch<RobloxProfile>(
80
+ "https://apis.roblox.com/oauth/v1/userinfo",
81
+ {
82
+ headers: {
83
+ authorization: `Bearer ${token.accessToken}`,
84
+ },
85
+ },
86
+ );
87
+
88
+ if (error) {
89
+ return null;
90
+ }
91
+
92
+ const userMap = await options.mapProfileToUser?.(profile);
93
+
94
+ return {
95
+ user: {
96
+ id: profile.sub,
97
+ name: profile.nickname || profile.preferred_username || "",
98
+ image: profile.picture,
99
+ email: profile.preferred_username || null, // Roblox does not provide email
100
+ emailVerified: true,
101
+ ...userMap,
102
+ },
103
+ data: {
104
+ ...profile,
105
+ },
106
+ };
107
+ },
108
+ options,
109
+ } satisfies OAuthProvider<RobloxProfile>;
110
+ };
@@ -0,0 +1,157 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import { BetterAuthError } from "../error";
3
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
4
+ import {
5
+ createAuthorizationURL,
6
+ validateAuthorizationCode,
7
+ } from "@better-auth/core/oauth2";
8
+ import { logger } from "@better-auth/core/env";
9
+ import { refreshAccessToken } from "@better-auth/core/oauth2";
10
+
11
+ export interface SalesforceProfile {
12
+ sub: string;
13
+ user_id: string;
14
+ organization_id: string;
15
+ preferred_username?: string;
16
+ email: string;
17
+ email_verified?: boolean;
18
+ name: string;
19
+ given_name?: string;
20
+ family_name?: string;
21
+ zoneinfo?: string;
22
+ photos?: {
23
+ picture?: string;
24
+ thumbnail?: string;
25
+ };
26
+ }
27
+
28
+ export interface SalesforceOptions extends ProviderOptions<SalesforceProfile> {
29
+ clientId: string;
30
+ environment?: "sandbox" | "production";
31
+ loginUrl?: string;
32
+ /**
33
+ * Override the redirect URI if auto-detection fails.
34
+ * Should match the Callback URL configured in your Salesforce Connected App.
35
+ * @example "http://localhost:3000/api/auth/callback/salesforce"
36
+ */
37
+ redirectURI?: string;
38
+ }
39
+
40
+ export const salesforce = (options: SalesforceOptions) => {
41
+ const environment = options.environment ?? "production";
42
+ const isSandbox = environment === "sandbox";
43
+ const authorizationEndpoint = options.loginUrl
44
+ ? `https://${options.loginUrl}/services/oauth2/authorize`
45
+ : isSandbox
46
+ ? "https://test.salesforce.com/services/oauth2/authorize"
47
+ : "https://login.salesforce.com/services/oauth2/authorize";
48
+
49
+ const tokenEndpoint = options.loginUrl
50
+ ? `https://${options.loginUrl}/services/oauth2/token`
51
+ : isSandbox
52
+ ? "https://test.salesforce.com/services/oauth2/token"
53
+ : "https://login.salesforce.com/services/oauth2/token";
54
+
55
+ const userInfoEndpoint = options.loginUrl
56
+ ? `https://${options.loginUrl}/services/oauth2/userinfo`
57
+ : isSandbox
58
+ ? "https://test.salesforce.com/services/oauth2/userinfo"
59
+ : "https://login.salesforce.com/services/oauth2/userinfo";
60
+
61
+ return {
62
+ id: "salesforce",
63
+ name: "Salesforce",
64
+
65
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
66
+ if (!options.clientId || !options.clientSecret) {
67
+ logger.error(
68
+ "Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options.",
69
+ );
70
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
71
+ }
72
+ if (!codeVerifier) {
73
+ throw new BetterAuthError("codeVerifier is required for Salesforce");
74
+ }
75
+
76
+ const _scopes = options.disableDefaultScope
77
+ ? []
78
+ : ["openid", "email", "profile"];
79
+ options.scope && _scopes.push(...options.scope);
80
+ scopes && _scopes.push(...scopes);
81
+
82
+ return createAuthorizationURL({
83
+ id: "salesforce",
84
+ options,
85
+ authorizationEndpoint,
86
+ scopes: _scopes,
87
+ state,
88
+ codeVerifier,
89
+ redirectURI: options.redirectURI || redirectURI,
90
+ });
91
+ },
92
+
93
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
94
+ return validateAuthorizationCode({
95
+ code,
96
+ codeVerifier,
97
+ redirectURI: options.redirectURI || redirectURI,
98
+ options,
99
+ tokenEndpoint,
100
+ });
101
+ },
102
+
103
+ refreshAccessToken: options.refreshAccessToken
104
+ ? options.refreshAccessToken
105
+ : async (refreshToken) => {
106
+ return refreshAccessToken({
107
+ refreshToken,
108
+ options: {
109
+ clientId: options.clientId,
110
+ clientSecret: options.clientSecret,
111
+ },
112
+ tokenEndpoint,
113
+ });
114
+ },
115
+
116
+ async getUserInfo(token) {
117
+ if (options.getUserInfo) {
118
+ return options.getUserInfo(token);
119
+ }
120
+
121
+ try {
122
+ const { data: user } = await betterFetch<SalesforceProfile>(
123
+ userInfoEndpoint,
124
+ {
125
+ headers: {
126
+ Authorization: `Bearer ${token.accessToken}`,
127
+ },
128
+ },
129
+ );
130
+
131
+ if (!user) {
132
+ logger.error("Failed to fetch user info from Salesforce");
133
+ return null;
134
+ }
135
+
136
+ const userMap = await options.mapProfileToUser?.(user);
137
+
138
+ return {
139
+ user: {
140
+ id: user.user_id,
141
+ name: user.name,
142
+ email: user.email,
143
+ image: user.photos?.picture || user.photos?.thumbnail,
144
+ emailVerified: user.email_verified ?? false,
145
+ ...userMap,
146
+ },
147
+ data: user,
148
+ };
149
+ } catch (error) {
150
+ logger.error("Failed to fetch user info from Salesforce:", error);
151
+ return null;
152
+ }
153
+ },
154
+
155
+ options,
156
+ } satisfies OAuthProvider<SalesforceProfile>;
157
+ };