better-auth 1.6.10 → 1.6.12

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 (90) hide show
  1. package/dist/api/index.d.mts +8 -2
  2. package/dist/api/routes/callback.d.mts +1 -1
  3. package/dist/api/routes/callback.mjs +36 -40
  4. package/dist/api/routes/email-verification.d.mts +1 -0
  5. package/dist/api/routes/email-verification.mjs +4 -3
  6. package/dist/api/routes/session.mjs +14 -9
  7. package/dist/api/routes/sign-in.d.mts +1 -0
  8. package/dist/api/routes/sign-in.mjs +2 -1
  9. package/dist/api/routes/sign-up.d.mts +1 -0
  10. package/dist/api/routes/sign-up.mjs +9 -7
  11. package/dist/api/routes/update-user.mjs +5 -5
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/parser.mjs +0 -1
  14. package/dist/client/plugins/index.d.mts +3 -3
  15. package/dist/client/proxy.mjs +2 -1
  16. package/dist/context/helpers.mjs +3 -2
  17. package/dist/cookies/cookie-utils.d.mts +24 -1
  18. package/dist/cookies/cookie-utils.mjs +85 -22
  19. package/dist/cookies/index.d.mts +2 -3
  20. package/dist/cookies/index.mjs +39 -11
  21. package/dist/cookies/session-store.mjs +4 -23
  22. package/dist/db/get-migration.mjs +4 -4
  23. package/dist/db/index.d.mts +2 -2
  24. package/dist/db/index.mjs +3 -2
  25. package/dist/db/internal-adapter.mjs +96 -1
  26. package/dist/db/schema.d.mts +15 -2
  27. package/dist/db/schema.mjs +26 -1
  28. package/dist/db/with-hooks.d.mts +1 -0
  29. package/dist/db/with-hooks.mjs +58 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/oauth2/errors.mjs +16 -1
  33. package/dist/oauth2/link-account.mjs +6 -4
  34. package/dist/oauth2/state.mjs +8 -2
  35. package/dist/package.mjs +1 -1
  36. package/dist/plugins/access/access.d.mts +3 -15
  37. package/dist/plugins/access/access.mjs +11 -6
  38. package/dist/plugins/access/index.d.mts +2 -2
  39. package/dist/plugins/access/types.d.mts +11 -4
  40. package/dist/plugins/admin/access/statement.d.mts +29 -93
  41. package/dist/plugins/admin/admin.mjs +0 -4
  42. package/dist/plugins/admin/client.d.mts +1 -1
  43. package/dist/plugins/admin/routes.mjs +1 -0
  44. package/dist/plugins/anonymous/client.d.mts +1 -0
  45. package/dist/plugins/anonymous/error-codes.d.mts +1 -0
  46. package/dist/plugins/anonymous/error-codes.mjs +1 -0
  47. package/dist/plugins/anonymous/index.d.mts +1 -0
  48. package/dist/plugins/anonymous/index.mjs +16 -2
  49. package/dist/plugins/bearer/index.mjs +4 -9
  50. package/dist/plugins/captcha/index.mjs +2 -2
  51. package/dist/plugins/device-authorization/error-codes.mjs +1 -0
  52. package/dist/plugins/device-authorization/index.d.mts +1 -0
  53. package/dist/plugins/device-authorization/routes.mjs +34 -3
  54. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  55. package/dist/plugins/generic-oauth/index.mjs +6 -6
  56. package/dist/plugins/generic-oauth/routes.mjs +34 -32
  57. package/dist/plugins/generic-oauth/types.d.mts +7 -0
  58. package/dist/plugins/index.d.mts +2 -2
  59. package/dist/plugins/last-login-method/client.mjs +2 -2
  60. package/dist/plugins/magic-link/index.d.mts +8 -1
  61. package/dist/plugins/magic-link/index.mjs +4 -17
  62. package/dist/plugins/mcp/authorize.mjs +8 -2
  63. package/dist/plugins/mcp/index.mjs +73 -34
  64. package/dist/plugins/multi-session/index.mjs +2 -2
  65. package/dist/plugins/oauth-proxy/index.mjs +44 -31
  66. package/dist/plugins/oauth-proxy/utils.mjs +3 -10
  67. package/dist/plugins/oidc-provider/authorize.mjs +8 -2
  68. package/dist/plugins/oidc-provider/index.mjs +63 -37
  69. package/dist/plugins/one-tap/index.mjs +13 -8
  70. package/dist/plugins/open-api/generator.mjs +16 -5
  71. package/dist/plugins/organization/access/statement.d.mts +68 -201
  72. package/dist/plugins/organization/adapter.mjs +61 -56
  73. package/dist/plugins/organization/client.d.mts +3 -1
  74. package/dist/plugins/organization/error-codes.d.mts +2 -0
  75. package/dist/plugins/organization/error-codes.mjs +3 -1
  76. package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
  77. package/dist/plugins/organization/routes/crud-invites.mjs +7 -2
  78. package/dist/plugins/organization/types.d.mts +12 -2
  79. package/dist/plugins/two-factor/index.mjs +3 -2
  80. package/dist/plugins/username/index.d.mts +24 -2
  81. package/dist/plugins/username/index.mjs +49 -3
  82. package/dist/state.d.mts +2 -2
  83. package/dist/state.mjs +18 -4
  84. package/dist/test-utils/headers.mjs +2 -7
  85. package/dist/test-utils/test-instance.d.mts +25 -6
  86. package/dist/test-utils/test-instance.mjs +11 -2
  87. package/dist/utils/index.d.mts +1 -1
  88. package/dist/utils/url.d.mts +2 -1
  89. package/dist/utils/url.mjs +9 -3
  90. package/package.json +15 -14
