better-auth 1.8.0-beta.7 → 1.8.0-beta.8

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.
@@ -1,6 +1,7 @@
1
1
  import { OverrideMerge, Prettify as Prettify$1, UnionToIntersection } from "../types/helper.mjs";
2
2
  import { AdditionalSessionFieldsInput, AdditionalUserFieldsInput } from "../types/models.mjs";
3
3
  import "../types/index.mjs";
4
+ import { generateGenericState, parseGenericState } from "../state.mjs";
4
5
  import "../index.mjs";
5
6
  import { isAPIError } from "../utils/is-api-error.mjs";
6
7
  import { DispatchContext, dispatchAuthEndpoint } from "./dispatch.mjs";
@@ -11,6 +12,7 @@ import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refre
11
12
  import { callbackOAuth } from "./routes/callback.mjs";
12
13
  import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
13
14
  import { error } from "./routes/error.mjs";
15
+ import { consumeIssuedIdTokenNonce, consumeNonce, issueIdTokenNonce, issueNonce, issuedNonceResponses } from "./routes/id-token-nonce.mjs";
14
16
  import { ok } from "./routes/ok.mjs";
15
17
  import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./routes/password.mjs";
16
18
  import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
@@ -1664,6 +1666,38 @@ export declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awai
1664
1666
  url: string;
1665
1667
  redirect: boolean;
1666
1668
  }>;
1669
+ readonly issueIdTokenNonce: import("better-call").StrictEndpoint<"/id-token/nonce", {
1670
+ method: "POST";
1671
+ body: import("zod").ZodObject<{
1672
+ provider: import("zod").ZodType<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown, import("zod/v4/core").$ZodTypeInternals<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown>>;
1673
+ }, import("zod/v4/core").$strip>;
1674
+ metadata: {
1675
+ noStore: boolean;
1676
+ openapi: {
1677
+ description: string;
1678
+ responses: {
1679
+ 200: {
1680
+ description: string;
1681
+ content: {
1682
+ "application/json": {
1683
+ schema: {
1684
+ type: "object";
1685
+ properties: {
1686
+ nonce: {
1687
+ type: "string";
1688
+ };
1689
+ };
1690
+ required: string[];
1691
+ };
1692
+ };
1693
+ };
1694
+ };
1695
+ };
1696
+ };
1697
+ };
1698
+ }, {
1699
+ nonce: string;
1700
+ }>;
1667
1701
  readonly listUserAccounts: import("better-call").StrictEndpoint<"/list-accounts", {
1668
1702
  method: "GET";
1669
1703
  use: Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
@@ -3678,6 +3712,38 @@ export declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext
3678
3712
  url: string;
3679
3713
  redirect: boolean;
3680
3714
  }>;
3715
+ readonly issueIdTokenNonce: import("better-call").StrictEndpoint<"/id-token/nonce", {
3716
+ method: "POST";
3717
+ body: import("zod").ZodObject<{
3718
+ provider: import("zod").ZodType<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown, import("zod/v4/core").$ZodTypeInternals<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown>>;
3719
+ }, import("zod/v4/core").$strip>;
3720
+ metadata: {
3721
+ noStore: boolean;
3722
+ openapi: {
3723
+ description: string;
3724
+ responses: {
3725
+ 200: {
3726
+ description: string;
3727
+ content: {
3728
+ "application/json": {
3729
+ schema: {
3730
+ type: "object";
3731
+ properties: {
3732
+ nonce: {
3733
+ type: "string";
3734
+ };
3735
+ };
3736
+ required: string[];
3737
+ };
3738
+ };
3739
+ };
3740
+ };
3741
+ };
3742
+ };
3743
+ };
3744
+ }, {
3745
+ nonce: string;
3746
+ }>;
3681
3747
  readonly listUserAccounts: import("better-call").StrictEndpoint<"/list-accounts", {
3682
3748
  method: "GET";
3683
3749
  use: Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
@@ -4048,4 +4114,4 @@ export declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext
4048
4114
  } extends (infer T_2) ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K]; } : never) & T_1> : never : never : never;
4049
4115
  };
4050
4116
  //#endregion
4051
- export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
4117
+ export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, consumeIssuedIdTokenNonce, consumeNonce, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, generateGenericState, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, issueIdTokenNonce, issueNonce, issuedNonceResponses, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, parseGenericState, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -5,6 +5,8 @@ import { onRequestRateLimit } from "./rate-limiter/index.mjs";
5
5
  import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
6
6
  import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
7
7
  import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
8
+ import { generateGenericState, parseGenericState } from "../state.mjs";
9
+ import { consumeIssuedIdTokenNonce, consumeNonce, issueIdTokenNonce, issueNonce, issuedNonceResponses } from "./routes/id-token-nonce.mjs";
8
10
  import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
9
11
  import { callbackOAuth } from "./routes/callback.mjs";
10
12
  import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
@@ -134,6 +136,7 @@ function getEndpoints(ctx, options) {
134
136
  revokeSessions,
135
137
  revokeOtherSessions,
136
138
  linkSocialAccount,
139
+ issueIdTokenNonce,
137
140
  listUserAccounts,
138
141
  deleteUserCallback,
139
142
  unlinkAccount,
@@ -222,4 +225,4 @@ const router = (ctx, options) => {
222
225
  });
223
226
  };
224
227
  //#endregion
225
- export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
228
+ export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, consumeIssuedIdTokenNonce, consumeNonce, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, generateGenericState, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, issueIdTokenNonce, issueNonce, issuedNonceResponses, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, parseGenericState, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -11,6 +11,7 @@ import { toBoolean } from "../../utils/boolean.mjs";
11
11
  import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
12
12
  import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
13
13
  import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
14
+ import { consumeIssuedIdTokenNonce } from "./id-token-nonce.mjs";
14
15
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
15
16
  import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
16
17
  import { SocialProviderListEnum } from "@better-auth/core/social-providers";
@@ -168,6 +169,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
168
169
  throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED);
169
170
  }
170
171
  const { token, nonce } = c.body.idToken;
