@better-auth/core 1.7.1 → 1.7.2

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.
@@ -6,13 +6,27 @@ import { AsyncLocalStorage } from "@better-auth/core/async_hooks";
6
6
  type AuthEndpointContext = Partial<InputContext<string, any> & EndpointContext<string, any>> & {
7
7
  context: AuthContext;
8
8
  };
9
+ type AuthEndpointContextStorage = AsyncLocalStorage<AuthEndpointContext>;
9
10
  /**
10
- * This is for internal use only. Most users should use `getCurrentAuthContext` instead.
11
+ * @deprecated Use `getCurrentAuthEndpointContext`,
12
+ * `tryGetCurrentAuthEndpointContext`, or `runWithEndpointContext` instead.
13
+ */
14
+ declare function getCurrentAuthContextAsyncLocalStorage(): Promise<AuthEndpointContextStorage>;
15
+ /**
16
+ * Returns the current auth endpoint context, or `undefined` when called outside
17
+ * of `runWithEndpointContext`.
18
+ */
19
+ declare function tryGetCurrentAuthEndpointContext(): AuthEndpointContext | undefined;
20
+ /**
21
+ * Returns the current auth endpoint context.
11
22
  *
12
- * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
23
+ * @throws When called outside of `runWithEndpointContext`.
24
+ */
25
+ declare function getCurrentAuthEndpointContext(): AuthEndpointContext;
26
+ /**
27
+ * @deprecated Use `getCurrentAuthEndpointContext` instead.
13
28
  */
14
- declare function getCurrentAuthContextAsyncLocalStorage(): Promise<AsyncLocalStorage<AuthEndpointContext>>;
15
29
  declare function getCurrentAuthContext(): Promise<AuthEndpointContext>;
16
- declare function runWithEndpointContext<T>(context: AuthEndpointContext, fn: () => T): Promise<T>;
30
+ declare function runWithEndpointContext<T>(authEndpointContext: AuthEndpointContext, fn: () => T): Promise<T>;
17
31
  //#endregion
18
- export { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext };
32
+ export { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext };
@@ -1,29 +1,48 @@
1
- import { __getBetterAuthGlobal } from "./global.mjs";
1
+ import { __getBetterAuthGlobal, __getCurrentEndpointContext } from "./global.mjs";
2
2
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
3
  //#region src/context/endpoint-context.ts