@@ -1,13 +1,15 @@
1
+ import { isAPIError } from "../../utils/is-api-error.mjs";
1
2
  import { getOrigin } from "../../utils/url.mjs";
2
3
  import { originCheck } from "../../api/middlewares/origin-check.mjs";
3
4
  import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
4
5
  import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
5
6
  import { setSessionCookie } from "../../cookies/index.mjs";
7
+ import { redirectOnError } from "../../oauth2/errors.mjs";
6
8
  import { parseGenericState } from "../../state.mjs";
7
9
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
8
10
  import { PACKAGE_VERSION } from "../../version.mjs";
9
11
  import { parseJSON } from "../../client/parser.mjs";
10
- import { checkSkipProxy, redirectOnError, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
12
+ import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
11
13
  import { defu } from "defu";
12
14
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
13
15
  import * as z from "zod";
@@ -90,18 +92,28 @@ const oAuthProxy = (opts) => {
90
92
  throw redirectOnError(ctx, errorURL, "payload_expired");
91
93
  }
92
94
  try {
93
- await parseGenericState(ctx, payload.state);
95
+ await parseGenericState(ctx, payload.state, { skipStateCookieCheck: true });
94
96
  } catch (e) {
95
97
  ctx.context.logger.warn("Failed to clean up OAuth state", e);
96
98
  }
97
- const result = await handleOAuthUserInfo(ctx, {
98
- userInfo: payload.userInfo,
99
- account: payload.account,
100
- callbackURL: payload.callbackURL,
101
- disableSignUp: payload.disableSignUp
102
- });
103
- if (result.error || !result.data) {
104
- ctx.context.logger.error("Failed to create user or session", result.error);
99
+ let result;
100
+ try {
101
+ result = await handleOAuthUserInfo(ctx, {
102
+ userInfo: payload.userInfo,
103
+ account: payload.account,
104
+ callbackURL: payload.callbackURL,
105
+ disableSignUp: payload.disableSignUp
106
+ });
107
+ } catch (e) {
108
+ if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
109
+ throw e;
110
+ }
111
+ if (result.error) {
112
+ ctx.context.logger.error("OAuth proxy callback error", result.error);
113
+ throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
114
+ }
115
+ if (!result.data) {
116
+ ctx.context.logger.error("OAuth proxy callback missing session data");
105
117
  throw redirectOnError(ctx, errorURL, "user_creation_failed");
106
118
  }
107
119
  await setSessionCookie(ctx, result.data);
@@ -141,6 +153,7 @@ const oAuthProxy = (opts) => {
141
153
  data: state
142
154
  }));
