better-auth 1.8.0-beta.6 → 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.
Files changed (33) hide show
  1. package/dist/api/index.d.mts +67 -1
  2. package/dist/api/index.mjs +4 -1
  3. package/dist/api/routes/account.mjs +2 -0
  4. package/dist/api/routes/id-token-nonce.d.mts +85 -0
  5. package/dist/api/routes/id-token-nonce.mjs +80 -0
  6. package/dist/api/routes/index.d.mts +2 -1
  7. package/dist/api/routes/sign-in.mjs +2 -0
  8. package/dist/client/proxy.mjs +4 -11
  9. package/dist/db/get-migration.mjs +9 -11
  10. package/dist/db/internal-adapter.mjs +37 -28
  11. package/dist/db/schema.mjs +17 -8
  12. package/dist/package.mjs +1 -1
  13. package/dist/plugins/admin/routes.mjs +13 -11
  14. package/dist/plugins/device-authorization/index.d.mts +1 -0
  15. package/dist/plugins/device-authorization/routes.mjs +18 -1
  16. package/dist/plugins/generic-oauth/index.d.mts +1 -4
  17. package/dist/plugins/generic-oauth/index.mjs +39 -23
  18. package/dist/plugins/generic-oauth/types.d.mts +10 -2
  19. package/dist/plugins/one-tap/client.d.mts +4 -0
  20. package/dist/plugins/one-tap/client.mjs +90 -77
  21. package/dist/plugins/one-tap/index.d.mts +40 -0
  22. package/dist/plugins/one-tap/index.mjs +112 -85
  23. package/dist/plugins/open-api/generator.mjs +2 -1
  24. package/dist/plugins/organization/adapter.mjs +4 -0
  25. package/dist/plugins/organization/routes/crud-invites.mjs +1 -1
  26. package/dist/plugins/siwe/index.mjs +41 -24
  27. package/dist/plugins/username/index.mjs +3 -2
  28. package/dist/state.d.mts +2 -5
  29. package/dist/state.mjs +2 -6
  30. package/dist/test-utils/http-test-instance.d.mts +8 -0
  31. package/dist/utils/hide-metadata.d.mts +2 -5
  32. package/dist/utils/hide-metadata.mjs +1 -4
  33. package/package.json +8 -8
@@ -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,14 +1,7 @@
1
1
  import { isAtom } from "../utils/is-atom.mjs";
2
2
  import { toKebabCase } from "@better-auth/core/utils/string";
3
+ import { isPayloadMethod } from "@better-fetch/fetch";
3
4
  //#region src/client/proxy.ts
