@better-auth/core 1.7.0-rc.1 → 1.7.0-rc.3

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 (213) hide show
  1. package/dist/api/index.d.mts +28 -28
  2. package/dist/async_hooks/index.d.mts +0 -1
  3. package/dist/async_hooks/pure.index.d.mts +0 -1
  4. package/dist/context/endpoint-context.d.mts +1 -1
  5. package/dist/context/global.d.mts +0 -1
  6. package/dist/context/global.mjs +1 -1
  7. package/dist/context/request-state.d.mts +0 -1
  8. package/dist/context/request-state.mjs +8 -5
  9. package/dist/context/transaction.d.mts +8 -3
  10. package/dist/context/transaction.mjs +27 -10
  11. package/dist/db/adapter/factory.d.mts +3 -5
  12. package/dist/db/adapter/factory.mjs +7 -5
  13. package/dist/db/adapter/get-default-field-name.d.mts +2 -9
  14. package/dist/db/adapter/get-default-model-name.d.mts +1 -5
  15. package/dist/db/adapter/get-default-model-name.mjs +6 -5
  16. package/dist/db/adapter/get-field-attributes.d.mts +4 -11
  17. package/dist/db/adapter/get-field-name.d.mts +2 -9
  18. package/dist/db/adapter/get-id-field.d.mts +4 -12
  19. package/dist/db/adapter/get-model-name.d.mts +1 -5
  20. package/dist/db/adapter/index.d.mts +7 -32
  21. package/dist/db/adapter/types.d.mts +5 -17
  22. package/dist/db/adapter/utils.d.mts +0 -1
  23. package/dist/db/database-index.d.mts +44 -0
  24. package/dist/db/database-index.mjs +168 -0
  25. package/dist/db/get-tables.d.mts +7 -1
  26. package/dist/db/get-tables.mjs +47 -6
  27. package/dist/db/index.d.mts +3 -3
  28. package/dist/db/index.mjs +2 -2
  29. package/dist/db/internal.d.mts +3 -0
  30. package/dist/db/internal.mjs +3 -0
  31. package/dist/db/plugin.d.mts +4 -3
  32. package/dist/db/schema/account.d.mts +16 -3
  33. package/dist/db/schema/account.mjs +35 -2
  34. package/dist/db/schema/rate-limit.d.mts +1 -1
  35. package/dist/db/schema/rate-limit.mjs +9 -0
  36. package/dist/db/schema/session.d.mts +1 -1
  37. package/dist/db/schema/shared.d.mts +0 -1
  38. package/dist/db/schema/user.d.mts +1 -1
  39. package/dist/db/schema/verification.d.mts +1 -1
  40. package/dist/db/type.d.mts +17 -9
  41. package/dist/env/logger.d.mts +25 -25
  42. package/dist/error/codes.d.mts +0 -1
  43. package/dist/error/index.d.mts +0 -1
  44. package/dist/index.d.mts +1 -1
  45. package/dist/instrumentation/attributes.d.mts +0 -1
  46. package/dist/instrumentation/pure.index.d.mts +0 -1
  47. package/dist/instrumentation/tracer.mjs +1 -1
  48. package/dist/oauth2/authorization-params.d.mts +0 -1
  49. package/dist/oauth2/client-assertion.d.mts +3 -13
  50. package/dist/oauth2/client-credentials-token.d.mts +3 -17
  51. package/dist/oauth2/client-credentials-token.mjs +1 -4
  52. package/dist/oauth2/create-authorization-url.d.mts +3 -21
  53. package/dist/oauth2/dpop.d.mts +2 -23
  54. package/dist/oauth2/dpop.mjs +1 -1
  55. package/dist/oauth2/index.d.mts +3 -3
  56. package/dist/oauth2/index.mjs +2 -2
  57. package/dist/oauth2/oauth-provider.d.mts +94 -36
  58. package/dist/oauth2/refresh-access-token.d.mts +3 -19
  59. package/dist/oauth2/refresh-access-token.mjs +3 -9
  60. package/dist/oauth2/reject-redirects.mjs +1 -1
  61. package/dist/oauth2/token-endpoint-auth.d.mts +0 -1
  62. package/dist/oauth2/utils.d.mts +0 -1
  63. package/dist/oauth2/utils.mjs +2 -3
  64. package/dist/oauth2/validate-authorization-code.d.mts +5 -29
  65. package/dist/oauth2/verify-id-token.d.mts +3 -2
  66. package/dist/oauth2/verify-id-token.mjs +3 -3
  67. package/dist/oauth2/verify.d.mts +18 -9
  68. package/dist/oauth2/verify.mjs +68 -5
  69. package/dist/social-providers/apple.d.mts +9 -24
  70. package/dist/social-providers/apple.mjs +2 -1
  71. package/dist/social-providers/atlassian.d.mts +18 -19
  72. package/dist/social-providers/atlassian.mjs +1 -1
  73. package/dist/social-providers/cognito.d.mts +33 -22
  74. package/dist/social-providers/cognito.mjs +2 -2
  75. package/dist/social-providers/discord.d.mts +7 -20
  76. package/dist/social-providers/discord.mjs +1 -1
  77. package/dist/social-providers/dropbox.d.mts +7 -22
  78. package/dist/social-providers/dropbox.mjs +1 -1
  79. package/dist/social-providers/facebook.d.mts +19 -27
  80. package/dist/social-providers/facebook.mjs +6 -17
  81. package/dist/social-providers/figma.d.mts +7 -22
  82. package/dist/social-providers/figma.mjs +1 -1
  83. package/dist/social-providers/github.d.mts +7 -23
  84. package/dist/social-providers/github.mjs +1 -1
  85. package/dist/social-providers/gitlab.d.mts +6 -43
  86. package/dist/social-providers/gitlab.mjs +1 -1
  87. package/dist/social-providers/google.d.mts +10 -32
  88. package/dist/social-providers/google.mjs +2 -1
  89. package/dist/social-providers/huggingface.d.mts +7 -22
  90. package/dist/social-providers/huggingface.mjs +1 -1
  91. package/dist/social-providers/index.d.mts +229 -787
  92. package/dist/social-providers/kakao.d.mts +8 -33
  93. package/dist/social-providers/kakao.mjs +1 -1
  94. package/dist/social-providers/kick.d.mts +7 -22
  95. package/dist/social-providers/kick.mjs +1 -1
  96. package/dist/social-providers/line.d.mts +8 -44
  97. package/dist/social-providers/line.mjs +4 -6
  98. package/dist/social-providers/linear.d.mts +7 -21
  99. package/dist/social-providers/linear.mjs +1 -1
  100. package/dist/social-providers/linkedin.d.mts +7 -21
  101. package/dist/social-providers/linkedin.mjs +1 -1
  102. package/dist/social-providers/microsoft-entra-id.d.mts +8 -18
  103. package/dist/social-providers/microsoft-entra-id.mjs +24 -4
  104. package/dist/social-providers/naver.d.mts +27 -42
  105. package/dist/social-providers/naver.mjs +1 -1
  106. package/dist/social-providers/notion.d.mts +7 -21
  107. package/dist/social-providers/notion.mjs +1 -1
  108. package/dist/social-providers/paybin.d.mts +8 -23
  109. package/dist/social-providers/paybin.mjs +2 -1
  110. package/dist/social-providers/paypal.d.mts +8 -33
  111. package/dist/social-providers/paypal.mjs +1 -1
  112. package/dist/social-providers/polar.d.mts +7 -22
  113. package/dist/social-providers/polar.mjs +1 -1
  114. package/dist/social-providers/railway.d.mts +7 -22
  115. package/dist/social-providers/railway.mjs +1 -1
  116. package/dist/social-providers/reddit.d.mts +7 -20
  117. package/dist/social-providers/reddit.mjs +1 -1
  118. package/dist/social-providers/roblox.d.mts +7 -20
  119. package/dist/social-providers/roblox.mjs +1 -1
  120. package/dist/social-providers/salesforce.d.mts +7 -22
  121. package/dist/social-providers/salesforce.mjs +1 -1
  122. package/dist/social-providers/slack.d.mts +7 -20
  123. package/dist/social-providers/slack.mjs +1 -1
  124. package/dist/social-providers/spotify.d.mts +7 -22
  125. package/dist/social-providers/spotify.mjs +1 -1
  126. package/dist/social-providers/tiktok.d.mts +10 -22
  127. package/dist/social-providers/tiktok.mjs +1 -1
  128. package/dist/social-providers/twitch.d.mts +7 -20
  129. package/dist/social-providers/twitch.mjs +1 -1
  130. package/dist/social-providers/twitter.d.mts +29 -26
  131. package/dist/social-providers/twitter.mjs +1 -1
  132. package/dist/social-providers/vercel.d.mts +7 -22
  133. package/dist/social-providers/vercel.mjs +1 -1
  134. package/dist/social-providers/vk.d.mts +8 -24
  135. package/dist/social-providers/vk.mjs +5 -6
  136. package/dist/social-providers/wechat.d.mts +7 -19
  137. package/dist/social-providers/wechat.mjs +1 -1
  138. package/dist/social-providers/zoom.d.mts +9 -25
  139. package/dist/social-providers/zoom.mjs +1 -1
  140. package/dist/types/context.d.mts +16 -9
  141. package/dist/types/cookie.d.mts +0 -1
  142. package/dist/types/helper.d.mts +1 -1
  143. package/dist/types/index.d.mts +1 -1
  144. package/dist/types/init-options.d.mts +37 -26
  145. package/dist/types/plugin-client.d.mts +1 -3
  146. package/dist/types/plugin.d.mts +2 -2
  147. package/dist/utils/async.d.mts +0 -1
  148. package/dist/utils/db.d.mts +1 -0
  149. package/dist/utils/deprecate.d.mts +0 -1
  150. package/dist/utils/error-codes.d.mts +2 -2
  151. package/dist/utils/host.d.mts +27 -1
  152. package/dist/utils/host.mjs +1 -1
  153. package/dist/utils/ip.d.mts +1 -0
  154. package/dist/utils/ip.mjs +1 -1
  155. package/dist/utils/is-api-error.d.mts +0 -1
  156. package/dist/utils/redirect-uri.d.mts +6 -2
  157. package/dist/utils/redirect-uri.mjs +11 -1
  158. package/package.json +10 -2
  159. package/src/context/request-state.ts +26 -6
  160. package/src/context/transaction.ts +37 -18
  161. package/src/db/adapter/factory.ts +34 -28
  162. package/src/db/adapter/get-default-model-name.ts +23 -14
  163. package/src/db/database-index.ts +351 -0
  164. package/src/db/get-tables.ts +89 -9
  165. package/src/db/index.ts +4 -0
  166. package/src/db/internal.ts +12 -0
  167. package/src/db/plugin.ts +3 -1
  168. package/src/db/schema/account.ts +27 -1
  169. package/src/db/schema/user.ts +1 -1
  170. package/src/db/type.ts +12 -0
  171. package/src/error/codes.ts +1 -0
  172. package/src/oauth2/index.ts +5 -0
  173. package/src/oauth2/oauth-provider.ts +105 -36
  174. package/src/oauth2/verify-id-token.ts +4 -2
  175. package/src/oauth2/verify.ts +121 -12
  176. package/src/social-providers/apple.ts +2 -1
  177. package/src/social-providers/atlassian.ts +1 -1
  178. package/src/social-providers/cognito.ts +2 -2
  179. package/src/social-providers/discord.ts +1 -1
  180. package/src/social-providers/dropbox.ts +1 -1
  181. package/src/social-providers/facebook.ts +21 -28
  182. package/src/social-providers/figma.ts +1 -1
  183. package/src/social-providers/github.ts +1 -1
  184. package/src/social-providers/gitlab.ts +1 -1
  185. package/src/social-providers/google.ts +2 -1
  186. package/src/social-providers/huggingface.ts +1 -1
  187. package/src/social-providers/kakao.ts +1 -1
  188. package/src/social-providers/kick.ts +1 -1
  189. package/src/social-providers/line.ts +7 -12
  190. package/src/social-providers/linear.ts +1 -1
  191. package/src/social-providers/linkedin.ts +1 -1
  192. package/src/social-providers/microsoft-entra-id.ts +36 -27
  193. package/src/social-providers/naver.ts +1 -1
  194. package/src/social-providers/notion.ts +1 -1
  195. package/src/social-providers/paybin.ts +2 -1
  196. package/src/social-providers/paypal.ts +1 -1
  197. package/src/social-providers/polar.ts +1 -1
  198. package/src/social-providers/railway.ts +1 -1
  199. package/src/social-providers/reddit.ts +1 -1
  200. package/src/social-providers/roblox.ts +1 -1
  201. package/src/social-providers/salesforce.ts +1 -1
  202. package/src/social-providers/slack.ts +1 -1
  203. package/src/social-providers/spotify.ts +1 -1
  204. package/src/social-providers/tiktok.ts +2 -2
  205. package/src/social-providers/twitch.ts +1 -1
  206. package/src/social-providers/twitter.ts +1 -1
  207. package/src/social-providers/vercel.ts +1 -1
  208. package/src/social-providers/vk.ts +2 -2
  209. package/src/social-providers/wechat.ts +1 -1
  210. package/src/social-providers/zoom.ts +1 -1
  211. package/src/types/context.ts +21 -14
  212. package/src/types/init-options.ts +15 -14
  213. package/src/utils/redirect-uri.ts +20 -0