143
155
  } catch {
156
+ ctx.context.logger.debug("OAuth proxy: could not decrypt state package, falling back to regular callback");
144
157
  return;
145
158
  }
146
159
  if (!statePackage.isOAuthProxy || !statePackage.state || !statePackage.stateCookie) {
@@ -248,29 +261,29 @@ const oAuthProxy = (opts) => {
248
261
  const oauthURL = new URL(providerURL);
249
262
  const originalState = oauthURL.searchParams.get("state");
250
263
  if (!originalState) return;
251
- let stateCookieValue;
252
- if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
253
- const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
254
- if (setCookieHeader) {
255
- const parsedCookies = parseSetCookieHeader(setCookieHeader);
256
- const stateCookie = ctx.context.createAuthCookie("oauth_state");
257
- stateCookieValue = parsedCookies.get(stateCookie.name)?.value;
258
- }
259
- } else {
260
- const verification = await ctx.context.internalAdapter.findVerificationValue(originalState);
261
- if (verification) stateCookieValue = await symmetricEncrypt({
262
- key: getEncryptionKey(ctx),
263
- data: verification.value
264
- });
265
- }
266
- if (!stateCookieValue) {
267
- ctx.context.logger.warn("No OAuth state cookie value found");
268
- return;
269
- }
270
264
  try {
265
+ let plaintextState;
266
+ if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
267
+ const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
268
+ if (setCookieHeader) {
269
+ const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
270
+ const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
271
+ if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
272
+ key: ctx.context.secretConfig,
273
+ data: encryptedCookieValue
274
+ });
275
+ }
276
+ } else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
277
+ if (!plaintextState) {
278
+ ctx.context.logger.warn("No OAuth state found for proxy");
279
+ return;
280
+ }
271
281
  const statePackage = {
272
282
  state: originalState,
273
- stateCookie: stateCookieValue,
283
+ stateCookie: await symmetricEncrypt({
284
+ key: getEncryptionKey(ctx),
285
+ data: plaintextState
286
+ }),
274
287
  isOAuthProxy: true
275
288
  };
276
289
  const encryptedPackage = await symmetricEncrypt({
@@ -283,7 +296,7 @@ const oAuthProxy = (opts) => {
283
296
  url: oauthURL.toString()
284
297
  };
285
298
  } catch (e) {
286
- ctx.context.logger.error("Failed to encrypt OAuth proxy state package:", e);
299
+ ctx.context.logger.error("Failed to prepare OAuth proxy state:", e);
287
300
  }
288
301
  })
289
302
  }, {
@@ -1,4 +1,4 @@
1
- import { getOrigin } from "../../utils/url.mjs";
1
+ import { getOrigin, trimTrailingSlashes } from "../../utils/url.mjs";
2
2
  import { env } from "@better-auth/core/env";
3
3
  //#region src/plugins/oauth-proxy/utils.ts
4
4
  /**
@@ -6,7 +6,7 @@ import { env } from "@better-auth/core/env";
6
6
  */
7
7
  function stripTrailingSlash(url) {
8
8
  if (!url) return "";
9
- return url.replace(/\/+$/, "");
9
+ return trimTrailingSlashes(url);
10
10
  }
11
11
  /**
12
12
  * Get base URL from vendor-specific environment variables
@@ -37,12 +37,5 @@ function checkSkipProxy(ctx, opts) {
37
37
  if (!currentURL) return false;
38
38
  return getOrigin(productionURL) === getOrigin(currentURL);
39
39
  }
40
- /**
41
- * Redirect to error URL with error code
42
- */
43
- function redirectOnError(ctx, errorURL, error) {
44
- const sep = errorURL.includes("?") ? "&" : "?";
45
- throw ctx.redirect(`${errorURL}${sep}error=${error}`);
46
- }
47
40
  //#endregion
48
- export { checkSkipProxy, redirectOnError, resolveCurrentURL, stripTrailingSlash };
41
+ export { checkSkipProxy, resolveCurrentURL, stripTrailingSlash };
@@ -93,8 +93,14 @@ async function authorize(ctx, options) {
93
93
  });
