@better-auth/core 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/api/index.d.mts +3 -0
  2. package/dist/context/global.mjs +1 -1
  3. package/dist/context/transaction.mjs +3 -0
  4. package/dist/db/adapter/atomic-fallback.mjs +134 -0
  5. package/dist/db/adapter/factory.mjs +22 -4
  6. package/dist/db/adapter/index.d.mts +15 -11
  7. package/dist/db/get-tables.mjs +1 -9
  8. package/dist/db/index.d.mts +2 -2
  9. package/dist/db/index.mjs +2 -2
  10. package/dist/db/internal.d.mts +3 -1
  11. package/dist/db/internal.mjs +3 -1
  12. package/dist/db/schema/account.d.mts +2 -13
  13. package/dist/db/schema/account.mjs +1 -19
  14. package/dist/db/schema-check.d.mts +48 -0
  15. package/dist/db/schema-check.mjs +80 -0
  16. package/dist/db/schema-diff.d.mts +104 -0
  17. package/dist/db/schema-diff.mjs +154 -0
  18. package/dist/instrumentation/tracer.mjs +1 -1
  19. package/dist/oauth2/index.d.mts +2 -2
  20. package/dist/oauth2/oauth-provider.d.mts +0 -10
  21. package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
  22. package/dist/oauth2/token-endpoint-auth.mjs +11 -0
  23. package/dist/social-providers/apple.d.mts +0 -1
  24. package/dist/social-providers/apple.mjs +0 -1
  25. package/dist/social-providers/cloudflare.d.mts +132 -0
  26. package/dist/social-providers/cloudflare.mjs +85 -0
  27. package/dist/social-providers/cognito.d.mts +0 -1
  28. package/dist/social-providers/cognito.mjs +0 -1
  29. package/dist/social-providers/facebook.d.mts +0 -1
  30. package/dist/social-providers/facebook.mjs +0 -1
  31. package/dist/social-providers/google.d.mts +0 -1
  32. package/dist/social-providers/google.mjs +0 -1
  33. package/dist/social-providers/index.d.mts +53 -21
  34. package/dist/social-providers/index.mjs +3 -1
  35. package/dist/social-providers/line.d.mts +0 -1
  36. package/dist/social-providers/line.mjs +0 -1
  37. package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
  38. package/dist/social-providers/microsoft-entra-id.mjs +0 -1
  39. package/dist/social-providers/paybin.d.mts +0 -1
  40. package/dist/social-providers/paybin.mjs +0 -1
  41. package/dist/social-providers/paypal.d.mts +3 -11
  42. package/dist/social-providers/paypal.mjs +20 -47
  43. package/dist/social-providers/reddit.mjs +17 -22
  44. package/dist/social-providers/tiktok.d.mts +1 -0
  45. package/dist/social-providers/tiktok.mjs +14 -9
  46. package/dist/types/context.d.mts +11 -0
  47. package/dist/types/init-options.d.mts +11 -0
  48. package/dist/utils/ip.mjs +11 -9
  49. package/package.json +2 -2
  50. package/src/context/transaction.ts +5 -0
  51. package/src/db/adapter/atomic-fallback.ts +237 -0
  52. package/src/db/adapter/factory.ts +33 -17
  53. package/src/db/adapter/index.ts +15 -11
  54. package/src/db/get-tables.ts +1 -14
  55. package/src/db/index.ts +0 -2
  56. package/src/db/internal.ts +19 -0
  57. package/src/db/schema/account.ts +3 -22
  58. package/src/db/schema/user.ts +1 -1
  59. package/src/db/schema-check.ts +107 -0
  60. package/src/db/schema-diff.ts +270 -0
  61. package/src/oauth2/index.ts +2 -0
  62. package/src/oauth2/oauth-provider.ts +0 -10
  63. package/src/oauth2/token-endpoint-auth.ts +39 -6
  64. package/src/social-providers/apple.ts +0 -1
  65. package/src/social-providers/cloudflare.ts +221 -0
  66. package/src/social-providers/cognito.ts +0 -1
  67. package/src/social-providers/facebook.ts +0 -1
  68. package/src/social-providers/google.ts +0 -1
  69. package/src/social-providers/index.ts +3 -0
  70. package/src/social-providers/line.ts +0 -1
  71. package/src/social-providers/microsoft-entra-id.ts +0 -1
  72. package/src/social-providers/paybin.ts +0 -1
  73. package/src/social-providers/paypal.ts +30 -71
  74. package/src/social-providers/reddit.ts +27 -36
  75. package/src/social-providers/tiktok.ts +18 -13
  76. package/src/types/context.ts +11 -0
  77. package/src/types/init-options.ts +11 -0
  78. package/src/utils/ip.ts +13 -9
