@better-auth/core 1.7.1 → 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 (96) hide show
  1. package/dist/api/index.d.mts +3 -0
  2. package/dist/context/endpoint-context.d.mts +19 -5
  3. package/dist/context/endpoint-context.mjs +35 -16
  4. package/dist/context/global.mjs +5 -2
  5. package/dist/context/index.d.mts +2 -2
  6. package/dist/context/index.mjs +2 -2
  7. package/dist/context/transaction.mjs +3 -0
  8. package/dist/db/adapter/atomic-fallback.mjs +134 -0
  9. package/dist/db/adapter/factory.mjs +22 -4
  10. package/dist/db/adapter/index.d.mts +15 -11
  11. package/dist/db/get-tables.mjs +1 -9
  12. package/dist/db/index.d.mts +2 -2
  13. package/dist/db/index.mjs +2 -2
  14. package/dist/db/internal.d.mts +3 -1
  15. package/dist/db/internal.mjs +3 -1
  16. package/dist/db/schema/account.d.mts +2 -13
  17. package/dist/db/schema/account.mjs +1 -19
  18. package/dist/db/schema-check.d.mts +48 -0
  19. package/dist/db/schema-check.mjs +80 -0
  20. package/dist/db/schema-diff.d.mts +104 -0
  21. package/dist/db/schema-diff.mjs +154 -0
  22. package/dist/env/logger.mjs +16 -1
  23. package/dist/instrumentation/tracer.mjs +1 -1
  24. package/dist/oauth2/index.d.mts +2 -2
  25. package/dist/oauth2/oauth-provider.d.mts +0 -10
  26. package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
  27. package/dist/oauth2/token-endpoint-auth.mjs +11 -0
  28. package/dist/social-providers/apple.d.mts +0 -1
  29. package/dist/social-providers/apple.mjs +0 -1
  30. package/dist/social-providers/cloudflare.d.mts +132 -0
  31. package/dist/social-providers/cloudflare.mjs +85 -0
  32. package/dist/social-providers/cognito.d.mts +0 -1
  33. package/dist/social-providers/cognito.mjs +0 -1
  34. package/dist/social-providers/facebook.d.mts +0 -1
  35. package/dist/social-providers/facebook.mjs +0 -1
  36. package/dist/social-providers/google.d.mts +0 -1
  37. package/dist/social-providers/google.mjs +0 -1
  38. package/dist/social-providers/index.d.mts +53 -21
  39. package/dist/social-providers/index.mjs +3 -1
  40. package/dist/social-providers/line.d.mts +0 -1
  41. package/dist/social-providers/line.mjs +0 -1
  42. package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
  43. package/dist/social-providers/microsoft-entra-id.mjs +0 -1
  44. package/dist/social-providers/paybin.d.mts +0 -1
  45. package/dist/social-providers/paybin.mjs +0 -1
  46. package/dist/social-providers/paypal.d.mts +3 -11
  47. package/dist/social-providers/paypal.mjs +20 -47
  48. package/dist/social-providers/reddit.mjs +22 -23
  49. package/dist/social-providers/roblox.mjs +5 -1
  50. package/dist/social-providers/tiktok.d.mts +1 -0
  51. package/dist/social-providers/tiktok.mjs +19 -10
  52. package/dist/social-providers/twitter.mjs +5 -1
  53. package/dist/social-providers/wechat.mjs +6 -1
  54. package/dist/types/context.d.mts +11 -0
  55. package/dist/types/init-options.d.mts +11 -0
  56. package/dist/utils/ip.mjs +11 -9
  57. package/dist/utils/url.d.mts +10 -1
  58. package/dist/utils/url.mjs +21 -1
  59. package/package.json +3 -3
  60. package/src/context/endpoint-context.ts +46 -21
  61. package/src/context/global.ts +7 -0
  62. package/src/context/index.ts +2 -0
  63. package/src/context/transaction.ts +5 -0
  64. package/src/db/adapter/atomic-fallback.ts +237 -0
  65. package/src/db/adapter/factory.ts +33 -17
  66. package/src/db/adapter/index.ts +15 -11
  67. package/src/db/get-tables.ts +1 -14
  68. package/src/db/index.ts +0 -2
  69. package/src/db/internal.ts +19 -0
  70. package/src/db/schema/account.ts +3 -22
  71. package/src/db/schema/user.ts +1 -1
  72. package/src/db/schema-check.ts +107 -0
  73. package/src/db/schema-diff.ts +270 -0
  74. package/src/env/logger.ts +22 -1
  75. package/src/oauth2/index.ts +2 -0
  76. package/src/oauth2/oauth-provider.ts +0 -10
  77. package/src/oauth2/token-endpoint-auth.ts +39 -6
  78. package/src/social-providers/apple.ts +0 -1
  79. package/src/social-providers/cloudflare.ts +221 -0
  80. package/src/social-providers/cognito.ts +0 -1
  81. package/src/social-providers/facebook.ts +0 -1
  82. package/src/social-providers/google.ts +0 -1
  83. package/src/social-providers/index.ts +3 -0
  84. package/src/social-providers/line.ts +0 -1
  85. package/src/social-providers/microsoft-entra-id.ts +0 -1
  86. package/src/social-providers/paybin.ts +0 -1
  87. package/src/social-providers/paypal.ts +30 -71
  88. package/src/social-providers/reddit.ts +34 -37
  89. package/src/social-providers/roblox.ts +5 -3
  90. package/src/social-providers/tiktok.ts +25 -14
  91. package/src/social-providers/twitter.ts +8 -2
  92. package/src/social-providers/wechat.ts +6 -6
  93. package/src/types/context.ts +11 -0
  94. package/src/types/init-options.ts +11 -0
  95. package/src/utils/ip.ts +13 -9
  96. package/src/utils/url.ts +43 -0