94
94
  if (invalidScopes.length) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`));
95
95
  if ((!query.code_challenge || !query.code_challenge_method) && options.requirePKCE) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required"));
96
- if (!query.code_challenge_method) query.code_challenge_method = "plain";
97
- if (!["s256", options.allowPlainCodeChallengeMethod ? "plain" : "s256"].includes(query.code_challenge_method?.toLowerCase() || "")) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method"));
96
+ if (query.code_challenge_method && !query.code_challenge) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge_method requires code_challenge"));
97
+ if (query.code_challenge) {
98
+ const allowedCodeChallengeMethods = options.allowPlainCodeChallengeMethod ? ["s256", "plain"] : ["s256"];
99
+ let codeChallengeMethod = query.code_challenge_method?.toLowerCase();
100
+ if (!codeChallengeMethod && options.allowPlainCodeChallengeMethod) codeChallengeMethod = "plain";
101
+ if (!codeChallengeMethod || !allowedCodeChallengeMethods.includes(codeChallengeMethod)) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method"));
102
+ query.code_challenge_method = codeChallengeMethod;
103
+ }
98
104
  const code = generateRandomString(32, "a-z", "A-Z", "0-9");
99
105
  const codeExpiresInMs = opts.codeExpiresIn * 1e3;
100
106
  const expiresAt = new Date(Date.now() + codeExpiresInMs);
@@ -1,5 +1,6 @@
1
1
  import { mergeSchema } from "../../db/schema.mjs";
2
2
  import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
3
+ import { constantTimeEqual } from "../../crypto/buffer.mjs";
3
4
  import { generateRandomString } from "../../crypto/random.mjs";
4
5
  import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
5
6
  import { expireCookie } from "../../cookies/index.mjs";
@@ -52,11 +53,7 @@ const getMetadata = (ctx, options) => {
52
53
  const jwtPlugin = ctx.context.getPlugin("jwt");
53
54
  const issuer = jwtPlugin && jwtPlugin.options?.jwt && jwtPlugin.options.jwt.issuer ? jwtPlugin.options.jwt.issuer : ctx.context.options.baseURL;
54
55
  const baseURL = ctx.context.baseURL;
55
- const supportedAlgs = options?.useJWTPlugin ? [
56
- "RS256",
57
- "EdDSA",
58
- "none"
59
- ] : ["HS256", "none"];
56
+ const supportedAlgs = options?.useJWTPlugin ? ["RS256", "EdDSA"] : ["HS256"];
60
57
  return {
61
58
  issuer,
62
59
  authorization_endpoint: `${baseURL}/oauth2/authorize`,
@@ -161,7 +158,7 @@ const oidcProvider = (options) => {
161
158
  defaultScope: "openid",
162
159
  accessTokenExpiresIn: DEFAULT_ACCESS_TOKEN_EXPIRES_IN,
163
160
  refreshTokenExpiresIn: DEFAULT_REFRESH_TOKEN_EXPIRES_IN,
164
- allowPlainCodeChallengeMethod: true,
161
+ allowPlainCodeChallengeMethod: false,
165
162
  storeClientSecret: "plain",
166
163
  ...options,
167
164
  scopes: [
@@ -190,14 +187,14 @@ const oidcProvider = (options) => {
190
187
  * Verify stored client secret against provided client secret
191
188
  */
192
189
  async function verifyStoredClientSecret(ctx, storedClientSecret, clientSecret) {
193
- if (opts.storeClientSecret === "encrypted") return await symmetricDecrypt({
190
+ if (opts.storeClientSecret === "encrypted") return constantTimeEqual(await symmetricDecrypt({
194
191
  key: ctx.context.secretConfig,
195
192
  data: storedClientSecret
196
- }) === clientSecret;
197
- if (opts.storeClientSecret === "hashed") return await defaultClientSecretHasher(clientSecret) === storedClientSecret;
198
- if (typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret) return await opts.storeClientSecret.hash(clientSecret) === storedClientSecret;
199
- if (typeof opts.storeClientSecret === "object" && "decrypt" in opts.storeClientSecret) return await opts.storeClientSecret.decrypt(storedClientSecret) === clientSecret;
200
- return clientSecret === storedClientSecret;
193
+ }), clientSecret);
194
+ if (opts.storeClientSecret === "hashed") return constantTimeEqual(await defaultClientSecretHasher(clientSecret), storedClientSecret);
195
+ if (typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret) return constantTimeEqual(await opts.storeClientSecret.hash(clientSecret), storedClientSecret);
196
+ if (typeof opts.storeClientSecret === "object" && "decrypt" in opts.storeClientSecret) return constantTimeEqual(await opts.storeClientSecret.decrypt(storedClientSecret), clientSecret);
197
+ return constantTimeEqual(clientSecret, storedClientSecret);
201
198
  }
202
199
  return {
203
200
  id: "oidc-provider",
@@ -378,25 +375,43 @@ const oidcProvider = (options) => {
378
375
  });
379
376
  let { client_id, client_secret } = body;
380
377
  const authorization = ctx.request?.headers.get("authorization") || null;
381
- if (authorization && !client_id && !client_secret && authorization.startsWith("Basic ")) try {
382
- const encoded = authorization.replace("Basic ", "");
383
- const decoded = new TextDecoder().decode(base64.decode(encoded));
384
- if (!decoded.includes(":")) throw new APIError("UNAUTHORIZED", {
378
+ if (authorization && !client_secret && authorization.startsWith("Basic ")) {
379
+ let decoded;
380
+ try {
381
+ const encoded = authorization.replace("Basic ", "");
382
+ decoded = new TextDecoder().decode(base64.decode(encoded));
383
+ } catch {
384
+ throw new APIError("UNAUTHORIZED", {
385
+ error_description: "invalid authorization header format",
386
+ error: "invalid_client"
387
+ });
388
+ }
389
+ const colonIndex = decoded.indexOf(":");
390
+ if (colonIndex === -1) throw new APIError("UNAUTHORIZED", {
385
391
  error_description: "invalid authorization header format",
386
392
  error: "invalid_client"
387
393
  });
388
- const [id, secret] = decoded.split(":");
394
+ let id;
395
+ let secret;
396
+ try {
397
+ id = decodeURIComponent(decoded.slice(0, colonIndex));
398
+ secret = decodeURIComponent(decoded.slice(colonIndex + 1));
399
+ } catch {
400
+ throw new APIError("UNAUTHORIZED", {
401
+ error_description: "invalid authorization header format",
402
+ error: "invalid_client"
403
+ });
404
+ }
389
405
  if (!id || !secret) throw new APIError("UNAUTHORIZED", {
390
406
  error_description: "invalid authorization header format",
391
407
  error: "invalid_client"
392
408
  });
393
- client_id = id;
394
- client_secret = secret;
395
- } catch {
396
- throw new APIError("UNAUTHORIZED", {
397
- error_description: "invalid authorization header format",
409
+ if (client_id && client_id.toString() !== id) throw new APIError("UNAUTHORIZED", {
410
+ error_description: "client_id in body does not match Authorization header",
398
411
  error: "invalid_client"
399
412
  });
413
+ client_id = id;
414
+ client_secret = secret;
400
415
  }
401
416
  const now = Date.now();
402
417
  const iat = Math.floor(now / 1e3);
@@ -428,6 +443,25 @@ const oidcProvider = (options) => {
428
443
  error_description: "refresh token expired",
429
444
  error: "invalid_grant"
430
445
  });
446
+ const refreshClient = await getClient(client_id.toString(), trustedClients);
447
+ if (!refreshClient) throw new APIError("UNAUTHORIZED", {
448
+ error_description: "invalid client_id",
449
+ error: "invalid_client"
450
+ });
451
+ if (refreshClient.disabled) throw new APIError("UNAUTHORIZED", {
452
+ error_description: "client is disabled",
453
+ error: "invalid_client"
454
+ });
455
+ if (refreshClient.type !== "public") {
456
+ if (!refreshClient.clientSecret || !client_secret) throw new APIError("UNAUTHORIZED", {
457
+ error_description: "client_secret is required for confidential clients",
458
+ error: "invalid_client"
459
+ });
460
+ if (!await verifyStoredClientSecret(ctx, refreshClient.clientSecret, client_secret.toString())) throw new APIError("UNAUTHORIZED", {
461
+ error_description: "invalid client_secret",
462
+ error: "invalid_client"
463
+ });
464
+ }
431
465
  const accessToken = generateRandomString(32, "a-z", "A-Z");
432
466
  const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
433
467
  await ctx.context.adapter.create({
@@ -460,20 +494,11 @@ const oidcProvider = (options) => {
460
494
  error_description: "code verifier is missing",
461
495
  error: "invalid_request"
462
496
  });
463
- /**
464
- * We need to check if the code is valid before we can proceed
465
- * with the rest of the request.
466
- */
467
- const verificationValue = await ctx.context.internalAdapter.findVerificationValue(code.toString());
497
+ const verificationValue = await ctx.context.internalAdapter.consumeVerificationValue(code.toString());
468
498
  if (!verificationValue) throw new APIError("UNAUTHORIZED", {
469
499
  error_description: "invalid code",
470
500
  error: "invalid_grant"
471
501
  });
472
- if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
473
- error_description: "code expired",
474
- error: "invalid_grant"
475
- });
476
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
477
502
  if (!client_id) throw new APIError("UNAUTHORIZED", {
478
503
  error_description: "client_id is required",
479
504
  error: "invalid_client"
@@ -527,12 +552,13 @@ const oidcProvider = (options) => {
527
552
  error: "invalid_client"
528
553
  });
529
554
  }
530
- if ((value.codeChallengeMethod === "plain" ? code_verifier : await createHash("SHA-256", "base64urlnopad").digest(code_verifier)) !== value.codeChallenge) throw new APIError("UNAUTHORIZED", {
531
- error_description: "code verification failed",
532
- error: "invalid_request"
533
- });
555
+ if (value.codeChallenge) {
556
+ if ((value.codeChallengeMethod === "plain" ? code_verifier : await createHash("SHA-256", "base64urlnopad").digest(code_verifier)) !== value.codeChallenge) throw new APIError("UNAUTHORIZED", {
557
+ error_description: "code verification failed",
558
+ error: "invalid_request"
559
+ });
560
+ }
534
561
  const requestedScopes = value.scope;
535
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
536
562
  const accessToken = generateRandomString(32, "a-z", "A-Z");
537
563
  const refreshToken = generateRandomString(32, "A-Z", "a-z");
538
564
  await ctx.context.adapter.create({
@@ -71,14 +71,19 @@ const oneTap = (options) => ({
71
71
  user: parseUserOutput(ctx.context.options, newUser.user)
72
72
  });
73
73
  }
74
- if (!await ctx.context.internalAdapter.findAccount(sub)) if ((ctx.context.options.account?.accountLinking)?.enabled !== false && (ctx.context.trustedProviders.includes("google") || email_verified)) await ctx.context.internalAdapter.linkAccount({
75
- userId: user.user.id,
76
- providerId: "google",
77
- accountId: sub,
78
- scope: "openid,profile,email",
79
- idToken
80
- });
81
- else throw new APIError("UNAUTHORIZED", { message: "Google sub doesn't match" });
74
+ if (!await ctx.context.internalAdapter.findAccount(sub)) {
75
+ const accountLinking = ctx.context.options.account?.accountLinking;
76
+ const providerEmailVerified = typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified);
77
+ const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
78
+ if (accountLinking?.enabled !== false && accountLinking?.disableImplicitLinking !== true && (!requireLocalEmailVerified || user.user.emailVerified) && (ctx.context.trustedProviders.includes("google") || providerEmailVerified)) await ctx.context.internalAdapter.linkAccount({
79
+ userId: user.user.id,
80
+ providerId: "google",
81
+ accountId: sub,
82
+ scope: "openid,profile,email",
83
+ idToken
84
+ });
85
+ else throw new APIError("UNAUTHORIZED", { message: "Google identity cannot be linked: implicit account-linking is disabled, the local email is not verified, or the Google email_verified claim is false and Google is not a trusted provider" });
86
+ }
82
87
  const session = await ctx.context.internalAdapter.createSession(user.user.id);
83
88
  await setSessionCookie(ctx, {
84
89
  user: user.user,
@@ -1,6 +1,7 @@
1
1
  import { db_exports } from "../../db/index.mjs";
2
2
  import { getEndpoints } from "../../api/index.mjs";
3
3
  import * as z from "zod";
4
+ import { toPascalCase } from "@better-auth/core/utils/string";
4
5
  //#region src/plugins/open-api/generator.ts
5
6
  const allowedType = new Set([
6
7
  "string",
@@ -156,7 +157,7 @@ function getResponse(responses) {
156
157
  } } },
157
158
  description: "Internal Server Error. This is a problem with the server that you cannot fix."
158
159
  },
159
- ...responses
160
+ ...responses ? structuredClone(responses) : {}
160
161
  };
161
162
  }
162
163
  function toOpenApiPath(path) {
@@ -196,6 +197,16 @@ async function generator(ctx, options) {
196
197
  return acc;
197
198
  }, {}) } };
198
199
  const paths = {};
200
+ const seenOperationIds = /* @__PURE__ */ new Set();
201
+ const uniqueOperationId = (operationId, method) => {
202
+ if (!operationId) return void 0;
203
+ const base = seenOperationIds.has(operationId) ? `${operationId}${toPascalCase(method)}` : operationId;
204
+ let result = base;
205
+ let n = 2;
206
+ while (seenOperationIds.has(result)) result = `${base}${n++}`;
207
+ seenOperationIds.add(result);
208
+ return result;
209
+ };
199
210
  Object.entries(baseEndpoints.api).forEach(([_, value]) => {
200
211
  if (!value.path || ctx.options.disabledPaths?.includes(value.path)) return;
201
212
  const options = value.options;
@@ -207,7 +218,7 @@ async function generator(ctx, options) {
207
218
  [method.toLowerCase()]: {
208
219
  tags: ["Default", ...options.metadata?.openapi?.tags || []],
209
220
  description: options.metadata?.openapi?.description,
210
- operationId: options.metadata?.openapi?.operationId,
221
+ operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
211
222
  security: [{ bearerAuth: [] }],
212
223
  parameters: getParameters(options),
213
224
  responses: getResponse(options.metadata?.openapi?.responses)
@@ -220,7 +231,7 @@ async function generator(ctx, options) {
220
231
  [method.toLowerCase()]: {
221
232
  tags: ["Default", ...options.metadata?.openapi?.tags || []],
222
233
  description: options.metadata?.openapi?.description,
223
- operationId: options.metadata?.openapi?.operationId,
234
+ operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
224
235
  security: [{ bearerAuth: [] }],
225
236
  parameters: getParameters(options),
226
237
  ...body ? { requestBody: body } : { requestBody: { content: { "application/json": { schema: {
@@ -253,7 +264,7 @@ async function generator(ctx, options) {
253
264
  [method.toLowerCase()]: {
254
265
  tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
255
266
  description: options.metadata?.openapi?.description,
256
- operationId: options.metadata?.openapi?.operationId,
267
+ operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
257
268
  security: [{ bearerAuth: [] }],
258
269
  parameters: getParameters(options),
259
270
  responses: getResponse(options.metadata?.openapi?.responses)
@@ -264,7 +275,7 @@ async function generator(ctx, options) {
264
275
  [method.toLowerCase()]: {
265
276
  tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
266
277
  description: options.metadata?.openapi?.description,
267
- operationId: options.metadata?.openapi?.operationId,
278
+ operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
268
279
  security: [{ bearerAuth: [] }],
269
280
  parameters: getParameters(options),
270
281
  requestBody: getRequestBody(options),