@@ -1,10 +1,17 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
1
  import { betterFetch } from "@better-fetch/fetch";
3
2
  import { decodeJwt } from "jose";
4
3
  import { logger } from "../env";
5
4
  import { BetterAuthError } from "../error";
6
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
7
- import { createAuthorizationURL } from "../oauth2";
5
+ import type {
6
+ OAuthProvider,
7
+ ProviderOptions,
8
+ TokenEndpointAuth,
9
+ } from "../oauth2";
10
+ import {
11
+ createAuthorizationURL,
12
+ refreshAccessToken,
13
+ validateAuthorizationCode,
14
+ } from "../oauth2";
8
15
 
9
16
  export interface PayPalProfile {
10
17
  sub?: string | undefined;
@@ -75,6 +82,13 @@ export const paypal = (options: PayPalOptions) => {
75
82
  const userInfoEndpoint = isSandbox
76
83
  ? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo"
77
84
  : "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
85
+ const tokenRequestOptions = {
86
+ clientId: options.clientId,
87
+ clientSecret: options.clientSecret,
88
+ };
89
+ const tokenEndpointAuth = {
90
+ method: "client_secret_basic",
91
+ } satisfies TokenEndpointAuth;
78
92
 
79
93
  return {
80
94
  id: "paypal",
@@ -115,47 +129,16 @@ export const paypal = (options: PayPalOptions) => {
115
129
  return url;
116
130
  },
117
131
 
118
- validateAuthorizationCode: async ({ code, redirectURI }) => {
119
- /**
120
- * PayPal requires Basic Auth for token exchange
121
- **/
122
-
123
- const credentials = base64.encode(
124
- `${options.clientId}:${options.clientSecret}`,
125
- );
126
-
132
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
127
133
  try {
128
- const response = await betterFetch(tokenEndpoint, {
129
- method: "POST",
130
- headers: {
131
- Authorization: `Basic ${credentials}`,
132
- Accept: "application/json",
133
- "Accept-Language": "en_US",
134
- "Content-Type": "application/x-www-form-urlencoded",
135
- },
136
- body: new URLSearchParams({
137
- grant_type: "authorization_code",
138
- code: code,
139
- redirect_uri: redirectURI,
140
- }).toString(),
134
+ return await validateAuthorizationCode({
135
+ code,
136
+ codeVerifier,
137
+ redirectURI: options.redirectURI || redirectURI,
138
+ options: tokenRequestOptions,
139
+ tokenEndpoint,
140
+ tokenEndpointAuth,
141
141
  });
142
-
143
- if (!response.data) {
144
- throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
145
- }
146
-
147
- const data = response.data as PayPalTokenResponse;
148
-
149
- const result = {
150
- accessToken: data.access_token,
151
- refreshToken: data.refresh_token,
152
- accessTokenExpiresAt: data.expires_in
153
- ? new Date(Date.now() + data.expires_in * 1000)
154
- : undefined,
155
- idToken: data.id_token,
156
- };
157
-
158
- return result;
159
142
  } catch (error) {
160
143
  logger.error("PayPal token exchange failed:", error);
161
144
  throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
@@ -165,37 +148,13 @@ export const paypal = (options: PayPalOptions) => {
165
148
  refreshAccessToken: options.refreshAccessToken
166
149
  ? options.refreshAccessToken
167
150
  : async (refreshToken) => {
168
- const credentials = base64.encode(
169
- `${options.clientId}:${options.clientSecret}`,
170
- );
171
-
172
151
  try {
173
- const response = await betterFetch(tokenEndpoint, {
174
- method: "POST",
175
- headers: {
176
- Authorization: `Basic ${credentials}`,
177
- Accept: "application/json",
178
- "Accept-Language": "en_US",
179
- "Content-Type": "application/x-www-form-urlencoded",
180
- },
181
- body: new URLSearchParams({
182
- grant_type: "refresh_token",
183
- refresh_token: refreshToken,
184
- }).toString(),
152
+ return await refreshAccessToken({
153
+ refreshToken,
154
+ options: tokenRequestOptions,
155
+ tokenEndpoint,
156
+ tokenEndpointAuth,
185
157
  });
186
-
187
- if (!response.data) {
188
- throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
189
- }
190
-
191
- const data = response.data as any;
192
- return {
193
- accessToken: data.access_token,
194
- refreshToken: data.refresh_token,
195
- accessTokenExpiresAt: data.expires_in
196
- ? new Date(Date.now() + data.expires_in * 1000)
197
- : undefined,
198
- };
199
158
  } catch (error) {
200
159
  logger.error("PayPal token refresh failed:", error);
201
160
  throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
@@ -1,10 +1,13 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
1
  import { betterFetch } from "@better-fetch/fetch";
3
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type {
3
+ OAuthProvider,
4
+ ProviderOptions,
5
+ TokenEndpointAuth,
6
+ } from "../oauth2";
4
7
  import {
5
8
  createAuthorizationURL,
6
- getOAuth2Tokens,
7
9
  refreshAccessToken,
10
+ validateAuthorizationCode,
8
11
  } from "../oauth2";
9
12
  import { createPlaceholderEmail } from "../utils/email";
10
13
 
@@ -23,6 +26,15 @@ export interface RedditOptions extends ProviderOptions<RedditProfile> {
23
26
  }
24
27
 
25
28
  export const reddit = (options: RedditOptions) => {
29
+ const tokenEndpoint = "https://www.reddit.com/api/v1/access_token";
30
+ const tokenRequestOptions = {
31
+ clientId: options.clientId,
32
+ clientSecret: options.clientSecret,
33
+ };
34
+ const tokenEndpointAuth = {
35
+ method: "client_secret_basic",
36
+ } satisfies TokenEndpointAuth;
37
+
26
38
  return {
27
39
  id: "reddit",
28
40
  name: "Reddit",
@@ -43,34 +55,17 @@ export const reddit = (options: RedditOptions) => {
43
55
  });
44
56
  },
45
57
  validateAuthorizationCode: async ({ code, redirectURI }) => {
46
- const body = new URLSearchParams({
47
- grant_type: "authorization_code",
58
+ return validateAuthorizationCode({
48
59
  code,
49
- redirect_uri: options.redirectURI || redirectURI,
50
- });
51
- const headers = {
52
- "content-type": "application/x-www-form-urlencoded",
53
- accept: "text/plain",
54
- "user-agent": "better-auth",
55
- Authorization: `Basic ${base64.encode(
56
- `${options.clientId}:${options.clientSecret}`,
57
- )}`,
58
- };
59
-
60
- const { data, error } = await betterFetch<object>(
61
- "https://www.reddit.com/api/v1/access_token",
62
- {
63
- method: "POST",
64
- headers,
65
- body: body.toString(),
60
+ redirectURI: options.redirectURI || redirectURI,
61
+ options: tokenRequestOptions,
62
+ tokenEndpoint,
63
+ tokenEndpointAuth,
64
+ headers: {
65
+ accept: "text/plain",
66
+ "user-agent": "better-auth",
66
67
  },
67
- );
68
-
69
- if (error) {
70
- throw error;
71
- }
72
-
73
- return getOAuth2Tokens(data);
68
+ });
74
69
  },
75
70
 
76
71
  refreshAccessToken: options.refreshAccessToken
@@ -78,13 +73,9 @@ export const reddit = (options: RedditOptions) => {
78
73
  : async (refreshToken) => {
79
74
  return refreshAccessToken({
80
75
  refreshToken,
81
- options: {
82
- clientId: options.clientId,
83
- clientKey: options.clientKey,
84
- clientSecret: options.clientSecret,
85
- },
86
- authentication: "basic",
87
- tokenEndpoint: "https://www.reddit.com/api/v1/access_token",
76
+ options: tokenRequestOptions,
77
+ tokenEndpoint,
78
+ tokenEndpointAuth,
88
79
  });
89
80
  },
90
81
  async getUserInfo(token) {
@@ -1,5 +1,9 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { OAuthProvider, ProviderOptions } from "../oauth2";
2
+ import type {
3
+ OAuthProvider,
4
+ ProviderOptions,
5
+ TokenEndpointAuth,
6
+ } from "../oauth2";
3
7
  import {
4
8
  RESERVED_AUTHORIZATION_PARAMS_SET,
5
9
  refreshAccessToken,
@@ -133,6 +137,13 @@ export interface TiktokOptions extends ProviderOptions<TiktokProfile> {
133
137
 
134
138
  export const tiktok = (options: TiktokOptions) => {
135
139
  const tokenEndpoint = "https://open.tiktokapis.com/v2/oauth/token/";
140
+ const tokenEndpointAuth = {
141
+ method: "custom",
142
+ customizeRequest({ body }) {
143
+ body.set("client_key", options.clientKey);
144
+ body.set("client_secret", options.clientSecret);
145
+ },
146
+ } satisfies TokenEndpointAuth;
136
147
  return {
137
148
  id: "tiktok",
138
149
  name: "TikTok",
@@ -159,15 +170,14 @@ export const tiktok = (options: TiktokOptions) => {
159
170
  return url;
160
171
  },
161
172
 
162
- validateAuthorizationCode: async ({ code, redirectURI }) => {
173
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
163
174
  return validateAuthorizationCode({
164
175
  code,
176
+ codeVerifier,
165
177
  redirectURI: options.redirectURI || redirectURI,
166
- options: {
167
- clientKey: options.clientKey,
168
- clientSecret: options.clientSecret,
169
- },
178
+ options: {},
170
179
  tokenEndpoint,
180
+ tokenEndpointAuth,
171
181
  });
172
182
  },
173
183
  refreshAccessToken: options.refreshAccessToken
@@ -175,14 +185,9 @@ export const tiktok = (options: TiktokOptions) => {
175
185
  : async (refreshToken) => {
176
186
  return refreshAccessToken({
177
187
  refreshToken,
178
- options: {
179
- clientSecret: options.clientSecret,
180
- },
188
+ options: {},
181
189
  tokenEndpoint,
182
- authentication: "post",
183
- extraParams: {
184
- client_key: options.clientKey,
185
- },
190
+ tokenEndpointAuth,
186
191
  });
187
192
  },
188
193
  async getUserInfo(token) {
@@ -10,6 +10,7 @@ import type {
10
10
  } from "../db";
11
11
  import type { DBAdapter, Where } from "../db/adapter";
12
12
  import type { AccountKey } from "../db/schema/account";
13
+ import type { SchemaCheck } from "../db/schema-check";
13
14
  import type { createLogger } from "../env";
14
15
  import type { OAuthProvider } from "../oauth2";
15
16
  import type {
@@ -417,6 +418,16 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
417
418
  "enabled" | "window" | "max" | "storage"
418
419
  >;
419
420
  adapter: DBAdapter<Options>;
421
+ /**
422
+ * Confirms the database can hold what this configuration writes.
423
+ *
424
+ * Shared by initialization and requests for this adapter instance;
425
+ * returns nothing once the schema is known to be clean.
426
+ * Context construction does not await the verdict, so migration
427
+ * tooling can still use a context whose schema needs repair.
428
+ * Absent when the check is disabled or the adapter registers none.
429
+ */
430
+ checkSchema?: SchemaCheck | undefined;
420
431
  internalAdapter: InternalAdapter<Options>;
421
432
  createAuthCookie: CreateCookieGetterFn;
422
433
  secret: string;
@@ -471,6 +471,17 @@ export type BetterAuthAdvancedOptions = {
471
471
  * @default false
472
472
  */
473
473
  joins?: boolean;
474
+ /**
475
+ * Validate the schema during initialization and report problems
476
+ * through the configured logger. Authentication requests await
477
+ * the same check and fail when the schema does not match.
478
+ * Kysely introspects the database; Drizzle and Prisma inspect
479
+ * local schema metadata without opening a connection.
480
+ * Set `false` to disable runtime schema validation.
481
+ *
482
+ * @default true
483
+ */
484
+ validateSchema?: boolean;
474
485
  }
475
486
  | undefined;
476
487
  /**
package/src/utils/ip.ts CHANGED
@@ -2,6 +2,9 @@ import * as z from "zod";
2
2
  import { isDevelopment, isTest } from "../env";
3
3
  import type { BetterAuthOptions } from "../types";
4
4
 
5
+ const ipv4Schema = z.ipv4();
6
+ const ipv6Schema = z.ipv6();
7
+
5
8
  /**
6
9
  * Normalizes an IP address for consistent rate limiting.
7
10
  *
@@ -27,14 +30,15 @@ interface NormalizeIPOptions {
27
30
  * Checks if an IP is valid IPv4 or IPv6
28
31
  */
29
32
  export function isValidIP(ip: string): boolean {
30
- return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
33
+ return isIPv4(ip) || isIPv6(ip);
34
+ }
35
+
36
+ function isIPv4(ip: string): boolean {
37
+ return z.validate(ipv4Schema, ip);
31
38
  }
32
39
 
33
- /**
34
- * Checks if an IP is IPv6
35
- */
36
40
  function isIPv6(ip: string): boolean {
37
- return z.ipv6().safeParse(ip).success;
41
+ return z.validate(ipv6Schema, ip);
38
42
  }
39
43
 
40
44
  /**
@@ -48,7 +52,7 @@ function extractIPv4FromMapped(ipv6: string): string | null {
48
52
  if (lower.startsWith("::ffff:")) {
49
53
  const ipv4Part = lower.substring(7);
50
54
  // Check if it's a valid IPv4
51
- if (z.ipv4().safeParse(ipv4Part).success) {
55
+ if (isIPv4(ipv4Part)) {
52
56
  return ipv4Part;
53
57
  }
54
58
  }
@@ -57,7 +61,7 @@ function extractIPv4FromMapped(ipv6: string): string | null {
57
61
  const parts = ipv6.split(":");
58
62
  if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
59
63
  const ipv4Part = parts[6];
60
- if (ipv4Part && z.ipv4().safeParse(ipv4Part).success) {
64
+ if (ipv4Part && isIPv4(ipv4Part)) {
61
65
  return ipv4Part;
62
66
  }
63
67
  }
@@ -176,7 +180,7 @@ export function normalizeIP(
176
180
  options: NormalizeIPOptions = {},
177
181
  ): string {
178
182
  // IPv4 addresses are already normalized
179
- if (z.ipv4().safeParse(ip).success) {
183
+ if (isIPv4(ip)) {
180
184
  return ip.toLowerCase();
181
185
  }
182
186
 
@@ -201,7 +205,7 @@ export function normalizeIP(
201
205
  * Raw bytes of an IP for CIDR comparison. Returns `null` for an invalid IP.
202
206
  */
203
207
  function ipToBytes(ip: string): Uint8Array | null {
204
- if (z.ipv4().safeParse(ip).success) {
208
+ if (isIPv4(ip)) {
205
209
  return Uint8Array.from(ip.split(".").map((octet) => Number(octet)));
206
210
  }
207
211
  if (!isIPv6(ip)) {