@@ -1,10 +1,15 @@
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,
6
10
  validateAuthorizationCode,
7
11
  } from "../oauth2";
12
+ import { createPlaceholderEmail } from "../utils/email";
8
13
 
9
14
  /**
10
15
  * [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)
@@ -132,6 +137,13 @@ export interface TiktokOptions extends ProviderOptions<TiktokProfile> {
132
137
 
133
138
  export const tiktok = (options: TiktokOptions) => {
134
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;
135
147
  return {
136
148
  id: "tiktok",
137
149
  name: "TikTok",
@@ -158,15 +170,14 @@ export const tiktok = (options: TiktokOptions) => {
158
170
  return url;
159
171
  },
160
172
 
161
- validateAuthorizationCode: async ({ code, redirectURI }) => {
173
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
162
174
  return validateAuthorizationCode({
163
175
  code,
176
+ codeVerifier,
164
177
  redirectURI: options.redirectURI || redirectURI,
165
- options: {
166
- clientKey: options.clientKey,
167
- clientSecret: options.clientSecret,
168
- },
178
+ options: {},
169
179
  tokenEndpoint,
180
+ tokenEndpointAuth,
170
181
  });
171
182
  },
172
183
  refreshAccessToken: options.refreshAccessToken
@@ -174,14 +185,9 @@ export const tiktok = (options: TiktokOptions) => {
174
185
  : async (refreshToken) => {
175
186
  return refreshAccessToken({
176
187
  refreshToken,
177
- options: {
178
- clientSecret: options.clientSecret,
179
- },
188
+ options: {},
180
189
  tokenEndpoint,
181
- authentication: "post",
182
- extraParams: {
183
- client_key: options.clientKey,
184
- },
190
+ tokenEndpointAuth,
185
191
  });
186
192
  },
187
193
  async getUserInfo(token) {
@@ -210,7 +216,12 @@ export const tiktok = (options: TiktokOptions) => {
210
216
 
211
217
  return {
212
218
  user: {
213
- email: profile.data.user.email || profile.data.user.username,
219
+ email:
220
+ profile.data.user.email ||
221
+ createPlaceholderEmail({
222
+ identifier: profile.data.user.open_id,
223
+ namespace: "tiktok",
224
+ }),
214
225
  name:
215
226
  profile.data.user.display_name || profile.data.user.username || "",
216
227
  image: profile.data.user.avatar_large_url,
@@ -5,6 +5,7 @@ import {
5
5
  refreshAccessToken,
6
6
  validateAuthorizationCode,
7
7
  } from "../oauth2";
8
+ import { createPlaceholderEmail } from "../utils/email";
8
9
 
9
10
  export interface TwitterProfile {
10
11
  data: {
@@ -187,9 +188,14 @@ export const twitter = (options: TwitterOption) => {
187
188
  return {
188
189
  user: {
189
190
  name: profile.data.name,
190
- email: profile.data.email || profile.data.username || null,
191
+ email:
192
+ profile.data.email ||
193
+ createPlaceholderEmail({
194
+ identifier: profile.data.id,
195
+ namespace: "twitter",
196
+ }),
191
197
  image: profile.data.profile_image_url,
192
- emailVerified: emailVerified,
198
+ emailVerified,
193
199
  ...userMap,
194
200
  },
195
201
  data: profile,
@@ -1,6 +1,7 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import type { OAuth2Tokens, OAuthProvider, ProviderOptions } from "../oauth2";
3
3
  import { RESERVED_AUTHORIZATION_PARAMS_SET } from "../oauth2";
4
+ import { createPlaceholderEmail } from "../utils/email";
4
5
 
5
6
  /**
6
7
  * WeChat user profile information
@@ -205,17 +206,16 @@ export const wechat = (options: WeChatOptions) => {
205
206
  }
206
207
 
207
208
  const userMap = await options.mapProfileToUser?.(profile);
209
+ const userId = profile.unionid || profile.openid || openid;
208
210
  return {
209
211
  user: {
210
212
  name: profile.nickname,
211
- // WeChat does not return an email, and the OAuth callback rejects a
212
- // missing one, so the default sign-in would always fail. Synthesize a
213
- // stable, non-routable placeholder (RFC 2606 `.invalid`) keyed to the
214
- // user's WeChat id, left unverified. Applications that collect a real
215
- // email override it via `mapProfileToUser`.
216
213
  email:
217
214
  profile.email ||
218
- `${profile.unionid || profile.openid || openid}@wechat.invalid`,
215
+ createPlaceholderEmail({
216
+ identifier: userId,
217
+ namespace: "wechat",
218
+ }),
219
219
  image: profile.headimgurl,
220
220
  emailVerified: false,
221
221
  ...userMap,
@@ -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)) {
package/src/utils/url.ts CHANGED
@@ -48,6 +48,49 @@ export function normalizePathname(
48
48
  return pathname;
49
49
  }
50
50
 
51
+ const URL_REFERENCE_ORIGIN = "https://better-auth.invalid";
52
+
53
+ /**
54
+ * Appends query parameters before the fragment of an absolute or root-relative URL.
55
+ * Existing query text is retained without parsing it into name-value pairs.
56
+ *
57
+ * This function only composes URLs. Callers must validate untrusted input.
58
+ *
59
+ * @throws TypeError if parsing fails or a relative input changes authority.
60
+ */
61
+ export function appendQueryParams(
62
+ input: string,
63
+ params: URLSearchParams,
64
+ ): string {
65
+ const relative = input.startsWith("/");
66
+ const hasAuthorityPrefix = input.startsWith("//") || input.startsWith("/\\");
67
+ if (hasAuthorityPrefix) {
68
+ throw new TypeError("Expected an absolute or root-relative URL");
69
+ }
70
+
71
+ const parsedURL = relative
72
+ ? new URL(input, URL_REFERENCE_ORIGIN)
73
+ : new URL(input);
74
+
75
+ if (relative && parsedURL.origin !== URL_REFERENCE_ORIGIN) {
76
+ throw new TypeError("Expected an absolute or root-relative URL");
77
+ }
78
+
79
+ const query = params.toString();
80
+ if (!query) {
81
+ return input;
82
+ }
83
+
84
+ const separator = parsedURL.search.endsWith("&") ? "" : "&";
85
+ parsedURL.search = parsedURL.search
86
+ ? `${parsedURL.search}${separator}${query}`
87
+ : query;
88
+
89
+ return relative
90
+ ? parsedURL.href.slice(parsedURL.origin.length)
91
+ : parsedURL.href;
92
+ }
93
+
51
94
  /**
52
95
  * Schemes that execute or embed code when navigated to or accepted as a
53
96
  * redirect target. These are never safe as an OAuth `redirect_uri` or as a