4
- const ensureAsyncStorage = async () => {
5
- const betterAuthGlobal = __getBetterAuthGlobal();
6
- const existing = betterAuthGlobal.context.endpointContextAsyncStorage;
4
+ const getExistingEndpointContextStorage = () => {
5
+ return __getBetterAuthGlobal().context.endpointContextAsyncStorage;
6
+ };
7
+ const getOrCreateEndpointContextStorage = async () => {
8
+ const existing = getExistingEndpointContextStorage();
7
9
  if (existing) return existing;
8
10
  const AsyncLocalStorage = await getAsyncLocalStorage();
9
- betterAuthGlobal.context.endpointContextAsyncStorage ??= new AsyncLocalStorage();
10
- return betterAuthGlobal.context.endpointContextAsyncStorage;
11
+ const globalContext = __getBetterAuthGlobal().context;
12
+ return globalContext.endpointContextAsyncStorage ??= new AsyncLocalStorage();
11
13
  };
12
14
  /**
13
- * This is for internal use only. Most users should use `getCurrentAuthContext` instead.
14
- *
15
- * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
15
+ * @deprecated Use `getCurrentAuthEndpointContext`,
16
+ * `tryGetCurrentAuthEndpointContext`, or `runWithEndpointContext` instead.
16
17
  */
17
18
  async function getCurrentAuthContextAsyncLocalStorage() {
18
- return ensureAsyncStorage();
19
+ return getOrCreateEndpointContextStorage();
20
+ }
21
+ /**
22
+ * Returns the current auth endpoint context, or `undefined` when called outside
23
+ * of `runWithEndpointContext`.
24
+ */
25
+ function tryGetCurrentAuthEndpointContext() {
26
+ return __getCurrentEndpointContext();
19
27
  }
28
+ /**
29
+ * Returns the current auth endpoint context.
30
+ *
31
+ * @throws When called outside of `runWithEndpointContext`.
32
+ */
33
+ function getCurrentAuthEndpointContext() {
34
+ const authEndpointContext = tryGetCurrentAuthEndpointContext();
35
+ if (!authEndpointContext) throw new Error("No auth context found. Please make sure you are calling this function within a `runWithEndpointContext` callback.");
36
+ return authEndpointContext;
37
+ }
38
+ /**
39
+ * @deprecated Use `getCurrentAuthEndpointContext` instead.
40
+ */
20
41
  async function getCurrentAuthContext() {
21
- const context = (await ensureAsyncStorage()).getStore();
22
- if (!context) throw new Error("No auth context found. Please make sure you are calling this function within a `runWithEndpointContext` callback.");
23
- return context;
42
+ return getCurrentAuthEndpointContext();
24
43
  }
25
- async function runWithEndpointContext(context, fn) {
26
- return (await ensureAsyncStorage()).run(context, fn);
44
+ async function runWithEndpointContext(authEndpointContext, fn) {
45
+ return (await getOrCreateEndpointContextStorage()).run(authEndpointContext, fn);
27
46
  }
28
47
  //#endregion
29
- export { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext };
48
+ export { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext };
@@ -2,7 +2,7 @@
2
2
  const symbol = Symbol.for("better-auth:global");
3
3
  let bind = null;
4
4
  const __context = {};
5
- const __betterAuthVersion = "1.7.1";
5
+ const __betterAuthVersion = "1.7.2";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -29,8 +29,11 @@ function __getBetterAuthGlobal() {
29
29
  }
30
30
  return globalThis[symbol];
31
31
  }
32
+ function __getCurrentEndpointContext() {
33
+ return __getBetterAuthGlobal().context.endpointContextAsyncStorage?.getStore();
34
+ }
32
35
  function getBetterAuthVersion() {
33
36
  return __getBetterAuthGlobal().version;
34
37
  }
35
38
  //#endregion
36
- export { __getBetterAuthGlobal, getBetterAuthVersion };
39
+ export { __getBetterAuthGlobal, __getCurrentEndpointContext, getBetterAuthVersion };
@@ -1,5 +1,5 @@
1
- import { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext } from "./endpoint-context.mjs";
1
+ import { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext } from "./endpoint-context.mjs";
2
2
  import { getBetterAuthVersion } from "./global.mjs";
3
3
  import { RequestState, RequestStateWeakMap, defineRequestState, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, runWithRequestState } from "./request-state.mjs";
4
4
  import { getCurrentAdapter, getCurrentDBAdapterAsyncLocalStorage, queueAfterTransactionHook, runWithAdapter, runWithTransaction } from "./transaction.mjs";
5
- export { type AuthEndpointContext, type RequestState, type RequestStateWeakMap, defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction };
5
+ export { type AuthEndpointContext, type RequestState, type RequestStateWeakMap, defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction, tryGetCurrentAuthEndpointContext };
@@ -1,5 +1,5 @@
1
1
  import { getBetterAuthVersion } from "./global.mjs";
2
- import { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext } from "./endpoint-context.mjs";
2
+ import { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext } from "./endpoint-context.mjs";
3
3
  import { defineRequestState, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, runWithRequestState } from "./request-state.mjs";
4
4
  import { getCurrentAdapter, getCurrentDBAdapterAsyncLocalStorage, queueAfterTransactionHook, runWithAdapter, runWithTransaction } from "./transaction.mjs";
5
- export { defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction };
5
+ export { defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction, tryGetCurrentAuthEndpointContext };
@@ -1,3 +1,4 @@
1
+ import { __getCurrentEndpointContext } from "../context/global.mjs";
1
2
  import { getColorDepth } from "./color-depth.mjs";
2
3
  //#region src/env/logger.ts
3
4
  const TTY_COLORS = {
@@ -74,6 +75,20 @@ const createLogger = (options) => {
74
75
  }
75
76
  };
76
77
  };
