better-auth 1.6.11 → 1.6.13

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 (84) hide show
  1. package/dist/api/index.d.mts +12 -48
  2. package/dist/api/routes/account.d.mts +2 -23
  3. package/dist/api/routes/account.mjs +94 -73
  4. package/dist/api/routes/callback.d.mts +1 -1
  5. package/dist/api/routes/callback.mjs +39 -42
  6. package/dist/api/routes/email-verification.d.mts +1 -0
  7. package/dist/api/routes/email-verification.mjs +4 -3
  8. package/dist/api/routes/password.mjs +1 -1
  9. package/dist/api/routes/session.mjs +15 -10
  10. package/dist/api/routes/sign-in.d.mts +1 -0
  11. package/dist/api/routes/sign-in.mjs +3 -2
  12. package/dist/api/routes/sign-up.d.mts +1 -0
  13. package/dist/api/routes/sign-up.mjs +9 -7
  14. package/dist/api/routes/update-user.mjs +7 -7
  15. package/dist/client/fetch-plugins.mjs +2 -1
  16. package/dist/client/parser.mjs +0 -1
  17. package/dist/client/plugins/index.d.mts +3 -3
  18. package/dist/client/proxy.mjs +2 -1
  19. package/dist/context/create-context.mjs +10 -14
  20. package/dist/context/helpers.mjs +3 -2
  21. package/dist/cookies/cookie-utils.d.mts +24 -1
  22. package/dist/cookies/cookie-utils.mjs +85 -22
  23. package/dist/cookies/index.d.mts +2 -3
  24. package/dist/cookies/index.mjs +39 -11
  25. package/dist/cookies/session-store.mjs +4 -23
  26. package/dist/db/get-migration.mjs +4 -4
  27. package/dist/db/index.d.mts +2 -2
  28. package/dist/db/index.mjs +3 -2
  29. package/dist/db/internal-adapter.mjs +56 -50
  30. package/dist/db/schema.d.mts +15 -2
  31. package/dist/db/schema.mjs +26 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.mjs +2 -2
  34. package/dist/oauth2/errors.mjs +16 -1
  35. package/dist/oauth2/index.d.mts +2 -2
  36. package/dist/oauth2/index.mjs +3 -3
  37. package/dist/oauth2/link-account.d.mts +27 -1
  38. package/dist/oauth2/link-account.mjs +27 -4
  39. package/dist/oauth2/state.mjs +8 -2
  40. package/dist/package.mjs +1 -1
  41. package/dist/plugins/access/access.mjs +11 -6
  42. package/dist/plugins/admin/admin.mjs +0 -4
  43. package/dist/plugins/admin/client.d.mts +1 -1
  44. package/dist/plugins/admin/routes.mjs +3 -3
  45. package/dist/plugins/anonymous/index.mjs +2 -2
  46. package/dist/plugins/bearer/index.mjs +4 -9
  47. package/dist/plugins/captcha/index.mjs +2 -2
  48. package/dist/plugins/email-otp/routes.mjs +1 -1
  49. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  50. package/dist/plugins/generic-oauth/index.mjs +6 -6
  51. package/dist/plugins/generic-oauth/routes.mjs +37 -34
  52. package/dist/plugins/generic-oauth/types.d.mts +7 -0
  53. package/dist/plugins/last-login-method/client.mjs +2 -2
  54. package/dist/plugins/magic-link/index.mjs +0 -1
  55. package/dist/plugins/mcp/index.mjs +2 -5
  56. package/dist/plugins/multi-session/index.mjs +2 -2
  57. package/dist/plugins/oauth-proxy/index.mjs +45 -32
  58. package/dist/plugins/oauth-proxy/utils.mjs +3 -10
  59. package/dist/plugins/oidc-provider/index.mjs +2 -5
  60. package/dist/plugins/one-tap/client.mjs +9 -2
  61. package/dist/plugins/one-tap/index.mjs +16 -39
  62. package/dist/plugins/open-api/generator.mjs +16 -5
  63. package/dist/plugins/organization/adapter.mjs +61 -56
  64. package/dist/plugins/organization/client.d.mts +2 -1
  65. package/dist/plugins/organization/error-codes.d.mts +1 -0
  66. package/dist/plugins/organization/error-codes.mjs +2 -1
  67. package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
  68. package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
  69. package/dist/plugins/organization/routes/crud-org.mjs +2 -2
  70. package/dist/plugins/organization/types.d.mts +3 -3
  71. package/dist/plugins/phone-number/routes.mjs +1 -1
  72. package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
  73. package/dist/plugins/two-factor/client.mjs +2 -1
  74. package/dist/plugins/two-factor/index.mjs +3 -2
  75. package/dist/plugins/username/index.d.mts +24 -2
  76. package/dist/plugins/username/index.mjs +49 -3
  77. package/dist/state.d.mts +2 -2
  78. package/dist/state.mjs +18 -4
  79. package/dist/test-utils/headers.mjs +2 -7
  80. package/dist/test-utils/test-instance.d.mts +36 -144
  81. package/dist/utils/index.d.mts +1 -1
  82. package/dist/utils/url.d.mts +2 -1
  83. package/dist/utils/url.mjs +9 -3
  84. package/package.json +15 -14
