@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,2743 @@
1
+ import * as z from 'zod';
2
+ import { betterFetch } from '@better-fetch/fetch';
3
+ import { APIError } from 'better-call';
4
+ import { decodeJwt, decodeProtectedHeader, jwtVerify, importJWK, createRemoteJWKSet } from 'jose';
5
+ import { refreshAccessToken, validateAuthorizationCode, createAuthorizationURL, getOAuth2Tokens, generateCodeChallenge } from '@better-auth/core/oauth2';
6
+ import { BetterAuthError } from '../error/index.mjs';
7
+ import { logger } from '@better-auth/core/env';
8
+ import { base64 } from '@better-auth/utils/base64';
9
+ import '@better-auth/core/utils';
10
+
11
+ const apple = (options) => {
12
+ const tokenEndpoint = "https://appleid.apple.com/auth/token";
13
+ return {
14
+ id: "apple",
15
+ name: "Apple",
16
+ async createAuthorizationURL({ state, scopes, redirectURI }) {
17
+ const _scope = options.disableDefaultScope ? [] : ["email", "name"];
18
+ options.scope && _scope.push(...options.scope);
19
+ scopes && _scope.push(...scopes);
20
+ const url = await createAuthorizationURL({
21
+ id: "apple",
22
+ options,
23
+ authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
24
+ scopes: _scope,
25
+ state,
26
+ redirectURI,
27
+ responseMode: "form_post",
28
+ responseType: "code id_token"
29
+ });
30
+ return url;
31
+ },
32
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
33
+ return validateAuthorizationCode({
34
+ code,
35
+ codeVerifier,
36
+ redirectURI,
37
+ options,
38
+ tokenEndpoint
39
+ });
40
+ },
41
+ async verifyIdToken(token, nonce) {
42
+ if (options.disableIdTokenSignIn) {
43
+ return false;
44
+ }
45
+ if (options.verifyIdToken) {
46
+ return options.verifyIdToken(token, nonce);
47
+ }
48
+ const decodedHeader = decodeProtectedHeader(token);
49
+ const { kid, alg: jwtAlg } = decodedHeader;
50
+ if (!kid || !jwtAlg) return false;
51
+ const publicKey = await getApplePublicKey(kid);
52
+ const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
53
+ algorithms: [jwtAlg],
54
+ issuer: "https://appleid.apple.com",
55
+ audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
56
+ maxTokenAge: "1h"
57
+ });
58
+ ["email_verified", "is_private_email"].forEach((field) => {
59
+ if (jwtClaims[field] !== void 0) {
60
+ jwtClaims[field] = Boolean(jwtClaims[field]);
61
+ }
62
+ });
63
+ if (nonce && jwtClaims.nonce !== nonce) {
64
+ return false;
65
+ }
66
+ return !!jwtClaims;
67
+ },
68
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
69
+ return refreshAccessToken({
70
+ refreshToken,
71
+ options: {
72
+ clientId: options.clientId,
73
+ clientKey: options.clientKey,
74
+ clientSecret: options.clientSecret
75
+ },
76
+ tokenEndpoint: "https://appleid.apple.com/auth/token"
77
+ });
78
+ },
79
+ async getUserInfo(token) {
80
+ if (options.getUserInfo) {
81
+ return options.getUserInfo(token);
82
+ }
83
+ if (!token.idToken) {
84
+ return null;
85
+ }
86
+ const profile = decodeJwt(token.idToken);
87
+ if (!profile) {
88
+ return null;
89
+ }
90
+ const name = token.user ? `${token.user.name?.firstName} ${token.user.name?.lastName}` : profile.name || profile.email;
91
+ const emailVerified = typeof profile.email_verified === "boolean" ? profile.email_verified : profile.email_verified === "true";
92
+ const enrichedProfile = {
93
+ ...profile,
94
+ name
95
+ };
96
+ const userMap = await options.mapProfileToUser?.(enrichedProfile);
97
+ return {
98
+ user: {
99
+ id: profile.sub,
100
+ name: enrichedProfile.name,
101
+ emailVerified,
102
+ email: profile.email,
103
+ ...userMap
104
+ },
105
+ data: enrichedProfile
106
+ };
107
+ },
108
+ options
109
+ };
110
+ };
111
+ const getApplePublicKey = async (kid) => {
112
+ const APPLE_BASE_URL = "https://appleid.apple.com";
113
+ const JWKS_APPLE_URI = "/auth/keys";
114
+ const { data } = await betterFetch(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);
115
+ if (!data?.keys) {
116
+ throw new APIError("BAD_REQUEST", {
117
+ message: "Keys not found"
118
+ });
119
+ }
120
+ const jwk = data.keys.find((key) => key.kid === kid);
121
+ if (!jwk) {
122
+ throw new Error(`JWK with kid ${kid} not found`);
123
+ }
124
+ return await importJWK(jwk, jwk.alg);
125
+ };
126
+
127
+ const atlassian = (options) => {
128
+ return {
129
+ id: "atlassian",
130
+ name: "Atlassian",
131
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
132
+ if (!options.clientId || !options.clientSecret) {
133
+ logger.error("Client Id and Secret are required for Atlassian");
134
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
135
+ }
136
+ if (!codeVerifier) {
137
+ throw new BetterAuthError("codeVerifier is required for Atlassian");
138
+ }
139
+ const _scopes = options.disableDefaultScope ? [] : ["read:jira-user", "offline_access"];
140
+ options.scope && _scopes.push(...options.scope);
141
+ scopes && _scopes.push(...scopes);
142
+ return createAuthorizationURL({
143
+ id: "atlassian",
144
+ options,
145
+ authorizationEndpoint: "https://auth.atlassian.com/authorize",
146
+ scopes: _scopes,
147
+ state,
148
+ codeVerifier,
149
+ redirectURI,
150
+ additionalParams: {
151
+ audience: "api.atlassian.com"
152
+ },
153
+ prompt: options.prompt
154
+ });
155
+ },
156
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
157
+ return validateAuthorizationCode({
158
+ code,
159
+ codeVerifier,
160
+ redirectURI,
161
+ options,
162
+ tokenEndpoint: "https://auth.atlassian.com/oauth/token"
163
+ });
164
+ },
165
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
166
+ return refreshAccessToken({
167
+ refreshToken,
168
+ options: {
169
+ clientId: options.clientId,
170
+ clientSecret: options.clientSecret
171
+ },
172
+ tokenEndpoint: "https://auth.atlassian.com/oauth/token"
173
+ });
174
+ },
175
+ async getUserInfo(token) {
176
+ if (options.getUserInfo) {
177
+ return options.getUserInfo(token);
178
+ }
179
+ if (!token.accessToken) {
180
+ return null;
181
+ }
182
+ try {
183
+ const { data: profile } = await betterFetch("https://api.atlassian.com/me", {
184
+ headers: { Authorization: `Bearer ${token.accessToken}` }
185
+ });
186
+ if (!profile) return null;
187
+ const userMap = await options.mapProfileToUser?.(profile);
188
+ return {
189
+ user: {
190
+ id: profile.account_id,
191
+ name: profile.name,
192
+ email: profile.email,
193
+ image: profile.picture,
194
+ emailVerified: false,
195
+ ...userMap
196
+ },
197
+ data: profile
198
+ };
199
+ } catch (error) {
200
+ logger.error("Failed to fetch user info from Figma:", error);
201
+ return null;
202
+ }
203
+ },
204
+ options
205
+ };
206
+ };
207
+
208
+ const cognito = (options) => {
209
+ if (!options.domain || !options.region || !options.userPoolId) {
210
+ logger.error(
211
+ "Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options."
212
+ );
213
+ throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED");
214
+ }
215
+ const cleanDomain = options.domain.replace(/^https?:\/\//, "");
216
+ const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`;
217
+ const tokenEndpoint = `https://${cleanDomain}/oauth2/token`;
218
+ const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`;
219
+ return {
220
+ id: "cognito",
221
+ name: "Cognito",
222
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
223
+ if (!options.clientId) {
224
+ logger.error(
225
+ "ClientId is required for Amazon Cognito. Make sure to provide them in the options."
226
+ );
227
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
228
+ }
229
+ if (options.requireClientSecret && !options.clientSecret) {
230
+ logger.error(
231
+ "Client Secret is required when requireClientSecret is true. Make sure to provide it in the options."
232
+ );
233
+ throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
234
+ }
235
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
236
+ options.scope && _scopes.push(...options.scope);
237
+ scopes && _scopes.push(...scopes);
238
+ const url = await createAuthorizationURL({
239
+ id: "cognito",
240
+ options: {
241
+ ...options
242
+ },
243
+ authorizationEndpoint,
244
+ scopes: _scopes,
245
+ state,
246
+ codeVerifier,
247
+ redirectURI,
248
+ prompt: options.prompt
249
+ });
250
+ return url;
251
+ },
252
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
253
+ return validateAuthorizationCode({
254
+ code,
255
+ codeVerifier,
256
+ redirectURI,
257
+ options,
258
+ tokenEndpoint
259
+ });
260
+ },
261
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
262
+ return refreshAccessToken({
263
+ refreshToken,
264
+ options: {
265
+ clientId: options.clientId,
266
+ clientKey: options.clientKey,
267
+ clientSecret: options.clientSecret
268
+ },
269
+ tokenEndpoint
270
+ });
271
+ },
272
+ async verifyIdToken(token, nonce) {
273
+ if (options.disableIdTokenSignIn) {
274
+ return false;
275
+ }
276
+ if (options.verifyIdToken) {
277
+ return options.verifyIdToken(token, nonce);
278
+ }
279
+ try {
280
+ const decodedHeader = decodeProtectedHeader(token);
281
+ const { kid, alg: jwtAlg } = decodedHeader;
282
+ if (!kid || !jwtAlg) return false;
283
+ const publicKey = await getCognitoPublicKey(
284
+ kid,
285
+ options.region,
286
+ options.userPoolId
287
+ );
288
+ const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
289
+ const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
290
+ algorithms: [jwtAlg],
291
+ issuer: expectedIssuer,
292
+ audience: options.clientId,
293
+ maxTokenAge: "1h"
294
+ });
295
+ if (nonce && jwtClaims.nonce !== nonce) {
296
+ return false;
297
+ }
298
+ return true;
299
+ } catch (error) {
300
+ logger.error("Failed to verify ID token:", error);
301
+ return false;
302
+ }
303
+ },
304
+ async getUserInfo(token) {
305
+ if (options.getUserInfo) {
306
+ return options.getUserInfo(token);
307
+ }
308
+ if (token.idToken) {
309
+ try {
310
+ const profile = decodeJwt(token.idToken);
311
+ if (!profile) {
312
+ return null;
313
+ }
314
+ const name = profile.name || profile.given_name || profile.username || profile.email;
315
+ const enrichedProfile = {
316
+ ...profile,
317
+ name
318
+ };
319
+ const userMap = await options.mapProfileToUser?.(enrichedProfile);
320
+ return {
321
+ user: {
322
+ id: profile.sub,
323
+ name: enrichedProfile.name,
324
+ email: profile.email,
325
+ image: profile.picture,
326
+ emailVerified: profile.email_verified,
327
+ ...userMap
328
+ },
329
+ data: enrichedProfile
330
+ };
331
+ } catch (error) {
332
+ logger.error("Failed to decode ID token:", error);
333
+ }
334
+ }
335
+ if (token.accessToken) {
336
+ try {
337
+ const { data: userInfo } = await betterFetch(
338
+ userInfoEndpoint,
339
+ {
340
+ headers: {
341
+ Authorization: `Bearer ${token.accessToken}`
342
+ }
343
+ }
344
+ );
345
+ if (userInfo) {
346
+ const userMap = await options.mapProfileToUser?.(userInfo);
347
+ return {
348
+ user: {
349
+ id: userInfo.sub,
350
+ name: userInfo.name || userInfo.given_name || userInfo.username,
351
+ email: userInfo.email,
352
+ image: userInfo.picture,
353
+ emailVerified: userInfo.email_verified,
354
+ ...userMap
355
+ },
356
+ data: userInfo
357
+ };
358
+ }
359
+ } catch (error) {
360
+ logger.error("Failed to fetch user info from Cognito:", error);
361
+ }
362
+ }
363
+ return null;
364
+ },
365
+ options
366
+ };
367
+ };
368
+ const getCognitoPublicKey = async (kid, region, userPoolId) => {
369
+ const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
370
+ try {
371
+ const { data } = await betterFetch(COGNITO_JWKS_URI);
372
+ if (!data?.keys) {
373
+ throw new APIError("BAD_REQUEST", {
374
+ message: "Keys not found"
375
+ });
376
+ }
377
+ const jwk = data.keys.find((key) => key.kid === kid);
378
+ if (!jwk) {
379
+ throw new Error(`JWK with kid ${kid} not found`);
380
+ }
381
+ return await importJWK(jwk, jwk.alg);
382
+ } catch (error) {
383
+ logger.error("Failed to fetch Cognito public key:", error);
384
+ throw error;
385
+ }
386
+ };
387
+
388
+ const discord = (options) => {
389
+ return {
390
+ id: "discord",
391
+ name: "Discord",
392
+ createAuthorizationURL({ state, scopes, redirectURI }) {
393
+ const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
394
+ scopes && _scopes.push(...scopes);
395
+ options.scope && _scopes.push(...options.scope);
396
+ const hasBotScope = _scopes.includes("bot");
397
+ const permissionsParam = hasBotScope && options.permissions !== void 0 ? `&permissions=${options.permissions}` : "";
398
+ return new URL(
399
+ `https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
400
+ "+"
401
+ )}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent(
402
+ options.redirectURI || redirectURI
403
+ )}&state=${state}&prompt=${options.prompt || "none"}${permissionsParam}`
404
+ );
405
+ },
406
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
407
+ return validateAuthorizationCode({
408
+ code,
409
+ redirectURI,
410
+ options,
411
+ tokenEndpoint: "https://discord.com/api/oauth2/token"
412
+ });
413
+ },
414
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
415
+ return refreshAccessToken({
416
+ refreshToken,
417
+ options: {
418
+ clientId: options.clientId,
419
+ clientKey: options.clientKey,
420
+ clientSecret: options.clientSecret
421
+ },
422
+ tokenEndpoint: "https://discord.com/api/oauth2/token"
423
+ });
424
+ },
425
+ async getUserInfo(token) {
426
+ if (options.getUserInfo) {
427
+ return options.getUserInfo(token);
428
+ }
429
+ const { data: profile, error } = await betterFetch(
430
+ "https://discord.com/api/users/@me",
431
+ {
432
+ headers: {
433
+ authorization: `Bearer ${token.accessToken}`
434
+ }
435
+ }
436
+ );
437
+ if (error) {
438
+ return null;
439
+ }
440
+ if (profile.avatar === null) {
441
+ const defaultAvatarNumber = profile.discriminator === "0" ? Number(BigInt(profile.id) >> BigInt(22)) % 6 : parseInt(profile.discriminator) % 5;
442
+ profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`;
443
+ } else {
444
+ const format = profile.avatar.startsWith("a_") ? "gif" : "png";
445
+ profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
446
+ }
447
+ const userMap = await options.mapProfileToUser?.(profile);
448
+ return {
449
+ user: {
450
+ id: profile.id,
451
+ name: profile.global_name || profile.username || "",
452
+ email: profile.email,
453
+ emailVerified: profile.verified,
454
+ image: profile.image_url,
455
+ ...userMap
456
+ },
457
+ data: profile
458
+ };
459
+ },
460
+ options
461
+ };
462
+ };
463
+
464
+ const facebook = (options) => {
465
+ return {
466
+ id: "facebook",
467
+ name: "Facebook",
468
+ async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) {
469
+ const _scopes = options.disableDefaultScope ? [] : ["email", "public_profile"];
470
+ options.scope && _scopes.push(...options.scope);
471
+ scopes && _scopes.push(...scopes);
472
+ return await createAuthorizationURL({
473
+ id: "facebook",
474
+ options,
475
+ authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
476
+ scopes: _scopes,
477
+ state,
478
+ redirectURI,
479
+ loginHint,
480
+ additionalParams: options.configId ? {
481
+ config_id: options.configId
482
+ } : {}
483
+ });
484
+ },
485
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
486
+ return validateAuthorizationCode({
487
+ code,
488
+ redirectURI,
489
+ options,
490
+ tokenEndpoint: "https://graph.facebook.com/oauth/access_token"
491
+ });
492
+ },
493
+ async verifyIdToken(token, nonce) {
494
+ if (options.disableIdTokenSignIn) {
495
+ return false;
496
+ }
497
+ if (options.verifyIdToken) {
498
+ return options.verifyIdToken(token, nonce);
499
+ }
500
+ if (token.split(".").length === 3) {
501
+ try {
502
+ const { payload: jwtClaims } = await jwtVerify(
503
+ token,
504
+ createRemoteJWKSet(
505
+ // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
506
+ new URL(
507
+ "https://limited.facebook.com/.well-known/oauth/openid/jwks/"
508
+ )
509
+ ),
510
+ {
511
+ algorithms: ["RS256"],
512
+ audience: options.clientId,
513
+ issuer: "https://www.facebook.com"
514
+ }
515
+ );
516
+ if (nonce && jwtClaims.nonce !== nonce) {
517
+ return false;
518
+ }
519
+ return !!jwtClaims;
520
+ } catch (error) {
521
+ return false;
522
+ }
523
+ }
524
+ return true;
525
+ },
526
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
527
+ return refreshAccessToken({
528
+ refreshToken,
529
+ options: {
530
+ clientId: options.clientId,
531
+ clientKey: options.clientKey,
532
+ clientSecret: options.clientSecret
533
+ },
534
+ tokenEndpoint: "https://graph.facebook.com/v18.0/oauth/access_token"
535
+ });
536
+ },
537
+ async getUserInfo(token) {
538
+ if (options.getUserInfo) {
539
+ return options.getUserInfo(token);
540
+ }
541
+ if (token.idToken && token.idToken.split(".").length === 3) {
542
+ const profile2 = decodeJwt(token.idToken);
543
+ const user = {
544
+ id: profile2.sub,
545
+ name: profile2.name,
546
+ email: profile2.email,
547
+ picture: {
548
+ data: {
549
+ url: profile2.picture,
550
+ height: 100,
551
+ width: 100,
552
+ is_silhouette: false
553
+ }
554
+ }
555
+ };
556
+ const userMap2 = await options.mapProfileToUser?.({
557
+ ...user,
558
+ email_verified: true
559
+ });
560
+ return {
561
+ user: {
562
+ ...user,
563
+ emailVerified: true,
564
+ ...userMap2
565
+ },
566
+ data: profile2
567
+ };
568
+ }
569
+ const fields = [
570
+ "id",
571
+ "name",
572
+ "email",
573
+ "picture",
574
+ ...options?.fields || []
575
+ ];
576
+ const { data: profile, error } = await betterFetch(
577
+ "https://graph.facebook.com/me?fields=" + fields.join(","),
578
+ {
579
+ auth: {
580
+ type: "Bearer",
581
+ token: token.accessToken
582
+ }
583
+ }
584
+ );
585
+ if (error) {
586
+ return null;
587
+ }
588
+ const userMap = await options.mapProfileToUser?.(profile);
589
+ return {
590
+ user: {
591
+ id: profile.id,
592
+ name: profile.name,
593
+ email: profile.email,
594
+ image: profile.picture.data.url,
595
+ emailVerified: profile.email_verified,
596
+ ...userMap
597
+ },
598
+ data: profile
599
+ };
600
+ },
601
+ options
602
+ };
603
+ };
604
+
605
+ const figma = (options) => {
606
+ return {
607
+ id: "figma",
608
+ name: "Figma",
609
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
610
+ if (!options.clientId || !options.clientSecret) {
611
+ logger.error(
612
+ "Client Id and Client Secret are required for Figma. Make sure to provide them in the options."
613
+ );
614
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
615
+ }
616
+ if (!codeVerifier) {
617
+ throw new BetterAuthError("codeVerifier is required for Figma");
618
+ }
619
+ const _scopes = options.disableDefaultScope ? [] : ["file_read"];
620
+ options.scope && _scopes.push(...options.scope);
621
+ scopes && _scopes.push(...scopes);
622
+ const url = await createAuthorizationURL({
623
+ id: "figma",
624
+ options,
625
+ authorizationEndpoint: "https://www.figma.com/oauth",
626
+ scopes: _scopes,
627
+ state,
628
+ codeVerifier,
629
+ redirectURI
630
+ });
631
+ return url;
632
+ },
633
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
634
+ return validateAuthorizationCode({
635
+ code,
636
+ codeVerifier,
637
+ redirectURI,
638
+ options,
639
+ tokenEndpoint: "https://www.figma.com/api/oauth/token"
640
+ });
641
+ },
642
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
643
+ return refreshAccessToken({
644
+ refreshToken,
645
+ options: {
646
+ clientId: options.clientId,
647
+ clientKey: options.clientKey,
648
+ clientSecret: options.clientSecret
649
+ },
650
+ tokenEndpoint: "https://www.figma.com/api/oauth/token"
651
+ });
652
+ },
653
+ async getUserInfo(token) {
654
+ if (options.getUserInfo) {
655
+ return options.getUserInfo(token);
656
+ }
657
+ try {
658
+ const { data: profile } = await betterFetch(
659
+ "https://api.figma.com/v1/me",
660
+ {
661
+ headers: {
662
+ Authorization: `Bearer ${token.accessToken}`
663
+ }
664
+ }
665
+ );
666
+ if (!profile) {
667
+ logger.error("Failed to fetch user from Figma");
668
+ return null;
669
+ }
670
+ const userMap = await options.mapProfileToUser?.(profile);
671
+ return {
672
+ user: {
673
+ id: profile.id,
674
+ name: profile.handle,
675
+ email: profile.email,
676
+ image: profile.img_url,
677
+ emailVerified: !!profile.email,
678
+ ...userMap
679
+ },
680
+ data: profile
681
+ };
682
+ } catch (error) {
683
+ logger.error("Failed to fetch user info from Figma:", error);
684
+ return null;
685
+ }
686
+ },
687
+ options
688
+ };
689
+ };
690
+
691
+ const github = (options) => {
692
+ const tokenEndpoint = "https://github.com/login/oauth/access_token";
693
+ return {
694
+ id: "github",
695
+ name: "GitHub",
696
+ createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
697
+ const _scopes = options.disableDefaultScope ? [] : ["read:user", "user:email"];
698
+ options.scope && _scopes.push(...options.scope);
699
+ scopes && _scopes.push(...scopes);
700
+ return createAuthorizationURL({
701
+ id: "github",
702
+ options,
703
+ authorizationEndpoint: "https://github.com/login/oauth/authorize",
704
+ scopes: _scopes,
705
+ state,
706
+ redirectURI,
707
+ loginHint,
708
+ prompt: options.prompt
709
+ });
710
+ },
711
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
712
+ return validateAuthorizationCode({
713
+ code,
714
+ redirectURI,
715
+ options,
716
+ tokenEndpoint
717
+ });
718
+ },
719
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
720
+ return refreshAccessToken({
721
+ refreshToken,
722
+ options: {
723
+ clientId: options.clientId,
724
+ clientKey: options.clientKey,
725
+ clientSecret: options.clientSecret
726
+ },
727
+ tokenEndpoint: "https://github.com/login/oauth/access_token"
728
+ });
729
+ },
730
+ async getUserInfo(token) {
731
+ if (options.getUserInfo) {
732
+ return options.getUserInfo(token);
733
+ }
734
+ const { data: profile, error } = await betterFetch(
735
+ "https://api.github.com/user",
736
+ {
737
+ headers: {
738
+ "User-Agent": "better-auth",
739
+ authorization: `Bearer ${token.accessToken}`
740
+ }
741
+ }
742
+ );
743
+ if (error) {
744
+ return null;
745
+ }
746
+ const { data: emails } = await betterFetch("https://api.github.com/user/emails", {
747
+ headers: {
748
+ Authorization: `Bearer ${token.accessToken}`,
749
+ "User-Agent": "better-auth"
750
+ }
751
+ });
752
+ if (!profile.email && emails) {
753
+ profile.email = (emails.find((e) => e.primary) ?? emails[0])?.email;
754
+ }
755
+ const emailVerified = emails?.find((e) => e.email === profile.email)?.verified ?? false;
756
+ const userMap = await options.mapProfileToUser?.(profile);
757
+ return {
758
+ user: {
759
+ id: profile.id,
760
+ name: profile.name || profile.login,
761
+ email: profile.email,
762
+ image: profile.avatar_url,
763
+ emailVerified,
764
+ ...userMap
765
+ },
766
+ data: profile
767
+ };
768
+ },
769
+ options
770
+ };
771
+ };
772
+
773
+ const google = (options) => {
774
+ return {
775
+ id: "google",
776
+ name: "Google",
777
+ async createAuthorizationURL({
778
+ state,
779
+ scopes,
780
+ codeVerifier,
781
+ redirectURI,
782
+ loginHint,
783
+ display
784
+ }) {
785
+ if (!options.clientId || !options.clientSecret) {
786
+ logger.error(
787
+ "Client Id and Client Secret is required for Google. Make sure to provide them in the options."
788
+ );
789
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
790
+ }
791
+ if (!codeVerifier) {
792
+ throw new BetterAuthError("codeVerifier is required for Google");
793
+ }
794
+ const _scopes = options.disableDefaultScope ? [] : ["email", "profile", "openid"];
795
+ options.scope && _scopes.push(...options.scope);
796
+ scopes && _scopes.push(...scopes);
797
+ const url = await createAuthorizationURL({
798
+ id: "google",
799
+ options,
800
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
801
+ scopes: _scopes,
802
+ state,
803
+ codeVerifier,
804
+ redirectURI,
805
+ prompt: options.prompt,
806
+ accessType: options.accessType,
807
+ display: display || options.display,
808
+ loginHint,
809
+ hd: options.hd,
810
+ additionalParams: {
811
+ include_granted_scopes: "true"
812
+ }
813
+ });
814
+ return url;
815
+ },
816
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
817
+ return validateAuthorizationCode({
818
+ code,
819
+ codeVerifier,
820
+ redirectURI,
821
+ options,
822
+ tokenEndpoint: "https://oauth2.googleapis.com/token"
823
+ });
824
+ },
825
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
826
+ return refreshAccessToken({
827
+ refreshToken,
828
+ options: {
829
+ clientId: options.clientId,
830
+ clientKey: options.clientKey,
831
+ clientSecret: options.clientSecret
832
+ },
833
+ tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token"
834
+ });
835
+ },
836
+ async verifyIdToken(token, nonce) {
837
+ if (options.disableIdTokenSignIn) {
838
+ return false;
839
+ }
840
+ if (options.verifyIdToken) {
841
+ return options.verifyIdToken(token, nonce);
842
+ }
843
+ const googlePublicKeyUrl = `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}`;
844
+ const { data: tokenInfo } = await betterFetch(googlePublicKeyUrl);
845
+ if (!tokenInfo) {
846
+ return false;
847
+ }
848
+ const isValid = tokenInfo.aud === options.clientId && (tokenInfo.iss === "https://accounts.google.com" || tokenInfo.iss === "accounts.google.com");
849
+ return isValid;
850
+ },
851
+ async getUserInfo(token) {
852
+ if (options.getUserInfo) {
853
+ return options.getUserInfo(token);
854
+ }
855
+ if (!token.idToken) {
856
+ return null;
857
+ }
858
+ const user = decodeJwt(token.idToken);
859
+ const userMap = await options.mapProfileToUser?.(user);
860
+ return {
861
+ user: {
862
+ id: user.sub,
863
+ name: user.name,
864
+ email: user.email,
865
+ image: user.picture,
866
+ emailVerified: user.email_verified,
867
+ ...userMap
868
+ },
869
+ data: user
870
+ };
871
+ },
872
+ options
873
+ };
874
+ };
875
+
876
+ const kick = (options) => {
877
+ return {
878
+ id: "kick",
879
+ name: "Kick",
880
+ createAuthorizationURL({ state, scopes, redirectURI, codeVerifier }) {
881
+ const _scopes = options.disableDefaultScope ? [] : ["user:read"];
882
+ options.scope && _scopes.push(...options.scope);
883
+ scopes && _scopes.push(...scopes);
884
+ return createAuthorizationURL({
885
+ id: "kick",
886
+ redirectURI,
887
+ options,
888
+ authorizationEndpoint: "https://id.kick.com/oauth/authorize",
889
+ scopes: _scopes,
890
+ codeVerifier,
891
+ state
892
+ });
893
+ },
894
+ async validateAuthorizationCode({ code, redirectURI, codeVerifier }) {
895
+ return validateAuthorizationCode({
896
+ code,
897
+ redirectURI,
898
+ options,
899
+ tokenEndpoint: "https://id.kick.com/oauth/token",
900
+ codeVerifier
901
+ });
902
+ },
903
+ async getUserInfo(token) {
904
+ if (options.getUserInfo) {
905
+ return options.getUserInfo(token);
906
+ }
907
+ const { data, error } = await betterFetch("https://api.kick.com/public/v1/users", {
908
+ method: "GET",
909
+ headers: {
910
+ Authorization: `Bearer ${token.accessToken}`
911
+ }
912
+ });
913
+ if (error) {
914
+ return null;
915
+ }
916
+ const profile = data.data[0];
917
+ const userMap = await options.mapProfileToUser?.(profile);
918
+ return {
919
+ user: {
920
+ id: profile.user_id,
921
+ name: profile.name,
922
+ email: profile.email,
923
+ image: profile.profile_picture,
924
+ emailVerified: true,
925
+ ...userMap
926
+ },
927
+ data: profile
928
+ };
929
+ },
930
+ options
931
+ };
932
+ };
933
+
934
+ const huggingface = (options) => {
935
+ return {
936
+ id: "huggingface",
937
+ name: "Hugging Face",
938
+ createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
939
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
940
+ options.scope && _scopes.push(...options.scope);
941
+ scopes && _scopes.push(...scopes);
942
+ return createAuthorizationURL({
943
+ id: "huggingface",
944
+ options,
945
+ authorizationEndpoint: "https://huggingface.co/oauth/authorize",
946
+ scopes: _scopes,
947
+ state,
948
+ codeVerifier,
949
+ redirectURI
950
+ });
951
+ },
952
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
953
+ return validateAuthorizationCode({
954
+ code,
955
+ codeVerifier,
956
+ redirectURI,
957
+ options,
958
+ tokenEndpoint: "https://huggingface.co/oauth/token"
959
+ });
960
+ },
961
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
962
+ return refreshAccessToken({
963
+ refreshToken,
964
+ options: {
965
+ clientId: options.clientId,
966
+ clientKey: options.clientKey,
967
+ clientSecret: options.clientSecret
968
+ },
969
+ tokenEndpoint: "https://huggingface.co/oauth/token"
970
+ });
971
+ },
972
+ async getUserInfo(token) {
973
+ if (options.getUserInfo) {
974
+ return options.getUserInfo(token);
975
+ }
976
+ const { data: profile, error } = await betterFetch(
977
+ "https://huggingface.co/oauth/userinfo",
978
+ {
979
+ method: "GET",
980
+ headers: {
981
+ Authorization: `Bearer ${token.accessToken}`
982
+ }
983
+ }
984
+ );
985
+ if (error) {
986
+ return null;
987
+ }
988
+ const userMap = await options.mapProfileToUser?.(profile);
989
+ return {
990
+ user: {
991
+ id: profile.sub,
992
+ name: profile.name || profile.preferred_username,
993
+ email: profile.email,
994
+ image: profile.picture,
995
+ emailVerified: profile.email_verified ?? false,
996
+ ...userMap
997
+ },
998
+ data: profile
999
+ };
1000
+ },
1001
+ options
1002
+ };
1003
+ };
1004
+
1005
+ const microsoft = (options) => {
1006
+ const tenant = options.tenantId || "common";
1007
+ const authority = options.authority || "https://login.microsoftonline.com";
1008
+ const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;
1009
+ const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
1010
+ return {
1011
+ id: "microsoft",
1012
+ name: "Microsoft EntraID",
1013
+ createAuthorizationURL(data) {
1014
+ const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"];
1015
+ options.scope && scopes.push(...options.scope);
1016
+ data.scopes && scopes.push(...data.scopes);
1017
+ return createAuthorizationURL({
1018
+ id: "microsoft",
1019
+ options,
1020
+ authorizationEndpoint,
1021
+ state: data.state,
1022
+ codeVerifier: data.codeVerifier,
1023
+ scopes,
1024
+ redirectURI: data.redirectURI,
1025
+ prompt: options.prompt,
1026
+ loginHint: data.loginHint
1027
+ });
1028
+ },
1029
+ validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
1030
+ return validateAuthorizationCode({
1031
+ code,
1032
+ codeVerifier,
1033
+ redirectURI,
1034
+ options,
1035
+ tokenEndpoint
1036
+ });
1037
+ },
1038
+ async getUserInfo(token) {
1039
+ if (options.getUserInfo) {
1040
+ return options.getUserInfo(token);
1041
+ }
1042
+ if (!token.idToken) {
1043
+ return null;
1044
+ }
1045
+ const user = decodeJwt(token.idToken);
1046
+ const profilePhotoSize = options.profilePhotoSize || 48;
1047
+ await betterFetch(
1048
+ `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
1049
+ {
1050
+ headers: {
1051
+ Authorization: `Bearer ${token.accessToken}`
1052
+ },
1053
+ async onResponse(context) {
1054
+ if (options.disableProfilePhoto || !context.response.ok) {
1055
+ return;
1056
+ }
1057
+ try {
1058
+ const response = context.response.clone();
1059
+ const pictureBuffer = await response.arrayBuffer();
1060
+ const pictureBase64 = base64.encode(pictureBuffer);
1061
+ user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
1062
+ } catch (e) {
1063
+ logger.error(
1064
+ e && typeof e === "object" && "name" in e ? e.name : "",
1065
+ e
1066
+ );
1067
+ }
1068
+ }
1069
+ }
1070
+ );
1071
+ const userMap = await options.mapProfileToUser?.(user);
1072
+ return {
1073
+ user: {
1074
+ id: user.sub,
1075
+ name: user.name,
1076
+ email: user.email,
1077
+ image: user.picture,
1078
+ emailVerified: true,
1079
+ ...userMap
1080
+ },
1081
+ data: user
1082
+ };
1083
+ },
1084
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1085
+ const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"];
1086
+ options.scope && scopes.push(...options.scope);
1087
+ return refreshAccessToken({
1088
+ refreshToken,
1089
+ options: {
1090
+ clientId: options.clientId,
1091
+ clientSecret: options.clientSecret
1092
+ },
1093
+ extraParams: {
1094
+ scope: scopes.join(" ")
1095
+ // Include the scopes in request to microsoft
1096
+ },
1097
+ tokenEndpoint
1098
+ });
1099
+ },
1100
+ options
1101
+ };
1102
+ };
1103
+
1104
+ const slack = (options) => {
1105
+ return {
1106
+ id: "slack",
1107
+ name: "Slack",
1108
+ createAuthorizationURL({ state, scopes, redirectURI }) {
1109
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
1110
+ scopes && _scopes.push(...scopes);
1111
+ options.scope && _scopes.push(...options.scope);
1112
+ const url = new URL("https://slack.com/openid/connect/authorize");
1113
+ url.searchParams.set("scope", _scopes.join(" "));
1114
+ url.searchParams.set("response_type", "code");
1115
+ url.searchParams.set("client_id", options.clientId);
1116
+ url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
1117
+ url.searchParams.set("state", state);
1118
+ return url;
1119
+ },
1120
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1121
+ return validateAuthorizationCode({
1122
+ code,
1123
+ redirectURI,
1124
+ options,
1125
+ tokenEndpoint: "https://slack.com/api/openid.connect.token"
1126
+ });
1127
+ },
1128
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1129
+ return refreshAccessToken({
1130
+ refreshToken,
1131
+ options: {
1132
+ clientId: options.clientId,
1133
+ clientKey: options.clientKey,
1134
+ clientSecret: options.clientSecret
1135
+ },
1136
+ tokenEndpoint: "https://slack.com/api/openid.connect.token"
1137
+ });
1138
+ },
1139
+ async getUserInfo(token) {
1140
+ if (options.getUserInfo) {
1141
+ return options.getUserInfo(token);
1142
+ }
1143
+ const { data: profile, error } = await betterFetch(
1144
+ "https://slack.com/api/openid.connect.userInfo",
1145
+ {
1146
+ headers: {
1147
+ authorization: `Bearer ${token.accessToken}`
1148
+ }
1149
+ }
1150
+ );
1151
+ if (error) {
1152
+ return null;
1153
+ }
1154
+ const userMap = await options.mapProfileToUser?.(profile);
1155
+ return {
1156
+ user: {
1157
+ id: profile["https://slack.com/user_id"],
1158
+ name: profile.name || "",
1159
+ email: profile.email,
1160
+ emailVerified: profile.email_verified,
1161
+ image: profile.picture || profile["https://slack.com/user_image_512"],
1162
+ ...userMap
1163
+ },
1164
+ data: profile
1165
+ };
1166
+ },
1167
+ options
1168
+ };
1169
+ };
1170
+
1171
+ const notion = (options) => {
1172
+ const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
1173
+ return {
1174
+ id: "notion",
1175
+ name: "Notion",
1176
+ createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
1177
+ const _scopes = options.disableDefaultScope ? [] : [];
1178
+ options.scope && _scopes.push(...options.scope);
1179
+ scopes && _scopes.push(...scopes);
1180
+ return createAuthorizationURL({
1181
+ id: "notion",
1182
+ options,
1183
+ authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
1184
+ scopes: _scopes,
1185
+ state,
1186
+ redirectURI,
1187
+ loginHint,
1188
+ additionalParams: {
1189
+ owner: "user"
1190
+ }
1191
+ });
1192
+ },
1193
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1194
+ return validateAuthorizationCode({
1195
+ code,
1196
+ redirectURI,
1197
+ options,
1198
+ tokenEndpoint,
1199
+ authentication: "basic"
1200
+ });
1201
+ },
1202
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1203
+ return refreshAccessToken({
1204
+ refreshToken,
1205
+ options: {
1206
+ clientId: options.clientId,
1207
+ clientKey: options.clientKey,
1208
+ clientSecret: options.clientSecret
1209
+ },
1210
+ tokenEndpoint
1211
+ });
1212
+ },
1213
+ async getUserInfo(token) {
1214
+ if (options.getUserInfo) {
1215
+ return options.getUserInfo(token);
1216
+ }
1217
+ const { data: profile, error } = await betterFetch("https://api.notion.com/v1/users/me", {
1218
+ headers: {
1219
+ Authorization: `Bearer ${token.accessToken}`,
1220
+ "Notion-Version": "2022-06-28"
1221
+ }
1222
+ });
1223
+ if (error || !profile) {
1224
+ return null;
1225
+ }
1226
+ const userProfile = profile.bot?.owner?.user;
1227
+ if (!userProfile) {
1228
+ return null;
1229
+ }
1230
+ const userMap = await options.mapProfileToUser?.(userProfile);
1231
+ return {
1232
+ user: {
1233
+ id: userProfile.id,
1234
+ name: userProfile.name || "Notion User",
1235
+ email: userProfile.person?.email || null,
1236
+ image: userProfile.avatar_url,
1237
+ emailVerified: !!userProfile.person?.email,
1238
+ ...userMap
1239
+ },
1240
+ data: userProfile
1241
+ };
1242
+ },
1243
+ options
1244
+ };
1245
+ };
1246
+
1247
+ const spotify = (options) => {
1248
+ return {
1249
+ id: "spotify",
1250
+ name: "Spotify",
1251
+ createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
1252
+ const _scopes = options.disableDefaultScope ? [] : ["user-read-email"];
1253
+ options.scope && _scopes.push(...options.scope);
1254
+ scopes && _scopes.push(...scopes);
1255
+ return createAuthorizationURL({
1256
+ id: "spotify",
1257
+ options,
1258
+ authorizationEndpoint: "https://accounts.spotify.com/authorize",
1259
+ scopes: _scopes,
1260
+ state,
1261
+ codeVerifier,
1262
+ redirectURI
1263
+ });
1264
+ },
1265
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
1266
+ return validateAuthorizationCode({
1267
+ code,
1268
+ codeVerifier,
1269
+ redirectURI,
1270
+ options,
1271
+ tokenEndpoint: "https://accounts.spotify.com/api/token"
1272
+ });
1273
+ },
1274
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1275
+ return refreshAccessToken({
1276
+ refreshToken,
1277
+ options: {
1278
+ clientId: options.clientId,
1279
+ clientKey: options.clientKey,
1280
+ clientSecret: options.clientSecret
1281
+ },
1282
+ tokenEndpoint: "https://accounts.spotify.com/api/token"
1283
+ });
1284
+ },
1285
+ async getUserInfo(token) {
1286
+ if (options.getUserInfo) {
1287
+ return options.getUserInfo(token);
1288
+ }
1289
+ const { data: profile, error } = await betterFetch(
1290
+ "https://api.spotify.com/v1/me",
1291
+ {
1292
+ method: "GET",
1293
+ headers: {
1294
+ Authorization: `Bearer ${token.accessToken}`
1295
+ }
1296
+ }
1297
+ );
1298
+ if (error) {
1299
+ return null;
1300
+ }
1301
+ const userMap = await options.mapProfileToUser?.(profile);
1302
+ return {
1303
+ user: {
1304
+ id: profile.id,
1305
+ name: profile.display_name,
1306
+ email: profile.email,
1307
+ image: profile.images[0]?.url,
1308
+ emailVerified: false,
1309
+ ...userMap
1310
+ },
1311
+ data: profile
1312
+ };
1313
+ },
1314
+ options
1315
+ };
1316
+ };
1317
+
1318
+ const twitch = (options) => {
1319
+ return {
1320
+ id: "twitch",
1321
+ name: "Twitch",
1322
+ createAuthorizationURL({ state, scopes, redirectURI }) {
1323
+ const _scopes = options.disableDefaultScope ? [] : ["user:read:email", "openid"];
1324
+ options.scope && _scopes.push(...options.scope);
1325
+ scopes && _scopes.push(...scopes);
1326
+ return createAuthorizationURL({
1327
+ id: "twitch",
1328
+ redirectURI,
1329
+ options,
1330
+ authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
1331
+ scopes: _scopes,
1332
+ state,
1333
+ claims: options.claims || [
1334
+ "email",
1335
+ "email_verified",
1336
+ "preferred_username",
1337
+ "picture"
1338
+ ]
1339
+ });
1340
+ },
1341
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1342
+ return validateAuthorizationCode({
1343
+ code,
1344
+ redirectURI,
1345
+ options,
1346
+ tokenEndpoint: "https://id.twitch.tv/oauth2/token"
1347
+ });
1348
+ },
1349
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1350
+ return refreshAccessToken({
1351
+ refreshToken,
1352
+ options: {
1353
+ clientId: options.clientId,
1354
+ clientKey: options.clientKey,
1355
+ clientSecret: options.clientSecret
1356
+ },
1357
+ tokenEndpoint: "https://id.twitch.tv/oauth2/token"
1358
+ });
1359
+ },
1360
+ async getUserInfo(token) {
1361
+ if (options.getUserInfo) {
1362
+ return options.getUserInfo(token);
1363
+ }
1364
+ const idToken = token.idToken;
1365
+ if (!idToken) {
1366
+ logger.error("No idToken found in token");
1367
+ return null;
1368
+ }
1369
+ const profile = decodeJwt(idToken);
1370
+ const userMap = await options.mapProfileToUser?.(profile);
1371
+ return {
1372
+ user: {
1373
+ id: profile.sub,
1374
+ name: profile.preferred_username,
1375
+ email: profile.email,
1376
+ image: profile.picture,
1377
+ emailVerified: profile.email_verified,
1378
+ ...userMap
1379
+ },
1380
+ data: profile
1381
+ };
1382
+ },
1383
+ options
1384
+ };
1385
+ };
1386
+
1387
+ const twitter = (options) => {
1388
+ return {
1389
+ id: "twitter",
1390
+ name: "Twitter",
1391
+ createAuthorizationURL(data) {
1392
+ const _scopes = options.disableDefaultScope ? [] : ["users.read", "tweet.read", "offline.access", "users.email"];
1393
+ options.scope && _scopes.push(...options.scope);
1394
+ data.scopes && _scopes.push(...data.scopes);
1395
+ return createAuthorizationURL({
1396
+ id: "twitter",
1397
+ options,
1398
+ authorizationEndpoint: "https://x.com/i/oauth2/authorize",
1399
+ scopes: _scopes,
1400
+ state: data.state,
1401
+ codeVerifier: data.codeVerifier,
1402
+ redirectURI: data.redirectURI
1403
+ });
1404
+ },
1405
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
1406
+ return validateAuthorizationCode({
1407
+ code,
1408
+ codeVerifier,
1409
+ authentication: "basic",
1410
+ redirectURI,
1411
+ options,
1412
+ tokenEndpoint: "https://api.x.com/2/oauth2/token"
1413
+ });
1414
+ },
1415
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1416
+ return refreshAccessToken({
1417
+ refreshToken,
1418
+ options: {
1419
+ clientId: options.clientId,
1420
+ clientKey: options.clientKey,
1421
+ clientSecret: options.clientSecret
1422
+ },
1423
+ authentication: "basic",
1424
+ tokenEndpoint: "https://api.x.com/2/oauth2/token"
1425
+ });
1426
+ },
1427
+ async getUserInfo(token) {
1428
+ if (options.getUserInfo) {
1429
+ return options.getUserInfo(token);
1430
+ }
1431
+ const { data: profile, error: profileError } = await betterFetch(
1432
+ "https://api.x.com/2/users/me?user.fields=profile_image_url",
1433
+ {
1434
+ method: "GET",
1435
+ headers: {
1436
+ Authorization: `Bearer ${token.accessToken}`
1437
+ }
1438
+ }
1439
+ );
1440
+ if (profileError) {
1441
+ return null;
1442
+ }
1443
+ const { data: emailData, error: emailError } = await betterFetch("https://api.x.com/2/users/me?user.fields=confirmed_email", {
1444
+ method: "GET",
1445
+ headers: {
1446
+ Authorization: `Bearer ${token.accessToken}`
1447
+ }
1448
+ });
1449
+ let emailVerified = false;
1450
+ if (!emailError && emailData?.data?.confirmed_email) {
1451
+ profile.data.email = emailData.data.confirmed_email;
1452
+ emailVerified = true;
1453
+ }
1454
+ const userMap = await options.mapProfileToUser?.(profile);
1455
+ return {
1456
+ user: {
1457
+ id: profile.data.id,
1458
+ name: profile.data.name,
1459
+ email: profile.data.email || profile.data.username || null,
1460
+ image: profile.data.profile_image_url,
1461
+ emailVerified,
1462
+ ...userMap
1463
+ },
1464
+ data: profile
1465
+ };
1466
+ },
1467
+ options
1468
+ };
1469
+ };
1470
+
1471
+ const dropbox = (options) => {
1472
+ const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
1473
+ return {
1474
+ id: "dropbox",
1475
+ name: "Dropbox",
1476
+ createAuthorizationURL: async ({
1477
+ state,
1478
+ scopes,
1479
+ codeVerifier,
1480
+ redirectURI
1481
+ }) => {
1482
+ const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
1483
+ options.scope && _scopes.push(...options.scope);
1484
+ scopes && _scopes.push(...scopes);
1485
+ const additionalParams = {};
1486
+ if (options.accessType) {
1487
+ additionalParams.token_access_type = options.accessType;
1488
+ }
1489
+ return await createAuthorizationURL({
1490
+ id: "dropbox",
1491
+ options,
1492
+ authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
1493
+ scopes: _scopes,
1494
+ state,
1495
+ redirectURI,
1496
+ codeVerifier,
1497
+ additionalParams
1498
+ });
1499
+ },
1500
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
1501
+ return await validateAuthorizationCode({
1502
+ code,
1503
+ codeVerifier,
1504
+ redirectURI,
1505
+ options,
1506
+ tokenEndpoint
1507
+ });
1508
+ },
1509
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1510
+ return refreshAccessToken({
1511
+ refreshToken,
1512
+ options: {
1513
+ clientId: options.clientId,
1514
+ clientKey: options.clientKey,
1515
+ clientSecret: options.clientSecret
1516
+ },
1517
+ tokenEndpoint: "https://api.dropbox.com/oauth2/token"
1518
+ });
1519
+ },
1520
+ async getUserInfo(token) {
1521
+ if (options.getUserInfo) {
1522
+ return options.getUserInfo(token);
1523
+ }
1524
+ const { data: profile, error } = await betterFetch(
1525
+ "https://api.dropboxapi.com/2/users/get_current_account",
1526
+ {
1527
+ method: "POST",
1528
+ headers: {
1529
+ Authorization: `Bearer ${token.accessToken}`
1530
+ }
1531
+ }
1532
+ );
1533
+ if (error) {
1534
+ return null;
1535
+ }
1536
+ const userMap = await options.mapProfileToUser?.(profile);
1537
+ return {
1538
+ user: {
1539
+ id: profile.account_id,
1540
+ name: profile.name?.display_name,
1541
+ email: profile.email,
1542
+ emailVerified: profile.email_verified || false,
1543
+ image: profile.profile_photo_url,
1544
+ ...userMap
1545
+ },
1546
+ data: profile
1547
+ };
1548
+ },
1549
+ options
1550
+ };
1551
+ };
1552
+
1553
+ const linear = (options) => {
1554
+ const tokenEndpoint = "https://api.linear.app/oauth/token";
1555
+ return {
1556
+ id: "linear",
1557
+ name: "Linear",
1558
+ createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
1559
+ const _scopes = options.disableDefaultScope ? [] : ["read"];
1560
+ options.scope && _scopes.push(...options.scope);
1561
+ scopes && _scopes.push(...scopes);
1562
+ return createAuthorizationURL({
1563
+ id: "linear",
1564
+ options,
1565
+ authorizationEndpoint: "https://linear.app/oauth/authorize",
1566
+ scopes: _scopes,
1567
+ state,
1568
+ redirectURI,
1569
+ loginHint
1570
+ });
1571
+ },
1572
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1573
+ return validateAuthorizationCode({
1574
+ code,
1575
+ redirectURI,
1576
+ options,
1577
+ tokenEndpoint
1578
+ });
1579
+ },
1580
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1581
+ return refreshAccessToken({
1582
+ refreshToken,
1583
+ options: {
1584
+ clientId: options.clientId,
1585
+ clientKey: options.clientKey,
1586
+ clientSecret: options.clientSecret
1587
+ },
1588
+ tokenEndpoint
1589
+ });
1590
+ },
1591
+ async getUserInfo(token) {
1592
+ if (options.getUserInfo) {
1593
+ return options.getUserInfo(token);
1594
+ }
1595
+ const { data: profile, error } = await betterFetch(
1596
+ "https://api.linear.app/graphql",
1597
+ {
1598
+ method: "POST",
1599
+ headers: {
1600
+ "Content-Type": "application/json",
1601
+ Authorization: `Bearer ${token.accessToken}`
1602
+ },
1603
+ body: JSON.stringify({
1604
+ query: `
1605
+ query {
1606
+ viewer {
1607
+ id
1608
+ name
1609
+ email
1610
+ avatarUrl
1611
+ active
1612
+ createdAt
1613
+ updatedAt
1614
+ }
1615
+ }
1616
+ `
1617
+ })
1618
+ }
1619
+ );
1620
+ if (error || !profile?.data?.viewer) {
1621
+ return null;
1622
+ }
1623
+ const userData = profile.data.viewer;
1624
+ const userMap = await options.mapProfileToUser?.(userData);
1625
+ return {
1626
+ user: {
1627
+ id: profile.data.viewer.id,
1628
+ name: profile.data.viewer.name,
1629
+ email: profile.data.viewer.email,
1630
+ image: profile.data.viewer.avatarUrl,
1631
+ emailVerified: true,
1632
+ ...userMap
1633
+ },
1634
+ data: userData
1635
+ };
1636
+ },
1637
+ options
1638
+ };
1639
+ };
1640
+
1641
+ const linkedin = (options) => {
1642
+ const authorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
1643
+ const tokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
1644
+ return {
1645
+ id: "linkedin",
1646
+ name: "Linkedin",
1647
+ createAuthorizationURL: async ({
1648
+ state,
1649
+ scopes,
1650
+ redirectURI,
1651
+ loginHint
1652
+ }) => {
1653
+ const _scopes = options.disableDefaultScope ? [] : ["profile", "email", "openid"];
1654
+ options.scope && _scopes.push(...options.scope);
1655
+ scopes && _scopes.push(...scopes);
1656
+ return await createAuthorizationURL({
1657
+ id: "linkedin",
1658
+ options,
1659
+ authorizationEndpoint,
1660
+ scopes: _scopes,
1661
+ state,
1662
+ loginHint,
1663
+ redirectURI
1664
+ });
1665
+ },
1666
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1667
+ return await validateAuthorizationCode({
1668
+ code,
1669
+ redirectURI,
1670
+ options,
1671
+ tokenEndpoint
1672
+ });
1673
+ },
1674
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1675
+ return refreshAccessToken({
1676
+ refreshToken,
1677
+ options: {
1678
+ clientId: options.clientId,
1679
+ clientKey: options.clientKey,
1680
+ clientSecret: options.clientSecret
1681
+ },
1682
+ tokenEndpoint
1683
+ });
1684
+ },
1685
+ async getUserInfo(token) {
1686
+ if (options.getUserInfo) {
1687
+ return options.getUserInfo(token);
1688
+ }
1689
+ const { data: profile, error } = await betterFetch(
1690
+ "https://api.linkedin.com/v2/userinfo",
1691
+ {
1692
+ method: "GET",
1693
+ headers: {
1694
+ Authorization: `Bearer ${token.accessToken}`
1695
+ }
1696
+ }
1697
+ );
1698
+ if (error) {
1699
+ return null;
1700
+ }
1701
+ const userMap = await options.mapProfileToUser?.(profile);
1702
+ return {
1703
+ user: {
1704
+ id: profile.sub,
1705
+ name: profile.name,
1706
+ email: profile.email,
1707
+ emailVerified: profile.email_verified || false,
1708
+ image: profile.picture,
1709
+ ...userMap
1710
+ },
1711
+ data: profile
1712
+ };
1713
+ },
1714
+ options
1715
+ };
1716
+ };
1717
+
1718
+ const cleanDoubleSlashes = (input = "") => {
1719
+ return input.split("://").map((str) => str.replace(/\/{2,}/g, "/")).join("://");
1720
+ };
1721
+ const issuerToEndpoints = (issuer) => {
1722
+ let baseUrl = issuer || "https://gitlab.com";
1723
+ return {
1724
+ authorizationEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/authorize`),
1725
+ tokenEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/token`),
1726
+ userinfoEndpoint: cleanDoubleSlashes(`${baseUrl}/api/v4/user`)
1727
+ };
1728
+ };
1729
+ const gitlab = (options) => {
1730
+ const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } = issuerToEndpoints(options.issuer);
1731
+ const issuerId = "gitlab";
1732
+ const issuerName = "Gitlab";
1733
+ return {
1734
+ id: issuerId,
1735
+ name: issuerName,
1736
+ createAuthorizationURL: async ({
1737
+ state,
1738
+ scopes,
1739
+ codeVerifier,
1740
+ loginHint,
1741
+ redirectURI
1742
+ }) => {
1743
+ const _scopes = options.disableDefaultScope ? [] : ["read_user"];
1744
+ options.scope && _scopes.push(...options.scope);
1745
+ scopes && _scopes.push(...scopes);
1746
+ return await createAuthorizationURL({
1747
+ id: issuerId,
1748
+ options,
1749
+ authorizationEndpoint,
1750
+ scopes: _scopes,
1751
+ state,
1752
+ redirectURI,
1753
+ codeVerifier,
1754
+ loginHint
1755
+ });
1756
+ },
1757
+ validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
1758
+ return validateAuthorizationCode({
1759
+ code,
1760
+ redirectURI,
1761
+ options,
1762
+ codeVerifier,
1763
+ tokenEndpoint
1764
+ });
1765
+ },
1766
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1767
+ return refreshAccessToken({
1768
+ refreshToken,
1769
+ options: {
1770
+ clientId: options.clientId,
1771
+ clientKey: options.clientKey,
1772
+ clientSecret: options.clientSecret
1773
+ },
1774
+ tokenEndpoint
1775
+ });
1776
+ },
1777
+ async getUserInfo(token) {
1778
+ if (options.getUserInfo) {
1779
+ return options.getUserInfo(token);
1780
+ }
1781
+ const { data: profile, error } = await betterFetch(
1782
+ userinfoEndpoint,
1783
+ { headers: { authorization: `Bearer ${token.accessToken}` } }
1784
+ );
1785
+ if (error || profile.state !== "active" || profile.locked) {
1786
+ return null;
1787
+ }
1788
+ const userMap = await options.mapProfileToUser?.(profile);
1789
+ return {
1790
+ user: {
1791
+ id: profile.id,
1792
+ name: profile.name ?? profile.username,
1793
+ email: profile.email,
1794
+ image: profile.avatar_url,
1795
+ emailVerified: true,
1796
+ ...userMap
1797
+ },
1798
+ data: profile
1799
+ };
1800
+ },
1801
+ options
1802
+ };
1803
+ };
1804
+
1805
+ const tiktok = (options) => {
1806
+ return {
1807
+ id: "tiktok",
1808
+ name: "TikTok",
1809
+ createAuthorizationURL({ state, scopes, redirectURI }) {
1810
+ const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
1811
+ options.scope && _scopes.push(...options.scope);
1812
+ scopes && _scopes.push(...scopes);
1813
+ return new URL(
1814
+ `https://www.tiktok.com/v2/auth/authorize?scope=${_scopes.join(
1815
+ ","
1816
+ )}&response_type=code&client_key=${options.clientKey}&redirect_uri=${encodeURIComponent(
1817
+ options.redirectURI || redirectURI
1818
+ )}&state=${state}`
1819
+ );
1820
+ },
1821
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1822
+ return validateAuthorizationCode({
1823
+ code,
1824
+ redirectURI: options.redirectURI || redirectURI,
1825
+ options: {
1826
+ clientKey: options.clientKey,
1827
+ clientSecret: options.clientSecret
1828
+ },
1829
+ tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/"
1830
+ });
1831
+ },
1832
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1833
+ return refreshAccessToken({
1834
+ refreshToken,
1835
+ options: {
1836
+ clientSecret: options.clientSecret
1837
+ },
1838
+ tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
1839
+ authentication: "post",
1840
+ extraParams: {
1841
+ client_key: options.clientKey
1842
+ }
1843
+ });
1844
+ },
1845
+ async getUserInfo(token) {
1846
+ if (options.getUserInfo) {
1847
+ return options.getUserInfo(token);
1848
+ }
1849
+ const fields = [
1850
+ "open_id",
1851
+ "avatar_large_url",
1852
+ "display_name",
1853
+ "username"
1854
+ ];
1855
+ const { data: profile, error } = await betterFetch(
1856
+ `https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`,
1857
+ {
1858
+ headers: {
1859
+ authorization: `Bearer ${token.accessToken}`
1860
+ }
1861
+ }
1862
+ );
1863
+ if (error) {
1864
+ return null;
1865
+ }
1866
+ return {
1867
+ user: {
1868
+ email: profile.data.user.email || profile.data.user.username,
1869
+ id: profile.data.user.open_id,
1870
+ name: profile.data.user.display_name || profile.data.user.username,
1871
+ image: profile.data.user.avatar_large_url,
1872
+ /** @note Tiktok does not provide emailVerified or even email*/
1873
+ emailVerified: profile.data.user.email ? true : false
1874
+ },
1875
+ data: profile
1876
+ };
1877
+ },
1878
+ options
1879
+ };
1880
+ };
1881
+
1882
+ const reddit = (options) => {
1883
+ return {
1884
+ id: "reddit",
1885
+ name: "Reddit",
1886
+ createAuthorizationURL({ state, scopes, redirectURI }) {
1887
+ const _scopes = options.disableDefaultScope ? [] : ["identity"];
1888
+ options.scope && _scopes.push(...options.scope);
1889
+ scopes && _scopes.push(...scopes);
1890
+ return createAuthorizationURL({
1891
+ id: "reddit",
1892
+ options,
1893
+ authorizationEndpoint: "https://www.reddit.com/api/v1/authorize",
1894
+ scopes: _scopes,
1895
+ state,
1896
+ redirectURI,
1897
+ duration: options.duration
1898
+ });
1899
+ },
1900
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1901
+ const body = new URLSearchParams({
1902
+ grant_type: "authorization_code",
1903
+ code,
1904
+ redirect_uri: options.redirectURI || redirectURI
1905
+ });
1906
+ const headers = {
1907
+ "content-type": "application/x-www-form-urlencoded",
1908
+ accept: "text/plain",
1909
+ "user-agent": "better-auth",
1910
+ Authorization: `Basic ${base64.encode(
1911
+ `${options.clientId}:${options.clientSecret}`
1912
+ )}`
1913
+ };
1914
+ const { data, error } = await betterFetch(
1915
+ "https://www.reddit.com/api/v1/access_token",
1916
+ {
1917
+ method: "POST",
1918
+ headers,
1919
+ body: body.toString()
1920
+ }
1921
+ );
1922
+ if (error) {
1923
+ throw error;
1924
+ }
1925
+ return getOAuth2Tokens(data);
1926
+ },
1927
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1928
+ return refreshAccessToken({
1929
+ refreshToken,
1930
+ options: {
1931
+ clientId: options.clientId,
1932
+ clientKey: options.clientKey,
1933
+ clientSecret: options.clientSecret
1934
+ },
1935
+ authentication: "basic",
1936
+ tokenEndpoint: "https://www.reddit.com/api/v1/access_token"
1937
+ });
1938
+ },
1939
+ async getUserInfo(token) {
1940
+ if (options.getUserInfo) {
1941
+ return options.getUserInfo(token);
1942
+ }
1943
+ const { data: profile, error } = await betterFetch(
1944
+ "https://oauth.reddit.com/api/v1/me",
1945
+ {
1946
+ headers: {
1947
+ Authorization: `Bearer ${token.accessToken}`,
1948
+ "User-Agent": "better-auth"
1949
+ }
1950
+ }
1951
+ );
1952
+ if (error) {
1953
+ return null;
1954
+ }
1955
+ const userMap = await options.mapProfileToUser?.(profile);
1956
+ return {
1957
+ user: {
1958
+ id: profile.id,
1959
+ name: profile.name,
1960
+ email: profile.oauth_client_id,
1961
+ emailVerified: profile.has_verified_email,
1962
+ image: profile.icon_img?.split("?")[0],
1963
+ ...userMap
1964
+ },
1965
+ data: profile
1966
+ };
1967
+ },
1968
+ options
1969
+ };
1970
+ };
1971
+
1972
+ const roblox = (options) => {
1973
+ return {
1974
+ id: "roblox",
1975
+ name: "Roblox",
1976
+ createAuthorizationURL({ state, scopes, redirectURI }) {
1977
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
1978
+ options.scope && _scopes.push(...options.scope);
1979
+ scopes && _scopes.push(...scopes);
1980
+ return new URL(
1981
+ `https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join(
1982
+ "+"
1983
+ )}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent(
1984
+ options.redirectURI || redirectURI
1985
+ )}&state=${state}&prompt=${options.prompt || "select_account consent"}`
1986
+ );
1987
+ },
1988
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
1989
+ return validateAuthorizationCode({
1990
+ code,
1991
+ redirectURI: options.redirectURI || redirectURI,
1992
+ options,
1993
+ tokenEndpoint: "https://apis.roblox.com/oauth/v1/token",
1994
+ authentication: "post"
1995
+ });
1996
+ },
1997
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
1998
+ return refreshAccessToken({
1999
+ refreshToken,
2000
+ options: {
2001
+ clientId: options.clientId,
2002
+ clientKey: options.clientKey,
2003
+ clientSecret: options.clientSecret
2004
+ },
2005
+ tokenEndpoint: "https://apis.roblox.com/oauth/v1/token"
2006
+ });
2007
+ },
2008
+ async getUserInfo(token) {
2009
+ if (options.getUserInfo) {
2010
+ return options.getUserInfo(token);
2011
+ }
2012
+ const { data: profile, error } = await betterFetch(
2013
+ "https://apis.roblox.com/oauth/v1/userinfo",
2014
+ {
2015
+ headers: {
2016
+ authorization: `Bearer ${token.accessToken}`
2017
+ }
2018
+ }
2019
+ );
2020
+ if (error) {
2021
+ return null;
2022
+ }
2023
+ const userMap = await options.mapProfileToUser?.(profile);
2024
+ return {
2025
+ user: {
2026
+ id: profile.sub,
2027
+ name: profile.nickname || profile.preferred_username || "",
2028
+ image: profile.picture,
2029
+ email: profile.preferred_username || null,
2030
+ // Roblox does not provide email
2031
+ emailVerified: true,
2032
+ ...userMap
2033
+ },
2034
+ data: {
2035
+ ...profile
2036
+ }
2037
+ };
2038
+ },
2039
+ options
2040
+ };
2041
+ };
2042
+
2043
+ const salesforce = (options) => {
2044
+ const environment = options.environment ?? "production";
2045
+ const isSandbox = environment === "sandbox";
2046
+ const authorizationEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/authorize` : isSandbox ? "https://test.salesforce.com/services/oauth2/authorize" : "https://login.salesforce.com/services/oauth2/authorize";
2047
+ const tokenEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/token` : isSandbox ? "https://test.salesforce.com/services/oauth2/token" : "https://login.salesforce.com/services/oauth2/token";
2048
+ const userInfoEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/userinfo` : isSandbox ? "https://test.salesforce.com/services/oauth2/userinfo" : "https://login.salesforce.com/services/oauth2/userinfo";
2049
+ return {
2050
+ id: "salesforce",
2051
+ name: "Salesforce",
2052
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
2053
+ if (!options.clientId || !options.clientSecret) {
2054
+ logger.error(
2055
+ "Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options."
2056
+ );
2057
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
2058
+ }
2059
+ if (!codeVerifier) {
2060
+ throw new BetterAuthError("codeVerifier is required for Salesforce");
2061
+ }
2062
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "email", "profile"];
2063
+ options.scope && _scopes.push(...options.scope);
2064
+ scopes && _scopes.push(...scopes);
2065
+ return createAuthorizationURL({
2066
+ id: "salesforce",
2067
+ options,
2068
+ authorizationEndpoint,
2069
+ scopes: _scopes,
2070
+ state,
2071
+ codeVerifier,
2072
+ redirectURI: options.redirectURI || redirectURI
2073
+ });
2074
+ },
2075
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
2076
+ return validateAuthorizationCode({
2077
+ code,
2078
+ codeVerifier,
2079
+ redirectURI: options.redirectURI || redirectURI,
2080
+ options,
2081
+ tokenEndpoint
2082
+ });
2083
+ },
2084
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2085
+ return refreshAccessToken({
2086
+ refreshToken,
2087
+ options: {
2088
+ clientId: options.clientId,
2089
+ clientSecret: options.clientSecret
2090
+ },
2091
+ tokenEndpoint
2092
+ });
2093
+ },
2094
+ async getUserInfo(token) {
2095
+ if (options.getUserInfo) {
2096
+ return options.getUserInfo(token);
2097
+ }
2098
+ try {
2099
+ const { data: user } = await betterFetch(
2100
+ userInfoEndpoint,
2101
+ {
2102
+ headers: {
2103
+ Authorization: `Bearer ${token.accessToken}`
2104
+ }
2105
+ }
2106
+ );
2107
+ if (!user) {
2108
+ logger.error("Failed to fetch user info from Salesforce");
2109
+ return null;
2110
+ }
2111
+ const userMap = await options.mapProfileToUser?.(user);
2112
+ return {
2113
+ user: {
2114
+ id: user.user_id,
2115
+ name: user.name,
2116
+ email: user.email,
2117
+ image: user.photos?.picture || user.photos?.thumbnail,
2118
+ emailVerified: user.email_verified ?? false,
2119
+ ...userMap
2120
+ },
2121
+ data: user
2122
+ };
2123
+ } catch (error) {
2124
+ logger.error("Failed to fetch user info from Salesforce:", error);
2125
+ return null;
2126
+ }
2127
+ },
2128
+ options
2129
+ };
2130
+ };
2131
+
2132
+ const vk = (options) => {
2133
+ return {
2134
+ id: "vk",
2135
+ name: "VK",
2136
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
2137
+ const _scopes = options.disableDefaultScope ? [] : ["email", "phone"];
2138
+ options.scope && _scopes.push(...options.scope);
2139
+ scopes && _scopes.push(...scopes);
2140
+ const authorizationEndpoint = "https://id.vk.com/authorize";
2141
+ return createAuthorizationURL({
2142
+ id: "vk",
2143
+ options,
2144
+ authorizationEndpoint,
2145
+ scopes: _scopes,
2146
+ state,
2147
+ redirectURI,
2148
+ codeVerifier
2149
+ });
2150
+ },
2151
+ validateAuthorizationCode: async ({
2152
+ code,
2153
+ codeVerifier,
2154
+ redirectURI,
2155
+ deviceId
2156
+ }) => {
2157
+ return validateAuthorizationCode({
2158
+ code,
2159
+ codeVerifier,
2160
+ redirectURI: options.redirectURI || redirectURI,
2161
+ options,
2162
+ deviceId,
2163
+ tokenEndpoint: "https://id.vk.com/oauth2/auth"
2164
+ });
2165
+ },
2166
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2167
+ return refreshAccessToken({
2168
+ refreshToken,
2169
+ options: {
2170
+ clientId: options.clientId,
2171
+ clientKey: options.clientKey,
2172
+ clientSecret: options.clientSecret
2173
+ },
2174
+ tokenEndpoint: "https://id.vk.com/oauth2/auth"
2175
+ });
2176
+ },
2177
+ async getUserInfo(data) {
2178
+ if (options.getUserInfo) {
2179
+ return options.getUserInfo(data);
2180
+ }
2181
+ if (!data.accessToken) {
2182
+ return null;
2183
+ }
2184
+ const formBody = new URLSearchParams({
2185
+ access_token: data.accessToken,
2186
+ client_id: options.clientId
2187
+ }).toString();
2188
+ const { data: profile, error } = await betterFetch(
2189
+ "https://id.vk.com/oauth2/user_info",
2190
+ {
2191
+ method: "POST",
2192
+ headers: {
2193
+ "Content-Type": "application/x-www-form-urlencoded"
2194
+ },
2195
+ body: formBody
2196
+ }
2197
+ );
2198
+ if (error) {
2199
+ return null;
2200
+ }
2201
+ if (!profile.user.email) {
2202
+ return null;
2203
+ }
2204
+ const userMap = await options.mapProfileToUser?.(profile);
2205
+ return {
2206
+ user: {
2207
+ id: profile.user.user_id,
2208
+ first_name: profile.user.first_name,
2209
+ last_name: profile.user.last_name,
2210
+ email: profile.user.email,
2211
+ image: profile.user.avatar,
2212
+ /** @note VK does not provide emailVerified*/
2213
+ emailVerified: !!profile.user.email,
2214
+ birthday: profile.user.birthday,
2215
+ sex: profile.user.sex,
2216
+ name: `${profile.user.first_name} ${profile.user.last_name}`,
2217
+ ...userMap
2218
+ },
2219
+ data: profile
2220
+ };
2221
+ },
2222
+ options
2223
+ };
2224
+ };
2225
+
2226
+ const zoom = (userOptions) => {
2227
+ const options = {
2228
+ pkce: true,
2229
+ ...userOptions
2230
+ };
2231
+ return {
2232
+ id: "zoom",
2233
+ name: "Zoom",
2234
+ createAuthorizationURL: async ({ state, redirectURI, codeVerifier }) => {
2235
+ const params = new URLSearchParams({
2236
+ response_type: "code",
2237
+ redirect_uri: options.redirectURI ? options.redirectURI : redirectURI,
2238
+ client_id: options.clientId,
2239
+ state
2240
+ });
2241
+ if (options.pkce) {
2242
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
2243
+ params.set("code_challenge_method", "S256");
2244
+ params.set("code_challenge", codeChallenge);
2245
+ }
2246
+ const url = new URL("https://zoom.us/oauth/authorize");
2247
+ url.search = params.toString();
2248
+ return url;
2249
+ },
2250
+ validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
2251
+ return validateAuthorizationCode({
2252
+ code,
2253
+ redirectURI: options.redirectURI || redirectURI,
2254
+ codeVerifier,
2255
+ options,
2256
+ tokenEndpoint: "https://zoom.us/oauth/token",
2257
+ authentication: "post"
2258
+ });
2259
+ },
2260
+ async getUserInfo(token) {
2261
+ if (options.getUserInfo) {
2262
+ return options.getUserInfo(token);
2263
+ }
2264
+ const { data: profile, error } = await betterFetch(
2265
+ "https://api.zoom.us/v2/users/me",
2266
+ {
2267
+ headers: {
2268
+ authorization: `Bearer ${token.accessToken}`
2269
+ }
2270
+ }
2271
+ );
2272
+ if (error) {
2273
+ return null;
2274
+ }
2275
+ const userMap = await options.mapProfileToUser?.(profile);
2276
+ return {
2277
+ user: {
2278
+ id: profile.id,
2279
+ name: profile.display_name,
2280
+ image: profile.pic_url,
2281
+ email: profile.email,
2282
+ emailVerified: Boolean(profile.verified),
2283
+ ...userMap
2284
+ },
2285
+ data: {
2286
+ ...profile
2287
+ }
2288
+ };
2289
+ }
2290
+ };
2291
+ };
2292
+
2293
+ const kakao = (options) => {
2294
+ return {
2295
+ id: "kakao",
2296
+ name: "Kakao",
2297
+ createAuthorizationURL({ state, scopes, redirectURI }) {
2298
+ const _scopes = options.disableDefaultScope ? [] : ["account_email", "profile_image", "profile_nickname"];
2299
+ options.scope && _scopes.push(...options.scope);
2300
+ scopes && _scopes.push(...scopes);
2301
+ return createAuthorizationURL({
2302
+ id: "kakao",
2303
+ options,
2304
+ authorizationEndpoint: "https://kauth.kakao.com/oauth/authorize",
2305
+ scopes: _scopes,
2306
+ state,
2307
+ redirectURI
2308
+ });
2309
+ },
2310
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
2311
+ return validateAuthorizationCode({
2312
+ code,
2313
+ redirectURI,
2314
+ options,
2315
+ tokenEndpoint: "https://kauth.kakao.com/oauth/token"
2316
+ });
2317
+ },
2318
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2319
+ return refreshAccessToken({
2320
+ refreshToken,
2321
+ options: {
2322
+ clientId: options.clientId,
2323
+ clientKey: options.clientKey,
2324
+ clientSecret: options.clientSecret
2325
+ },
2326
+ tokenEndpoint: "https://kauth.kakao.com/oauth/token"
2327
+ });
2328
+ },
2329
+ async getUserInfo(token) {
2330
+ if (options.getUserInfo) {
2331
+ return options.getUserInfo(token);
2332
+ }
2333
+ const { data: profile, error } = await betterFetch(
2334
+ "https://kapi.kakao.com/v2/user/me",
2335
+ {
2336
+ headers: {
2337
+ Authorization: `Bearer ${token.accessToken}`
2338
+ }
2339
+ }
2340
+ );
2341
+ if (error || !profile) {
2342
+ return null;
2343
+ }
2344
+ const userMap = await options.mapProfileToUser?.(profile);
2345
+ const account = profile.kakao_account || {};
2346
+ const kakaoProfile = account.profile || {};
2347
+ const user = {
2348
+ id: String(profile.id),
2349
+ name: kakaoProfile.nickname || account.name || void 0,
2350
+ email: account.email,
2351
+ image: kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url,
2352
+ emailVerified: !!account.is_email_valid && !!account.is_email_verified,
2353
+ ...userMap
2354
+ };
2355
+ return {
2356
+ user,
2357
+ data: profile
2358
+ };
2359
+ },
2360
+ options
2361
+ };
2362
+ };
2363
+
2364
+ const naver = (options) => {
2365
+ return {
2366
+ id: "naver",
2367
+ name: "Naver",
2368
+ createAuthorizationURL({ state, scopes, redirectURI }) {
2369
+ const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
2370
+ options.scope && _scopes.push(...options.scope);
2371
+ scopes && _scopes.push(...scopes);
2372
+ return createAuthorizationURL({
2373
+ id: "naver",
2374
+ options,
2375
+ authorizationEndpoint: "https://nid.naver.com/oauth2.0/authorize",
2376
+ scopes: _scopes,
2377
+ state,
2378
+ redirectURI
2379
+ });
2380
+ },
2381
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
2382
+ return validateAuthorizationCode({
2383
+ code,
2384
+ redirectURI,
2385
+ options,
2386
+ tokenEndpoint: "https://nid.naver.com/oauth2.0/token"
2387
+ });
2388
+ },
2389
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2390
+ return refreshAccessToken({
2391
+ refreshToken,
2392
+ options: {
2393
+ clientId: options.clientId,
2394
+ clientKey: options.clientKey,
2395
+ clientSecret: options.clientSecret
2396
+ },
2397
+ tokenEndpoint: "https://nid.naver.com/oauth2.0/token"
2398
+ });
2399
+ },
2400
+ async getUserInfo(token) {
2401
+ if (options.getUserInfo) {
2402
+ return options.getUserInfo(token);
2403
+ }
2404
+ const { data: profile, error } = await betterFetch(
2405
+ "https://openapi.naver.com/v1/nid/me",
2406
+ {
2407
+ headers: {
2408
+ Authorization: `Bearer ${token.accessToken}`
2409
+ }
2410
+ }
2411
+ );
2412
+ if (error || !profile || profile.resultcode !== "00") {
2413
+ return null;
2414
+ }
2415
+ const userMap = await options.mapProfileToUser?.(profile);
2416
+ const res = profile.response || {};
2417
+ const user = {
2418
+ id: res.id,
2419
+ name: res.name || res.nickname,
2420
+ email: res.email,
2421
+ image: res.profile_image,
2422
+ emailVerified: false,
2423
+ ...userMap
2424
+ };
2425
+ return {
2426
+ user,
2427
+ data: profile
2428
+ };
2429
+ },
2430
+ options
2431
+ };
2432
+ };
2433
+
2434
+ const line = (options) => {
2435
+ const authorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize";
2436
+ const tokenEndpoint = "https://api.line.me/oauth2/v2.1/token";
2437
+ const userInfoEndpoint = "https://api.line.me/oauth2/v2.1/userinfo";
2438
+ const verifyIdTokenEndpoint = "https://api.line.me/oauth2/v2.1/verify";
2439
+ return {
2440
+ id: "line",
2441
+ name: "LINE",
2442
+ async createAuthorizationURL({
2443
+ state,
2444
+ scopes,
2445
+ codeVerifier,
2446
+ redirectURI,
2447
+ loginHint
2448
+ }) {
2449
+ const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
2450
+ options.scope && _scopes.push(...options.scope);
2451
+ scopes && _scopes.push(...scopes);
2452
+ return await createAuthorizationURL({
2453
+ id: "line",
2454
+ options,
2455
+ authorizationEndpoint,
2456
+ scopes: _scopes,
2457
+ state,
2458
+ codeVerifier,
2459
+ redirectURI,
2460
+ loginHint
2461
+ });
2462
+ },
2463
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
2464
+ return validateAuthorizationCode({
2465
+ code,
2466
+ codeVerifier,
2467
+ redirectURI,
2468
+ options,
2469
+ tokenEndpoint
2470
+ });
2471
+ },
2472
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2473
+ return refreshAccessToken({
2474
+ refreshToken,
2475
+ options: {
2476
+ clientId: options.clientId,
2477
+ clientSecret: options.clientSecret
2478
+ },
2479
+ tokenEndpoint
2480
+ });
2481
+ },
2482
+ async verifyIdToken(token, nonce) {
2483
+ if (options.disableIdTokenSignIn) {
2484
+ return false;
2485
+ }
2486
+ if (options.verifyIdToken) {
2487
+ return options.verifyIdToken(token, nonce);
2488
+ }
2489
+ const body = new URLSearchParams();
2490
+ body.set("id_token", token);
2491
+ body.set("client_id", options.clientId);
2492
+ if (nonce) body.set("nonce", nonce);
2493
+ const { data, error } = await betterFetch(
2494
+ verifyIdTokenEndpoint,
2495
+ {
2496
+ method: "POST",
2497
+ headers: {
2498
+ "content-type": "application/x-www-form-urlencoded"
2499
+ },
2500
+ body
2501
+ }
2502
+ );
2503
+ if (error || !data) {
2504
+ return false;
2505
+ }
2506
+ if (data.aud !== options.clientId) return false;
2507
+ if (nonce && data.nonce && data.nonce !== nonce) return false;
2508
+ return true;
2509
+ },
2510
+ async getUserInfo(token) {
2511
+ if (options.getUserInfo) {
2512
+ return options.getUserInfo(token);
2513
+ }
2514
+ let profile = null;
2515
+ if (token.idToken) {
2516
+ try {
2517
+ profile = decodeJwt(token.idToken);
2518
+ } catch {
2519
+ }
2520
+ }
2521
+ if (!profile) {
2522
+ const { data } = await betterFetch(userInfoEndpoint, {
2523
+ headers: {
2524
+ authorization: `Bearer ${token.accessToken}`
2525
+ }
2526
+ });
2527
+ profile = data || null;
2528
+ }
2529
+ if (!profile) return null;
2530
+ const userMap = await options.mapProfileToUser?.(profile);
2531
+ const id = profile.sub || profile.userId;
2532
+ const name = profile.name || profile.displayName;
2533
+ const image = profile.picture || profile.pictureUrl || void 0;
2534
+ const email = profile.email;
2535
+ return {
2536
+ user: {
2537
+ id,
2538
+ name,
2539
+ email,
2540
+ image,
2541
+ // LINE does not expose email verification status in ID token/userinfo
2542
+ emailVerified: false,
2543
+ ...userMap
2544
+ },
2545
+ data: profile
2546
+ };
2547
+ },
2548
+ options
2549
+ };
2550
+ };
2551
+
2552
+ const paypal = (options) => {
2553
+ const environment = options.environment || "sandbox";
2554
+ const isSandbox = environment === "sandbox";
2555
+ const authorizationEndpoint = isSandbox ? "https://www.sandbox.paypal.com/signin/authorize" : "https://www.paypal.com/signin/authorize";
2556
+ const tokenEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/oauth2/token" : "https://api-m.paypal.com/v1/oauth2/token";
2557
+ const userInfoEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo" : "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
2558
+ return {
2559
+ id: "paypal",
2560
+ name: "PayPal",
2561
+ async createAuthorizationURL({ state, codeVerifier, redirectURI }) {
2562
+ if (!options.clientId || !options.clientSecret) {
2563
+ logger.error(
2564
+ "Client Id and Client Secret is required for PayPal. Make sure to provide them in the options."
2565
+ );
2566
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
2567
+ }
2568
+ const _scopes = [];
2569
+ const url = await createAuthorizationURL({
2570
+ id: "paypal",
2571
+ options,
2572
+ authorizationEndpoint,
2573
+ scopes: _scopes,
2574
+ state,
2575
+ codeVerifier,
2576
+ redirectURI,
2577
+ prompt: options.prompt
2578
+ });
2579
+ return url;
2580
+ },
2581
+ validateAuthorizationCode: async ({ code, redirectURI }) => {
2582
+ const credentials = base64.encode(
2583
+ `${options.clientId}:${options.clientSecret}`
2584
+ );
2585
+ try {
2586
+ const response = await betterFetch(tokenEndpoint, {
2587
+ method: "POST",
2588
+ headers: {
2589
+ Authorization: `Basic ${credentials}`,
2590
+ Accept: "application/json",
2591
+ "Accept-Language": "en_US",
2592
+ "Content-Type": "application/x-www-form-urlencoded"
2593
+ },
2594
+ body: new URLSearchParams({
2595
+ grant_type: "authorization_code",
2596
+ code,
2597
+ redirect_uri: redirectURI
2598
+ }).toString()
2599
+ });
2600
+ if (!response.data) {
2601
+ throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
2602
+ }
2603
+ const data = response.data;
2604
+ const result = {
2605
+ accessToken: data.access_token,
2606
+ refreshToken: data.refresh_token,
2607
+ accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
2608
+ idToken: data.id_token
2609
+ };
2610
+ return result;
2611
+ } catch (error) {
2612
+ logger.error("PayPal token exchange failed:", error);
2613
+ throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
2614
+ }
2615
+ },
2616
+ refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
2617
+ const credentials = base64.encode(
2618
+ `${options.clientId}:${options.clientSecret}`
2619
+ );
2620
+ try {
2621
+ const response = await betterFetch(tokenEndpoint, {
2622
+ method: "POST",
2623
+ headers: {
2624
+ Authorization: `Basic ${credentials}`,
2625
+ Accept: "application/json",
2626
+ "Accept-Language": "en_US",
2627
+ "Content-Type": "application/x-www-form-urlencoded"
2628
+ },
2629
+ body: new URLSearchParams({
2630
+ grant_type: "refresh_token",
2631
+ refresh_token: refreshToken
2632
+ }).toString()
2633
+ });
2634
+ if (!response.data) {
2635
+ throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
2636
+ }
2637
+ const data = response.data;
2638
+ return {
2639
+ accessToken: data.access_token,
2640
+ refreshToken: data.refresh_token,
2641
+ accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0
2642
+ };
2643
+ } catch (error) {
2644
+ logger.error("PayPal token refresh failed:", error);
2645
+ throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
2646
+ }
2647
+ },
2648
+ async verifyIdToken(token, nonce) {
2649
+ if (options.disableIdTokenSignIn) {
2650
+ return false;
2651
+ }
2652
+ if (options.verifyIdToken) {
2653
+ return options.verifyIdToken(token, nonce);
2654
+ }
2655
+ try {
2656
+ const payload = decodeJwt(token);
2657
+ return !!payload.sub;
2658
+ } catch (error) {
2659
+ logger.error("Failed to verify PayPal ID token:", error);
2660
+ return false;
2661
+ }
2662
+ },
2663
+ async getUserInfo(token) {
2664
+ if (options.getUserInfo) {
2665
+ return options.getUserInfo(token);
2666
+ }
2667
+ if (!token.accessToken) {
2668
+ logger.error("Access token is required to fetch PayPal user info");
2669
+ return null;
2670
+ }
2671
+ try {
2672
+ const response = await betterFetch(
2673
+ `${userInfoEndpoint}?schema=paypalv1.1`,
2674
+ {
2675
+ headers: {
2676
+ Authorization: `Bearer ${token.accessToken}`,
2677
+ Accept: "application/json"
2678
+ }
2679
+ }
2680
+ );
2681
+ if (!response.data) {
2682
+ logger.error("Failed to fetch user info from PayPal");
2683
+ return null;
2684
+ }
2685
+ const userInfo = response.data;
2686
+ const userMap = await options.mapProfileToUser?.(userInfo);
2687
+ const result = {
2688
+ user: {
2689
+ id: userInfo.user_id,
2690
+ name: userInfo.name,
2691
+ email: userInfo.email,
2692
+ image: userInfo.picture,
2693
+ emailVerified: userInfo.email_verified,
2694
+ ...userMap
2695
+ },
2696
+ data: userInfo
2697
+ };
2698
+ return result;
2699
+ } catch (error) {
2700
+ logger.error("Failed to fetch user info from PayPal:", error);
2701
+ return null;
2702
+ }
2703
+ },
2704
+ options
2705
+ };
2706
+ };
2707
+
2708
+ const socialProviders = {
2709
+ apple,
2710
+ atlassian,
2711
+ cognito,
2712
+ discord,
2713
+ facebook,
2714
+ figma,
2715
+ github,
2716
+ microsoft,
2717
+ google,
2718
+ huggingface,
2719
+ slack,
2720
+ spotify,
2721
+ twitch,
2722
+ twitter,
2723
+ dropbox,
2724
+ kick,
2725
+ linear,
2726
+ linkedin,
2727
+ gitlab,
2728
+ tiktok,
2729
+ reddit,
2730
+ roblox,
2731
+ salesforce,
2732
+ vk,
2733
+ zoom,
2734
+ notion,
2735
+ kakao,
2736
+ naver,
2737
+ line,
2738
+ paypal
2739
+ };
2740
+ const socialProviderList = Object.keys(socialProviders);
2741
+ const SocialProviderListEnum = z.enum(socialProviderList).or(z.string());
2742
+
2743
+ export { SocialProviderListEnum, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paypal, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vk, zoom };