@better-auth/core 1.3.26 → 1.3.28

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 (130) hide show
  1. package/.turbo/turbo-build.log +60 -9
  2. package/build.config.ts +7 -0
  3. package/dist/db/adapter/index.cjs +2 -0
  4. package/dist/db/adapter/index.d.cts +14 -0
  5. package/dist/db/adapter/index.d.mts +14 -0
  6. package/dist/db/adapter/index.d.ts +14 -0
  7. package/dist/db/adapter/index.mjs +1 -0
  8. package/dist/db/index.cjs +89 -0
  9. package/dist/db/index.d.cts +16 -107
  10. package/dist/db/index.d.mts +16 -107
  11. package/dist/db/index.d.ts +16 -107
  12. package/dist/db/index.mjs +69 -0
  13. package/dist/env/index.cjs +312 -0
  14. package/dist/env/index.d.cts +36 -0
  15. package/dist/env/index.d.mts +36 -0
  16. package/dist/env/index.d.ts +36 -0
  17. package/dist/env/index.mjs +297 -0
  18. package/dist/error/index.cjs +44 -0
  19. package/dist/error/index.d.cts +33 -0
  20. package/dist/error/index.d.mts +33 -0
  21. package/dist/error/index.d.ts +33 -0
  22. package/dist/error/index.mjs +41 -0
  23. package/dist/index.d.cts +179 -1
  24. package/dist/index.d.mts +179 -1
  25. package/dist/index.d.ts +179 -1
  26. package/dist/middleware/index.cjs +25 -0
  27. package/dist/middleware/index.d.cts +14 -0
  28. package/dist/middleware/index.d.mts +14 -0
  29. package/dist/middleware/index.d.ts +14 -0
  30. package/dist/middleware/index.mjs +21 -0
  31. package/dist/oauth2/index.cjs +368 -0
  32. package/dist/oauth2/index.d.cts +100 -0
  33. package/dist/oauth2/index.d.mts +100 -0
  34. package/dist/oauth2/index.d.ts +100 -0
  35. package/dist/oauth2/index.mjs +357 -0
  36. package/dist/shared/core.BJPBStdk.d.ts +1693 -0
  37. package/dist/shared/core.Bl6TpxyD.d.mts +181 -0
  38. package/dist/shared/core.Bqe5IGAi.d.ts +13 -0
  39. package/dist/shared/core.BwoNUcJQ.d.cts +53 -0
  40. package/dist/shared/core.BwoNUcJQ.d.mts +53 -0
  41. package/dist/shared/core.BwoNUcJQ.d.ts +53 -0
  42. package/dist/shared/core.CajxAutx.d.cts +143 -0
  43. package/dist/shared/core.CajxAutx.d.mts +143 -0
  44. package/dist/shared/core.CajxAutx.d.ts +143 -0
  45. package/dist/shared/core.CkkLHQWc.d.mts +1693 -0
  46. package/dist/shared/core.DkdZ1o38.d.ts +181 -0
  47. package/dist/shared/core.Dl-70uns.d.cts +84 -0
  48. package/dist/shared/core.Dl-70uns.d.mts +84 -0
  49. package/dist/shared/core.Dl-70uns.d.ts +84 -0
  50. package/dist/shared/core.DyEdx0m7.d.cts +181 -0
  51. package/dist/shared/core.E9DfzGLz.d.mts +13 -0
  52. package/dist/shared/core.HqYn20Fi.d.cts +13 -0
  53. package/dist/shared/core.gYIBmdi1.d.cts +1693 -0
  54. package/dist/social-providers/index.cjs +2793 -0
  55. package/dist/social-providers/index.d.cts +3903 -0
  56. package/dist/social-providers/index.d.mts +3903 -0
  57. package/dist/social-providers/index.d.ts +3903 -0
  58. package/dist/social-providers/index.mjs +2743 -0
  59. package/dist/utils/index.cjs +7 -0
  60. package/dist/utils/index.d.cts +10 -0
  61. package/dist/utils/index.d.mts +10 -0
  62. package/dist/utils/index.d.ts +10 -0
  63. package/dist/utils/index.mjs +5 -0
  64. package/package.json +109 -2
  65. package/src/db/adapter/index.ts +448 -0
  66. package/src/db/index.ts +13 -0
  67. package/src/db/plugin.ts +11 -0
  68. package/src/db/schema/account.ts +34 -0
  69. package/src/db/schema/rate-limit.ts +21 -0
  70. package/src/db/schema/session.ts +17 -0
  71. package/src/db/schema/shared.ts +7 -0
  72. package/src/db/schema/user.ts +16 -0
  73. package/src/db/schema/verification.ts +15 -0
  74. package/src/db/type.ts +50 -0
  75. package/src/env/color-depth.ts +172 -0
  76. package/src/env/env-impl.ts +123 -0
  77. package/src/env/index.ts +23 -0
  78. package/src/env/logger.test.ts +33 -0
  79. package/src/env/logger.ts +145 -0
  80. package/src/error/codes.ts +31 -0
  81. package/src/error/index.ts +11 -0
  82. package/src/index.ts +1 -1
  83. package/src/middleware/index.ts +33 -0
  84. package/src/oauth2/client-credentials-token.ts +102 -0
  85. package/src/oauth2/create-authorization-url.ts +85 -0
  86. package/src/oauth2/index.ts +22 -0
  87. package/src/oauth2/oauth-provider.ts +194 -0
  88. package/src/oauth2/refresh-access-token.ts +124 -0
  89. package/src/oauth2/utils.ts +36 -0
  90. package/src/oauth2/validate-authorization-code.ts +156 -0
  91. package/src/social-providers/apple.ts +213 -0
  92. package/src/social-providers/atlassian.ts +130 -0
  93. package/src/social-providers/cognito.ts +269 -0
  94. package/src/social-providers/discord.ts +172 -0
  95. package/src/social-providers/dropbox.ts +112 -0
  96. package/src/social-providers/facebook.ts +204 -0
  97. package/src/social-providers/figma.ts +115 -0
  98. package/src/social-providers/github.ts +154 -0
  99. package/src/social-providers/gitlab.ts +152 -0
  100. package/src/social-providers/google.ts +171 -0
  101. package/src/social-providers/huggingface.ts +116 -0
  102. package/src/social-providers/index.ts +118 -0
  103. package/src/social-providers/kakao.ts +178 -0
  104. package/src/social-providers/kick.ts +95 -0
  105. package/src/social-providers/line.ts +169 -0
  106. package/src/social-providers/linear.ts +120 -0
  107. package/src/social-providers/linkedin.ts +110 -0
  108. package/src/social-providers/microsoft-entra-id.ts +243 -0
  109. package/src/social-providers/naver.ts +112 -0
  110. package/src/social-providers/notion.ts +106 -0
  111. package/src/social-providers/paypal.ts +261 -0
  112. package/src/social-providers/reddit.ts +122 -0
  113. package/src/social-providers/roblox.ts +110 -0
  114. package/src/social-providers/salesforce.ts +157 -0
  115. package/src/social-providers/slack.ts +114 -0
  116. package/src/social-providers/spotify.ts +93 -0
  117. package/src/social-providers/tiktok.ts +211 -0
  118. package/src/social-providers/twitch.ts +111 -0
  119. package/src/social-providers/twitter.ts +194 -0
  120. package/src/social-providers/vk.ts +128 -0
  121. package/src/social-providers/zoom.ts +218 -0
  122. package/src/types/context.ts +313 -0
  123. package/src/types/cookie.ts +7 -0
  124. package/src/types/helper.ts +5 -0
  125. package/src/types/index.ts +20 -1
  126. package/src/types/init-options.ts +1161 -0
  127. package/src/types/plugin-client.ts +69 -0
  128. package/src/types/plugin.ts +134 -0
  129. package/src/utils/error-codes.ts +51 -0
  130. package/src/utils/index.ts +1 -0