@@ -228,7 +228,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
228
228
  allowedMediaTypes: string[];
229
229
  scope: "server";
230
230
  };
231
- }, void>;
231
+ }, never>;
232
232
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
233
233
  method: ("GET" | "POST")[];
234
234
  operationId: string;
@@ -327,6 +327,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
327
327
  callbackURL: zod.ZodOptional<zod.ZodString>;
328
328
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
329
329
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
330
+ cloneRequest: true;
330
331
  metadata: {
331
332
  allowedMediaTypes: string[];
332
333
  $Infer: {
@@ -493,6 +494,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
493
494
  method: "POST";
494
495
  operationId: string;
495
496
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
497
+ cloneRequest: true;
496
498
  body: zod.ZodObject<{
497
499
  email: zod.ZodString;
498
500
  password: zod.ZodString;
@@ -724,6 +726,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
724
726
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
725
727
  method: "POST";
726
728
  operationId: string;
729
+ cloneRequest: true;
727
730
  body: zod.ZodObject<{
728
731
  email: zod.ZodEmail;
729
732
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -1928,29 +1931,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
1928
1931
  }>;
1929
1932
  readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
1930
1933
  method: "GET";
1931
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
1932
- session: {
1933
- session: Record<string, any> & {
1934
- id: string;
1935
- createdAt: Date;
1936
- updatedAt: Date;
1937
- userId: string;
1938
- expiresAt: Date;
1939
- token: string;
1940
- ipAddress?: string | null | undefined;
1941
- userAgent?: string | null | undefined;
1942
- };
1943
- user: Record<string, any> & {
1944
- id: string;
1945
- createdAt: Date;
1946
- updatedAt: Date;
1947
- email: string;
1948
- emailVerified: boolean;
1949
- name: string;
1950
- image?: string | null | undefined;
1951
- };
1952
- };
1953
- }>)[];
1954
1934
  metadata: {
1955
1935
  openapi: {
1956
1936
  description: string;
@@ -2000,6 +1980,8 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
2000
1980
  };
2001
1981
  query: zod.ZodOptional<zod.ZodObject<{
2002
1982
  accountId: zod.ZodOptional<zod.ZodString>;
1983
+ providerId: zod.ZodOptional<zod.ZodString>;
1984
+ userId: zod.ZodOptional<zod.ZodString>;
2003
1985
  }, zod_v4_core0.$strip>>;
2004
1986
  }, {
2005
1987
  user: _better_auth_core_oauth20.OAuth2UserInfo;
@@ -2216,7 +2198,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2216
2198
  allowedMediaTypes: string[];
2217
2199
  scope: "server";
2218
2200
  };
2219
- }, void>;
2201
+ }, never>;
2220
2202
  readonly getSession: better_call0.StrictEndpoint<"/get-session", {
2221
2203
  method: ("GET" | "POST")[];
2222
2204
  operationId: string;
@@ -2315,6 +2297,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2315
2297
  callbackURL: zod.ZodOptional<zod.ZodString>;
2316
2298
  rememberMe: zod.ZodOptional<zod.ZodBoolean>;
2317
2299
  }, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