77
- const logger = createLogger();
78
+ const defaultLogger = createLogger();
79
+ const getCurrentLogger = () => {
80
+ const currentLogger = __getCurrentEndpointContext()?.context.logger;
81
+ return currentLogger && currentLogger !== logger ? currentLogger : defaultLogger;
82
+ };
83
+ const logger = {
84
+ debug: (...params) => getCurrentLogger().debug(...params),
85
+ info: (...params) => getCurrentLogger().info(...params),
86
+ success: (...params) => getCurrentLogger().success(...params),
87
+ warn: (...params) => getCurrentLogger().warn(...params),
88
+ error: (...params) => getCurrentLogger().error(...params),
89
+ get level() {
90
+ return getCurrentLogger().level;
91
+ }
92
+ };
78
93
  //#endregion
79
94
  export { TTY_COLORS, createLogger, levels, logger, shouldPublishLog };
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
2
2
  import { getOpenTelemetryAPI } from "./api.mjs";
3
3
  //#region src/instrumentation/tracer.ts
4
4
  const INSTRUMENTATION_SCOPE = "better-auth";
5
- const INSTRUMENTATION_VERSION = "1.7.1";
5
+ const INSTRUMENTATION_VERSION = "1.7.2";
6
6
  /**
7
7
  * Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
8
8
  * callbacks). These are APIErrors with 3xx status codes and should not be
@@ -1,6 +1,7 @@
1
1
  import { getOAuth2Tokens } from "../oauth2/utils.mjs";
2
2
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
3
3
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
4
+ import { createPlaceholderEmail } from "../utils/email.mjs";
4
5
  import { base64 } from "@better-auth/utils/base64";
5
6
  import { betterFetch } from "@better-fetch/fetch";
6
7
  //#region src/social-providers/reddit.ts
@@ -63,7 +64,10 @@ const reddit = (options) => {
63
64
  } });
64
65
  if (error) return null;
65
66
  const userMap = await options.mapProfileToUser?.(profile);
66
- const email = userMap?.email || `${profile.id}@reddit.invalid`;
67
+ const email = userMap?.email || createPlaceholderEmail({
68
+ identifier: profile.id,
69
+ namespace: "reddit"
70
+ });
67
71
  return {
68
72
  user: {
69
73
  name: profile.name,
@@ -1,6 +1,7 @@
1
1
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
2
2
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
3
3
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
4
+ import { createPlaceholderEmail } from "../utils/email.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
5
6
  //#region src/social-providers/roblox.ts
6
7
  const roblox = (options) => {
@@ -53,7 +54,10 @@ const roblox = (options) => {
53
54
  user: {
54
55
  name: profile.nickname || profile.preferred_username || "",
55
56
  image: profile.picture,
56
- email: profile.preferred_username || null,
57
+ email: createPlaceholderEmail({
58
+ identifier: profile.sub,
59
+ namespace: "roblox"
60
+ }),
57
61
  emailVerified: false,
58
62
  ...userMap
59
63
  },
@@ -1,6 +1,7 @@
1
1
  import { RESERVED_AUTHORIZATION_PARAMS_SET } from "../oauth2/create-authorization-url.mjs";
2
2
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
3
3
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
4
+ import { createPlaceholderEmail } from "../utils/email.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
5
6
  //#region src/social-providers/tiktok.ts
6
7
  const tiktok = (options) => {
@@ -57,7 +58,10 @@ const tiktok = (options) => {
57
58
  if (error) return null;
58
59
  return {
59
60
  user: {
60
- email: profile.data.user.email || profile.data.user.username,
61
+ email: profile.data.user.email || createPlaceholderEmail({
62
+ identifier: profile.data.user.open_id,
63
+ namespace: "tiktok"
64
+ }),
61
65
  name: profile.data.user.display_name || profile.data.user.username || "",
62
66
  image: profile.data.user.avatar_large_url,
63
67
  emailVerified: false
@@ -1,6 +1,7 @@
1
1
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
2
2
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
3
3
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
4
+ import { createPlaceholderEmail } from "../utils/email.mjs";
4
5
  import { betterFetch } from "@better-fetch/fetch";
5
6
  //#region src/social-providers/twitter.ts
6
7
  const twitter = (options) => {
@@ -71,7 +72,10 @@ const twitter = (options) => {
71
72
  return {
72
73
  user: {
73
74
  name: profile.data.name,
74
- email: profile.data.email || profile.data.username || null,
75
+ email: profile.data.email || createPlaceholderEmail({
76
+ identifier: profile.data.id,
77
+ namespace: "twitter"
78
+ }),
75
79
  image: profile.data.profile_image_url,
76
80
  emailVerified,
77
81
  ...userMap
@@ -1,4 +1,5 @@
1
1
  import { RESERVED_AUTHORIZATION_PARAMS_SET } from "../oauth2/create-authorization-url.mjs";
2
+ import { createPlaceholderEmail } from "../utils/email.mjs";
2
3
  import { betterFetch } from "@better-fetch/fetch";
3
4
  //#region src/social-providers/wechat.ts
4
5
  const wechat = (options) => {
@@ -69,10 +70,14 @@ const wechat = (options) => {
69
70
  }).toString(), { method: "GET" });
70
71
  if (error || !profile || profile.errcode) return null;
71
72
  const userMap = await options.mapProfileToUser?.(profile);
73
+ const userId = profile.unionid || profile.openid || openid;
72
74
  return {
73
75
  user: {
74
76
  name: profile.nickname,
75
- email: profile.email || `${profile.unionid || profile.openid || openid}@wechat.invalid`,
77
+ email: profile.email || createPlaceholderEmail({
78
+ identifier: userId,
79
+ namespace: "wechat"
80
+ }),
76
81
  image: profile.headimgurl,
77
82
  emailVerified: false,
78
83
  ...userMap
@@ -16,6 +16,15 @@
16
16
  * // Returns: "/sso/saml2/callback/provider1"
17
17
  */