@@ -9,7 +9,7 @@ import {
9
9
  refreshAccessToken,
10
10
  validateAuthorizationCode,
11
11
  } from "../oauth2";
12
- export interface FacebookProfile {
12
+ export interface FacebookGraphProfile {
13
13
  id: string;
14
14
  name: string;
15
15
  email?: string;
@@ -24,6 +24,17 @@ export interface FacebookProfile {
24
24
  };
25
25
  }
26
26
 
27
+ export interface FacebookLimitedLoginProfile {
28
+ sub: string;
29
+ email: string;
30
+ name: string;
31
+ picture: string;
32
+ }
33
+
34
+ export type FacebookProfile =
35
+ | FacebookGraphProfile
36
+ | FacebookLimitedLoginProfile;
37
+
27
38
  interface FacebookDebugTokenData {
28
39
  app_id?: string;
29
40
  is_valid?: boolean;
@@ -95,6 +106,9 @@ export const facebook = (options: FacebookOptions) => {
95
106
  return {
96
107
  id: "facebook",
97
108
  name: "Facebook",
109
+ accountSubject: ({ profile }) =>
110
+ "sub" in profile ? profile.sub : profile.id,
111
+ accountIssuer: "https://www.facebook.com",
98
112
  async createAuthorizationURL({
99
113
  state,
100
114
  scopes,
@@ -167,38 +181,18 @@ export const facebook = (options: FacebookOptions) => {
167
181
  }
168
182
 
169
183
  if (token.idToken && token.idToken.split(".").length === 3) {
170
- const profile = decodeJwt(token.idToken) as {
171
- sub: string;
172
- email: string;
173
- name: string;
174
- picture: string;
175
- };
176
-
177
- const user = {
178
- id: profile.sub,
179
- name: profile.name,
180
- email: profile.email,
181
- picture: {
182
- data: {
183
- url: profile.picture,
184
- height: 100,
185
- width: 100,
186
- is_silhouette: false,
187
- },
188
- },
189
- };
184
+ const profile = decodeJwt(token.idToken) as FacebookLimitedLoginProfile;
190
185
 
191
186
  // https://developers.facebook.com/docs/facebook-login/limited-login/permissions
192
187
  // Facebook ID token does not include email_verified claim.
193
188
  // We default to false for security consistency.
194
- const userMap = await options.mapProfileToUser?.({
195
- ...user,
196
- email_verified: false,
197
- });
189
+ const userMap = await options.mapProfileToUser?.(profile);
198
190
 
199
191
  return {
200
192
  user: {
201
- ...user,
193
+ name: profile.name,
194
+ email: profile.email,
195
+ image: profile.picture,
202
196
  emailVerified: false,
203
197
  ...userMap,
204
198
  },
@@ -228,7 +222,7 @@ export const facebook = (options: FacebookOptions) => {
228
222
  "picture",
229
223
  ...(options?.fields || []),
230
224
  ];
231
- const { data: profile, error } = await betterFetch<FacebookProfile>(
225
+ const { data: profile, error } = await betterFetch<FacebookGraphProfile>(
232
226
  "https://graph.facebook.com/me?fields=" + fields.join(","),
233
227
  {
234
228
  auth: {
@@ -247,7 +241,6 @@ export const facebook = (options: FacebookOptions) => {
247
241
  const userMap = await options.mapProfileToUser?.(profile);
248
242
  return {
249
243
  user: {
250
- id: profile.id,
251
244
  name: profile.name,
252
245
  email: profile.email,
253
246
  image: profile.picture.data.url,
@@ -24,6 +24,7 @@ export const figma = (options: FigmaOptions) => {
24
24
  return {
25
25
  id: "figma",
26
26
  name: "Figma",
27
+ accountSubject: ({ profile }) => profile.id,
27
28
  async createAuthorizationURL({
28
29
  state,
29
30
  scopes,
@@ -106,7 +107,6 @@ export const figma = (options: FigmaOptions) => {
106
107
 
107
108
  return {
108
109
  user: {
109
- id: profile.id,
110
110
  name: profile.handle,
111
111
  email: profile.email,
112
112
  image: profile.img_url,
@@ -63,6 +63,7 @@ export const github = (options: GithubOptions) => {
63
63
  return {
64
64
  id: "github",
65
65
  name: "GitHub",
66
+ accountSubject: ({ profile }) => profile.id,
66
67
  createAuthorizationURL({
67
68
  state,
68
69
  scopes,
@@ -171,7 +172,6 @@ export const github = (options: GithubOptions) => {
171
172
  const userMap = await options.mapProfileToUser?.(profile);
172
173
  return {
173
174
  user: {
174
- id: profile.id,
175
175
  name: profile.name || profile.login || "",
176
176
  email: profile.email,
177
177
  image: profile.avatar_url,
@@ -81,6 +81,7 @@ export const gitlab = (options: GitlabOptions) => {
81
81
  return {
82
82
  id: issuerId,
83
83
  name: issuerName,
84
+ accountSubject: ({ profile }) => profile.id,
84
85
  createAuthorizationURL: async ({
85
86
  state,
86
87
  scopes,
@@ -142,7 +143,6 @@ export const gitlab = (options: GitlabOptions) => {
142
143
  // We check for it first, then default to false for security consistency.
143
144
  return {
144
145
  user: {
145
- id: profile.id,
146
146
  name: profile.name ?? profile.username ?? "",
147
147
  email: profile.email,
148
148
  image: profile.avatar_url,
@@ -150,6 +150,8 @@ export const google = (options: GoogleOptions) => {
150
150
  return {
151
151
  id: "google",
152
152
  name: "Google",
153
+ accountSubject: ({ profile }) => profile.sub,
154
+ accountIssuer: "https://accounts.google.com",
153
155
  async createAuthorizationURL({
154
156
  state,
155
157
  scopes,
@@ -254,7 +256,6 @@ export const google = (options: GoogleOptions) => {
254
256
  const userMap = await options.mapProfileToUser?.(user);
255
257
  return {
256
258
  user: {
257
- id: user.sub,
258
259
  name: user.name,
259
260
  email: user.email,
260
261
  image: user.picture,
@@ -47,6 +47,7 @@ export const huggingface = (options: HuggingFaceOptions) => {
47
47
  return {
48
48
  id: "huggingface",
49
49
  name: "Hugging Face",
50
+ accountSubject: ({ profile }) => profile.sub,
50
51
  createAuthorizationURL({
51
52
  state,
52
53
  scopes,
@@ -111,7 +112,6 @@ export const huggingface = (options: HuggingFaceOptions) => {
111
112
  const userMap = await options.mapProfileToUser?.(profile);
112
113
  return {
113
114
  user: {
114
- id: profile.sub,
115
115
  name: profile.name || profile.preferred_username || "",
116
116
  email: profile.email,
117
117
  image: profile.picture,
@@ -106,6 +106,7 @@ export const kakao = (options: KakaoOptions) => {
106
106
  return {
107
107
  id: "kakao",
108
108
  name: "Kakao",
109
+ accountSubject: ({ profile }) => profile.id,
109
110
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
110
111
  const _scopes = options.disableDefaultScope
111
112
  ? []
@@ -162,7 +163,6 @@ export const kakao = (options: KakaoOptions) => {
162
163
  const account = profile.kakao_account || {};
163
164
  const kakaoProfile = account.profile || {};
164
165
  const user = {
165
- id: String(profile.id),
166
166
  name: kakaoProfile.nickname || account.name || "",
167
167
  email: account.email,
168
168
  image:
@@ -33,6 +33,7 @@ export const kick = (options: KickOptions) => {
33
33
  return {
34
34
  id: "kick",
35
35
  name: "Kick",
36
+ accountSubject: ({ profile }) => profile.user_id,
36
37
  createAuthorizationURL({
37
38
  state,
38
39
  scopes,
@@ -101,7 +102,6 @@ export const kick = (options: KickOptions) => {
101
102
  // We default to false for security consistency.
102
103
  return {
103
104
  user: {
104
- id: profile.user_id,
105
105
  name: profile.name,
106
106
  email: profile.email,
107
107
  image: profile.profile_picture,
@@ -50,6 +50,8 @@ export const line = (options: LineOptions) => {
50
50
  return {
51
51
  id: "line",
52
52
  name: "LINE",
53
+ accountSubject: ({ profile }) => profile.sub,
54
+ accountIssuer: "https://access.line.me",
53
55
  async createAuthorizationURL({
54
56
  state,
55
57
  scopes,
@@ -142,24 +144,17 @@ export const line = (options: LineOptions) => {
142
144
  profile = data || null;
143
145
  }
144
146
  if (!profile) return null;
145
- const userMap = await options.mapProfileToUser?.(profile as any);
146
- // ID preference order
147
- const id = (profile as any).sub || (profile as any).userId;
148
- const name = (profile as any).name || (profile as any).displayName || "";
149
- const image =
150
- (profile as any).picture || (profile as any).pictureUrl || undefined;
151
- const email = (profile as any).email;
147
+ const userMap = await options.mapProfileToUser?.(profile);
152
148
  return {
153
149
  user: {
154
- id,
155
- name,
156
- email,
157
- image,
150
+ name: profile.name || "",
151
+ email: profile.email,
152
+ image: profile.picture,
158
153
  // LINE does not expose email verification status in ID token/userinfo
159
154
  emailVerified: false,
160
155
  ...userMap,
161
156
  },
162
- data: profile as any,
157
+ data: profile,
163
158
  };
164
159
  },
165
160
  options,
@@ -31,6 +31,7 @@ export const linear = (options: LinearOptions) => {
31
31
  return {
32
32
  id: "linear",
33
33
  name: "Linear",
34
+ accountSubject: ({ profile }) => profile.id,
34
35
  createAuthorizationURL({
35
36
  state,
36
37
  scopes,
@@ -113,7 +114,6 @@ export const linear = (options: LinearOptions) => {
113
114
  // We default to false for security consistency.
114
115
  return {
115
116
  user: {
116
- id: profile.data.viewer.id,
117
117
  name: profile.data.viewer.name,
118
118
  email: profile.data.viewer.email,
119
119
  image: profile.data.viewer.avatarUrl,
@@ -32,6 +32,7 @@ export const linkedin = (options: LinkedInOptions) => {
32
32
  return {
33
33
  id: "linkedin",
34
34
  name: "Linkedin",
35
+ accountSubject: ({ profile }) => profile.sub,
35
36
  createAuthorizationURL: async ({
36
37
  state,
37
38
  scopes,
@@ -97,7 +98,6 @@ export const linkedin = (options: LinkedInOptions) => {
97
98
  const userMap = await options.mapProfileToUser?.(profile);
98
99
  return {
99
100
  user: {
100
- id: profile.sub,
101
101
  name: profile.name,
102
102
  email: profile.email,
103
103
  emailVerified: profile.email_verified ?? false,
@@ -187,6 +187,8 @@ export const microsoft = (options: MicrosoftOptions) => {
187
187
  return {
188
188
  id: "microsoft",
189
189
  name: "Microsoft EntraID",
190
+ accountSubject: ({ profile }) => profile.oid,
191
+ accountIssuer: ({ profile }) => profile.iss,
190
192
  createAuthorizationURL(data) {
191
193
  // Microsoft Entra supports public clients (SPA / native apps with
192
194
  // PKCE only), so clientSecret is intentionally not required here.
@@ -277,33 +279,41 @@ export const microsoft = (options: MicrosoftOptions) => {
277
279
  return null;
278
280
  }
279
281
  const user = decodeJwt(token.idToken) as MicrosoftEntraIDProfile;
282
+ if (typeof user.oid !== "string" || user.oid.trim().length === 0) {
283
+ logger.error(
284
+ "Microsoft Entra ID token did not include a valid oid claim; unable to resolve a stable account identifier.",
285
+ );
286
+ return null;
287
+ }
280
288
  const profilePhotoSize = options.profilePhotoSize || 48;
281
- await betterFetch<ArrayBuffer>(
282
- `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
283
- {
284
- headers: {
285
- Authorization: `Bearer ${token.accessToken}`,
286
- },
287
- async onResponse(context) {
288
- if (options.disableProfilePhoto || !context.response.ok) {
289
- return;
290
- }
291
- try {
292
- const response = context.response.clone();
293
- const pictureBuffer = await response.arrayBuffer();
294
- const pictureBase64 = base64.encode(pictureBuffer);
295
- user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
296
- } catch (e) {
297
- logger.error(
298
- e && typeof e === "object" && "name" in e
299
- ? (e.name as string)
300
- : "",
301
- e,
302
- );
303
- }
289
+ if (!options.disableProfilePhoto && token.accessToken) {
290
+ await betterFetch<ArrayBuffer>(
291
+ `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
292
+ {
293
+ headers: {
294
+ Authorization: `Bearer ${token.accessToken}`,
295
+ },
296
+ async onResponse(context) {
297
+ if (!context.response.ok) {
298
+ return;
299
+ }
300
+ try {
301
+ const response = context.response.clone();
302
+ const pictureBuffer = await response.arrayBuffer();
303
+ const pictureBase64 = base64.encode(pictureBuffer);
304
+ user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
305
+ } catch (e) {
306
+ logger.error(
307
+ e && typeof e === "object" && "name" in e
308
+ ? (e.name as string)
309
+ : "",
310
+ e,
311
+ );
312
+ }
313
+ },
304
314
  },
305
- },
306
- );
315
+ );
316
+ }
307
317
  const userMap = await options.mapProfileToUser?.(user);
308
318
  // Microsoft Entra ID does NOT include email_verified claim by default.
309
319
  // It must be configured as an optional claim in the app registration.
@@ -319,7 +329,6 @@ export const microsoft = (options: MicrosoftOptions) => {
319
329
  : false;
320
330
  return {
321
331
  user: {
322
- id: user.sub,
323
332
  name: user.name,
324
333
  email: user.email,
325
334
  image: user.picture,
@@ -351,7 +360,7 @@ export const microsoft = (options: MicrosoftOptions) => {
351
360
  });
352
361
  },
353
362
  options,
354
- } satisfies OAuthProvider;
363
+ } satisfies OAuthProvider<MicrosoftEntraIDProfile>;
355
364
  };
356
365
 
357
366
  export const getMicrosoftPublicKey = async (
@@ -44,6 +44,7 @@ export const naver = (options: NaverOptions) => {
44
44
  return {
45
45
  id: "naver",
46
46
  name: "Naver",
47
+ accountSubject: ({ profile }) => profile.response.id,
47
48
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
48
49
  const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
49
50
  if (options.scope) _scopes.push(...options.scope);
@@ -97,7 +98,6 @@ export const naver = (options: NaverOptions) => {
97
98
  const userMap = await options.mapProfileToUser?.(profile);
98
99
  const res = profile.response || {};
99
100
  const user = {
100
- id: res.id,
101
101
  name: res.name || res.nickname || "",
102
102
  email: res.email,
103
103
  image: res.profile_image,
@@ -28,6 +28,7 @@ export const notion = (options: NotionOptions) => {
28
28
  return {
29
29
  id: "notion",
30
30
  name: "Notion",
31
+ accountSubject: ({ profile }) => profile.id,
31
32
  createAuthorizationURL({
32
33
  state,
33
34
  scopes,
@@ -100,7 +101,6 @@ export const notion = (options: NotionOptions) => {
100
101
  const userMap = await options.mapProfileToUser?.(userProfile);
101
102
  return {
102
103
  user: {
103
- id: userProfile.id,
104
104
  name: userProfile.name || "",
105
105
  email: userProfile.person?.email || null,
106
106
  image: userProfile.avatar_url,
@@ -36,6 +36,8 @@ export const paybin = (options: PaybinOptions) => {
36
36
  return {
37
37
  id: "paybin",
38
38
  name: "Paybin",
39
+ accountSubject: ({ profile }) => profile.sub,
40
+ accountIssuer: issuer,
39
41
  async createAuthorizationURL({
40
42
  state,
41
43
  scopes,
@@ -105,7 +107,6 @@ export const paybin = (options: PaybinOptions) => {
105
107
  const userMap = await options.mapProfileToUser?.(user);
106
108
  return {
107
109
  user: {
108
- id: user.sub,
109
110
  name: user.name || user.preferred_username || "",
110
111
  email: user.email,
111
112
  image: user.picture,
@@ -79,6 +79,7 @@ export const paypal = (options: PayPalOptions) => {
79
79
  return {
80
80
  id: "paypal",
81
81
  name: "PayPal",
82
+ accountSubject: ({ profile }) => profile.user_id,
82
83
  async createAuthorizationURL({
83
84
  state,
84
85
  codeVerifier,
@@ -252,7 +253,6 @@ export const paypal = (options: PayPalOptions) => {
252
253
 
253
254
  const result = {
254
255
  user: {
255
- id: userInfo.user_id,
256
256
  name: userInfo.name,
257
257
  email: userInfo.email,
258
258
  image: userInfo.picture,
@@ -37,6 +37,7 @@ export const polar = (options: PolarOptions) => {
37
37
  return {
38
38
  id: "polar",
39
39
  name: "Polar",
40
+ accountSubject: ({ profile }) => profile.id,
40
41
  createAuthorizationURL({
41
42
  state,
42
43
  scopes,
@@ -103,7 +104,6 @@ export const polar = (options: PolarOptions) => {
103
104
  // We check for it first, then default to false for security consistency.
104
105
  return {
105
106
  user: {
106
- id: profile.id,
107
107
  name: profile.public_name || profile.username || "",
108
108
  email: profile.email,
109
109
  image: profile.avatar_url,
@@ -29,6 +29,7 @@ export const railway = (options: RailwayOptions) => {
29
29
  return {
30
30
  id: "railway",
31
31
  name: "Railway",
32
+ accountSubject: ({ profile }) => profile.sub,
32
33
  createAuthorizationURL({
33
34
  state,
34
35
  scopes,
@@ -92,7 +93,6 @@ export const railway = (options: RailwayOptions) => {
92
93
  // We default to false for security consistency.
93
94
  return {
94
95
  user: {
95
- id: profile.sub,
96
96
  name: profile.name,
97
97
  email: profile.email,
98
98
  image: profile.picture,
@@ -25,6 +25,7 @@ export const reddit = (options: RedditOptions) => {
25
25
  return {
26
26
  id: "reddit",
27
27
  name: "Reddit",
28
+ accountSubject: ({ profile }) => profile.id,
28
29
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
29
30
  const _scopes = options.disableDefaultScope ? [] : ["identity"];
30
31
  if (options.scope) _scopes.push(...options.scope);
@@ -112,7 +113,6 @@ export const reddit = (options: RedditOptions) => {
112
113
  const email = userMap?.email || `${profile.id}@reddit.invalid`;
113
114
  return {
114
115
  user: {
115
- id: profile.id,
116
116
  name: profile.name,
117
117
  image: profile.icon_img?.split("?")[0]!,
118
118
  ...userMap,
@@ -41,6 +41,7 @@ export const roblox = (options: RobloxOptions) => {
41
41
  return {
42
42
  id: "roblox",
43
43
  name: "Roblox",
44
+ accountSubject: ({ profile }) => profile.sub,
44
45
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
45
46
  const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
46
47
  if (options.scope) _scopes.push(...options.scope);
@@ -100,7 +101,6 @@ export const roblox = (options: RobloxOptions) => {
100
101
  // We default to false for security consistency.
101
102
  return {
102
103
  user: {
103
- id: profile.sub,
104
104
  name: profile.nickname || profile.preferred_username || "",
105
105
  image: profile.picture,
106
106
  email: profile.preferred_username || null, // Roblox does not provide email
@@ -63,6 +63,7 @@ export const salesforce = (options: SalesforceOptions) => {
63
63
  return {
64
64
  id: "salesforce",
65
65
  name: "Salesforce",
66
+ accountSubject: ({ profile }) => profile.user_id,
66
67
 
67
68
  async createAuthorizationURL({
68
69
  state,
@@ -146,7 +147,6 @@ export const salesforce = (options: SalesforceOptions) => {
146
147
 
147
148
  return {
148
149
  user: {
149
- id: user.user_id,
150
150
  name: user.name,
151
151
  email: user.email,
152
152
  image: user.photos?.picture || user.photos?.thumbnail,
@@ -46,6 +46,7 @@ export const slack = (options: SlackOptions) => {
46
46
  return {
47
47
  id: "slack",
48
48
  name: "Slack",
49
+ accountSubject: ({ profile }) => profile["https://slack.com/user_id"],
49
50
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
50
51
  const _scopes = options.disableDefaultScope
51
52
  ? []
@@ -103,7 +104,6 @@ export const slack = (options: SlackOptions) => {
103
104
  const userMap = await options.mapProfileToUser?.(profile);
104
105
  return {
105
106
  user: {
106
- id: profile["https://slack.com/user_id"],
107
107
  name: profile.name || "",
108
108
  email: profile.email,
109
109
  emailVerified: profile.email_verified,
@@ -24,6 +24,7 @@ export const spotify = (options: SpotifyOptions) => {
24
24
  return {
25
25
  id: "spotify",
26
26
  name: "Spotify",
27
+ accountSubject: ({ profile }) => profile.id,
27
28
  createAuthorizationURL({
28
29
  state,
29
30
  scopes,
@@ -86,7 +87,6 @@ export const spotify = (options: SpotifyOptions) => {
86
87
  const userMap = await options.mapProfileToUser?.(profile);
87
88
  return {
88
89
  user: {
89
- id: profile.id,
90
90
  name: profile.display_name,
91
91
  email: profile.email,
92
92
  image: profile.images[0]?.url,
@@ -123,7 +123,7 @@ export interface TiktokProfile extends Record<string, any> {
123
123
  | undefined;
124
124
  }
125
125
 
126
- export interface TiktokOptions extends ProviderOptions {
126
+ export interface TiktokOptions extends ProviderOptions<TiktokProfile> {
127
127
  // Client ID is not used in TikTok, we delete it from the options
128
128
  clientId?: never | undefined;
129
129
  clientSecret: string;
@@ -135,6 +135,7 @@ export const tiktok = (options: TiktokOptions) => {
135
135
  return {
136
136
  id: "tiktok",
137
137
  name: "TikTok",
138
+ accountSubject: ({ profile }) => profile.data.user.open_id,
138
139
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
139
140
  const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
140
141
  if (options.scope) _scopes.push(...options.scope);
@@ -210,7 +211,6 @@ export const tiktok = (options: TiktokOptions) => {
210
211
  return {
211
212
  user: {
212
213
  email: profile.data.user.email || profile.data.user.username,
213
- id: profile.data.user.open_id,
214
214
  name:
215
215
  profile.data.user.display_name || profile.data.user.username || "",
216
216
  image: profile.data.user.avatar_large_url,
@@ -42,6 +42,7 @@ export const twitch = (options: TwitchOptions) => {
42
42
  return {
43
43
  id: "twitch",
44
44
  name: "Twitch",
45
+ accountSubject: ({ profile }) => profile.sub,
45
46
  createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
46
47
  const _scopes = options.disableDefaultScope
47
48
  ? []
@@ -98,7 +99,6 @@ export const twitch = (options: TwitchOptions) => {
98
99
  const userMap = await options.mapProfileToUser?.(profile);
99
100
  return {
100
101
  user: {
101
- id: profile.sub,
102
102
  name: profile.preferred_username,
103
103
  email: profile.email,
104
104
  image: profile.picture,
@@ -108,6 +108,7 @@ export const twitter = (options: TwitterOption) => {
108
108
  return {
109
109
  id: "twitter",
110
110
  name: "Twitter",
111
+ accountSubject: ({ profile }) => profile.data.id,
111
112
  createAuthorizationURL(data) {
112
113
  const _scopes = options.disableDefaultScope
113
114
  ? []
@@ -185,7 +186,6 @@ export const twitter = (options: TwitterOption) => {
185
186
  const userMap = await options.mapProfileToUser?.(profile);
186
187
  return {
187
188
  user: {
188
- id: profile.data.id,
189
189
  name: profile.data.name,
190
190
  email: profile.data.email || profile.data.username || null,
191
191
  image: profile.data.profile_image_url,
@@ -20,6 +20,7 @@ export const vercel = (options: VercelOptions) => {
20
20
  return {
21
21
  id: "vercel",
22
22
  name: "Vercel",
23
+ accountSubject: ({ profile }) => profile.sub,
23
24
  createAuthorizationURL({
24
25
  state,
25
26
  scopes,
@@ -79,7 +80,6 @@ export const vercel = (options: VercelOptions) => {
79
80
  const userMap = await options.mapProfileToUser?.(profile);
80
81
  return {
81
82
  user: {
82
- id: profile.sub,
83
83
  name: profile.name ?? profile.preferred_username ?? "",
84
84
  email: profile.email,
85
85
  image: profile.picture,
@@ -20,7 +20,7 @@ export interface VkProfile {
20
20
  };
21
21
  }
22
22
 
23
- export interface VkOption extends ProviderOptions {
23
+ export interface VkOption extends ProviderOptions<VkProfile> {
24
24
  clientId: string;
25
25
  scheme?: ("light" | "dark") | undefined;
26
26
  }
@@ -30,6 +30,7 @@ export const vk = (options: VkOption) => {
30
30
  return {
31
31
  id: "vk",
32
32
  name: "VK",
33
+ accountSubject: ({ profile }) => profile.user.user_id,
33
34
  async createAuthorizationURL({
34
35
  state,
35
36
  scopes,
@@ -113,7 +114,6 @@ export const vk = (options: VkOption) => {
113
114
 
114
115
  return {
115
116
  user: {
116
- id: profile.user.user_id,
117
117
  first_name: profile.user.first_name,
118
118
  last_name: profile.user.last_name,
119
119
  email: profile.user.email,