2300
+ cloneRequest: true;
2318
2301
  metadata: {
2319
2302
  allowedMediaTypes: string[];
2320
2303
  $Infer: {
@@ -2481,6 +2464,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2481
2464
  method: "POST";
2482
2465
  operationId: string;
2483
2466
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
2467
+ cloneRequest: true;
2484
2468
  body: zod.ZodObject<{
2485
2469
  email: zod.ZodString;
2486
2470
  password: zod.ZodString;
@@ -2712,6 +2696,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2712
2696
  readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
2713
2697
  method: "POST";
2714
2698
  operationId: string;
2699
+ cloneRequest: true;
2715
2700
  body: zod.ZodObject<{
2716
2701
  email: zod.ZodEmail;
2717
2702
  callbackURL: zod.ZodOptional<zod.ZodString>;
@@ -3916,29 +3901,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3916
3901
  }>;
3917
3902
  readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
3918
3903
  method: "GET";
3919
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
3920
- session: {
3921
- session: Record<string, any> & {
3922
- id: string;
3923
- createdAt: Date;
3924
- updatedAt: Date;
3925
- userId: string;
3926
- expiresAt: Date;
3927
- token: string;
3928
- ipAddress?: string | null | undefined;
3929
- userAgent?: string | null | undefined;
3930
- };
3931
- user: Record<string, any> & {
3932
- id: string;
3933
- createdAt: Date;
3934
- updatedAt: Date;
3935
- email: string;
3936
- emailVerified: boolean;
3937
- name: string;
3938
- image?: string | null | undefined;
3939
- };
3940
- };
3941
- }>)[];
3942
3904
  metadata: {
3943
3905
  openapi: {
3944
3906
  description: string;
@@ -3988,6 +3950,8 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3988
3950
  };
3989
3951
  query: zod.ZodOptional<zod.ZodObject<{
3990
3952
  accountId: zod.ZodOptional<zod.ZodString>;
3953
+ providerId: zod.ZodOptional<zod.ZodString>;
3954
+ userId: zod.ZodOptional<zod.ZodString>;
3991
3955
  }, zod_v4_core0.$strip>>;
3992
3956
  }, {
3993
3957
  user: _better_auth_core_oauth20.OAuth2UserInfo;
@@ -328,29 +328,6 @@ declare const refreshToken: better_call0.StrictEndpoint<"/refresh-token", {
328
328
  }>;
329
329
  declare const accountInfo: better_call0.StrictEndpoint<"/account-info", {
330
330
  method: "GET";
331
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
332
- session: {
333
- session: Record<string, any> & {
334
- id: string;
335
- createdAt: Date;
336
- updatedAt: Date;
337
- userId: string;
338
- expiresAt: Date;
339
- token: string;
340
- ipAddress?: string | null | undefined;
341
- userAgent?: string | null | undefined;
342
- };
343
- user: Record<string, any> & {
344
- id: string;
345
- createdAt: Date;
346
- updatedAt: Date;
347
- email: string;
348
- emailVerified: boolean;
349
- name: string;
350
- image?: string | null | undefined;
351
- };
352
- };
353
- }>)[];
354
331
  metadata: {
355
332
  openapi: {
356
333
  description: string;
@@ -400,6 +377,8 @@ declare const accountInfo: better_call0.StrictEndpoint<"/account-info", {
400
377
  };
401
378
  query: z.ZodOptional<z.ZodObject<{
402
379
  accountId: z.ZodOptional<z.ZodString>;
380
+ providerId: z.ZodOptional<z.ZodString>;
381
+ userId: z.ZodOptional<z.ZodString>;
403
382
  }, z.core.$strip>>;
404
383
  }, {
405
384
  user: _better_auth_core_oauth20.OAuth2UserInfo;
@@ -2,8 +2,9 @@ import { parseAccountOutput } from "../../db/schema.mjs";
2
2
  import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
3
3
  import { getAwaitableValue } from "../../context/helpers.mjs";
4
4
  import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
5
- import { generateState } from "../../oauth2/state.mjs";
6
5
  import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils.mjs";
6
+ import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
7
+ import { generateState } from "../../oauth2/state.mjs";
7
8
  import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "./session.mjs";
8
9
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
9
10
  import { SocialProviderListEnum } from "@better-auth/core/social-providers";
@@ -166,14 +167,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
166
167
  code: "LINKING_FAILED"
167
168
  });
168
169
  }