172
+ await consumeIssuedIdTokenNonce(c, provider, nonce);
171
173
  if (!await verifyProviderIdToken(provider, token, nonce, c)) {
172
174
  c.context.logger.warn("Invalid id token", { provider: c.body.provider });
173
175
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
@@ -0,0 +1,85 @@
1
+ import { GenericEndpointContext } from "@better-auth/core";
2
+ import { OAuthProvider } from "@better-auth/core/oauth2";
3
+ import * as z from "zod";
4
+ //#region src/api/routes/id-token-nonce.d.ts
5
+ /**
6
+ * Store a new single-use nonce under `scope` and return it. It expires after
7
+ * five minutes.
8
+ */
9
+ export declare function issueNonce(ctx: GenericEndpointContext, scope: string): Promise<string>;
10
+ /**
11
+ * Consume a nonce that {@link issueNonce} stored under `scope`. Throws
12
+ * `INVALID_ID_TOKEN_NONCE` when it is missing, malformed, unknown, expired, or
13
+ * already used. Call it before verifying the token, so a failed attempt burns
14
+ * the nonce too.
15
+ */
16
+ export declare function consumeNonce(ctx: GenericEndpointContext, scope: string, nonce: string | undefined): Promise<void>;
17
+ /** OpenAPI responses for an endpoint that returns `{ nonce }` from {@link issueNonce}. */
18
+ export declare const issuedNonceResponses: {
19
+ 200: {
20
+ description: string;
21
+ content: {
22
+ "application/json": {
23
+ schema: {
24
+ type: "object";
25
+ properties: {
26
+ nonce: {
27
+ type: "string";
28
+ };
29
+ };
30
+ required: string[];
31
+ };
32
+ };
33
+ };
34
+ };
35
+ };
36
+ type ProviderWithOptions = Pick<OAuthProvider, "id" | "options">;
37
+ /**
38
+ * Consume the issued nonce presented with a client-submitted ID token, from
39
+ * `/sign-in/social` or `/link-social`. A no-op for providers without
40
+ * `requireIssuedIdTokenNonce`.
41
+ */
42
+ export declare function consumeIssuedIdTokenNonce(ctx: GenericEndpointContext, provider: ProviderWithOptions, nonce: string | undefined): Promise<void>;
43
+ /**
44
+ * ### Endpoint
45
+ *
46
+ * POST `/id-token/nonce`
47
+ *
48
+ * Issues a single-use nonce for a client to pass to the provider's SDK before
49
+ * submitting the resulting ID token to `/sign-in/social` or `/link-social`.
50
+ * Only available for providers that set `requireIssuedIdTokenNonce`.
51
+ */
52
+ export declare const issueIdTokenNonce: import("better-call").StrictEndpoint<"/id-token/nonce", {
53
+ method: "POST";
54
+ body: z.ZodObject<{
55
+ provider: z.ZodType<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown, z.core.$ZodTypeInternals<(string & {}) | "github" | "apple" | "atlassian" | "cloudflare" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "huggingface" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linear" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat", unknown>>;
56
+ }, z.core.$strip>;
57
+ metadata: {
58
+ noStore: boolean;
59
+ openapi: {
60
+ description: string;
61
+ responses: {
62
+ 200: {
63
+ description: string;
64
+ content: {
65
+ "application/json": {
66
+ schema: {
67
+ type: "object";
68
+ properties: {
69
+ nonce: {
70
+ type: "string";
71
+ };
72
+ };
73
+ required: string[];
74
+ };
75
+ };
76
+ };
77
+ };
78
+ };
79
+ };
80
+ };
81
+ }, {
82
+ nonce: string;
83
+ }>;
84
+ //#endregion
85
+ export {};
@@ -0,0 +1,80 @@
1
+ import { generateRandomString } from "../../crypto/random.mjs";
2
+ import { getAwaitableValue } from "../../context/helpers.mjs";
3
+ import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
4
+ import { SocialProviderListEnum } from "@better-auth/core/social-providers";
5
+ import { createAuthEndpoint } from "@better-auth/core/api";
6
+ import * as z from "zod";
7
+ //#region src/api/routes/id-token-nonce.ts
8
+ const ISSUED_NONCE_EXPIRES_IN_MS = 3e5;
9
+ const ISSUED_NONCE_LENGTH = 32;
10
+ const ISSUED_NONCE_PATTERN = new RegExp(`^[a-zA-Z0-9]{${ISSUED_NONCE_LENGTH}}$`);
11
+ /**
12
+ * Store a new single-use nonce under `scope` and return it. It expires after
13
+ * five minutes.
14
+ */
15
+ async function issueNonce(ctx, scope) {
16
+ const nonce = generateRandomString(ISSUED_NONCE_LENGTH, "a-z", "A-Z", "0-9");
17
+ await ctx.context.internalAdapter.createVerificationValue({
18
+ identifier: `${scope}:${nonce}`,
19
+ value: scope,
20
+ expiresAt: new Date(Date.now() + ISSUED_NONCE_EXPIRES_IN_MS)
21
+ });
22
+ return nonce;
23
+ }
24
+ /**
25
+ * Consume a nonce that {@link issueNonce} stored under `scope`. Throws
26
+ * `INVALID_ID_TOKEN_NONCE` when it is missing, malformed, unknown, expired, or
27
+ * already used. Call it before verifying the token, so a failed attempt burns
28
+ * the nonce too.
29
+ */
30
+ async function consumeNonce(ctx, scope, nonce) {
31
+ if (!(nonce && ISSUED_NONCE_PATTERN.test(nonce) ? await ctx.context.internalAdapter.consumeVerificationValue(`${scope}:${nonce}`) : null)) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_ID_TOKEN_NONCE);
32
+ }
33
+ /** OpenAPI responses for an endpoint that returns `{ nonce }` from {@link issueNonce}. */
34
+ const issuedNonceResponses = { 200: {
35
+ description: "Success",
36
+ content: { "application/json": { schema: {
37
+ type: "object",
38
+ properties: { nonce: { type: "string" } },
39
+ required: ["nonce"]
40
+ } } }
41
+ } };
42
+ const idTokenNonceScope = (providerId) => `id-token-nonce:${providerId}`;
43
+ const requiresIssuedNonce = (provider) => provider.options?.requireIssuedIdTokenNonce === true;
44
+ /**
45
+ * Consume the issued nonce presented with a client-submitted ID token, from
46
+ * `/sign-in/social` or `/link-social`. A no-op for providers without
47
+ * `requireIssuedIdTokenNonce`.
48
+ */
49
+ async function consumeIssuedIdTokenNonce(ctx, provider, nonce) {
50
+ if (!requiresIssuedNonce(provider)) return;
51
+ await consumeNonce(ctx, idTokenNonceScope(provider.id), nonce);
52
+ }
53
+ /**
54
+ * ### Endpoint
55
+ *
56
+ * POST `/id-token/nonce`
57
+ *
58
+ * Issues a single-use nonce for a client to pass to the provider's SDK before
59
+ * submitting the resulting ID token to `/sign-in/social` or `/link-social`.
60
+ * Only available for providers that set `requireIssuedIdTokenNonce`.
61
+ */
62
+ const issueIdTokenNonce = createAuthEndpoint("/id-token/nonce", {
63
+ method: "POST",
64
+ body: z.object({ provider: SocialProviderListEnum }),
65
+ metadata: {
66
+ noStore: true,
67
+ openapi: {
68
+ description: "Issue a single-use nonce to bind a provider ID token to one sign-in or account-linking request",
69
+ responses: issuedNonceResponses
70
+ }
71
+ }
72
+ }, async (ctx) => {
73
+ const provider = await getAwaitableValue(ctx.context.socialProviders, { value: ctx.body.provider });
74
+ if (!provider) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
75
+ if (!requiresIssuedNonce(provider)) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ID_TOKEN_NONCE_NOT_ENABLED);
76
+ const nonce = await issueNonce(ctx, idTokenNonceScope(provider.id));
77
+ return ctx.json({ nonce });
78
+ });
79
+ //#endregion
80
+ export { consumeIssuedIdTokenNonce, consumeNonce, issueIdTokenNonce, issueNonce, issuedNonceResponses };
@@ -2,6 +2,7 @@ import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refre
2
2
  import { callbackOAuth } from "./callback.mjs";
3
3
  import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./email-verification.mjs";
4
4
  import { error } from "./error.mjs";