18
18
  declare function normalizePathname(requestUrl: string, basePath: string): string;
19
+ /**
20
+ * Appends query parameters before the fragment of an absolute or root-relative URL.
21
+ * Existing query text is retained without parsing it into name-value pairs.
22
+ *
23
+ * This function only composes URLs. Callers must validate untrusted input.
24
+ *
25
+ * @throws TypeError if parsing fails or a relative input changes authority.
26
+ */
27
+ declare function appendQueryParams(input: string, params: URLSearchParams): string;
19
28
  /**
20
29
  * Schemes that execute or embed code when navigated to or accepted as a
21
30
  * redirect target. These are never safe as an OAuth `redirect_uri` or as a
@@ -34,4 +43,4 @@ declare const DANGEROUS_URL_SCHEMES: string[];
34
43
  */
35
44
  declare function isSafeUrlScheme(value: string): boolean;
36
45
  //#endregion
37
- export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
46
+ export { DANGEROUS_URL_SCHEMES, appendQueryParams, isSafeUrlScheme, normalizePathname };
@@ -28,6 +28,26 @@ function normalizePathname(requestUrl, basePath) {
28
28
  if (pathname.startsWith(normalizedBasePath + "/")) return pathname.slice(normalizedBasePath.length).replace(/\/+$/, "") || "/";
29
29
  return pathname;
30
30
  }