4
- function getMethod(path, knownPathMethods, args) {
5
- const method = knownPathMethods[path];
6
- const { fetchOptions, query: _query, ...body } = args || {};
7
- if (method) return method;
8
- if (fetchOptions?.method) return fetchOptions.method;
9
- if (body && Object.keys(body).length > 0) return "POST";
10
- return "GET";
11
- }
12
5
  function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomListeners) {
13
6
  function createProxy(path = []) {
14
7
  return new Proxy(function() {}, {
@@ -35,13 +28,13 @@ function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomLis
35
28
  ...fetchOptions,
36
29
  ...argFetchOptions
37
30
  };
38
- const method = getMethod(routePath, knownPathMethods, arg);
31
+ const method = knownPathMethods[routePath] || options.method || (Object.keys(body).length > 0 ? "POST" : "GET");
39
32
  return await client(routePath, {
40
33
  ...options,
41
- body: method === "GET" ? void 0 : {
34
+ body: isPayloadMethod(method) ? {
42
35
  ...body,
43
36
  ...options?.body || {}
44
- },
37
+ } : void 0,
45
38
  query: query || options?.query,
46
39
  method,
47
40
  async onSuccess(context) {
@@ -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 = [];
@@ -79,6 +79,16 @@ const createInternalAdapter = (adapter, ctx) => {
79
79
  const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
80
80
  return activeSessions ? safeJSONParse(activeSessions) || [] : [];
81
81
  }
82
+ async function setActiveSessionReferences(userId, sessionReferences, now) {
83
+ if (!secondaryStorage) return;
84
+ const sorted = [...sessionReferences].sort((a, b) => a.expiresAt - b.expiresAt);
85
+ const ttl = getTTLSeconds(sorted.at(-1)?.expiresAt ?? 0, now);
86
+ if (ttl > 0) {
87
+ await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), ttl);
88
+ return;
89
+ }
90
+ await secondaryStorage.delete(`active-sessions-${userId}`);
91
+ }
82
92
  const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
83
93
  const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
84
94
  async function isRevokedSession(session, revokedBeforeByUser) {
@@ -93,17 +103,9 @@ const createInternalAdapter = (adapter, ctx) => {
93
103
  await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
94
104
  const deletedTokens = new Set(sessionReferences.map((session) => session.token));
95
105
  for (const token of deletedTokens) await secondaryStorage.delete(token);
96
- const activeSessionsKey = `active-sessions-${userId}`;
97
106
  const currentSessionReferences = await getActiveSessionReferences(userId);
98
107
  const now = Date.now();
99
- const remainingSessionReferences = currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token));
100
- remainingSessionReferences.sort((a, b) => a.expiresAt - b.expiresAt);
101
- const furthestExpiration = remainingSessionReferences.at(-1)?.expiresAt;
102
- if (furthestExpiration) {
103
- await secondaryStorage.set(activeSessionsKey, JSON.stringify(remainingSessionReferences), getTTLSeconds(furthestExpiration, now));
104
- return;
105
- }
106
- await secondaryStorage.delete(activeSessionsKey);
108
+ await setActiveSessionReferences(userId, currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token)), now);
107
109
  }
108
110
  async function queueCachedUserSessionDeletion(userId, sessionReferences) {
109
111
  if (!secondaryStorage) return;
@@ -129,6 +131,20 @@ const createInternalAdapter = (adapter, ctx) => {
129
131
  if (verificationConsumeLocks.get(key) === next) verificationConsumeLocks.delete(key);
130
132
  }
131
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
+ }
132
148
  return {
133
149
  createOAuthUser: async (user, account) => {
134
150
  return runWithTransaction(adapter, async () => {
@@ -310,12 +326,10 @@ const createInternalAdapter = (adapter, ctx) => {
310
326
  list = safeJSONParse(currentList) || [];
311
327
  list = list.filter((session) => session.expiresAt > now && session.token !== data.token);
312
328
  }
313
- const sorted = [...list, {
329
+ await setActiveSessionReferences(userId, [...list, {
314
330
  token: data.token,
315
331
  expiresAt: data.expiresAt.getTime()
316
- }].sort((a, b) => a.expiresAt - b.expiresAt);
317
- const furthestSessionTTL = getTTLSeconds(sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime(), now);
318
- if (furthestSessionTTL > 0) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), furthestSessionTTL);
332
+ }], now);
319
333
  const user = await (await getCurrentAdapter(adapter)).findOne({
320
334
  model: "user",
321
335
  where: [{
@@ -467,13 +481,11 @@ const createInternalAdapter = (adapter, ctx) => {
467
481
  }), sessionTTL);
468
482
  const listKey = `active-sessions-${updatedSession.userId}`;
469
483
  const listRaw = await secondaryStorage.get(listKey);
470
- const sorted = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now).concat([{
484
+ const filtered = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now).concat([{
471
485
  token: sessionToken,
472
486
  expiresAt: expiresMs
473
- }]).sort((a, b) => a.expiresAt - b.expiresAt);
474
- const furthestSessionExp = sorted.at(-1)?.expiresAt;
475
- if (furthestSessionExp && furthestSessionExp > now) await secondaryStorage.set(listKey, JSON.stringify(sorted), getTTLSeconds(furthestSessionExp, now));
476
- else await secondaryStorage.delete(listKey);
487
+ }]);
488
+ await setActiveSessionReferences(updatedSession.userId, filtered, now);
477
489
  }
478
490
  return updatedSession;
479
491
  },
@@ -494,10 +506,7 @@ const createInternalAdapter = (adapter, ctx) => {
494
506
  if (currentList) {
495
507
  const list = safeJSONParse(currentList) || [];
496
508
  const now = Date.now();
497
- const filtered = list.filter((session) => session.expiresAt > now && session.token !== token);
498
- const furthestSessionExp = filtered.sort((a, b) => a.expiresAt - b.expiresAt).at(-1)?.expiresAt;
499
- if (filtered.length > 0 && furthestSessionExp && furthestSessionExp > Date.now()) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(filtered), getTTLSeconds(furthestSessionExp, now));
500
- else await secondaryStorage.delete(`active-sessions-${userId}`);
509
+ await setActiveSessionReferences(userId, list.filter((session) => session.expiresAt > now && session.token !== token), now);
501
510
  } else logger.error("Active sessions list not found in secondary storage");
502
511
  }