5
+ import { consumeIssuedIdTokenNonce, consumeNonce, issueIdTokenNonce, issueNonce, issuedNonceResponses } from "./id-token-nonce.mjs";
5
6
  import { ok } from "./ok.mjs";
6
7
  import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./password.mjs";
7
8
  import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
@@ -10,4 +11,4 @@ import { signOut } from "./sign-out.mjs";
10
11
  import { signUpEmail } from "./sign-up.mjs";
11
12
  import { updateSession } from "./update-session.mjs";
12
13
  import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./update-user.mjs";
13
- export { accountInfo, callbackOAuth, changeEmail, changePassword, createEmailVerificationToken, deleteUser, deleteUserCallback, error, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
14
+ export { accountInfo, callbackOAuth, changeEmail, changePassword, consumeIssuedIdTokenNonce, consumeNonce, createEmailVerificationToken, deleteUser, deleteUserCallback, error, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, issueIdTokenNonce, issueNonce, issuedNonceResponses, linkSocialAccount, listSessions, listUserAccounts, ok, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -7,6 +7,7 @@ import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2
7
7
  import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
8
8
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
9
9
  import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
10
+ import { consumeIssuedIdTokenNonce } from "./id-token-nonce.mjs";
10
11
  import { safeCloneRequest } from "../../utils/request.mjs";
11
12
  import { createEmailVerificationToken } from "./email-verification.mjs";
12
13
  import { assertPasswordNotTooLong } from "../../utils/password.mjs";
@@ -157,6 +158,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
157
158
  throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED);
158
159
  }
159
160
  const { token, nonce } = c.body.idToken;
161
+ await consumeIssuedIdTokenNonce(c, provider, nonce);
160
162
  if (!await verifyProviderIdToken(provider, token, nonce, c)) {
161
163
  c.context.logger.warn("Invalid id token", { provider: c.body.provider });
162
164
  throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
@@ -1,7 +1,7 @@
1
1
  import { buildSchema } from "./get-schema.mjs";
2
2
  import { createLogger } from "@better-auth/core/env";
3
3
  import { BetterAuthError } from "@better-auth/core/error";
4
- import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
4
+ import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, introspectPostgresTables, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
5
5
  import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
6
6
  import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, invalidateSchemaChecks } from "@better-auth/core/db/internal";
7
7
  import { sql } from "kysely";
@@ -354,9 +354,8 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
354
354
  logger.error("Only kysely adapter is supported for migrations. You can use `generate` command to generate the schema, if you're using a different adapter.");
355
355
  process.exit(1);
356
356
  }
357
- const allTableMetadata = await db.introspection.getTables();
358
357
  let target;
359
- let tableMetadata = allTableMetadata;
358
+ let tableMetadata;
360
359
  switch (dbType) {
361
360
  case "postgres": {
362
361
  const schema = schemaName ?? await getPostgresSchema(db);
@@ -373,11 +372,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
373
372
  } catch (error) {
374
373
  logger.debug(`Could not verify schema existence: ${error instanceof Error ? error.message : String(error)}`);
375
374
  }
376
- /**
377
- * Kysely 0.28 does not expose `isForeign`, while 0.29 adds foreign table metadata.
378
- * @see https://github.com/kysely-org/kysely/pull/1494
379
- */
380
- tableMetadata = allTableMetadata.filter((table) => table.schema === schema && !table.isView && !("isForeign" in table && table.isForeign));
375
+ tableMetadata = (await introspectPostgresTables(db, [schema])).filter((table) => !table.isView && !table.isForeign);
381
376
  logger.debug(`Found ${tableMetadata.length} table(s) in schema '${schema}': ${tableMetadata.map((table) => table.name).join(", ") || "(none)"}`);
382
377
  break;
383
378
  }
@@ -388,15 +383,18 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
388
383
  schema
389
384
  };
390
385
  logger.debug(`SQL Server migration: Using schema '${schema}' (from the current user's default schema)`);
391
- tableMetadata = allTableMetadata.filter((table) => table.schema === schema);
386
+ tableMetadata = (await db.introspection.getTables()).filter((table) => table.schema === schema);
392
387
  break;
393
388
  }
394
389
  case "mysql":
395
390
  target = { type: "mysql" };
391
+ tableMetadata = await db.introspection.getTables();
396
392
  break;
397
- case "sqlite": target = { type: "sqlite" };
393
+ case "sqlite":
394
+ target = { type: "sqlite" };
395
+ tableMetadata = await db.introspection.getTables();
398
396
  }
399
- const databaseIndexMap = await getDatabaseIndexMap(db, target, allTableMetadata.map((table) => table.name), introspectIndexes);
397
+ const databaseIndexMap = await getDatabaseIndexMap(db, target, tableMetadata.map((table) => table.name), introspectIndexes);
400
398
  const databaseColumnBounds = await getDatabaseColumnBounds(db, target);
401
399
  const schemaProblems = diffSchema(toPhysicalSchema(db, betterAuthSchema), toIntrospectedTables(tableMetadata)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "database"));
402
400
  const toBeCreated = [];
@@ -131,6 +131,20 @@ const createInternalAdapter = (adapter, ctx) => {
131
131
  if (verificationConsumeLocks.get(key) === next) verificationConsumeLocks.delete(key);
132
132
  }
133
133
  }
134
+ async function deleteExpiredVerifications() {
135
+ if (options.verification?.disableCleanup) return;
136
+ await queueAfterTransactionHook(async () => {
137
+ try {
138
+ await deleteManyWithHooks([{
139
+ field: "expiresAt",
140
+ value: /* @__PURE__ */ new Date(),
141
+ operator: "lt"
142
+ }], "verification", void 0);
143
+ } catch (error) {
144
+ logger.error("Failed to delete expired verification values", error);
145
+ }
146
+ });
147
+ }
134
148
  return {
135
149
  createOAuthUser: async (user, account) => {
136
150
  return runWithTransaction(adapter, async () => {
@@ -795,11 +809,7 @@ const createInternalAdapter = (adapter, ctx) => {
795
809
  }
796
810
  let verification = await findByIdentifier(storedIdentifier);
797
811
  if (!verification.length && storageOption && storageOption !== "plain") verification = await findByIdentifier(identifier);
798
- if (!options.verification?.disableCleanup) await deleteManyWithHooks([{
799
- field: "expiresAt",
800
- value: /* @__PURE__ */ new Date(),
801
- operator: "lt"
802
- }], "verification", void 0);
812
+ await deleteExpiredVerifications();
803
813
  return verification[0] || null;
804
814
  },
805
815
  deleteVerificationByIdentifier: async (identifier) => {
@@ -898,6 +908,7 @@ const createInternalAdapter = (adapter, ctx) => {
898
908
  if (consumed) break;
899
909
  }
900
910
  if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
911
+ if (consumed) await deleteExpiredVerifications();
901
912
  }
902
913
  if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
903
914
  return consumed;
@@ -930,7 +941,10 @@ const createInternalAdapter = (adapter, ctx) => {
930
941
  * is best-effort there (it is intended for development and tests).
931
942
  */
932
943
  reserveVerificationValue: async (data) => {
933
- const reservationId = encodeDeterministicId(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), options);
944
+ const reservationId = encodeDeterministicId(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), options, {
945
+ adapter,
946
+ model: "verification"
947
+ });
934
948
  const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier);
935
949
  const storedIdentifier = await processIdentifier(data.identifier, storageOption);
936
950
  if (secondaryStorage && !options.verification?.storeInDatabase) throw new BetterAuthError("reserveVerificationValue requires database-backed verification storage. Set verification.storeInDatabase to true for flows that reserve verification values.");
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.8.0-beta.7";
2
+ var version = "1.8.0-beta.8";
3
3
  //#endregion
4
4
  export { version };
@@ -634,6 +634,21 @@ const deviceVerify = (grant) => createAuthEndpoint("/device", {
634
634
  increment: {},
635
635
  set: { userId: session.user.id }
636
636
  })) deviceCodeRecord.userId = session.user.id;
637
+ else {
638
+ const current = await ctx.context.adapter.findOne({
639
+ model: "deviceCode",
640
+ where: [{
641
+ field: "id",
642
+ value: deviceCodeRecord.id
643
+ }]
644
+ });
645
+ if (!current) throw new APIError("BAD_REQUEST", {
646
+ error: "invalid_request",
647
+ error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
648
+ });
649
+ deviceCodeRecord.userId = current.userId;
650
+ deviceCodeRecord.status = current.status;
651
+ }
637
652
  }