169
- if (c.context.options.account?.accountLinking?.updateUserInfoOnLink === true) try {
170
- await c.context.internalAdapter.updateUser(session.user.id, {
171
- name: linkingUserInfo.user?.name,
172
- image: linkingUserInfo.user?.image
173
- });
174
- } catch (e) {
175
- console.warn("Could not update user - " + e.toString());
176
- }
170
+ await applyUpdateUserInfoOnLink(c, session.user.id, linkingUserInfo.user);
177
171
  return c.json({
178
172
  url: "",
179
173
  status: true,
@@ -222,50 +216,44 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
222
216
  await ctx.context.internalAdapter.deleteAccount(accountExist.id);
223
217
  return ctx.json({ status: true });
224
218
  });
225
- const getAccessToken = createAuthEndpoint("/get-access-token", {
226
- method: "POST",
227
- body: z.object({
228
- providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
229
- accountId: z.string().meta({ description: "The account ID associated with the refresh token" }).optional(),
230
- userId: z.string().meta({ description: "The user ID associated with the account" }).optional()
231
- }),
232
- metadata: { openapi: {
233
- description: "Get a valid access token, doing a refresh if needed",
234
- responses: {
235
- 200: {
236
- description: "A Valid access token",
237
- content: { "application/json": { schema: {
238
- type: "object",
239
- properties: {
240
- tokenType: { type: "string" },
241
- idToken: { type: "string" },
242
- accessToken: { type: "string" },
243
- accessTokenExpiresAt: {
244
- type: "string",
245
- format: "date-time"
246
- }
247
- }
248
- } } }
249
- },
250
- 400: { description: "Invalid refresh token or provider configuration" }
251
- }
252
- } }
253
- }, async (ctx) => {
254
- const { providerId, accountId, userId } = ctx.body || {};
255
- const req = ctx.request;
219
+ /**
220
+ * Resolves the user id an account-token operation should act on.
221
+ *
222
+ * A caller reaching the server over HTTP (a request or session headers are
223
+ * present) must have a valid session, and that session's user always wins.
224
+ * A trusted server-side `auth.api` caller with no session may instead name a
225
+ * `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
226
+ * unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
227
+ * nor a `userId` is available.
228
+ */
229
+ async function resolveUserId(ctx, userId) {
256
230
  const session = await getSessionFromCtx(ctx);
257
- if (req && !session) throw ctx.error("UNAUTHORIZED");
231
+ if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
258
232
  const resolvedUserId = session?.user?.id || userId;
259
- if (!resolvedUserId) throw ctx.error("UNAUTHORIZED");
233
+ if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
234
+ message: "Either userId or session is required",
235
+ code: "USER_ID_OR_SESSION_REQUIRED"
236
+ });
237
+ return resolvedUserId;
238
+ }
239
+ /**
240
+ * Fetches a currently-valid access token for a user's provider account,
241
+ * refreshing and persisting it when it is within five seconds of expiry.
242
+ * Shared by the `/get-access-token` endpoint and `/account-info` so both
243
+ * resolve and refresh tokens through one path.
244
+ */
245
+ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId, account: resolvedAccount }) {
260
246
  const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
261
247
  if (!provider) throw APIError.from("BAD_REQUEST", {
262
248
  message: `Provider ${providerId} is not supported.`,
263
249
  code: "PROVIDER_NOT_SUPPORTED"
264
250
  });
265
- const accountData = await getAccountCookie(ctx);
266
- let account = void 0;
267
- if (accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId)) account = accountData;
268
- else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
251
+ let account = resolvedAccount;
252
+ if (!account) {
253
+ const accountData = await getAccountCookie(ctx);
254
+ if (accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId)) account = accountData;
255
+ else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
256
+ }
269
257
  if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