31
+ const URL_REFERENCE_ORIGIN = "https://better-auth.invalid";
32
+ /**
33
+ * Appends query parameters before the fragment of an absolute or root-relative URL.
34
+ * Existing query text is retained without parsing it into name-value pairs.
35
+ *
36
+ * This function only composes URLs. Callers must validate untrusted input.
37
+ *
38
+ * @throws TypeError if parsing fails or a relative input changes authority.
39
+ */
40
+ function appendQueryParams(input, params) {
41
+ const relative = input.startsWith("/");
42
+ if (input.startsWith("//") || input.startsWith("/\\")) throw new TypeError("Expected an absolute or root-relative URL");
43
+ const parsedURL = relative ? new URL(input, URL_REFERENCE_ORIGIN) : new URL(input);
44
+ if (relative && parsedURL.origin !== URL_REFERENCE_ORIGIN) throw new TypeError("Expected an absolute or root-relative URL");
45
+ const query = params.toString();
46
+ if (!query) return input;
47
+ const separator = parsedURL.search.endsWith("&") ? "" : "&";
48
+ parsedURL.search = parsedURL.search ? `${parsedURL.search}${separator}${query}` : query;
49
+ return relative ? parsedURL.href.slice(parsedURL.origin.length) : parsedURL.href;
50
+ }
31
51
  /**
32
52
  * Schemes that execute or embed code when navigated to or accepted as a
33
53
  * redirect target. These are never safe as an OAuth `redirect_uri` or as a
@@ -58,4 +78,4 @@ function isSafeUrlScheme(value) {
58
78
  return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
59
79
  }
60
80
  //#endregion
61
- export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
81
+ export { DANGEROUS_URL_SCHEMES, appendQueryParams, isSafeUrlScheme, normalizePathname };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,8 +45,8 @@
45
45
  "node": "./dist/async_hooks/index.mjs",
46
46
  "deno": "./dist/async_hooks/index.mjs",
47
47
  "bun": "./dist/async_hooks/index.mjs",
48
- "edge": "./dist/async_hooks/pure.index.mjs",
49
48
  "workerd": "./dist/async_hooks/index.mjs",
49
+ "edge": "./dist/async_hooks/pure.index.mjs",
50
50
  "browser": "./dist/async_hooks/pure.index.mjs",
51
51
  "default": "./dist/async_hooks/index.mjs"
52
52
  },
@@ -2,7 +2,7 @@ import type { AsyncLocalStorage } from "@better-auth/core/async_hooks";
2
2
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
3
  import type { EndpointContext, InputContext } from "better-call";
4
4
  import type { AuthContext } from "../types";
5
- import { __getBetterAuthGlobal } from "./global";
5
+ import { __getBetterAuthGlobal, __getCurrentEndpointContext } from "./global";
6
6
 
7
7
  export type AuthEndpointContext = Partial<
8
8
  InputContext<string, any> & EndpointContext<string, any>
@@ -10,43 +10,68 @@ export type AuthEndpointContext = Partial<
10
10
  context: AuthContext;
11
11
  };
12
12
 
13
- const ensureAsyncStorage = async () => {
14
- const betterAuthGlobal = __getBetterAuthGlobal();
15
- const existing = betterAuthGlobal.context.endpointContextAsyncStorage;
13
+ type AuthEndpointContextStorage = AsyncLocalStorage<AuthEndpointContext>;
14
+
15
+ const getExistingEndpointContextStorage = () => {
16
+ return __getBetterAuthGlobal().context.endpointContextAsyncStorage as
17
+ | AuthEndpointContextStorage
18
+ | undefined;
19
+ };
20
+
21
+ const getOrCreateEndpointContextStorage = async () => {
22
+ const existing = getExistingEndpointContextStorage();
16
23
  if (existing) {
17
- return existing as AsyncLocalStorage<AuthEndpointContext>;
24
+ return existing;
18
25
  }
19
26
  const AsyncLocalStorage = await getAsyncLocalStorage();
20
- betterAuthGlobal.context.endpointContextAsyncStorage ??=
21
- new AsyncLocalStorage<AuthEndpointContext>();
22
- return betterAuthGlobal.context
23
- .endpointContextAsyncStorage as AsyncLocalStorage<AuthEndpointContext>;
27
+ const globalContext = __getBetterAuthGlobal().context;
28
+ const storage = (globalContext.endpointContextAsyncStorage ??=
29
+ new AsyncLocalStorage<AuthEndpointContext>()) as AuthEndpointContextStorage;
30
+ return storage;
24
31
  };
25
32
 
26
33
  /**
27
- * This is for internal use only. Most users should use `getCurrentAuthContext` instead.
28
- *
29
- * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
34
+ * @deprecated Use `getCurrentAuthEndpointContext`,
35
+ * `tryGetCurrentAuthEndpointContext`, or `runWithEndpointContext` instead.
30
36
  */
