@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,172 @@
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
+ export interface DiscordProfile extends Record<string, any> {
8
+ /** the user's id (i.e. the numerical snowflake) */
9
+ id: string;
10
+ /** the user's username, not unique across the platform */
11
+ username: string;
12
+ /** the user's Discord-tag */
13
+ discriminator: string;
14
+ /** the user's display name, if it is set */
15
+ global_name: string | null;
16
+ /**
17
+ * the user's avatar hash:
18
+ * https://discord.com/developers/docs/reference#image-formatting
19
+ */
20
+ avatar: string | null;
21
+ /** whether the user belongs to an OAuth2 application */
22
+ bot?: boolean;
23
+ /**
24
+ * whether the user is an Official Discord System user (part of the urgent
25
+ * message system)
26
+ */
27
+ system?: boolean;
28
+ /** whether the user has two factor enabled on their account */
29
+ mfa_enabled: boolean;
30
+ /**
31
+ * the user's banner hash:
32
+ * https://discord.com/developers/docs/reference#image-formatting
33
+ */
34
+ banner: string | null;
35
+
36
+ /** the user's banner color encoded as an integer representation of hexadecimal color code */
37
+ accent_color: number | null;
38
+
39
+ /**
40
+ * the user's chosen language option:
41
+ * https://discord.com/developers/docs/reference#locales
42
+ */
43
+ locale: string;
44
+ /** whether the email on this account has been verified */
45
+ verified: boolean;
46
+ /** the user's email */
47
+ email: string;
48
+ /**
49
+ * the flags on a user's account:
50
+ * https://discord.com/developers/docs/resources/user#user-object-user-flags
51
+ */
52
+ flags: number;
53
+ /**
54
+ * the type of Nitro subscription on a user's account:
55
+ * https://discord.com/developers/docs/resources/user#user-object-premium-types
56
+ */
57
+ premium_type: number;
58
+ /**
59
+ * the public flags on a user's account:
60
+ * https://discord.com/developers/docs/resources/user#user-object-user-flags
61
+ */
62
+ public_flags: number;
63
+ /** undocumented field; corresponds to the user's custom nickname */
64
+ display_name: string | null;
65
+ /**
66
+ * undocumented field; corresponds to the Discord feature where you can e.g.
67
+ * put your avatar inside of an ice cube
68
+ */
69
+ avatar_decoration: string | null;
70
+ /**
71
+ * undocumented field; corresponds to the premium feature where you can
72
+ * select a custom banner color
73
+ */
74
+ banner_color: string | null;
75
+ /** undocumented field; the CDN URL of their profile picture */
76
+ image_url: string;
77
+ }
78
+
79
+ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
80
+ clientId: string;
81
+ prompt?: "none" | "consent";
82
+ permissions?: number;
83
+ }
84
+
85
+ export const discord = (options: DiscordOptions) => {
86
+ return {
87
+ id: "discord",
88
+ name: "Discord",
89
+ createAuthorizationURL({ state, scopes, redirectURI }) {
90
+ const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
91
+ scopes && _scopes.push(...scopes);
92
+ options.scope && _scopes.push(...options.scope);
93
+ const hasBotScope = _scopes.includes("bot");
94
+ const permissionsParam =
95
+ hasBotScope && options.permissions !== undefined
96
+ ? `&permissions=${options.permissions}`
97
+ : "";
98
+ return new URL(
99
+ `https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
100
+ "+",
101
+ )}&response_type=code&client_id=${
102
+ options.clientId
103
+ }&redirect_uri=${encodeURIComponent(
104
+ options.redirectURI || redirectURI,
105
+ )}&state=${state}&prompt=${
106
+ options.prompt || "none"
107
+ }${permissionsParam}`,
108
+ );
109
+ },
110
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
111
+ return validateAuthorizationCode({
112
+ code,
113
+ redirectURI,
114
+ options,
115
+ tokenEndpoint: "https://discord.com/api/oauth2/token",
116
+ });
117
+ },
118
+ refreshAccessToken: options.refreshAccessToken
119
+ ? options.refreshAccessToken
120
+ : async (refreshToken) => {
121
+ return refreshAccessToken({
122
+ refreshToken,
123
+ options: {
124
+ clientId: options.clientId,
125
+ clientKey: options.clientKey,
126
+ clientSecret: options.clientSecret,
127
+ },
128
+ tokenEndpoint: "https://discord.com/api/oauth2/token",
129
+ });
130
+ },
131
+ async getUserInfo(token) {
132
+ if (options.getUserInfo) {
133
+ return options.getUserInfo(token);
134
+ }
135
+ const { data: profile, error } = await betterFetch<DiscordProfile>(
136
+ "https://discord.com/api/users/@me",
137
+ {
138
+ headers: {
139
+ authorization: `Bearer ${token.accessToken}`,
140
+ },
141
+ },
142
+ );
143
+
144
+ if (error) {
145
+ return null;
146
+ }
147
+ if (profile.avatar === null) {
148
+ const defaultAvatarNumber =
149
+ profile.discriminator === "0"
150
+ ? Number(BigInt(profile.id) >> BigInt(22)) % 6
151
+ : parseInt(profile.discriminator) % 5;
152
+ profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`;
153
+ } else {
154
+ const format = profile.avatar.startsWith("a_") ? "gif" : "png";
155
+ profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
156
+ }
157
+ const userMap = await options.mapProfileToUser?.(profile);
158
+ return {
159
+ user: {
160
+ id: profile.id,
161
+ name: profile.global_name || profile.username || "",
162
+ email: profile.email,
163
+ emailVerified: profile.verified,
164
+ image: profile.image_url,
165
+ ...userMap,
166
+ },
167
+ data: profile,
168
+ };
169
+ },
170
+ options,
171
+ } satisfies OAuthProvider<DiscordProfile>;
172
+ };
@@ -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
+ refreshAccessToken,
6
+ validateAuthorizationCode,
7
+ } from "@better-auth/core/oauth2";
8
+
9
+ export interface DropboxProfile {
10
+ account_id: string;
11
+ name: {
12
+ given_name: string;
13
+ surname: string;
14
+ familiar_name: string;
15
+ display_name: string;
16
+ abbreviated_name: string;
17
+ };
18
+ email: string;
19
+ email_verified: boolean;
20
+ profile_photo_url: string;
21
+ }
22
+
23
+ export interface DropboxOptions extends ProviderOptions<DropboxProfile> {
24
+ clientId: string;
25
+ accessType?: "offline" | "online" | "legacy";
26
+ }
27
+
28
+ export const dropbox = (options: DropboxOptions) => {
29
+ const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
30
+
31
+ return {
32
+ id: "dropbox",
33
+ name: "Dropbox",
34
+ createAuthorizationURL: async ({
35
+ state,
36
+ scopes,
37
+ codeVerifier,
38
+ redirectURI,
39
+ }) => {
40
+ const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
41
+ options.scope && _scopes.push(...options.scope);
42
+ scopes && _scopes.push(...scopes);
43
+ const additionalParams: Record<string, string> = {};
44
+ if (options.accessType) {
45
+ additionalParams.token_access_type = options.accessType;
46
+ }
47
+ return await createAuthorizationURL({
48
+ id: "dropbox",
49
+ options,
50
+ authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
51
+ scopes: _scopes,
52
+ state,
53
+ redirectURI,
54
+ codeVerifier,
55
+ additionalParams,
56
+ });
57
+ },
58
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
59
+ return await validateAuthorizationCode({
60
+ code,
61
+ codeVerifier,
62
+ redirectURI,
63
+ options,
64
+ tokenEndpoint,
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://api.dropbox.com/oauth2/token",
78
+ });
79
+ },
80
+ async getUserInfo(token) {
81
+ if (options.getUserInfo) {
82
+ return options.getUserInfo(token);
83
+ }
84
+ const { data: profile, error } = await betterFetch<DropboxProfile>(
85
+ "https://api.dropboxapi.com/2/users/get_current_account",
86
+ {
87
+ method: "POST",
88
+ headers: {
89
+ Authorization: `Bearer ${token.accessToken}`,
90
+ },
91
+ },
92
+ );
93
+
94
+ if (error) {
95
+ return null;
96
+ }
97
+ const userMap = await options.mapProfileToUser?.(profile);
98
+ return {
99
+ user: {
100
+ id: profile.account_id,
101
+ name: profile.name?.display_name,
102
+ email: profile.email,
103
+ emailVerified: profile.email_verified || false,
104
+ image: profile.profile_photo_url,
105
+ ...userMap,
106
+ },
107
+ data: profile,
108
+ };
109
+ },
110
+ options,
111
+ } satisfies OAuthProvider<DropboxProfile>;
112
+ };
@@ -0,0 +1,204 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
3
+ import {
4
+ createAuthorizationURL,
5
+ validateAuthorizationCode,
6
+ } from "@better-auth/core/oauth2";
7
+ import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose";
8
+ import { refreshAccessToken } from "@better-auth/core/oauth2";
9
+ export interface FacebookProfile {
10
+ id: string;
11
+ name: string;
12
+ email: string;
13
+ email_verified: boolean;
14
+ picture: {
15
+ data: {
16
+ height: number;
17
+ is_silhouette: boolean;
18
+ url: string;
19
+ width: number;
20
+ };
21
+ };
22
+ }
23
+
24
+ export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
25
+ clientId: string;
26
+ /**
27
+ * Extend list of fields to retrieve from the Facebook user profile.
28
+ *
29
+ * @default ["id", "name", "email", "picture"]
30
+ */
31
+ fields?: string[];
32
+
33
+ /**
34
+ * The config id to use when undergoing oauth
35
+ */
36
+ configId?: string;
37
+ }
38
+
39
+ export const facebook = (options: FacebookOptions) => {
40
+ return {
41
+ id: "facebook",
42
+ name: "Facebook",
43
+ async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) {
44
+ const _scopes = options.disableDefaultScope
45
+ ? []
46
+ : ["email", "public_profile"];
47
+ options.scope && _scopes.push(...options.scope);
48
+ scopes && _scopes.push(...scopes);
49
+ return await createAuthorizationURL({
50
+ id: "facebook",
51
+ options,
52
+ authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
53
+ scopes: _scopes,
54
+ state,
55
+ redirectURI,
56
+ loginHint,
57
+ additionalParams: options.configId
58
+ ? {
59
+ config_id: options.configId,
60
+ }
61
+ : {},
62
+ });
63
+ },
64
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
65
+ return validateAuthorizationCode({
66
+ code,
67
+ redirectURI,
68
+ options,
69
+ tokenEndpoint: "https://graph.facebook.com/oauth/access_token",
70
+ });
71
+ },
72
+ async verifyIdToken(token, nonce) {
73
+ if (options.disableIdTokenSignIn) {
74
+ return false;
75
+ }
76
+
77
+ if (options.verifyIdToken) {
78
+ return options.verifyIdToken(token, nonce);
79
+ }
80
+
81
+ /* limited login */
82
+ // check is limited token
83
+ if (token.split(".").length === 3) {
84
+ try {
85
+ const { payload: jwtClaims } = await jwtVerify(
86
+ token,
87
+ createRemoteJWKSet(
88
+ // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
89
+ new URL(
90
+ "https://limited.facebook.com/.well-known/oauth/openid/jwks/",
91
+ ),
92
+ ),
93
+ {
94
+ algorithms: ["RS256"],
95
+ audience: options.clientId,
96
+ issuer: "https://www.facebook.com",
97
+ },
98
+ );
99
+
100
+ if (nonce && jwtClaims.nonce !== nonce) {
101
+ return false;
102
+ }
103
+
104
+ return !!jwtClaims;
105
+ } catch (error) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /* access_token */
111
+ return true;
112
+ },
113
+ refreshAccessToken: options.refreshAccessToken
114
+ ? options.refreshAccessToken
115
+ : async (refreshToken) => {
116
+ return refreshAccessToken({
117
+ refreshToken,
118
+ options: {
119
+ clientId: options.clientId,
120
+ clientKey: options.clientKey,
121
+ clientSecret: options.clientSecret,
122
+ },
123
+ tokenEndpoint:
124
+ "https://graph.facebook.com/v18.0/oauth/access_token",
125
+ });
126
+ },
127
+ async getUserInfo(token) {
128
+ if (options.getUserInfo) {
129
+ return options.getUserInfo(token);
130
+ }
131
+
132
+ if (token.idToken && token.idToken.split(".").length === 3) {
133
+ const profile = decodeJwt(token.idToken) as {
134
+ sub: string;
135
+ email: string;
136
+ name: string;
137
+ picture: string;
138
+ };
139
+
140
+ const user = {
141
+ id: profile.sub,
142
+ name: profile.name,
143
+ email: profile.email,
144
+ picture: {
145
+ data: {
146
+ url: profile.picture,
147
+ height: 100,
148
+ width: 100,
149
+ is_silhouette: false,
150
+ },
151
+ },
152
+ };
153
+
154
+ // https://developers.facebook.com/docs/facebook-login/limited-login/permissions
155
+ const userMap = await options.mapProfileToUser?.({
156
+ ...user,
157
+ email_verified: true,
158
+ });
159
+
160
+ return {
161
+ user: {
162
+ ...user,
163
+ emailVerified: true,
164
+ ...userMap,
165
+ },
166
+ data: profile,
167
+ };
168
+ }
169
+
170
+ const fields = [
171
+ "id",
172
+ "name",
173
+ "email",
174
+ "picture",
175
+ ...(options?.fields || []),
176
+ ];
177
+ const { data: profile, error } = await betterFetch<FacebookProfile>(
178
+ "https://graph.facebook.com/me?fields=" + fields.join(","),
179
+ {
180
+ auth: {
181
+ type: "Bearer",
182
+ token: token.accessToken,
183
+ },
184
+ },
185
+ );
186
+ if (error) {
187
+ return null;
188
+ }
189
+ const userMap = await options.mapProfileToUser?.(profile);
190
+ return {
191
+ user: {
192
+ id: profile.id,
193
+ name: profile.name,
194
+ email: profile.email,
195
+ image: profile.picture.data.url,
196
+ emailVerified: profile.email_verified,
197
+ ...userMap,
198
+ },
199
+ data: profile,
200
+ };
201
+ },
202
+ options,
203
+ } satisfies OAuthProvider<FacebookProfile>;
204
+ };
@@ -0,0 +1,115 @@
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 FigmaProfile {
12
+ id: string;
13
+ email: string;
14
+ handle: string;
15
+ img_url: string;
16
+ }
17
+
18
+ export interface FigmaOptions extends ProviderOptions<FigmaProfile> {
19
+ clientId: string;
20
+ }
21
+
22
+ export const figma = (options: FigmaOptions) => {
23
+ return {
24
+ id: "figma",
25
+ name: "Figma",
26
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
27
+ if (!options.clientId || !options.clientSecret) {
28
+ logger.error(
29
+ "Client Id and Client Secret are required for Figma. Make sure to provide them in the options.",
30
+ );
31
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
32
+ }
33
+ if (!codeVerifier) {
34
+ throw new BetterAuthError("codeVerifier is required for Figma");
35
+ }
36
+
37
+ const _scopes = options.disableDefaultScope ? [] : ["file_read"];
38
+ options.scope && _scopes.push(...options.scope);
39
+ scopes && _scopes.push(...scopes);
40
+
41
+ const url = await createAuthorizationURL({
42
+ id: "figma",
43
+ options,
44
+ authorizationEndpoint: "https://www.figma.com/oauth",
45
+ scopes: _scopes,
46
+ state,
47
+ codeVerifier,
48
+ redirectURI,
49
+ });
50
+
51
+ return url;
52
+ },
53
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
54
+ return validateAuthorizationCode({
55
+ code,
56
+ codeVerifier,
57
+ redirectURI,
58
+ options,
59
+ tokenEndpoint: "https://www.figma.com/api/oauth/token",
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://www.figma.com/api/oauth/token",
73
+ });
74
+ },
75
+ async getUserInfo(token) {
76
+ if (options.getUserInfo) {
77
+ return options.getUserInfo(token);
78
+ }
79
+
80
+ try {
81
+ const { data: profile } = await betterFetch<FigmaProfile>(
82
+ "https://api.figma.com/v1/me",
83
+ {
84
+ headers: {
85
+ Authorization: `Bearer ${token.accessToken}`,
86
+ },
87
+ },
88
+ );
89
+
90
+ if (!profile) {
91
+ logger.error("Failed to fetch user from Figma");
92
+ return null;
93
+ }
94
+
95
+ const userMap = await options.mapProfileToUser?.(profile);
96
+
97
+ return {
98
+ user: {
99
+ id: profile.id,
100
+ name: profile.handle,
101
+ email: profile.email,
102
+ image: profile.img_url,
103
+ emailVerified: !!profile.email,
104
+ ...userMap,
105
+ },
106
+ data: profile,
107
+ };
108
+ } catch (error) {
109
+ logger.error("Failed to fetch user info from Figma:", error);
110
+ return null;
111
+ }
112
+ },
113
+ options,
114
+ } satisfies OAuthProvider<FigmaProfile>;
115
+ };