270
258
  try {
271
259
  let newTokens = null;
@@ -297,19 +285,55 @@ const getAccessToken = createAuthEndpoint("/get-access-token", {
297
285
  return account.accessTokenExpiresAt;
298
286
  }
299
287
  })();
300
- const tokens = {
288
+ return {
301
289
  accessToken: newTokens?.accessToken ?? await decryptOAuthToken(account.accessToken ?? "", ctx.context),
302
290
  accessTokenExpiresAt,
303
291
  scopes: account.scope?.split(",") ?? [],
304
292
  idToken: newTokens?.idToken ?? account.idToken ?? void 0
305
293
  };
306
- return ctx.json(tokens);
307
294
  } catch (_error) {
308
295
  throw APIError.from("BAD_REQUEST", {
309
296
  message: "Failed to get a valid access token",
310
297
  code: "FAILED_TO_GET_ACCESS_TOKEN"
311
298
  });
312
299
  }
300
+ }
301
+ const getAccessToken = createAuthEndpoint("/get-access-token", {
302
+ method: "POST",
303
+ body: z.object({
304
+ providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
305
+ accountId: z.string().meta({ description: "The account ID associated with the refresh token" }).optional(),
306
+ userId: z.string().meta({ description: "The user ID associated with the account" }).optional()
307
+ }),
308
+ metadata: { openapi: {
309
+ description: "Get a valid access token, doing a refresh if needed",
310
+ responses: {
311
+ 200: {
312
+ description: "A Valid access token",
313
+ content: { "application/json": { schema: {
314
+ type: "object",
315
+ properties: {
316
+ tokenType: { type: "string" },
317
+ idToken: { type: "string" },
318
+ accessToken: { type: "string" },
319
+ accessTokenExpiresAt: {
320
+ type: "string",
321
+ format: "date-time"
322
+ }
323
+ }
324
+ } } }
325
+ },
326
+ 400: { description: "Invalid refresh token or provider configuration" }
327
+ }
328
+ } }
329
+ }, async (ctx) => {
330
+ const { providerId, accountId, userId } = ctx.body || {};
331
+ const tokens = await getValidAccessToken(ctx, {
332
+ resolvedUserId: await resolveUserId(ctx, userId),
333
+ providerId,
334
+ accountId
335
+ });
336
+ return ctx.json(tokens);
313
337
  });
314
338
  const refreshToken = createAuthEndpoint("/refresh-token", {
315
339
  method: "POST",
@@ -346,14 +370,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
346
370
  } }
347
371
  }, async (ctx) => {
348
372
  const { providerId, accountId, userId } = ctx.body;
349
- const req = ctx.request;
350
- const session = await getSessionFromCtx(ctx);
351
- if (req && !session) throw ctx.error("UNAUTHORIZED");
352
- const resolvedUserId = session?.user?.id || userId;
353
- if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
354
- message: `Either userId or session is required`,
355
- code: "USER_ID_OR_SESSION_REQUIRED"
356
- });
373
+ const resolvedUserId = await resolveUserId(ctx, userId);
357
374
  const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
358
375
  if (!provider) throw APIError.from("BAD_REQUEST", {
359
376
  message: `Provider ${providerId} is not supported.`,
@@ -418,10 +435,13 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
418
435
  });
419
436
  }
420
437
  });