31
37
  export async function getCurrentAuthContextAsyncLocalStorage() {
32
- return ensureAsyncStorage();
38
+ return getOrCreateEndpointContextStorage();
39
+ }
40
+
41
+ /**
42
+ * Returns the current auth endpoint context, or `undefined` when called outside
43
+ * of `runWithEndpointContext`.
44
+ */
45
+ export function tryGetCurrentAuthEndpointContext() {
46
+ return __getCurrentEndpointContext<AuthEndpointContext>();
33
47
  }
34
48
 
35
- export async function getCurrentAuthContext(): Promise<AuthEndpointContext> {
36
- const als = await ensureAsyncStorage();
37
- const context = als.getStore();
38
- if (!context) {
49
+ /**
50
+ * Returns the current auth endpoint context.
51
+ *
52
+ * @throws When called outside of `runWithEndpointContext`.
53
+ */
54
+ export function getCurrentAuthEndpointContext() {
55
+ const authEndpointContext = tryGetCurrentAuthEndpointContext();
56
+ if (!authEndpointContext) {
39
57
  throw new Error(
40
58
  "No auth context found. Please make sure you are calling this function within a `runWithEndpointContext` callback.",
41
59
  );
42
60
  }
43
- return context;
61
+ return authEndpointContext;
62
+ }
63
+
64
+ /**
65
+ * @deprecated Use `getCurrentAuthEndpointContext` instead.
66
+ */
67
+ export async function getCurrentAuthContext() {
68
+ return getCurrentAuthEndpointContext();
44
69
  }
45
70
 
46
71
  export async function runWithEndpointContext<T>(
47
- context: AuthEndpointContext,
72
+ authEndpointContext: AuthEndpointContext,
48
73
  fn: () => T,
49
74
  ): Promise<T> {
50
- const als = await ensureAsyncStorage();
51
- return als.run(context, fn);
75
+ const storage = await getOrCreateEndpointContextStorage();
76
+ return storage.run(authEndpointContext, fn);
52
77
  }
@@ -52,6 +52,13 @@ export function __getBetterAuthGlobal(): BetterAuthGlobal {
52
52
  return (globalThis as any)[symbol] as BetterAuthGlobal;
53
53
  }
54
54
 
55
+ export function __getCurrentEndpointContext<T>(): T | undefined {
56
+ const storage = __getBetterAuthGlobal().context.endpointContextAsyncStorage as
57
+ | AsyncLocalStorage<T>
58
+ | undefined;
59
+ return storage?.getStore();
60
+ }
61
+
55
62
  export function getBetterAuthVersion(): string {
56
63
  return __getBetterAuthGlobal().version;
57
64
  }
@@ -2,7 +2,9 @@ export {
2
2
  type AuthEndpointContext,
3
3
  getCurrentAuthContext,
4
4
  getCurrentAuthContextAsyncLocalStorage,
5
+ getCurrentAuthEndpointContext,
5
6
  runWithEndpointContext,
7
+ tryGetCurrentAuthEndpointContext,
6
8
  } from "./endpoint-context";
7
9
  export { getBetterAuthVersion } from "./global";
8
10
  export {
package/src/env/logger.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { AuthEndpointContext } from "../context/endpoint-context";
2
+ import { __getCurrentEndpointContext } from "../context/global";
1
3
  import { getColorDepth } from "./color-depth";
2
4
 
3
5
  export const TTY_COLORS = {
@@ -142,4 +144,23 @@ export const createLogger = (options?: Logger | undefined): InternalLogger => {
142
144
  };
143
145
  };
144
146
 
145
- export const logger = createLogger();
147
+ const defaultLogger = createLogger();
148
+
149
+ const getCurrentLogger = (): InternalLogger => {
150
+ const currentLogger =
151
+ __getCurrentEndpointContext<AuthEndpointContext>()?.context.logger;
152
+ return currentLogger && currentLogger !== logger
153
+ ? currentLogger
154
+ : defaultLogger;
155
+ };
156
+
157
+ export const logger: InternalLogger = {
158
+ debug: (...params) => getCurrentLogger().debug(...params),
159
+ info: (...params) => getCurrentLogger().info(...params),
160
+ success: (...params) => getCurrentLogger().success(...params),
161
+ warn: (...params) => getCurrentLogger().warn(...params),
162
+ error: (...params) => getCurrentLogger().error(...params),
163
+ get level() {
164
+ return getCurrentLogger().level;
165
+ },
166
+ };
@@ -6,6 +6,7 @@ import {
6
6
  getOAuth2Tokens,
7
7
  refreshAccessToken,
8
8
  } from "../oauth2";
9
+ import { createPlaceholderEmail } from "../utils/email";
9
10
 
10
11
  export interface RedditProfile {
11
12
  id: string;
@@ -110,7 +111,12 @@ export const reddit = (options: RedditOptions) => {
110
111
  // non-routable placeholder (RFC 2606 `.invalid`) keyed to the user's
111
112
  // Reddit id rather than the routable `reddit.com`, which could collide
112
113
  // with a real address. Left unverified; `mapProfileToUser` can override.
113
- const email = userMap?.email || `${profile.id}@reddit.invalid`;
114
+ const email =
115
+ userMap?.email ||
116
+ createPlaceholderEmail({
117
+ identifier: profile.id,
118
+ namespace: "reddit",
119
+ });
114
120
  return {
115
121
  user: {
116
122
  name: profile.name,
@@ -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 RobloxProfile extends Record<string, any> {
10
11
  /** the user's id */
@@ -97,13 +98,14 @@ export const roblox = (options: RobloxOptions) => {
97
98
  }
98
99
 
99
100
  const userMap = await options.mapProfileToUser?.(profile);
100
- // Roblox does not provide email or email_verified claim.
101
- // We default to false for security consistency.
102
101
  return {
103
102
  user: {
104
103
  name: profile.nickname || profile.preferred_username || "",
105
104
  image: profile.picture,
106
- email: profile.preferred_username || null, // Roblox does not provide email
105
+ email: createPlaceholderEmail({
106
+ identifier: profile.sub,
107
+ namespace: "roblox",
108
+ }),
107
109
  emailVerified: false,
108
110
  ...userMap,
109
111
  },
@@ -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
  /**
10
11
  * [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)
@@ -210,7 +211,12 @@ export const tiktok = (options: TiktokOptions) => {
210
211
 
211
212
  return {
212
213
  user: {
213
- email: profile.data.user.email || profile.data.user.username,
214
+ email:
215
+ profile.data.user.email ||
216
+ createPlaceholderEmail({
217
+ identifier: profile.data.user.open_id,
218
+ namespace: "tiktok",
219
+ }),
214
220
  name:
215
221
  profile.data.user.display_name || profile.data.user.username || "",
216
222
  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,
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