638
653
  const canReviewRequest = session?.user.id !== void 0 && deviceCodeRecord.userId === session.user.id;
639
654
  return ctx.json({
@@ -25,10 +25,7 @@ declare module "@better-auth/core" {
25
25
  * Extracts common fields from GenericOAuthConfig for provider helpers.
26
26
  */
27
27
  export type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "endSessionEndpoint" | "postLogoutRedirectURI" | "disableProviderLogout" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
28
- type PendingDiscovery = Map<string, {
29
- config: GenericOAuthConfig;
30
- attempt?: Promise<void> | undefined;
31
- }>;
28
+ type PendingDiscovery = Map<string, GenericOAuthConfig>;
32
29
  /**
33
30
  * A generic OAuth plugin that registers any OAuth/OIDC provider
34
31
  * as a first-class social provider.
@@ -25,6 +25,7 @@ function isClientSecretTokenEndpointAuth(tokenEndpointAuth) {
25
25
  }
26
26
  /** A request waits on a discovery retry, so bound it (the SSO plugin uses 10s too). */
27
27
  const DISCOVERY_RETRY_TIMEOUT_MS = 1e4;
28
+ const MAX_DISCOVERY_REDIRECTS = 5;
28
29
  /**
29
30
  * Providers skipped because startup discovery failed. Init stores them on each auth
30
31
  * instance's context, so one plugin object can serve several instances. It is a context
@@ -44,18 +45,35 @@ const readIdTokenClaims = (idToken) => {
44
45
  }
45
46
  };
46
47
  async function fetchDiscovery(url, headers, timeout) {
47
- const result = await betterFetch(url, {
48
- method: "GET",
49
- headers,
50
- signal: timeout ? AbortSignal.timeout(timeout) : void 0
51
- });
52
- if (result.error || !result.data) return null;
53
- if (result.data.issuer) try {
54
- new URL(result.data.issuer);
55
- } catch {
56
- return null;
48
+ const signal = timeout ? AbortSignal.timeout(timeout) : void 0;
49
+ let target = url;
50
+ for (let hop = 0;; hop++) {
51
+ const redirect = {};
52
+ const result = await betterFetch(target, {
53
+ method: "GET",
54
+ headers,
55
+ redirect: headers ? "manual" : void 0,
56
+ signal,
57
+ onError: ({ response }) => {
58
+ if (response.status >= 300 && response.status < 400) redirect.location = response.headers.get("location");
59
+ }
60
+ });
61
+ if (headers && redirect.location) {
62
+ const next = new URL(redirect.location, target);
63
+ if (next.origin !== new URL(url).origin) throw new Error(`discovery redirected to ${next.origin}; with discoveryHeaders set, only same-origin redirects are followed. Set discoveryUrl to the final URL.`);
64
+ if (hop >= MAX_DISCOVERY_REDIRECTS) throw new Error(`discovery redirected more than ${MAX_DISCOVERY_REDIRECTS} times. Set discoveryUrl to the final URL.`);
65
+ target = next.href;
66
+ continue;
67
+ }
68
+ if (headers && result.error?.status === 0) throw new Error("discovery redirected; with discoveryHeaders set, set discoveryUrl to the final URL.");
69
+ if (result.error || !result.data) return null;
70
+ if (result.data.issuer) try {
71
+ new URL(result.data.issuer);
72
+ } catch {
73
+ return null;
74
+ }
75
+ return result.data;
57
76
  }
58
- return result.data;
59
77
  }
60
78
  async function fetchUserInfo(tokens, userInfoUrl, idTokenClaims) {
61
79
  if (tokens.idToken) {
@@ -100,6 +118,7 @@ const genericOAuth = (options) => {
100
118
  const buildProviders = async (configs, ctx, discoveryTimeout) => {
101
119
  const genericProviders = [];
102
120
  for (const c of configs) {
121
+ if (Object.keys(c.authorizationHeaders ?? {}).some((name) => name.toLowerCase() === "authorization")) throw new Error(`Provider "${c.providerId}": authorizationHeaders cannot set "Authorization". Use tokenEndpointAuth: { method: "custom", customizeRequest } instead.`);
103
122
  let authorizationUrl = c.authorizationUrl;
104
123
  let tokenUrl = c.tokenUrl;
105
124
  let userInfoUrl = c.userInfoUrl;
@@ -128,8 +147,9 @@ const genericOAuth = (options) => {
128
147
  ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
129
148
  continue;
130
149
  }
150
+ const jwksHeaders = jwksUrl.origin === new URL(c.discoveryUrl).origin ? c.discoveryHeaders : void 0;
131
151
  idTokenConfig = {
132
- jwks: createRemoteJWKSet(jwksUrl),
152
+ jwks: createRemoteJWKSet(jwksUrl, jwksHeaders && { headers: jwksHeaders }),
133
153
  issuer: discovered.issuer,
134
154
  audience: c.clientId,
135
155
  algorithms: isOidc ? signingAlgs : void 0
@@ -287,7 +307,8 @@ const genericOAuth = (options) => {
287
307
  authentication: c.authentication,
288
308
  tokenEndpointAuth,
289
309
  tokenEndpoint: tokenUrl,
290
- extraParams: resolvedRefreshParams
310
+ extraParams: resolvedRefreshParams,
311
+ headers: c.authorizationHeaders
291
312
  });
292
313
  return applyDefaultAccessTokenExpiry(tokens, c.accessTokenExpiresIn);
293
314
  },
@@ -303,18 +324,13 @@ const genericOAuth = (options) => {
303
324
  }
304
325
  return genericProviders;
305
326
  };
306
- const retryDiscovery = (ctx, providerId) => {
327
+ const retryDiscovery = async (ctx, providerId) => {
307
328
  const pendingDiscovery = getPendingDiscovery(ctx);
308
329
  const pending = pendingDiscovery?.get(providerId);
309
330
  if (!pending) return;
310
- pending.attempt ??= buildProviders([pending.config], ctx, DISCOVERY_RETRY_TIMEOUT_MS).then(([provider]) => {
311
- if (!provider) return;
312
- pendingDiscovery?.delete(providerId);
313
- ctx.socialProviders.unshift(provider);
314
- }).finally(() => {
315
- pending.attempt = void 0;
316
- });
317
- return pending.attempt;
331
+ const [provider] = await buildProviders([pending], ctx, DISCOVERY_RETRY_TIMEOUT_MS);
332
+ if (!provider || !pendingDiscovery?.delete(providerId)) return;
333
+ ctx.socialProviders.unshift(provider);
318
334
  };
319
335
  const requestedProviderId = (context) => {
320
336
  const id = context.body?.provider ?? context.body?.providerId ?? context.query?.provider ?? context.params?.id;
@@ -326,7 +342,7 @@ const genericOAuth = (options) => {
326
342
  init: async (ctx) => {
327
343
  const genericProviders = await buildProviders(options.config, ctx);
328
344
  const built = new Set(genericProviders.map((p) => p.id));
329
- const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId, { config: c }]));
345
+ const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId, c]));
330
346
  const existingIds = new Set(ctx.socialProviders.map((p) => p.id));
331
347
  for (const gp of genericProviders) if (existingIds.has(gp.id)) ctx.logger.warn(`Generic OAuth provider "${gp.id}" shadows a built-in social provider with the same ID`);
332
348
  return { context: {
@@ -211,13 +211,21 @@ export interface GenericOAuthConfig<ID extends string = string> {
211
211
  */
212
212
  authentication?: ("basic" | "post") | undefined;
213
213
  /**
214
- * Custom headers to include in the discovery request.
214
+ * Custom headers to include in the discovery request, and in the JWKS request when
215
+ * `jwks_uri` has the same origin as `discoveryUrl`. When set, discovery follows
216
+ * redirects only within that origin and fails on a redirect to another origin.
215
217
  * Useful for providers like Epic that require specific headers (e.g., Epic-Client-ID).
216
218
  */
217
219
  discoveryHeaders?: Record<string, string> | undefined;
218
220
  /**
219
- * Custom headers to include in the authorization request.
221
+ * Custom headers to include in token endpoint requests: the authorization
222
+ * code exchange and token refresh. A custom `getToken` replaces the default
223
+ * code exchange, so it must add these headers itself.
220
224
  * Useful for providers like Qonto that require specific headers (e.g., X-Qonto-Staging-Token for local development).
225
+ *
226
+ * `Authorization` is not allowed in any letter case, and initialization fails
227
+ * if it is present. Set it from `tokenEndpointAuth` with `method: "custom"`
228
+ * and `customizeRequest` instead.
221
229
  */
222
230
  authorizationHeaders?: Record<string, string> | undefined;
223
231
  /**
@@ -138,6 +138,10 @@ export interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "cl
138
138
  * This lets you render an alternative UI (e.g. a Google Sign-In button) to restart the process.
139
139
  */
140
140
  onPromptNotification?: ((notification?: any | undefined) => void) | undefined;
141
+ /**
142
+ * @deprecated Ignored. The client fetches a single-use nonce from
143
+ * `/one-tap/nonce` for every prompt and button render.
144
+ */
141
145
  nonce?: string | undefined;
142
146
  /**
143
147
  * Button mode configuration. When provided, renders a "Sign In with Google" button
@@ -2,6 +2,9 @@ import { PACKAGE_VERSION } from "../../version.mjs";
2
2
  import { isSafeUrlScheme } from "@better-auth/core/utils/url";
3
3
  //#region src/plugins/one-tap/client.ts
4
4
  let isRequestInProgress = false;
5
+ const BUTTON_NONCE_REFRESH_MS = 24e4;
6
+ const BUTTON_NONCE_RETRY_MS = 1e4;
7
+ const buttonRefreshTimers = /* @__PURE__ */ new WeakMap();
5
8
  function isFedCMSupported() {
6
9
  return typeof window !== "undefined" && "IdentityCredential" in window;
7
10
  }
@@ -33,102 +36,112 @@ const oneTapClient = (options) => {
33
36
  console.warn("Google One Tap is only available in browser environments");
34
37
  return;
35
38
  }
36
- if (opts?.button) {
37
- await loadGoogleScript();
38
- const container = typeof opts.button.container === "string" ? document.querySelector(opts.button.container) : opts.button.container;
39
- if (!container) {
40
- console.error("Google One Tap: Button container not found", opts.button.container);
41
- return;
42
- }
43
- async function callback(idToken) {
44
- if ((await $fetch("/one-tap/callback", {
45
- method: "POST",
46
- body: {
47
- idToken,
48
- callbackURL: opts?.callbackURL
49
- },
50
- ...opts?.fetchOptions,
51
- ...fetchOptions
52
- }))?.error) return;
53
- if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
54
- const target = opts?.callbackURL ?? "/";
55
- if (isSafeUrlScheme(target)) window.location.href = target;
56
- }
57
- }
58
- const autoSelect = opts?.autoSelect ?? options.autoSelect;
59
- const cancelOnTapOutside = opts?.cancelOnTapOutside ?? options.cancelOnTapOutside;
60
- const contextValue = opts?.context ?? options.context ?? "signin";
61
- window.google?.accounts.id.initialize({
62
- client_id: options.clientId,
63
- callback: async (response) => {
64
- try {
65
- await callback(response.credential);
66
- } catch (error) {
67
- console.error("Error during button callback:", error);
68
- }
69
- },
70
- auto_select: autoSelect,
71
- cancel_on_tap_outside: cancelOnTapOutside,
72
- context: contextValue,
73
- ux_mode: opts?.uxMode || "popup",
74
- nonce: opts?.nonce,
75
- itp_support: true,
76
- ...opts.button.fedCM && {
77
- use_fedcm_for_button: true,
78
- button_auto_select: autoSelect
79
- },
80
- ...options.additionalOptions
81
- });
82
- window.google?.accounts.id.renderButton(container, opts.button.config ?? { type: "icon" });
83
- return;
84
- }
85
- async function callback(idToken) {
39
+ if (opts?.nonce !== void 0) console.warn("Google One Tap: the `nonce` option is ignored; the nonce is issued by the server.");
40
+ /** Returns whether it navigated away. */
41
+ async function callback(idToken, nonce) {
86
42
  if ((await $fetch("/one-tap/callback", {
87
43
  method: "POST",
88
44
  body: {
89
45
  idToken,
46
+ nonce,
90
47
  callbackURL: opts?.callbackURL
91
48
  },
92
49
  ...opts?.fetchOptions,
93
50
  ...fetchOptions
94
- }))?.error) return;
51
+ }))?.error) return false;
95
52
  if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