@@ -0,0 +1,112 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ validateAuthorizationCode,
6
+ refreshAccessToken,
7
+ } from "@better-auth/core/oauth2";
8
+
9
+ export interface NaverProfile {
10
+ /** API response result code */
11
+ resultcode: string;
12
+ /** API response message */
13
+ message: string;
14
+ response: {
15
+ /** Unique Naver user identifier */
16
+ id: string;
17
+ /** User nickname */
18
+ nickname: string;
19
+ /** User real name */
20
+ name: string;
21
+ /** User email address */
22
+ email: string;
23
+ /** Gender (F: female, M: male, U: unknown) */
24
+ gender: string;
25
+ /** Age range */
26
+ age: string;
27
+ /** Birthday (MM-DD format) */
28
+ birthday: string;
29
+ /** Birth year */
30
+ birthyear: string;
31
+ /** Profile image URL */
32
+ profile_image: string;
33
+ /** Mobile phone number */
34
+ mobile: string;
35
+ };
36
+ }
37
+
38
+ export interface NaverOptions extends ProviderOptions<NaverProfile> {
39
+ clientId: string;
40
+ }
41
+
42
+ export const naver = (options: NaverOptions) => {
43
+ return {
44
+ id: "naver",
45
+ name: "Naver",
46
+ createAuthorizationURL({ state, scopes, redirectURI }) {
47
+ const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
48
+ options.scope && _scopes.push(...options.scope);
49
+ scopes && _scopes.push(...scopes);
50
+ return createAuthorizationURL({
51
+ id: "naver",
52
+ options,
53
+ authorizationEndpoint: "https://nid.naver.com/oauth2.0/authorize",
54
+ scopes: _scopes,
55
+ state,
56
+ redirectURI,
57
+ });
58
+ },
59
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
60
+ return validateAuthorizationCode({
61
+ code,
62
+ redirectURI,
63
+ options,
64
+ tokenEndpoint: "https://nid.naver.com/oauth2.0/token",
65
+ });
66
+ },
67
+ refreshAccessToken: options.refreshAccessToken
68
+ ? options.refreshAccessToken
69
+ : async (refreshToken) => {
70
+ return refreshAccessToken({
71
+ refreshToken,
72
+ options: {
73
+ clientId: options.clientId,
74
+ clientKey: options.clientKey,
75
+ clientSecret: options.clientSecret,
76
+ },
77
+ tokenEndpoint: "https://nid.naver.com/oauth2.0/token",
78
+ });
79
+ },
80
+ async getUserInfo(token) {
81
+ if (options.getUserInfo) {
82
+ return options.getUserInfo(token);
83
+ }
84
+ const { data: profile, error } = await betterFetch<NaverProfile>(
85
+ "https://openapi.naver.com/v1/nid/me",
86
+ {
87
+ headers: {
88
+ Authorization: `Bearer ${token.accessToken}`,
89
+ },
90
+ },
91
+ );
92
+ if (error || !profile || profile.resultcode !== "00") {
93
+ return null;
94
+ }
95
+ const userMap = await options.mapProfileToUser?.(profile);
96
+ const res = profile.response || {};
97
+ const user = {
98
+ id: res.id,
99
+ name: res.name || res.nickname,
100
+ email: res.email,
101
+ image: res.profile_image,
102
+ emailVerified: false,
103
+ ...userMap,
104
+ };
105
+ return {
106
+ user,
107
+ data: profile,
108
+ };
109
+ },
110
+ options,
111
+ } satisfies OAuthProvider<NaverProfile>;
112
+ };
@@ -0,0 +1,106 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ refreshAccessToken,
6
+ validateAuthorizationCode,
7
+ } from "@better-auth/core/oauth2";
8
+
9
+ export interface NotionProfile {
10
+ object: "user";
11
+ id: string;
12
+ type: "person" | "bot";
13
+ name?: string;
14
+ avatar_url?: string;
15
+ person?: {
16
+ email?: string;
17
+ };
18
+ }
19
+
20
+ export interface NotionOptions extends ProviderOptions<NotionProfile> {
21
+ clientId: string;
22
+ }
23
+
24
+ export const notion = (options: NotionOptions) => {
25
+ const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
26
+ return {
27
+ id: "notion",
28
+ name: "Notion",
29
+ createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
30
+ const _scopes: string[] = options.disableDefaultScope ? [] : [];
31
+ options.scope && _scopes.push(...options.scope);
32
+ scopes && _scopes.push(...scopes);
33
+ return createAuthorizationURL({
34
+ id: "notion",
35
+ options,
36
+ authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
37
+ scopes: _scopes,
38
+ state,
39
+ redirectURI,
40
+ loginHint,
41
+ additionalParams: {
42
+ owner: "user",
43
+ },
44
+ });
45
+ },
46
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
47
+ return validateAuthorizationCode({
48
+ code,
49
+ redirectURI,
50
+ options,
51
+ tokenEndpoint,
52
+ authentication: "basic",
53
+ });
54
+ },
55
+ refreshAccessToken: options.refreshAccessToken
56
+ ? options.refreshAccessToken
57
+ : async (refreshToken) => {
58
+ return refreshAccessToken({
59
+ refreshToken,
60
+ options: {
61
+ clientId: options.clientId,
62
+ clientKey: options.clientKey,
63
+ clientSecret: options.clientSecret,
64
+ },
65
+ tokenEndpoint,
66
+ });
67
+ },
68
+ async getUserInfo(token) {
69
+ if (options.getUserInfo) {
70
+ return options.getUserInfo(token);
71
+ }
72
+ const { data: profile, error } = await betterFetch<{
73
+ bot: {
74
+ owner: {
75
+ user: NotionProfile;
76
+ };
77
+ };
78
+ }>("https://api.notion.com/v1/users/me", {
79
+ headers: {
80
+ Authorization: `Bearer ${token.accessToken}`,
81
+ "Notion-Version": "2022-06-28",
82
+ },
83
+ });
84
+ if (error || !profile) {
85
+ return null;
86
+ }
87
+ const userProfile = profile.bot?.owner?.user;
88
+ if (!userProfile) {
89
+ return null;
90
+ }
91
+ const userMap = await options.mapProfileToUser?.(userProfile);
92
+ return {
93
+ user: {
94
+ id: userProfile.id,
95
+ name: userProfile.name || "Notion User",
96
+ email: userProfile.person?.email || null,
97
+ image: userProfile.avatar_url,
98
+ emailVerified: !!userProfile.person?.email,
99
+ ...userMap,
100
+ },
101
+ data: userProfile,
102
+ };
103
+ },
104
+ options,
105
+ } satisfies OAuthProvider<NotionProfile>;
106
+ };
@@ -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
+ };