503
512
  await secondaryStorage.delete(token);
@@ -800,11 +809,7 @@ const createInternalAdapter = (adapter, ctx) => {
800
809
  }
801
810
  let verification = await findByIdentifier(storedIdentifier);
802
811
  if (!verification.length && storageOption && storageOption !== "plain") verification = await findByIdentifier(identifier);
803
- if (!options.verification?.disableCleanup) await deleteManyWithHooks([{
804
- field: "expiresAt",
805
- value: /* @__PURE__ */ new Date(),
806
- operator: "lt"
807
- }], "verification", void 0);
812
+ await deleteExpiredVerifications();
808
813
  return verification[0] || null;
809
814
  },
810
815
  deleteVerificationByIdentifier: async (identifier) => {
@@ -903,6 +908,7 @@ const createInternalAdapter = (adapter, ctx) => {
903
908
  if (consumed) break;
904
909
  }
905
910
  if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
911
+ if (consumed) await deleteExpiredVerifications();
906
912
  }
907
913
  if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
908
914
  return consumed;
@@ -935,7 +941,10 @@ const createInternalAdapter = (adapter, ctx) => {
935
941
  * is best-effort there (it is intended for development and tests).
936
942
  */
937
943
  reserveVerificationValue: async (data) => {
938
- 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
+ });
939
948
  const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier);
940
949
  const storedIdentifier = await processIdentifier(data.identifier, storageOption);
941
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.");
@@ -149,16 +149,25 @@ function getSessionDefaultFields(options) {
149
149
  }
150
150
  function mergeSchema(schema, newSchema) {
151
151
  if (!newSchema) return schema;
152
+ const merged = { ...schema };
152
153
  for (const table in newSchema) {
153
- const newModelName = newSchema[table]?.modelName;
154
- if (newModelName) schema[table].modelName = newModelName;
155
- for (const field in schema[table].fields) {
156
- const newField = newSchema[table]?.fields?.[field];
157
- if (!newField) continue;
158
- schema[table].fields[field].fieldName = newField;
159
- }
154
+ const base = schema[table];
155
+ const override = newSchema[table];
156
+ if (!base || !override) continue;
157
+ const fields = Object.fromEntries(Object.entries(base.fields).map(([key, field]) => {
158
+ const fieldName = override.fields?.[key];
159
+ return [key, fieldName ? {
160
+ ...field,
161
+ fieldName
162
+ } : field];
163
+ }));
164
+ merged[table] = {
165
+ ...base,
166
+ ...override.modelName ? { modelName: override.modelName } : {},
167
+ fields
168
+ };
160
169
  }
161
- return schema;
170
+ return merged;
162
171
  }
163
172
  //#endregion
164
173
  export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.8.0-beta.6";
2
+ var version = "1.8.0-beta.8";
3
3
  //#endregion
4
4
  export { version };