96
53
  const target = opts?.callbackURL ?? "/";
97
- if (isSafeUrlScheme(target)) window.location.href = target;
54
+ if (isSafeUrlScheme(target)) {
55
+ window.location.href = target;
56
+ return true;
57
+ }
98
58
  }
59
+ return false;
99
60
  }
61
+ const { body: _body, query: _query, params: _params, output: _output, errorSchema: _errorSchema, onResponse: _onResponse, onSuccess: _onSuccess, onError: _onError, onRetry: _onRetry, ...nonceFetchOptions } = {
62
+ ...opts?.fetchOptions,
63
+ ...fetchOptions
64
+ };
65
+ const fetchNonce = async () => {
66
+ const { data, error } = await $fetch("/one-tap/nonce", {
67
+ ...nonceFetchOptions,
68
+ method: "POST",
69
+ throw: false
70
+ });
71
+ if (!data?.nonce) throw new Error("Google One Tap: failed to fetch a nonce", { cause: error });
72
+ return data.nonce;
73
+ };
100
74
  const autoSelect = opts?.autoSelect ?? options.autoSelect;
101
- const cancelOnTapOutside = opts?.cancelOnTapOutside ?? options.cancelOnTapOutside;
102
- const contextValue = opts?.context ?? options.context ?? "signin";
75
+ const initialize = (nonce, onCredential) => window.google?.accounts.id.initialize({
76
+ client_id: options.clientId,
77
+ callback: onCredential,
78
+ auto_select: autoSelect,
79
+ cancel_on_tap_outside: opts?.cancelOnTapOutside ?? options.cancelOnTapOutside,
80
+ context: opts?.context ?? options.context ?? "signin",
81
+ ux_mode: opts?.uxMode || "popup",
82
+ /**
83
+ * @see {@link https://developers.google.com/identity/gsi/web/guides/overview}
84
+ */
85
+ itp_support: true,
86
+ ...opts?.button?.fedCM && {
87
+ use_fedcm_for_button: true,
88
+ button_auto_select: autoSelect
89
+ },
90
+ ...options.additionalOptions,
91
+ nonce
92
+ });
93
+ if (opts?.button) {
94
+ await loadGoogleScript();
95
+ const container = typeof opts.button.container === "string" ? document.querySelector(opts.button.container) : opts.button.container;
96
+ if (!container) {
97
+ console.error("Google One Tap: Button container not found", opts.button.container);
98
+ return;
99
+ }
100
+ const buttonConfig = opts.button.config ?? { type: "icon" };
101
+ const renderButton = async () => {
102
+ clearTimeout(buttonRefreshTimers.get(container));
103
+ const nonce = await fetchNonce();
104
+ initialize(nonce, async (response) => {
105
+ let navigated = false;
106
+ try {
107
+ navigated = await callback(response.credential, nonce);
108
+ } catch (error) {
109
+ console.error("Error during button callback:", error);
110
+ }
111
+ if (!navigated) await refreshButton();
112
+ });
113
+ container.replaceChildren();
114
+ buttonRefreshTimers.set(container, setTimeout(refreshButton, BUTTON_NONCE_REFRESH_MS));
115
+ window.google?.accounts.id.renderButton(container, buttonConfig);
116
+ };
117
+ const refreshButton = async () => {
118
+ if (!container.isConnected) return;
119
+ try {
120
+ await renderButton();
121
+ } catch (error) {
122
+ console.error("Google One Tap: failed to refresh the button", error);
123
+ buttonRefreshTimers.set(container, setTimeout(refreshButton, BUTTON_NONCE_RETRY_MS));
124
+ }
125
+ };
126
+ await renderButton();
127
+ return;
128
+ }
103
129
  isRequestInProgress = true;
