@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,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
+ };
@@ -0,0 +1,114 @@
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 SlackProfile extends Record<string, any> {
9
+ ok: boolean;
10
+ sub: string;
11
+ "https://slack.com/user_id": string;
12
+ "https://slack.com/team_id": string;
13
+ email: string;
14
+ email_verified: boolean;
15
+ date_email_verified: number;
16
+ name: string;
17
+ picture: string;
18
+ given_name: string;
19
+ family_name: string;
20
+ locale: string;
21
+ "https://slack.com/team_name": string;
22
+ "https://slack.com/team_domain": string;
23
+ "https://slack.com/user_image_24": string;
24
+ "https://slack.com/user_image_32": string;
25
+ "https://slack.com/user_image_48": string;
26
+ "https://slack.com/user_image_72": string;
27
+ "https://slack.com/user_image_192": string;
28
+ "https://slack.com/user_image_512": string;
29
+ "https://slack.com/team_image_34": string;
30
+ "https://slack.com/team_image_44": string;
31
+ "https://slack.com/team_image_68": string;
32
+ "https://slack.com/team_image_88": string;
33
+ "https://slack.com/team_image_102": string;
34
+ "https://slack.com/team_image_132": string;
35
+ "https://slack.com/team_image_230": string;
36
+ "https://slack.com/team_image_default": boolean;
37
+ }
38
+
39
+ export interface SlackOptions extends ProviderOptions<SlackProfile> {
40
+ clientId: string;
41
+ }
42
+
43
+ export const slack = (options: SlackOptions) => {
44
+ return {
45
+ id: "slack",
46
+ name: "Slack",
47
+ createAuthorizationURL({ state, scopes, redirectURI }) {
48
+ const _scopes = options.disableDefaultScope
49
+ ? []
50
+ : ["openid", "profile", "email"];
51
+ scopes && _scopes.push(...scopes);
52
+ options.scope && _scopes.push(...options.scope);
53
+ const url = new URL("https://slack.com/openid/connect/authorize");
54
+ url.searchParams.set("scope", _scopes.join(" "));
55
+ url.searchParams.set("response_type", "code");
56
+ url.searchParams.set("client_id", options.clientId);
57
+ url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
58
+ url.searchParams.set("state", state);
59
+ return url;
60
+ },
61
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
62
+ return validateAuthorizationCode({
63
+ code,
64
+ redirectURI,
65
+ options,
66
+ tokenEndpoint: "https://slack.com/api/openid.connect.token",
67
+ });
68
+ },
69
+ refreshAccessToken: options.refreshAccessToken
70
+ ? options.refreshAccessToken
71
+ : async (refreshToken) => {
72
+ return refreshAccessToken({
73
+ refreshToken,
74
+ options: {
75
+ clientId: options.clientId,
76
+ clientKey: options.clientKey,
77
+ clientSecret: options.clientSecret,
78
+ },
79
+ tokenEndpoint: "https://slack.com/api/openid.connect.token",
80
+ });
81
+ },
82
+ async getUserInfo(token) {
83
+ if (options.getUserInfo) {
84
+ return options.getUserInfo(token);
85
+ }
86
+ const { data: profile, error } = await betterFetch<SlackProfile>(
87
+ "https://slack.com/api/openid.connect.userInfo",
88
+ {
89
+ headers: {
90
+ authorization: `Bearer ${token.accessToken}`,
91
+ },
92
+ },
93
+ );
94
+
95
+ if (error) {
96
+ return null;
97
+ }
98
+
99
+ const userMap = await options.mapProfileToUser?.(profile);
100
+ return {
101
+ user: {
102
+ id: profile["https://slack.com/user_id"],
103
+ name: profile.name || "",
104
+ email: profile.email,
105
+ emailVerified: profile.email_verified,
106
+ image: profile.picture || profile["https://slack.com/user_image_512"],
107
+ ...userMap,
108
+ },
109
+ data: profile,
110
+ };
111
+ },
112
+ options,
113
+ } satisfies OAuthProvider<SlackProfile>;
114
+ };
@@ -0,0 +1,93 @@
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 SpotifyProfile {
10
+ id: string;
11
+ display_name: string;
12
+ email: string;
13
+ images: {
14
+ url: string;
15
+ }[];
16
+ }
17
+
18
+ export interface SpotifyOptions extends ProviderOptions<SpotifyProfile> {
19
+ clientId: string;
20
+ }
21
+
22
+ export const spotify = (options: SpotifyOptions) => {
23
+ return {
24
+ id: "spotify",
25
+ name: "Spotify",
26
+ createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
27
+ const _scopes = options.disableDefaultScope ? [] : ["user-read-email"];
28
+ options.scope && _scopes.push(...options.scope);
29
+ scopes && _scopes.push(...scopes);
30
+ return createAuthorizationURL({
31
+ id: "spotify",
32
+ options,
33
+ authorizationEndpoint: "https://accounts.spotify.com/authorize",
34
+ scopes: _scopes,
35
+ state,
36
+ codeVerifier,
37
+ redirectURI,
38
+ });
39
+ },
40
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
41
+ return validateAuthorizationCode({
42
+ code,
43
+ codeVerifier,
44
+ redirectURI,
45
+ options,
46
+ tokenEndpoint: "https://accounts.spotify.com/api/token",
47
+ });
48
+ },
49
+ refreshAccessToken: options.refreshAccessToken
50
+ ? options.refreshAccessToken
51
+ : async (refreshToken) => {
52
+ return refreshAccessToken({
53
+ refreshToken,
54
+ options: {
55
+ clientId: options.clientId,
56
+ clientKey: options.clientKey,
57
+ clientSecret: options.clientSecret,
58
+ },
59
+ tokenEndpoint: "https://accounts.spotify.com/api/token",
60
+ });
61
+ },
62
+ async getUserInfo(token) {
63
+ if (options.getUserInfo) {
64
+ return options.getUserInfo(token);
65
+ }
66
+ const { data: profile, error } = await betterFetch<SpotifyProfile>(
67
+ "https://api.spotify.com/v1/me",
68
+ {
69
+ method: "GET",
70
+ headers: {
71
+ Authorization: `Bearer ${token.accessToken}`,
72
+ },
73
+ },
74
+ );
75
+ if (error) {
76
+ return null;
77
+ }
78
+ const userMap = await options.mapProfileToUser?.(profile);
79
+ return {
80
+ user: {
81
+ id: profile.id,
82
+ name: profile.display_name,
83
+ email: profile.email,
84
+ image: profile.images[0]?.url,
85
+ emailVerified: false,
86
+ ...userMap,
87
+ },
88
+ data: profile,
89
+ };
90
+ },
91
+ options,
92
+ } satisfies OAuthProvider<SpotifyProfile>;
93
+ };
@@ -0,0 +1,211 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ refreshAccessToken,
5
+ validateAuthorizationCode,
6
+ } from "@better-auth/core/oauth2";
7
+
8
+ /**
9
+ * [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)
10
+ */
11
+ export interface TiktokProfile extends Record<string, any> {
12
+ data: {
13
+ user: {
14
+ /**
15
+ * The unique identification of the user in the current application.Open id
16
+ * for the client.
17
+ *
18
+ * To return this field, add `fields=open_id` in the user profile request's query parameter.
19
+ */
20
+ open_id: string;
21
+ /**
22
+ * The unique identification of the user across different apps for the same developer.
23
+ * For example, if a partner has X number of clients,
24
+ * it will get X number of open_id for the same TikTok user,
25
+ * but one persistent union_id for the particular user.
26
+ *
27
+ * To return this field, add `fields=union_id` in the user profile request's query parameter.
28
+ */
29
+ union_id?: string;
30
+ /**
31
+ * User's profile image.
32
+ *
33
+ * To return this field, add `fields=avatar_url` in the user profile request's query parameter.
34
+ */
35
+ avatar_url?: string;
36
+ /**
37
+ * User`s profile image in 100x100 size.
38
+ *
39
+ * To return this field, add `fields=avatar_url_100` in the user profile request's query parameter.
40
+ */
41
+ avatar_url_100?: string;
42
+ /**
43
+ * User's profile image with higher resolution
44
+ *
45
+ * To return this field, add `fields=avatar_url_100` in the user profile request's query parameter.
46
+ */
47
+ avatar_large_url: string;
48
+ /**
49
+ * User's profile name
50
+ *
51
+ * To return this field, add `fields=display_name` in the user profile request's query parameter.
52
+ */
53
+ display_name: string;
54
+ /**
55
+ * User's username.
56
+ *
57
+ * To return this field, add `fields=username` in the user profile request's query parameter.
58
+ */
59
+ username: string;
60
+ /** @note Email is currently unsupported by TikTok */
61
+ email?: string;
62
+ /**
63
+ * User's bio description if there is a valid one.
64
+ *
65
+ * To return this field, add `fields=bio_description` in the user profile request's query parameter.
66
+ */
67
+ bio_description?: string;
68
+ /**
69
+ * The link to user's TikTok profile page.
70
+ *
71
+ * To return this field, add `fields=profile_deep_link` in the user profile request's query parameter.
72
+ */
73
+ profile_deep_link?: string;
74
+ /**
75
+ * Whether TikTok has provided a verified badge to the account after confirming
76
+ * that it belongs to the user it represents.
77
+ *
78
+ * To return this field, add `fields=is_verified` in the user profile request's query parameter.
79
+ */
80
+ is_verified?: boolean;
81
+ /**
82
+ * User's followers count.
83
+ *
84
+ * To return this field, add `fields=follower_count` in the user profile request's query parameter.
85
+ */
86
+ follower_count?: number;
87
+ /**
88
+ * The number of accounts that the user is following.
89
+ *
90
+ * To return this field, add `fields=following_count` in the user profile request's query parameter.
91
+ */
92
+ following_count?: number;
93
+ /**
94
+ * The total number of likes received by the user across all of their videos.
95
+ *
96
+ * To return this field, add `fields=likes_count` in the user profile request's query parameter.
97
+ */
98
+ likes_count?: number;
99
+ /**
100
+ * The total number of publicly posted videos by the user.
101
+ *
102
+ * To return this field, add `fields=video_count` in the user profile request's query parameter.
103
+ */
104
+ video_count?: number;
105
+ };
106
+ };
107
+ error?: {
108
+ /**
109
+ * The error category in string.
110
+ */
111
+ code?: string;
112
+ /**
113
+ * The error message in string.
114
+ */
115
+ message?: string;
116
+ /**
117
+ * The error message in string.
118
+ */
119
+ log_id?: string;
120
+ };
121
+ }
122
+
123
+ export interface TiktokOptions extends ProviderOptions {
124
+ // Client ID is not used in TikTok, we delete it from the options
125
+ clientId?: never;
126
+ clientSecret: string;
127
+ clientKey: string;
128
+ }
129
+
130
+ export const tiktok = (options: TiktokOptions) => {
131
+ return {
132
+ id: "tiktok",
133
+ name: "TikTok",
134
+ createAuthorizationURL({ state, scopes, redirectURI }) {
135
+ const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
136
+ options.scope && _scopes.push(...options.scope);
137
+ 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
+ );
145
+ },
146
+
147
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
148
+ return validateAuthorizationCode({
149
+ code,
150
+ redirectURI: options.redirectURI || redirectURI,
151
+ options: {
152
+ clientKey: options.clientKey,
153
+ clientSecret: options.clientSecret,
154
+ },
155
+ tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
156
+ });
157
+ },
158
+ refreshAccessToken: options.refreshAccessToken
159
+ ? options.refreshAccessToken
160
+ : async (refreshToken) => {
161
+ return refreshAccessToken({
162
+ refreshToken,
163
+ options: {
164
+ clientSecret: options.clientSecret,
165
+ },
166
+ tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
167
+ authentication: "post",
168
+ extraParams: {
169
+ client_key: options.clientKey,
170
+ },
171
+ });
172
+ },
173
+ async getUserInfo(token) {
174
+ if (options.getUserInfo) {
175
+ return options.getUserInfo(token);
176
+ }
177
+
178
+ const fields = [
179
+ "open_id",
180
+ "avatar_large_url",
181
+ "display_name",
182
+ "username",
183
+ ];
184
+ const { data: profile, error } = await betterFetch<TiktokProfile>(
185
+ `https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`,
186
+ {
187
+ headers: {
188
+ authorization: `Bearer ${token.accessToken}`,
189
+ },
190
+ },
191
+ );
192
+
193
+ if (error) {
194
+ return null;
195
+ }
196
+
197
+ return {
198
+ user: {
199
+ email: profile.data.user.email || profile.data.user.username,
200
+ id: profile.data.user.open_id,
201
+ name: profile.data.user.display_name || profile.data.user.username,
202
+ image: profile.data.user.avatar_large_url,
203
+ /** @note Tiktok does not provide emailVerified or even email*/
204
+ emailVerified: profile.data.user.email ? true : false,
205
+ },
206
+ data: profile,
207
+ };
208
+ },
209
+ options,
210
+ } satisfies OAuthProvider<TiktokProfile, TiktokOptions>;
211
+ };
@@ -0,0 +1,111 @@
1
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
2
+ import { logger } from "@better-auth/core/env";
3
+ import {
4
+ createAuthorizationURL,
5
+ validateAuthorizationCode,
6
+ refreshAccessToken,
7
+ } from "@better-auth/core/oauth2";
8
+ import { decodeJwt } from "jose";
9
+
10
+ /**
11
+ * @see https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#requesting-claims
12
+ */
13
+ export interface TwitchProfile {
14
+ /**
15
+ * The sub of the user
16
+ */
17
+ sub: string;
18
+ /**
19
+ * The preferred username of the user
20
+ */
21
+ preferred_username: string;
22
+ /**
23
+ * The email of the user
24
+ */
25
+ email: string;
26
+ /**
27
+ * Indicate if this user has a verified email.
28
+ */
29
+ email_verified: boolean;
30
+ /**
31
+ * The picture of the user
32
+ */
33
+ picture: string;
34
+ }
35
+
36
+ export interface TwitchOptions extends ProviderOptions<TwitchProfile> {
37
+ clientId: string;
38
+ claims?: string[];
39
+ }
40
+ export const twitch = (options: TwitchOptions) => {
41
+ return {
42
+ id: "twitch",
43
+ name: "Twitch",
44
+ createAuthorizationURL({ state, scopes, redirectURI }) {
45
+ const _scopes = options.disableDefaultScope
46
+ ? []
47
+ : ["user:read:email", "openid"];
48
+ options.scope && _scopes.push(...options.scope);
49
+ scopes && _scopes.push(...scopes);
50
+ return createAuthorizationURL({
51
+ id: "twitch",
52
+ redirectURI,
53
+ options,
54
+ authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
55
+ scopes: _scopes,
56
+ state,
57
+ claims: options.claims || [
58
+ "email",
59
+ "email_verified",
60
+ "preferred_username",
61
+ "picture",
62
+ ],
63
+ });
64
+ },
65
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
66
+ return validateAuthorizationCode({
67
+ code,
68
+ redirectURI,
69
+ options,
70
+ tokenEndpoint: "https://id.twitch.tv/oauth2/token",
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
+ tokenEndpoint: "https://id.twitch.tv/oauth2/token",
84
+ });
85
+ },
86
+ async getUserInfo(token) {
87
+ if (options.getUserInfo) {
88
+ return options.getUserInfo(token);
89
+ }
90
+ const idToken = token.idToken;
91
+ if (!idToken) {
92
+ logger.error("No idToken found in token");
93
+ return null;
94
+ }
95
+ const profile = decodeJwt(idToken) as TwitchProfile;
96
+ const userMap = await options.mapProfileToUser?.(profile);
97
+ return {
98
+ user: {
99
+ id: profile.sub,
100
+ name: profile.preferred_username,
101
+ email: profile.email,
102
+ image: profile.picture,
103
+ emailVerified: profile.email_verified,
104
+ ...userMap,
105
+ },
106
+ data: profile,
107
+ };
108
+ },
109
+ options,
110
+ } satisfies OAuthProvider<TwitchProfile>;
111
+ };