421
- const accountInfoQuerySchema = z.optional(z.object({ accountId: z.string().meta({ description: "The provider given account id for which to get the account info" }).optional() }));
438
+ const accountInfoQuerySchema = z.optional(z.object({
439
+ accountId: z.string().meta({ description: "The provider given account id for which to get the account info" }).optional(),
440
+ providerId: z.string().meta({ description: "The provider ID to disambiguate provider-issued account IDs" }).optional(),
441
+ userId: z.string().meta({ description: "The user ID associated with the account" }).optional()
442
+ }));
422
443
  const accountInfo = createAuthEndpoint("/account-info", {
423
444
  method: "GET",
424
- use: [sessionMiddleware],
425
445
  metadata: { openapi: {
426
446
  description: "Get the account info provided by the provider",
427
447
  responses: { "200": {
@@ -453,7 +473,8 @@ const accountInfo = createAuthEndpoint("/account-info", {
453
473
  } },
454
474
  query: accountInfoQuerySchema
455
475
  }, async (ctx) => {
456
- const providedAccountId = ctx.query?.accountId;
476
+ const { accountId: providedAccountId, providerId: providedProviderId, userId } = ctx.query || {};
477
+ const resolvedUserId = await resolveUserId(ctx, userId);
457
478
  let account = void 0;
458
479
  if (!providedAccountId) {
459
480
  if (ctx.context.options.account?.storeAccountCookie) {
@@ -461,24 +482,24 @@ const accountInfo = createAuthEndpoint("/account-info", {
461
482
  if (accountData) account = accountData;
462
483
  }
463
484
  } else {
464
- const accountData = await ctx.context.internalAdapter.findAccount(providedAccountId);
465
- if (accountData) account = accountData;
485
+ const matchingAccounts = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).filter((acc) => acc.accountId === providedAccountId && (!providedProviderId || acc.providerId === providedProviderId));
486
+ if (matchingAccounts.length > 1) throw APIError.from("BAD_REQUEST", {
487
+ message: "Multiple accounts share this account ID. Pass a providerId to disambiguate.",
488
+ code: "AMBIGUOUS_ACCOUNT"
489
+ });
490
+ account = matchingAccounts[0];
466
491
  }
467
- if (!account || account.userId !== ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
492
+ if (!account || account.userId !== resolvedUserId) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
468
493
  const provider = await getAwaitableValue(ctx.context.socialProviders, { value: account.providerId });
469
- if (!provider) throw APIError.from("INTERNAL_SERVER_ERROR", {
470
- message: `Provider account provider is ${account.providerId} but it is not configured`,
494
+ if (!provider) throw APIError.from("BAD_REQUEST", {
495
+ message: "Account is not associated with a configured social provider.",
471
496
  code: "PROVIDER_NOT_CONFIGURED"
472
497
  });
473
- const tokens = await getAccessToken({
474
- ...ctx,
475
- method: "POST",
476
- body: {
477
- accountId: account.accountId,
478
- providerId: account.providerId
479
- },
480
- returnHeaders: false,
481
- returnStatus: false
498
+ const tokens = await getValidAccessToken(ctx, {
499
+ resolvedUserId,
500
+ providerId: account.providerId,
501
+ accountId: account.accountId,
502
+ account
482
503
  });
483
504
  if (!tokens.accessToken) throw APIError.from("BAD_REQUEST", {
484
505
  message: "Access token not found",
@@ -25,6 +25,6 @@ declare const callbackOAuth: better_call0.StrictEndpoint<"/callback/:id", {
25
25
  allowedMediaTypes: string[];
26
26
  scope: "server";
27
27
  };
28
- }, void>;
28
+ }, never>;
29
29
  //#endregion
30
30
  export { callbackOAuth };
@@ -1,9 +1,10 @@
1
+ import { isAPIError } from "../../utils/is-api-error.mjs";
1
2
  import { setSessionCookie } from "../../cookies/index.mjs";
2
3
  import { getAwaitableValue } from "../../context/helpers.mjs";
3
- import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
4
- import { parseState } from "../../oauth2/state.mjs";
4
+ import { missingEmailLogMessage, redirectOnError } from "../../oauth2/errors.mjs";
5
5
  import { setTokenUtil } from "../../oauth2/utils.mjs";
6
- import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
6
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
7
+ import { parseState } from "../../oauth2/state.mjs";
7
8
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
8
9
  import { safeJSONParse } from "@better-auth/core/utils/json";
9
10
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -47,31 +48,20 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
47
48
  else throw new Error("Unsupported method");
48
49
  } catch (e) {
49
50
  c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
50
- throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
51
- }
52
- const { code, error, state, error_description, device_id, user: userData } = queryOrBody;
53
- if (!state) {
54
- c.context.logger.error("State not found", error);
55
- const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`;
56
- throw c.redirect(url);
51
+ redirectOnError(c, defaultErrorURL, "invalid_callback_request");
57
52
  }
53
+ const { code, error, error_description, device_id, user: userData } = queryOrBody;
58
54
  const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp } = await parseState(c);
59
- function redirectOnError(error, description) {
60
- const baseURL = errorURL ?? defaultErrorURL;
61
- const params = new URLSearchParams({ error });
62
- if (description) params.set("error_description", description);
63
- const url = `${baseURL}${baseURL.includes("?") ? "&" : "?"}${params.toString()}`;
64
- throw c.redirect(url);
65
- }
66
- if (error) redirectOnError(error, error_description);
55
+ const resolvedErrorURL = errorURL ?? defaultErrorURL;
56
+ if (error) redirectOnError(c, resolvedErrorURL, error, error_description);
67
57
  if (!code) {
68
58
  c.context.logger.error("Code not found");
69
- throw redirectOnError("no_code");
59
+ redirectOnError(c, resolvedErrorURL, "no_code");
70
60
  }
71
61
  const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
72
62
  if (!provider) {
73
63
  c.context.logger.error("Oauth provider with id", c.params.id, "not found");
74
- throw redirectOnError("oauth_provider_not_found");
64
+ redirectOnError(c, resolvedErrorURL, "oauth_provider_not_found");
75
65
  }
76
66
  let tokens;
77
67
  try {
@@ -83,9 +73,9 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
83
73
  });
84
74
  } catch (e) {
85
75
  c.context.logger.error("", e);
86
- throw redirectOnError("invalid_code");
76
+ redirectOnError(c, resolvedErrorURL, "invalid_code");
87
77
  }
88
- if (!tokens) throw redirectOnError("invalid_code");
78
+ if (!tokens) redirectOnError(c, resolvedErrorURL, "invalid_code");
89
79
  const parsedUserData = userData ? safeJSONParse(userData) : null;
90
80
  const userInfo = await provider.getUserInfo({
91
81
  ...tokens,
@@ -93,22 +83,22 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
93
83
  }).then((res) => res?.user);
94
84
  if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
95
85
  c.context.logger.error("Unable to get user info");
96
- return redirectOnError("unable_to_get_user_info");
86
+ redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
97
87
  }
98
88
  const providerAccountId = String(userInfo.id);
99
89
  if (!callbackURL) {
100
90
  c.context.logger.error("No callback URL found");
101
- throw redirectOnError("no_callback_url");
91
+ redirectOnError(c, resolvedErrorURL, "no_callback_url");
102
92
  }
103
93
  if (link) {
104
94
  if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
105
95
  c.context.logger.error("Unable to link account - untrusted provider");
106
- return redirectOnError("unable_to_link_account");
96
+ redirectOnError(c, resolvedErrorURL, "unable_to_link_account");
107
97
  }
108
- if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
98
+ if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) redirectOnError(c, resolvedErrorURL, "email_doesn't_match");
109
99
  const existingAccount = await c.context.internalAdapter.findAccountByProviderId(providerAccountId, provider.id);
110
100
  if (existingAccount) {
111
- if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
101
+ if (existingAccount.userId.toString() !== link.userId.toString()) redirectOnError(c, resolvedErrorURL, "account_already_linked_to_different_user");
112
102
  const updateData = Object.fromEntries(Object.entries({
113
103
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
114
104
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
@@ -126,7 +116,8 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
126
116
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
127
117
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
128
118
  scope: tokens.scopes?.join(",")
129
- })) return redirectOnError("unable_to_link_account");
119
+ })) redirectOnError(c, resolvedErrorURL, "unable_to_link_account");
120
+ await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
130
121
  let toRedirectTo;
131
122
  try {
132
123
  toRedirectTo = callbackURL.toString();
@@ -137,7 +128,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
137
128
  }
138
129
  if (!userInfo.email) {
139
130
  c.context.logger.error(missingEmailLogMessage(provider.id));
140
- return redirectOnError("email_not_found");
131
+ redirectOnError(c, resolvedErrorURL, "email_not_found");
141
132
  }
142
133
  const accountData = {
143
134
  providerId: provider.id,
@@ -145,21 +136,27 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
145
136
  ...tokens,
146
137
  scope: tokens.scopes?.join(",")
147
138
  };
148
- const result = await handleOAuthUserInfo(c, {
149
- userInfo: {
150
- ...userInfo,
151
- id: providerAccountId,
152
- email: userInfo.email,
153
- name: userInfo.name || ""
154
- },
155
- account: accountData,
156
- callbackURL,
157
- disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp,
158
- overrideUserInfo: provider.options?.overrideUserInfoOnSignIn
159
- });
139
+ let result;
140
+ try {
141
+ result = await handleOAuthUserInfo(c, {
142
+ userInfo: {
143
+ ...userInfo,
144
+ id: providerAccountId,
145
+ email: userInfo.email,
146
+ name: userInfo.name || ""
147
+ },
148
+ account: accountData,
149
+ callbackURL,
150
+ disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp,
151
+ overrideUserInfo: provider.options?.overrideUserInfoOnSignIn
152
+ });
153
+ } catch (e) {
154
+ if (isAPIError(e) && e.body?.code) redirectOnError(c, resolvedErrorURL, e.body.code, e.body.message);
155
+ throw e;
156
+ }
160
157
  if (result.error) {
161
158
  c.context.logger.error(result.error.split(" ").join("_"));
162
- return redirectOnError(result.error.split(" ").join("_"));
159
+ redirectOnError(c, resolvedErrorURL, result.error.split(" ").join("_"));
163
160
  }
164
161
  const { session, user } = result.data;
165
162
  await setSessionCookie(c, {
@@ -27,6 +27,7 @@ declare function sendVerificationEmailFn(ctx: GenericEndpointContext, user: User
27
27
  declare const sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
28
28
  method: "POST";
29
29
  operationId: string;
30
+ cloneRequest: true;
30
31
  body: z.ZodObject<{
31
32
  email: z.ZodEmail;
32
33
  callbackURL: z.ZodOptional<z.ZodString>;
@@ -31,11 +31,12 @@ async function sendVerificationEmailFn(ctx, user) {
31
31
  user,
32
32
  url,
33
33
  token
34
- }, ctx.request));
34
+ }, ctx.request?.clone()));
35
35
  }
36
36
  const sendVerificationEmail = createAuthEndpoint("/send-verification-email", {
37
37
  method: "POST",
38
38
  operationId: "sendVerificationEmail",
39
+ cloneRequest: true,
39
40
  body: z.object({
40
41
  email: z.email().meta({ description: "The email to send the verification email to" }),
41
42
  callbackURL: z.string().meta({ description: "The URL to use for email verification callback" }).optional()
@@ -185,7 +186,7 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
185
186
  },
186
187
  url,
187
188
  token: newToken
188
- }, ctx.request));
189
+ }, ctx.request?.clone()));
189
190
  if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
190
191
  return ctx.json({ status: true });
191
192
  }
@@ -238,7 +239,7 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
238
239
  user: updatedUser,
239
240
  url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
240
241
  token: newToken
241
- }, ctx.request));
242
+ }, ctx.request?.clone()));
242
243
  await setSessionCookie(ctx, {
243
244
  session: activeSession.session,
244
245
  user: {
@@ -161,7 +161,7 @@ const resetPassword = createAuthEndpoint("/reset-password", {
161
161
  const user = await ctx.context.internalAdapter.findUserById(userId);
162
162
  if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
163
163
  }
164
- if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteSessions(userId);
164
+ if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(userId);
165
165
  return ctx.json({ status: true });
166
166
  });
167
167
  const verifyPassword = createAuthEndpoint("/verify-password", {