104
130
  try {
105
- await loadGoogleScript();
131
+ const [, nonce] = await Promise.all([loadGoogleScript(), fetchNonce()]);
106
132
  await new Promise((resolve, reject) => {
107
133
  let isResolved = false;
108
134
  const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
109
135
  const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
110
- window.google?.accounts.id.initialize({
111
- client_id: options.clientId,
112
- callback: async (response) => {
113
- isResolved = true;
114
- try {
115
- await callback(response.credential);
116
- resolve();
117
- } catch (error) {
118
- console.error("Error during One Tap callback:", error);
119
- reject(error);
120
- }
121
- },
122
- auto_select: autoSelect,
123
- cancel_on_tap_outside: cancelOnTapOutside,
124
- context: contextValue,
125
- ux_mode: opts?.uxMode || "popup",
126
- nonce: opts?.nonce,
127
- /**
128
- * @see {@link https://developers.google.com/identity/gsi/web/guides/overview}
129
- */
130
- itp_support: true,
131
- ...options.additionalOptions
136
+ initialize(nonce, async (response) => {
137
+ isResolved = true;
138
+ try {
139
+ await callback(response.credential, nonce);
140
+ resolve();
141
+ } catch (error) {
142
+ console.error("Error during One Tap callback:", error);
143
+ reject(error);
144
+ }
132
145
  });
133
146
  const handlePrompt = (attempt) => {
134
147
  if (isResolved) return;
@@ -26,10 +26,50 @@ export declare const oneTap: (options?: OneTapOptions | undefined) => {
26
26
  id: "one-tap";
27
27
  version: string;
28
28
  endpoints: {
29
+ /**
30
+ * ### Endpoint
31
+ *
32
+ * POST `/one-tap/nonce`
33
+ *
34
+ * Issues a single-use nonce to pass to Google before requesting the
35
+ * One Tap ID token. `/one-tap/callback` consumes it and requires the
36
+ * token's `nonce` claim to match.
37
+ */
38
+ oneTapNonce: import("better-call").StrictEndpoint<"/one-tap/nonce", {
39
+ method: "POST";
40
+ metadata: {
41
+ noStore: boolean;
42
+ openapi: {
43
+ summary: string;
44
+ description: string;
45
+ responses: {
46
+ 200: {
47
+ description: string;
48
+ content: {
49
+ "application/json": {
50
+ schema: {
51
+ type: "object";
52
+ properties: {
53
+ nonce: {
54
+ type: "string";
55
+ };
56
+ };
57
+ required: string[];
58
+ };
59
+ };
60
+ };
61
+ };
62
+ };
63
+ };
64
+ };
65
+ }, {
66
+ nonce: string;
67
+ }>;
29
68
  oneTapCallback: import("better-call").StrictEndpoint<"/one-tap/callback", {
30
69
  method: "POST";
31
70
  body: z.ZodObject<{
32
71
  idToken: z.ZodString;
72
+ nonce: z.ZodString;
33
73
  callbackURL: z.ZodOptional<z.ZodString>;
34
74
  }, z.core.$strip>;
35
75
  metadata: {
@@ -5,6 +5,7 @@ import { toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
5
5
  import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/errors.mjs";
6
6
  import { toBoolean } from "../../utils/boolean.mjs";
7
7
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
8
+ import { consumeNonce, issueNonce, issuedNonceResponses } from "../../api/routes/id-token-nonce.mjs";
8
9
  import { APIError as APIError$1 } from "../../api/index.mjs";
9
10
  import { PACKAGE_VERSION } from "../../version.mjs";
10
11
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -14,6 +15,7 @@ import * as z from "zod";
14
15
  //#region src/plugins/one-tap/index.ts
15
16
  const oneTapCallbackBodySchema = z.object({
16
17
  idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }),
18
+ nonce: z.string().meta({ description: "Nonce issued by /one-tap/nonce and passed to Google when the ID token was requested" }),
17
19
  /**
18
20
  * Sent so the global origin-check middleware validates the post-login
19
21
  * redirect target against `trustedOrigins`. Without it the client performs
@@ -21,98 +23,123 @@ const oneTapCallbackBodySchema = z.object({
21
23
  */
22
24
  callbackURL: z.string().meta({ description: "URL to redirect to after a successful sign-in" }).optional()
23
25
  });
26
+ const ONE_TAP_NONCE_SCOPE = "one-tap-nonce";
24
27
  const oneTap = (options) => ({
25
28
  id: "one-tap",
26
29
  version: PACKAGE_VERSION,
27
- endpoints: { oneTapCallback: createAuthEndpoint("/one-tap/callback", {
28
- method: "POST",
29
- body: oneTapCallbackBodySchema,
30
- metadata: { openapi: {
31
- summary: "One tap callback",
32
- description: "Use this endpoint to authenticate with Google One Tap",
33
- responses: {
34
- 200: {
35
- description: "Successful response",
36
- content: { "application/json": { schema: {
37
- type: "object",
38
- properties: {
39
- session: { $ref: "#/components/schemas/Session" },
40
- user: { $ref: "#/components/schemas/User" }
41
- }
42
- } } }
43
- },
44
- 400: { description: "Invalid token" }
30
+ endpoints: {
31
+ /**
32
+ * ### Endpoint
33
+ *
34
+ * POST `/one-tap/nonce`
35
+ *
36
+ * Issues a single-use nonce to pass to Google before requesting the
37
+ * One Tap ID token. `/one-tap/callback` consumes it and requires the
38
+ * token's `nonce` claim to match.
39
+ */
40
+ oneTapNonce: createAuthEndpoint("/one-tap/nonce", {
41
+ method: "POST",
42
+ metadata: {
43
+ noStore: true,
44
+ openapi: {
45
+ summary: "Issue a One Tap nonce",
46
+ description: "Issue a single-use nonce to bind a Google One Tap ID token to one sign-in",
47
+ responses: issuedNonceResponses
48
+ }
49
+ }
50
+ }, async (ctx) => ctx.json({ nonce: await issueNonce(ctx, ONE_TAP_NONCE_SCOPE) })),
51
+ oneTapCallback: createAuthEndpoint("/one-tap/callback", {
52
+ method: "POST",
53
+ body: oneTapCallbackBodySchema,
54
+ metadata: { openapi: {
55
+ summary: "One tap callback",
56
+ description: "Use this endpoint to authenticate with Google One Tap",
57
+ responses: {
58
+ 200: {
59
+ description: "Successful response",
60
+ content: { "application/json": { schema: {
61
+ type: "object",
62
+ properties: {
63
+ session: { $ref: "#/components/schemas/Session" },
64
+ user: { $ref: "#/components/schemas/User" }
65
+ }
66
+ } } }
67
+ },
68
+ 400: { description: "Invalid token" }
69
+ }
70
+ } }
71
+ }, async (ctx) => {
72
+ const { idToken, nonce } = ctx.body;
73
+ await consumeNonce(ctx, ONE_TAP_NONCE_SCOPE, nonce);
74
+ const googleOAuthProvider = await getAwaitableValue(ctx.context.socialProviders, { value: "google" });
75
+ const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
76
+ const audience = options?.clientId || googleProvider?.clientId;
77
+ if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError$1("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
78
+ const payload = await verifyGoogleIdToken({
79
+ token: idToken,
80
+ audience,
81
+ nonce
82
+ });
83
+ if (!payload) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
84
+ if (!payload.sub) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
85
+ const configuredHostedDomain = googleProvider?.hd;
86
+ if (!isGoogleHostedDomainAllowed(configuredHostedDomain, payload.hd)) {
87
+ ctx.context.logger.error(`Google One Tap sign-in rejected: id token hosted domain (hd) "${payload.hd ?? "<missing>"}" does not satisfy the configured "hd" option "${configuredHostedDomain}".`);
88
+ throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
45
89
  }
46
- } }
47
- }, async (ctx) => {
48
- const { idToken } = ctx.body;
49
- const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
50
- const audience = options?.clientId || googleProvider?.clientId;
51
- if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError$1("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
52
- const payload = await verifyGoogleIdToken({
53
- token: idToken,
54
- audience
55
- });
56
- if (!payload) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
57
- if (!payload.sub) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
58
- const configuredHostedDomain = googleProvider?.hd;
59
- if (!isGoogleHostedDomainAllowed(configuredHostedDomain, payload.hd)) {
60
- ctx.context.logger.error(`Google One Tap sign-in rejected: id token hosted domain (hd) "${payload.hd ?? "<missing>"}" does not satisfy the configured "hd" option "${configuredHostedDomain}".`);
61
- throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
62
- }
63
- const googleOAuthProvider = await getAwaitableValue(ctx.context.socialProviders, { value: "google" });
64
- const providerResult = googleOAuthProvider ? await googleOAuthProvider.getUserInfo({ idToken }) : {
65
- user: {
66
- name: payload.name,
67
- email: payload.email,
68
- image: payload.picture,
69
- emailVerified: toBoolean(payload.email_verified)
70
- },
71
- data: payload
72
- };
73
- if (!providerResult?.user) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
74
- const providerUser = providerResult.user;
75
- const { sub } = payload;
76
- const { email: rawEmail, name, image } = providerUser;
77
- if (typeof rawEmail !== "string" || !rawEmail) throw new APIError$1("BAD_REQUEST", { message: "Email not available in token" });
78
- if (typeof sub !== "string" || !sub) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
79
- const email = rawEmail.toLowerCase();
80
- const emailVerified = toBoolean(providerUser.emailVerified);
81
- const result = await handleOAuthUserInfo(ctx, {
82
- userInfo: {
83
- ...providerUser,
84
- id: sub,
85
- email,
86
- emailVerified,
87
- name: typeof name === "string" ? name : "",
88
- image: typeof image === "string" ? image : void 0
89
- },
90
- account: {
91
- providerId: "google",
92
- accountId: sub,
93
- idToken,
94
- scope: "openid,profile,email"
95
- },
96
- disableSignUp: options?.disableSignup || googleProvider?.disableSignUp,
97
- overrideUserInfo: googleProvider?.overrideUserInfoOnSignIn,
98
- source: {
99
- method: "oauth",
100
- oauth: {
90
+ const providerResult = googleOAuthProvider ? await googleOAuthProvider.getUserInfo({ idToken }) : {
91
+ user: {
92
+ name: payload.name,
93
+ email: payload.email,
94
+ image: payload.picture,
95
+ emailVerified: toBoolean(payload.email_verified)
96
+ },
97
+ data: payload
98
+ };
99
+ if (!providerResult?.user) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
100
+ const providerUser = providerResult.user;
101
+ const { sub } = payload;
102
+ const { email: rawEmail, name, image } = providerUser;
103
+ if (typeof rawEmail !== "string" || !rawEmail) throw new APIError$1("BAD_REQUEST", { message: "Email not available in token" });
104
+ if (typeof sub !== "string" || !sub) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
105
+ const email = rawEmail.toLowerCase();
106
+ const emailVerified = toBoolean(providerUser.emailVerified);
107
+ const result = await handleOAuthUserInfo(ctx, {
108
+ userInfo: {
109
+ ...providerUser,
110
+ id: sub,
111
+ email,
112
+ emailVerified,
113
+ name: typeof name === "string" ? name : "",
114
+ image: typeof image === "string" ? image : void 0
115
+ },
116
+ account: {
101
117
  providerId: "google",
102
- profile: toOAuthProfileRecord(providerResult.data)
118
+ accountId: sub,
119
+ idToken,
120
+ scope: "openid,profile,email"
121
+ },
122
+ disableSignUp: options?.disableSignup || googleProvider?.disableSignUp,
123
+ overrideUserInfo: googleProvider?.overrideUserInfoOnSignIn,
124
+ source: {
125
+ method: "oauth",
126
+ oauth: {
127
+ providerId: "google",
128
+ profile: toOAuthProfileRecord(providerResult.data)
129
+ }
103
130
  }
131
+ });
132
+ if (result.error) {
133
+ if (result.error === OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_VERIFIED) throw APIError$1.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
134
+ throw new APIError$1("UNAUTHORIZED", { message: result.error });
104
135
  }
105
- });
106
- if (result.error) {
107
- if (result.error === OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_VERIFIED) throw APIError$1.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
108
- throw new APIError$1("UNAUTHORIZED", { message: result.error });
109
- }
110
- await setSessionCookie(ctx, result.data);
111
- return ctx.json({
112
- token: result.data.session.token,
113
- user: parseUserOutput(ctx.context.options, result.data.user)
114
- });
115
- }) },
136
+ await setSessionCookie(ctx, result.data);
137
+ return ctx.json({
138
+ token: result.data.session.token,
139
+ user: parseUserOutput(ctx.context.options, result.data.user)
140
+ });
141
+ })
142
+ },
116
143
  options
117
144
  });
118
145
  //#endregion
@@ -274,9 +274,9 @@ const siwe = (options) => {
274
274
  });
275
275
  } catch (error) {
276
276
  if (isAPIError(error)) throw error;
277
+ ctx.context.logger.error("Failed to verify SIWE message", error);
277
278
  throw APIError.fromStatus("UNAUTHORIZED", {
278
279
  message: "Something went wrong. Please try again later.",
279
- error: error instanceof Error ? error.message : "Unknown error",
280
280
  status: 401
281
281
  });
282
282
  }
package/dist/state.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GenericEndpointContext } from "@better-auth/core";
2
2
  import { BetterAuthError } from "@better-auth/core/error";
3
3
  import * as z from "zod";
4
+ import { getAuthStateVerificationIdentifier } from "@better-auth/core/utils/state";
4
5
  //#region src/state.d.ts
5
6
  declare const stateDataSchema: z.ZodObject<{
6
7
  callbackURL: z.ZodString;
@@ -19,10 +20,6 @@ declare const stateDataSchema: z.ZodObject<{
19
20
  serverContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
21
  }, z.core.$loose>;
21
22
  export type StateData = z.infer<typeof stateDataSchema>;
22
- /**
23
- * Verification key for database-backed OAuth and SAML relay state.
24
- */
25
- export declare const getAuthStateVerificationIdentifier: (state: string) => string;
26
23
  export declare function generateGenericState(c: GenericEndpointContext, stateData: StateData, settings?: {
27
24
  cookieName: string;
28
25
  }): Promise<{
@@ -50,4 +47,4 @@ export declare function parseGenericState(c: GenericEndpointContext, state: stri
50
47
  serverContext?: Record<string, unknown> | undefined;
51
48
  }>;
52
49
  //#endregion
53
- export {};
50
+ export { getAuthStateVerificationIdentifier };
package/dist/state.mjs CHANGED
@@ -4,6 +4,7 @@ import { expireCookie } from "./cookies/index.mjs";
4
4
  import { derivePurposeKey } from "./crypto/purpose.mjs";
5
5
  import { BetterAuthError } from "@better-auth/core/error";
6
6
  import * as z from "zod";
7
+ import { getAuthStateVerificationIdentifier } from "@better-auth/core/utils/state";
7
8
  //#region src/state.ts
8
9
  const stateDataSchema = z.looseObject({
9
10
  callbackURL: z.string(),
@@ -37,10 +38,6 @@ const stateDataSchema = z.looseObject({
37
38
  */
38
39
  serverContext: z.record(z.string(), z.unknown()).optional()
39
40
  });
40
- /**
41
- * Verification key for database-backed OAuth and SAML relay state.
42
- */
43
- const getAuthStateVerificationIdentifier = (state) => `auth-state:${state}`;
44
41
  const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
45
42
  var StateError = class extends BetterAuthError {
46
43
  code;
@@ -200,6 +200,14 @@ export declare function getHttpTestInstance<O extends Partial<BetterAuthOptions>
200
200
  readonly code: "ID_TOKEN_NOT_SUPPORTED";
201
201
  message: string;
202
202
  };
203
+ INVALID_ID_TOKEN_NONCE: {
204
+ readonly code: "INVALID_ID_TOKEN_NONCE";
205
+ message: string;
206
+ };
207
+ ID_TOKEN_NONCE_NOT_ENABLED: {
208
+ readonly code: "ID_TOKEN_NONCE_NOT_ENABLED";
209
+ message: string;
210
+ };
203
211
  FAILED_TO_GET_USER_INFO: {
204
212
  readonly code: "FAILED_TO_GET_USER_INFO";
205
213
  message: string;
@@ -1,5 +1,2 @@
1
- //#region src/utils/hide-metadata.d.ts
2
- export declare const HIDE_METADATA: {
3
- readonly scope: "server";
4
- };
5
- //#endregion
1
+ import { HIDE_METADATA } from "@better-auth/core/api";
2
+ export { HIDE_METADATA };
@@ -1,4 +1 @@
1
- //#region src/utils/hide-metadata.ts
2
- const HIDE_METADATA = { scope: "server" };
3
- //#endregion
4
- export { HIDE_METADATA };
1
+ export { HIDE_METADATA } from "@better-auth/core/api";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.8.0-beta.7",
3
+ "version": "1.8.0-beta.8",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -503,13 +503,13 @@
503
503
  "kysely": "^0.28.17 || ^0.29.0",
504
504
  "nanostores": "^1.3.0",
505
505
  "zod": "^4.5.4",
506
- "@better-auth/core": "1.8.0-beta.7",
507
- "@better-auth/drizzle-adapter": "1.8.0-beta.7",
508
- "@better-auth/kysely-adapter": "1.8.0-beta.7",
509
- "@better-auth/memory-adapter": "1.8.0-beta.7",
510
- "@better-auth/mongo-adapter": "1.8.0-beta.7",
511
- "@better-auth/prisma-adapter": "1.8.0-beta.7",
512
- "@better-auth/telemetry": "1.8.0-beta.7"
506
+ "@better-auth/core": "1.8.0-beta.8",
507
+ "@better-auth/drizzle-adapter": "1.8.0-beta.8",
508
+ "@better-auth/kysely-adapter": "1.8.0-beta.8",
509
+ "@better-auth/memory-adapter": "1.8.0-beta.8",
510
+ "@better-auth/mongo-adapter": "1.8.0-beta.8",
511
+ "@better-auth/prisma-adapter": "1.8.0-beta.8",
512
+ "@better-auth/telemetry": "1.8.0-beta.8"
513
513
  },
514
514
  "devDependencies": {
515
515
  "@lynx-js/react": "^0.121.2",