better-auth 1.7.0-beta.0 → 1.7.0-beta.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.
Files changed (93) hide show
  1. package/dist/api/index.d.mts +4 -0
  2. package/dist/api/routes/account.mjs +1 -1
  3. package/dist/api/routes/callback.d.mts +2 -0
  4. package/dist/api/routes/callback.mjs +23 -14
  5. package/dist/api/routes/email-verification.mjs +1 -1
  6. package/dist/api/routes/session.mjs +2 -2
  7. package/dist/api/routes/sign-in.mjs +1 -1
  8. package/dist/api/to-auth-endpoints.mjs +84 -14
  9. package/dist/auth/base.mjs +5 -24
  10. package/dist/client/config.mjs +1 -1
  11. package/dist/client/path-to-object.d.mts +35 -1
  12. package/dist/client/plugins/index.d.mts +4 -3
  13. package/dist/client/plugins/index.mjs +2 -1
  14. package/dist/client/query.mjs +4 -4
  15. package/dist/context/create-context.mjs +2 -2
  16. package/dist/context/helpers.mjs +63 -4
  17. package/dist/cookies/cookie-utils.d.mts +13 -1
  18. package/dist/cookies/cookie-utils.mjs +16 -1
  19. package/dist/cookies/index.d.mts +2 -2
  20. package/dist/cookies/index.mjs +5 -5
  21. package/dist/crypto/index.mjs +2 -2
  22. package/dist/db/index.mjs +1 -1
  23. package/dist/db/internal-adapter.mjs +1 -1
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.mjs +2 -2
  26. package/dist/integrations/next-js.mjs +2 -10
  27. package/dist/integrations/svelte-kit.mjs +5 -10
  28. package/dist/integrations/tanstack-start-solid.mjs +2 -10
  29. package/dist/integrations/tanstack-start.mjs +2 -10
  30. package/dist/oauth2/error-codes.d.mts +20 -0
  31. package/dist/oauth2/error-codes.mjs +20 -0
  32. package/dist/oauth2/state.mjs +1 -1
  33. package/dist/package.mjs +1 -1
  34. package/dist/plugins/admin/admin.mjs +1 -1
  35. package/dist/plugins/admin/routes.mjs +1 -1
  36. package/dist/plugins/anonymous/index.mjs +1 -1
  37. package/dist/plugins/custom-session/index.d.mts +6 -5
  38. package/dist/plugins/custom-session/index.mjs +3 -15
  39. package/dist/plugins/device-authorization/routes.mjs +1 -1
  40. package/dist/plugins/email-otp/routes.mjs +1 -1
  41. package/dist/plugins/generic-oauth/client.d.mts +6 -6
  42. package/dist/plugins/generic-oauth/client.mjs +6 -0
  43. package/dist/plugins/generic-oauth/error-codes.d.mts +1 -6
  44. package/dist/plugins/generic-oauth/error-codes.mjs +2 -7
  45. package/dist/plugins/generic-oauth/index.d.mts +9 -156
  46. package/dist/plugins/generic-oauth/index.mjs +125 -74
  47. package/dist/plugins/generic-oauth/providers/auth0.d.mts +1 -1
  48. package/dist/plugins/generic-oauth/providers/gumroad.d.mts +1 -1
  49. package/dist/plugins/generic-oauth/providers/hubspot.d.mts +1 -1
  50. package/dist/plugins/generic-oauth/providers/keycloak.d.mts +1 -1
  51. package/dist/plugins/generic-oauth/providers/microsoft-entra-id.d.mts +1 -1
  52. package/dist/plugins/generic-oauth/providers/okta.d.mts +1 -1
  53. package/dist/plugins/generic-oauth/providers/patreon.d.mts +1 -1
  54. package/dist/plugins/generic-oauth/providers/slack.d.mts +1 -1
  55. package/dist/plugins/generic-oauth/types.d.mts +18 -25
  56. package/dist/plugins/index.d.mts +4 -4
  57. package/dist/plugins/index.mjs +2 -2
  58. package/dist/plugins/jwt/client.d.mts +1 -1
  59. package/dist/plugins/jwt/index.d.mts +3 -3
  60. package/dist/plugins/jwt/index.mjs +2 -2
  61. package/dist/plugins/jwt/sign.d.mts +15 -3
  62. package/dist/plugins/jwt/sign.mjs +31 -12
  63. package/dist/plugins/jwt/types.d.mts +13 -1
  64. package/dist/plugins/jwt/utils.mjs +1 -1
  65. package/dist/plugins/last-login-method/index.mjs +1 -1
  66. package/dist/plugins/mcp/index.mjs +20 -8
  67. package/dist/plugins/oauth-proxy/index.mjs +3 -3
  68. package/dist/plugins/oidc-provider/index.mjs +2 -2
  69. package/dist/plugins/organization/organization.d.mts +6 -6
  70. package/dist/plugins/organization/routes/crud-invites.mjs +1 -1
  71. package/dist/plugins/organization/routes/crud-org.mjs +1 -1
  72. package/dist/plugins/organization/routes/crud-team.d.mts +4 -0
  73. package/dist/plugins/organization/routes/crud-team.mjs +40 -3
  74. package/dist/plugins/phone-number/index.d.mts +30 -0
  75. package/dist/plugins/phone-number/index.mjs +9 -1
  76. package/dist/plugins/phone-number/routes.mjs +6 -1
  77. package/dist/plugins/test-utils/index.d.mts +11 -2
  78. package/dist/plugins/test-utils/index.mjs +11 -2
  79. package/dist/plugins/two-factor/backup-codes/index.d.mts +2 -1
  80. package/dist/plugins/two-factor/backup-codes/index.mjs +12 -17
  81. package/dist/plugins/two-factor/client.d.mts +1 -1
  82. package/dist/plugins/two-factor/index.d.mts +2 -2
  83. package/dist/plugins/two-factor/index.mjs +14 -5
  84. package/dist/plugins/two-factor/types.d.mts +5 -0
  85. package/dist/test-utils/test-instance.d.mts +12 -0
  86. package/dist/test-utils/test-instance.mjs +7 -1
  87. package/dist/utils/index.d.mts +1 -1
  88. package/dist/utils/is-api-error.d.mts +1 -5
  89. package/dist/utils/is-api-error.mjs +1 -7
  90. package/dist/utils/url.d.mts +22 -15
  91. package/dist/utils/url.mjs +65 -28
  92. package/package.json +11 -11
  93. package/dist/plugins/generic-oauth/routes.mjs +0 -411
@@ -216,6 +216,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
216
216
  error_description: zod.ZodOptional<zod.ZodString>;
217
217
  state: zod.ZodOptional<zod.ZodString>;
218
218
  user: zod.ZodOptional<zod.ZodString>;
219
+ iss: zod.ZodOptional<zod.ZodString>;
219
220
  }, zod_v4_core0.$strip>>;
220
221
  query: zod.ZodOptional<zod.ZodObject<{
221
222
  code: zod.ZodOptional<zod.ZodString>;
@@ -224,6 +225,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
224
225
  error_description: zod.ZodOptional<zod.ZodString>;
225
226
  state: zod.ZodOptional<zod.ZodString>;
226
227
  user: zod.ZodOptional<zod.ZodString>;
228
+ iss: zod.ZodOptional<zod.ZodString>;
227
229
  }, zod_v4_core0.$strip>>;
228
230
  metadata: {
229
231
  allowedMediaTypes: string[];
@@ -2205,6 +2207,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2205
2207
  error_description: zod.ZodOptional<zod.ZodString>;
2206
2208
  state: zod.ZodOptional<zod.ZodString>;
2207
2209
  user: zod.ZodOptional<zod.ZodString>;
2210
+ iss: zod.ZodOptional<zod.ZodString>;
2208
2211
  }, zod_v4_core0.$strip>>;
2209
2212
  query: zod.ZodOptional<zod.ZodObject<{
2210
2213
  code: zod.ZodOptional<zod.ZodString>;
@@ -2213,6 +2216,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2213
2216
  error_description: zod.ZodOptional<zod.ZodString>;
2214
2217
  state: zod.ZodOptional<zod.ZodString>;
2215
2218
  user: zod.ZodOptional<zod.ZodString>;
2219
+ iss: zod.ZodOptional<zod.ZodString>;
2216
2220
  }, zod_v4_core0.$strip>>;
2217
2221
  metadata: {
2218
2222
  allowedMediaTypes: string[];
@@ -1,6 +1,6 @@
1
1
  import { parseAccountOutput } from "../../db/schema.mjs";
2
- import { getAwaitableValue } from "../../context/helpers.mjs";
3
2
  import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
3
+ import { getAwaitableValue } from "../../context/helpers.mjs";
4
4
  import { generateState } from "../../oauth2/state.mjs";
5
5
  import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils.mjs";
6
6
  import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "./session.mjs";
@@ -12,6 +12,7 @@ declare const callbackOAuth: better_call0.StrictEndpoint<"/callback/:id", {
12
12
  error_description: z.ZodOptional<z.ZodString>;
13
13
  state: z.ZodOptional<z.ZodString>;
14
14
  user: z.ZodOptional<z.ZodString>;
15
+ iss: z.ZodOptional<z.ZodString>;
15
16
  }, z.core.$strip>>;
16
17
  query: z.ZodOptional<z.ZodObject<{
17
18
  code: z.ZodOptional<z.ZodString>;
@@ -20,6 +21,7 @@ declare const callbackOAuth: better_call0.StrictEndpoint<"/callback/:id", {
20
21
  error_description: z.ZodOptional<z.ZodString>;
21
22
  state: z.ZodOptional<z.ZodString>;
22
23
  user: z.ZodOptional<z.ZodString>;
24
+ iss: z.ZodOptional<z.ZodString>;
23
25
  }, z.core.$strip>>;
24
26
  metadata: {
25
27
  allowedMediaTypes: string[];
@@ -1,7 +1,8 @@
1
- import { getAwaitableValue } from "../../context/helpers.mjs";
2
1
  import { setSessionCookie } from "../../cookies/index.mjs";
2
+ import { getAwaitableValue } from "../../context/helpers.mjs";
3
3
  import { parseState } from "../../oauth2/state.mjs";
4
4
  import { setTokenUtil } from "../../oauth2/utils.mjs";
5
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
5
6
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
6
7
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
8
  import { safeJSONParse } from "@better-auth/core/utils/json";
@@ -14,7 +15,8 @@ const schema = z.object({
14
15
  device_id: z.string().optional(),
15
16
  error_description: z.string().optional(),
16
17
  state: z.string().optional(),
17
- user: z.string().optional()
18
+ user: z.string().optional(),
19
+ iss: z.string().optional()
18
20
  });
19
21
  const callbackOAuth = createAuthEndpoint("/callback/:id", {
20
22
  method: ["GET", "POST"],
@@ -48,7 +50,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
48
50
  c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
49
51
  throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
50
52
  }
51
- const { code, error, state, error_description, device_id, user: userData } = queryOrBody;
53
+ const { code, error, state, error_description, device_id, user: userData, iss } = queryOrBody;
52
54
  if (!state) {
53
55
  c.context.logger.error("State not found", error);
54
56
  const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`;
@@ -65,12 +67,19 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
65
67
  if (error) redirectOnError(error, error_description);
66
68
  if (!code) {
67
69
  c.context.logger.error("Code not found");
68
- throw redirectOnError("no_code");
70
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CODE);
69
71
  }
70
72
  const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
71
73
  if (!provider) {
72
74
  c.context.logger.error("Oauth provider with id", c.params.id, "not found");
73
- throw redirectOnError("oauth_provider_not_found");
75
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.PROVIDER_NOT_FOUND);
76
+ }
77
+ if (iss && provider.issuer && iss !== provider.issuer) {
78
+ c.context.logger.error("OAuth issuer mismatch", {
79
+ expected: provider.issuer,
80
+ received: iss
81
+ });
82
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ISSUER_MISMATCH);
74
83
  }
75
84
  let tokens;
76
85
  try {
@@ -82,9 +91,9 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
82
91
  });
83
92
  } catch (e) {
84
93
  c.context.logger.error("", e);
85
- throw redirectOnError("invalid_code");
94
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
86
95
  }
87
- if (!tokens) throw redirectOnError("invalid_code");
96
+ if (!tokens) throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
88
97
  const parsedUserData = userData ? safeJSONParse(userData) : null;
89
98
  const userInfo = await provider.getUserInfo({
90
99
  ...tokens,
@@ -92,21 +101,21 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
92
101
  }).then((res) => res?.user);
93
102
  if (!userInfo) {
94
103
  c.context.logger.error("Unable to get user info");
95
- return redirectOnError("unable_to_get_user_info");
104
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO);
96
105
  }
97
106
  if (!callbackURL) {
98
107
  c.context.logger.error("No callback URL found");
99
- throw redirectOnError("no_callback_url");
108
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CALLBACK_URL);
100
109
  }
101
110
  if (link) {
102
111
  if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
103
112
  c.context.logger.error("Unable to link account - untrusted provider");
104
- return redirectOnError("unable_to_link_account");
113
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
105
114
  }
106
- if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
115
+ if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
107
116
  const existingAccount = await c.context.internalAdapter.findAccountByProviderId(String(userInfo.id), provider.id);
108
117
  if (existingAccount) {
109
- if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
118
+ if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
110
119
  const updateData = Object.fromEntries(Object.entries({
111
120
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
112
121
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
@@ -124,7 +133,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
124
133
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
125
134
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
126
135
  scope: tokens.scopes?.join(",")
127
- })) return redirectOnError("unable_to_link_account");
136
+ })) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
128
137
  let toRedirectTo;
129
138
  try {
130
139
  toRedirectTo = callbackURL.toString();
@@ -135,7 +144,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
135
144
  }
136
145
  if (!userInfo.email) {
137
146
  c.context.logger.error("Provider did not return email. This could be due to misconfiguration in the provider settings.");
138
- return redirectOnError("email_not_found");
147
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_FOUND);
139
148
  }
140
149
  const accountData = {
141
150
  providerId: provider.id,
@@ -1,6 +1,6 @@
1
1
  import { originCheck } from "../middlewares/origin-check.mjs";
2
- import { parseUserOutput } from "../../db/schema.mjs";
3
2
  import { signJWT } from "../../crypto/jwt.mjs";
3
+ import { parseUserOutput } from "../../db/schema.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getSessionFromCtx } from "./session.mjs";
6
6
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -1,7 +1,7 @@
1
1
  import { isAPIError } from "../../utils/is-api-error.mjs";
2
- import { getDate } from "../../utils/date.mjs";
3
- import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
4
2
  import { symmetricDecodeJWT, verifyJWT } from "../../crypto/jwt.mjs";
3
+ import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
4
+ import { getDate } from "../../utils/date.mjs";
5
5
  import { getChunkedCookie, getSessionQuerySchema } from "../../cookies/session-store.mjs";
6
6
  import { deleteSessionCookie, expireCookie, setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
7
7
  import { getShouldSkipSessionRefresh } from "../state/should-session-refresh.mjs";
@@ -1,7 +1,7 @@
1
1
  import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
2
2
  import { parseUserOutput } from "../../db/schema.mjs";
3
- import { getAwaitableValue } from "../../context/helpers.mjs";
4
3
  import { setSessionCookie } from "../../cookies/index.mjs";
4
+ import { getAwaitableValue } from "../../context/helpers.mjs";
5
5
  import { generateState } from "../../oauth2/state.mjs";
6
6
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
7
7
  import { createEmailVerificationToken } from "./email-verification.mjs";
@@ -1,7 +1,9 @@
1
1
  import { isAPIError } from "../utils/is-api-error.mjs";
2
+ import { isDynamicBaseURLConfig, isRequestLike } from "../utils/url.mjs";
3
+ import { pickSource, resolveDynamicTrustedProxyHeaders, resolveRequestContext } from "../context/helpers.mjs";
2
4
  import { hasRequestState, runWithEndpointContext, runWithRequestState } from "@better-auth/core/context";
3
5
  import { shouldPublishLog } from "@better-auth/core/env";
4
- import { APIError } from "@better-auth/core/error";
6
+ import { APIError, BetterAuthError } from "@better-auth/core/error";
5
7
  import { createDefu } from "defu";
6
8
  import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_ROUTE, ATTR_OPERATION_ID, withSpan } from "@better-auth/core/instrumentation";
7
9
  import { kAPIErrorHeaderSymbol, toResponse } from "better-call";
@@ -18,6 +20,27 @@ function getOperationId(endpoint, key) {
18
20
  const opts = endpoint.options;
19
21
  return opts.operationId ?? opts.metadata?.openapi?.operationId ?? key;
20
22
  }
23
+ /**
24
+ * Resolves the per-call `AuthContext` for endpoints with a dynamic `baseURL`.
25
+ *
26
+ * - `rawCtx.baseURL` already set: HTTP handler rehydrated upstream; return as-is.
27
+ * - Direct `auth.api` call with a source or a configured `fallback`: resolve here.
28
+ * - Neither: throw `APIError` with a helpful message. Leaving `baseURL = ""`
29
+ * would let plugins build `new URL("")` and crash cryptically downstream.
30
+ */
31
+ async function resolveDynamicContext(rawCtx, input) {
32
+ if (rawCtx.baseURL) return rawCtx;
33
+ const source = pickSource(input);
34
+ const config = rawCtx.options.baseURL;
35
+ const hasFallback = isDynamicBaseURLConfig(config) && Boolean(config.fallback);
36
+ if (source === void 0 && !hasFallback) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Dynamic baseURL could not be resolved for this direct auth.api call. Pass `headers: request.headers` (or `request`) to the call, or add `fallback` to your baseURL config." });
37
+ try {
38
+ return await resolveRequestContext(rawCtx, source, resolveDynamicTrustedProxyHeaders(rawCtx.options));
39
+ } catch (err) {
40
+ if (err instanceof BetterAuthError) throw new APIError("INTERNAL_SERVER_ERROR", { message: err.message });
41
+ throw err;
42
+ }
43
+ }
21
44
  function toAuthEndpoints(endpoints, ctx) {
22
45
  const api = {};
23
46
  for (const [key, endpoint] of Object.entries(endpoints)) {
@@ -26,9 +49,10 @@ function toAuthEndpoints(endpoints, ctx) {
26
49
  const endpointMethod = endpoint?.options?.method;
27
50
  const defaultMethod = Array.isArray(endpointMethod) ? endpointMethod[0] : endpointMethod;
28
51
  const run = async () => {
29
- const authContext = await ctx;
52
+ const rawContext = await ctx;
30
53
  const methodName = context?.method ?? context?.request?.method ?? defaultMethod ?? "?";
31
54
  const route = endpoint.path ?? "/:virtual";
55
+ const authContext = isDynamicBaseURLConfig(rawContext.options.baseURL) ? await resolveDynamicContext(rawContext, context) : rawContext;
32
56
  let internalContext = {
33
57
  ...context,
34
58
  context: {
@@ -40,7 +64,7 @@ function toAuthEndpoints(endpoints, ctx) {
40
64
  path: endpoint.path,
41
65
  headers: context?.headers ? new Headers(context?.headers) : void 0
42
66
  };
43
- const hasRequest = context?.request instanceof Request;
67
+ const hasRequest = isRequestLike(context?.request);
44
68
  const shouldReturnResponse = context?.asResponse ?? hasRequest;
45
69
  return withSpan(`${methodName} ${route}`, {
46
70
  [ATTR_HTTP_ROUTE]: route,
@@ -74,16 +98,43 @@ function toAuthEndpoints(endpoints, ctx) {
74
98
  [ATTR_HTTP_ROUTE]: route,
75
99
  [ATTR_OPERATION_ID]: operationId
76
100
  }, () => endpoint(internalContext))).catch((e) => {
77
- if (isAPIError(e))
78
- /**
79
- * API Errors from response are caught
80
- * and returned to hooks
81
- */
82
- return {
83
- response: e,
84
- status: e.statusCode,
85
- headers: e.headers ? new Headers(e.headers) : null
86
- };
101
+ if (isAPIError(e)) {
102
+ /**
103
+ * API Errors from response are caught
104
+ * and returned to hooks.
105
+ *
106
+ * Headers come from two sources that must both
107
+ * survive:
108
+ * - `kAPIErrorHeaderSymbol`: ctx.responseHeaders
109
+ * accumulated via c.setCookie / c.setHeader
110
+ * before the throw.
111
+ * - `e.headers`: explicit headers on the APIError
112
+ * (e.g. `location` from c.redirect).
113
+ *
114
+ * Start from the accumulated ctx headers, then
115
+ * apply e.headers on top — appending `set-cookie`
116
+ * and setting others — so explicit APIError
117
+ * headers override while cookies accumulate.
118
+ */
119
+ const ctxHeaders = e[kAPIErrorHeaderSymbol];
120
+ const errHeaders = e.headers ? new Headers(e.headers) : null;
121
+ let headers = null;
122
+ if (ctxHeaders || errHeaders) {
123
+ headers = new Headers();
124
+ ctxHeaders?.forEach((value, key) => {
125
+ headers.append(key, value);
126
+ });
127
+ errHeaders?.forEach((value, key) => {
128
+ if (key.toLowerCase() === "set-cookie") headers.append(key, value);
129
+ else headers.set(key, value);
130
+ });
131
+ }
132
+ return {
133
+ response: e,
134
+ status: e.statusCode,
135
+ headers
136
+ };
137
+ }
87
138
  throw e;
88
139
  });
89
140
  if (result && result instanceof Response) return result;
@@ -92,7 +143,26 @@ function toAuthEndpoints(endpoints, ctx) {
92
143
  const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
93
144
  if (after.response) result.response = after.response;
94
145
  if (isAPIError(result.response) && shouldPublishLog(authContext.logger.level, "debug")) result.response.stack = result.response.errorStack;
95
- if (isAPIError(result.response) && !shouldReturnResponse) throw result.response;
146
+ if (isAPIError(result.response) && !shouldReturnResponse) {
147
+ /**
148
+ * Non-response path: we re-throw the raw APIError
149
+ * to callers of `auth.api.*`. `result.headers`
150
+ * holds the merged ctx + explicit headers (see
151
+ * catch block above) — rewrite
152
+ * `kAPIErrorHeaderSymbol` with the merged set so
153
+ * downstream pipelines (e.g. better-call's
154
+ * response builder, or an outer hook catch) see
155
+ * the same headers we'd have written on the
156
+ * response.
157
+ */
158
+ if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
159
+ enumerable: false,
160
+ configurable: true,
161
+ writable: false,
162
+ value: result.headers
163
+ });
164
+ throw result.response;
165
+ }
96
166
  return shouldReturnResponse ? toResponse(result.response, {
97
167
  headers: result.headers,
98
168
  status: result.status
@@ -1,6 +1,5 @@
1
- import { getBaseURL, getOrigin, isDynamicBaseURLConfig, resolveBaseURL } from "../utils/url.mjs";
2
- import { getTrustedOrigins, getTrustedProviders } from "../context/helpers.mjs";
3
- import { createCookieGetter, getCookies } from "../cookies/index.mjs";
1
+ import { getBaseURL, getOrigin, isDynamicBaseURLConfig } from "../utils/url.mjs";
2
+ import { getTrustedOrigins, getTrustedProviders, resolveDynamicTrustedProxyHeaders, resolveRequestContext } from "../context/helpers.mjs";
4
3
  import { getEndpoints, router } from "../api/index.mjs";
5
4
  import { runWithAdapter } from "@better-auth/core/context";
6
5
  import { BASE_ERROR_CODES, BetterAuthError } from "@better-auth/core/error";
@@ -13,26 +12,8 @@ const createBetterAuth = (options, initFn) => {
13
12
  const ctx = await authContext;
14
13
  const basePath = ctx.options.basePath || "/api/auth";
15
14
  let handlerCtx;
16
- if (isDynamicBaseURLConfig(options.baseURL)) {
17
- handlerCtx = Object.create(Object.getPrototypeOf(ctx), Object.getOwnPropertyDescriptors(ctx));
18
- const baseURL = resolveBaseURL(options.baseURL, basePath, request);
19
- if (baseURL) {
20
- handlerCtx.baseURL = baseURL;
21
- handlerCtx.options = {
22
- ...ctx.options,
23
- baseURL: getOrigin(baseURL) || void 0
24
- };
25
- } else throw new BetterAuthError("Could not resolve base URL from request. Check your allowedHosts config.");
26
- const trustedOriginOptions = {
27
- ...handlerCtx.options,
28
- baseURL: options.baseURL
29
- };
30
- handlerCtx.trustedOrigins = await getTrustedOrigins(trustedOriginOptions, request);
31
- if (options.advanced?.crossSubDomainCookies?.enabled) {
32
- handlerCtx.authCookies = getCookies(handlerCtx.options);
33
- handlerCtx.createAuthCookie = createCookieGetter(handlerCtx.options);
34
- }
35
- } else {
15
+ if (isDynamicBaseURLConfig(options.baseURL)) handlerCtx = await resolveRequestContext(ctx, request, resolveDynamicTrustedProxyHeaders(ctx.options));
16
+ else {
36
17
  handlerCtx = ctx;
37
18
  if (!ctx.options.baseURL) {
38
19
  const baseURL = getBaseURL(void 0, basePath, request, void 0, ctx.options.advanced?.trustedProxyHeaders);
@@ -42,8 +23,8 @@ const createBetterAuth = (options, initFn) => {
42
23
  } else throw new BetterAuthError("Could not get base URL from request. Please provide a valid base URL.");
43
24
  }
44
25
  handlerCtx.trustedOrigins = await getTrustedOrigins(ctx.options, request);
26
+ handlerCtx.trustedProviders = await getTrustedProviders(ctx.options, request);
45
27
  }
46
- handlerCtx.trustedProviders = await getTrustedProviders(handlerCtx.options, request);
47
28
  const { handler } = router(handlerCtx, options);
48
29
  return runWithAdapter(handlerCtx.adapter, () => handler(request));
49
30
  },
@@ -67,7 +67,7 @@ const getClientConfig = (options, loadEnv) => {
67
67
  const atomListeners = [{
68
68
  signal: "$sessionSignal",
69
69
  matcher(path) {
70
- return path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/change-email";
70
+ return path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/revoke-other-sessions" || path === "/change-email" || path === "/change-password";
71
71
  },
72
72
  callback(path) {
73
73
  if (path === "/sign-out") broadcastSessionUpdate("signout");
@@ -1,10 +1,41 @@
1
1
  import { HasRequiredKeys, IsAny, Prettify as Prettify$1, UnionToIntersection } from "../types/helper.mjs";
2
2
  import { InferAdditionalFromClient, InferSessionFromClient, InferUserFromClient } from "./types.mjs";
3
3
  import { BetterAuthClientOptions, ClientFetchOption } from "@better-auth/core";
4
+ import { SocialProviderList } from "@better-auth/core/social-providers";
4
5
  import { Endpoint, InputContext, StandardSchemaV1 } from "better-call";
5
6
  import { BetterFetchResponse } from "@better-fetch/fetch";
6
7
 
7
8
  //#region src/client/path-to-object.d.ts
9
+ /**
10
+ * Extract generic OAuth provider IDs from the client options.
11
+ * Supports both `$InferAuth` (server type bridge) and client plugins
12
+ * with `$InferServerPlugin`.
13
+ */
14
+ type InferGenericOAuthProviderIds<O extends BetterAuthClientOptions> = (O extends {
15
+ $InferAuth: {
16
+ options: {
17
+ plugins: Array<infer P>;
18
+ };
19
+ };
20
+ } ? P extends {
21
+ id: "generic-oauth";
22
+ options: {
23
+ config: Array<{
24
+ providerId: infer ID;
25
+ }>;
26
+ };
27
+ } ? ID & string : never : never) | (O extends {
28
+ plugins: Array<infer P>;
29
+ } ? P extends {
30
+ $InferServerPlugin: {
31
+ id: "generic-oauth";
32
+ options: {
33
+ config: Array<{
34
+ providerId: infer ID;
35
+ }>;
36
+ };
37
+ };
38
+ } ? ID & string : never : never);
8
39
  type KeepNullishFromOriginal<Original, Replaced> = Replaced | (undefined extends Original ? undefined : never) | (null extends Original ? null : never);
9
40
  type ReplaceTopLevelField<Data, Field extends "user" | "session", Replaced> = Data extends object ? Field extends keyof Data ? Omit<Data, Field> & { [K in Field]: KeepNullishFromOriginal<Data[K], Replaced> } : Data : Data;
10
41
  type ReplaceAuthUserAndSession<Data, ClientOpts extends BetterAuthClientOptions> = ReplaceTopLevelField<ReplaceTopLevelField<Data, "user", InferUserFromClient<ClientOpts>>, "session", InferSessionFromClient<ClientOpts>>;
@@ -51,7 +82,10 @@ type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record
51
82
  scope: "http";
52
83
  } | {
53
84
  scope: "server";
54
- } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredKeys<InferCtx<C, FetchOptions>> extends true ? [Prettify$1<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>, FetchOptions?] : [Prettify$1<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
85
+ } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredKeys<InferCtx<C, FetchOptions>> extends true ? [Prettify$1<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : T["path"] extends `/sign-in/social` ? Omit<InferCtx<C, FetchOptions>, "provider"> & {
86
+ provider: SocialProviderList[number] | InferGenericOAuthProviderIds<COpts> | (string & {});
87
+ fetchOptions?: FetchOptions | undefined;
88
+ } : InferCtx<C, FetchOptions>>, FetchOptions?] : [Prettify$1<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
55
89
  CUSTOM_SESSION: boolean;
56
90
  } ? MergeCustomSessionWithInferred<NonNullable<Awaited<R>>, COpts> : T["path"] extends "/get-session" ? {
57
91
  user: InferUserFromClient<COpts>;
@@ -14,13 +14,13 @@ import { OktaOptions, okta } from "../../plugins/generic-oauth/providers/okta.mj
14
14
  import { PatreonOptions, patreon } from "../../plugins/generic-oauth/providers/patreon.mjs";
15
15
  import { SlackOptions, slack } from "../../plugins/generic-oauth/providers/slack.mjs";
16
16
  import { BaseOAuthProviderOptions } from "../../plugins/generic-oauth/index.mjs";
17
- import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "../../plugins/jwt/types.mjs";
17
+ import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "../../plugins/jwt/types.mjs";
18
18
  import { AuthorizationQuery, Client, CodeVerificationValue, OAuthAccessToken, OIDCMetadata, OIDCOptions, TokenBody } from "../../plugins/oidc-provider/types.mjs";
19
19
  import { MULTI_SESSION_ERROR_CODES } from "../../plugins/multi-session/error-codes.mjs";
20
20
  import { MultiSessionConfig } from "../../plugins/multi-session/index.mjs";
21
21
  import { OneTimeTokenOptions } from "../../plugins/one-time-token/index.mjs";
22
22
  import { PhoneNumberOptions, UserWithPhoneNumber } from "../../plugins/phone-number/types.mjs";
23
- import { BackupCodeOptions, backupCode2fa, generateBackupCodes, getBackupCodes, verifyBackupCode } from "../../plugins/two-factor/backup-codes/index.mjs";
23
+ import { BackupCodeOptions, backupCode2fa, encodeBackupCodes, generateBackupCodes, getBackupCodes, verifyBackupCode } from "../../plugins/two-factor/backup-codes/index.mjs";
24
24
  import { OTPOptions, otp2fa } from "../../plugins/two-factor/otp/index.mjs";
25
25
  import { TOTPOptions, totp2fa } from "../../plugins/two-factor/totp/index.mjs";
26
26
  import { TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor } from "../../plugins/two-factor/types.mjs";
@@ -37,6 +37,7 @@ import { customSessionClient } from "../../plugins/custom-session/client.mjs";
37
37
  import { deviceAuthorizationClient } from "../../plugins/device-authorization/client.mjs";
38
38
  import { EMAIL_OTP_ERROR_CODES } from "../../plugins/email-otp/error-codes.mjs";
39
39
  import { emailOTPClient } from "../../plugins/email-otp/client.mjs";
40
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
40
41
  import { GENERIC_OAUTH_ERROR_CODES } from "../../plugins/generic-oauth/error-codes.mjs";
41
42
  import { genericOAuthClient } from "../../plugins/generic-oauth/client.mjs";
42
43
  import { jwtClient } from "../../plugins/jwt/client.mjs";
@@ -52,4 +53,4 @@ import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
52
53
  import { siweClient } from "../../plugins/siwe/client.mjs";
53
54
  import { usernameClient } from "../../plugins/username/client.mjs";
54
55
  import { InferServerPlugin } from "./infer-plugin.mjs";
55
- export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
56
+ export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_CALLBACK_ERROR_CODES, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, RequiredKeysOf, ResolvedSigningKey, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
@@ -1,3 +1,4 @@
1
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
1
2
  import { inferAdditionalFields } from "../../plugins/additional-fields/client.mjs";
2
3
  import { ADMIN_ERROR_CODES } from "../../plugins/admin/error-codes.mjs";
3
4
  import { adminClient } from "../../plugins/admin/client.mjs";
@@ -27,4 +28,4 @@ import { twoFactorClient } from "../../plugins/two-factor/client.mjs";
27
28
  import { USERNAME_ERROR_CODES } from "../../plugins/username/error-codes.mjs";
28
29
  import { usernameClient } from "../../plugins/username/client.mjs";
29
30
  import { InferServerPlugin } from "./infer-plugin.mjs";
30
- export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, EMAIL_OTP_ERROR_CODES, GENERIC_OAUTH_ERROR_CODES, InferServerPlugin, MULTI_SESSION_ERROR_CODES, ORGANIZATION_ERROR_CODES, PHONE_NUMBER_ERROR_CODES, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, adminClient, anonymousClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient };
31
+ export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, EMAIL_OTP_ERROR_CODES, GENERIC_OAUTH_ERROR_CODES, InferServerPlugin, MULTI_SESSION_ERROR_CODES, OAUTH_CALLBACK_ERROR_CODES, ORGANIZATION_ERROR_CODES, PHONE_NUMBER_ERROR_CODES, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, adminClient, anonymousClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient };
@@ -72,15 +72,15 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
72
72
  });
73
73
  };
74
74
  initializedAtom = Array.isArray(initializedAtom) ? initializedAtom : [initializedAtom];
75
- let isMounted = false;
75
+ let isInitialized = false;
76
76
  for (const initAtom of initializedAtom) initAtom.subscribe(async () => {
77
77
  if (isServer()) return;
78
- if (isMounted) await fn();
78
+ if (isInitialized) await fn();
79
79
  else onMount(value, () => {
80
80
  const timeoutId = setTimeout(async () => {
81
- if (!isMounted) {
81
+ if (!isInitialized) {
82
+ isInitialized = true;
82
83
  await fn();
83
- isMounted = true;
84
84
  }
85
85
  }, 0);
86
86
  return () => {
@@ -1,10 +1,10 @@
1
1
  import { getBaseURL, isDynamicBaseURLConfig } from "../utils/url.mjs";
2
2
  import { matchesOriginPattern } from "../auth/trusted-origins.mjs";
3
- import { createInternalAdapter } from "../db/internal-adapter.mjs";
4
3
  import { isPromise } from "../utils/is-promise.mjs";
5
- import { getInternalPlugins, getTrustedOrigins, getTrustedProviders, runPluginInit } from "./helpers.mjs";
6
4
  import { hashPassword, verifyPassword } from "../crypto/password.mjs";
7
5
  import { createCookieGetter, getCookies } from "../cookies/index.mjs";
6
+ import { createInternalAdapter } from "../db/internal-adapter.mjs";
7
+ import { getInternalPlugins, getTrustedOrigins, getTrustedProviders, runPluginInit } from "./helpers.mjs";
8
8
  import { checkPassword } from "../utils/password.mjs";
9
9
  import { checkEndpointConflicts } from "../api/index.mjs";
10
10
  import { DEFAULT_SECRET } from "../utils/constants.mjs";
@@ -1,8 +1,11 @@
1
- import { getBaseURL, isDynamicBaseURLConfig } from "../utils/url.mjs";
2
- import { createInternalAdapter } from "../db/internal-adapter.mjs";
1
+ import { getBaseURL, getOrigin, isDynamicBaseURLConfig, isRequestLike, resolveBaseURL } from "../utils/url.mjs";
3
2
  import { isPromise } from "../utils/is-promise.mjs";
3
+ import { createCookieGetter, getCookies } from "../cookies/index.mjs";
4
+ import { createInternalAdapter } from "../db/internal-adapter.mjs";
4
5
  import { env } from "@better-auth/core/env";
6
+ import { BetterAuthError } from "@better-auth/core/error";
5
7
  import { defu } from "defu";
8
+ import { isLoopbackHost } from "@better-auth/core/utils/host";
6
9
  //#region src/context/helpers.ts
7
10
  async function runPluginInit(context) {
8
11
  let options = context.options;
@@ -60,7 +63,7 @@ async function getTrustedOrigins(options, request) {
60
63
  const allowedHosts = options.baseURL.allowedHosts;
61
64
  for (const host of allowedHosts) if (!host.includes("://")) {
62
65
  trustedOrigins.push(`https://${host}`);
63
- if (host.includes("localhost") || host.includes("127.0.0.1")) trustedOrigins.push(`http://${host}`);
66
+ if (isLoopbackHost(host)) trustedOrigins.push(`http://${host}`);
64
67
  } else trustedOrigins.push(host);
65
68
  if (options.baseURL.fallback) try {
66
69
  trustedOrigins.push(new URL(options.baseURL.fallback).origin);
@@ -80,6 +83,62 @@ async function getTrustedOrigins(options, request) {
80
83
  if (envTrustedOrigins) trustedOrigins.push(...envTrustedOrigins.split(","));
81
84
  return trustedOrigins.filter((v) => Boolean(v));
82
85
  }
86
+ /**
87
+ * Picks a `Request`-like or `Headers` value from a direct `auth.api` call.
88
+ * Headers are only accepted when they carry a host: without one, host
89
+ * resolution would fall back to `null` and the caller should use `fallback`
90
+ * or pass a `Request` instead.
91
+ */
92
+ function pickSource(input) {
93
+ if (isRequestLike(input?.request)) return input.request;
94
+ if (!input?.headers) return void 0;
95
+ const headers = input.headers instanceof Headers ? input.headers : new Headers(input.headers);
96
+ if (!headers.has("host") && !headers.has("x-forwarded-host")) return;
97
+ return headers;
98
+ }
99
+ /**
100
+ * Returns the effective `trustedProxyHeaders` value for dynamic `baseURL`
101
+ * resolution. When the user hasn't set `advanced.trustedProxyHeaders`,
102
+ * proxy headers (`x-forwarded-host` / `x-forwarded-proto`) are trusted by
103
+ * default so deployments behind a reverse proxy work without extra config.
104
+ */
105
+ function resolveDynamicTrustedProxyHeaders(options) {
106
+ return options.advanced?.trustedProxyHeaders ?? true;
107
+ }
108
+ /**
109
+ * Per-request clone with `baseURL`, `trustedOrigins`, `trustedProviders`
110
+ * and cookies rehydrated for the resolved host. Throws `BetterAuthError`
111
+ * when the URL cannot be resolved; callers on the direct-API path convert
112
+ * this to `APIError`.
113
+ */
114
+ async function resolveRequestContext(ctx, source, trustedProxyHeaders) {
115
+ const dynamicBaseURLConfig = ctx.options.baseURL;
116
+ const baseURL = resolveBaseURL(dynamicBaseURLConfig, ctx.options.basePath || "/api/auth", source, void 0, trustedProxyHeaders);
117
+ if (!baseURL) throw new BetterAuthError("Could not resolve base URL from request. Check your allowedHosts config.");
118
+ const resolved = Object.create(Object.getPrototypeOf(ctx), Object.getOwnPropertyDescriptors(ctx));
119
+ resolved.baseURL = baseURL;
120
+ resolved.options = {
121
+ ...ctx.options,
122
+ baseURL: getOrigin(baseURL) || void 0
123
+ };
124
+ const trustedOriginOptions = {
125
+ ...resolved.options,
126
+ baseURL: dynamicBaseURLConfig
127
+ };
128
+ const needsRequest = typeof ctx.options.trustedOrigins === "function" || typeof ctx.options.account?.accountLinking?.trustedProviders === "function";
129
+ let callbackRequest;
130
+ if (needsRequest) if (isRequestLike(source)) callbackRequest = source;
131
+ else if (source) callbackRequest = new Request(baseURL, { headers: source });
132
+ else callbackRequest = void 0;
133
+ else callbackRequest = void 0;
134
+ resolved.trustedOrigins = await getTrustedOrigins(trustedOriginOptions, callbackRequest);
135
+ resolved.trustedProviders = await getTrustedProviders(resolved.options, callbackRequest);
136
+ if (ctx.options.advanced?.crossSubDomainCookies?.enabled) {
137
+ resolved.authCookies = getCookies(resolved.options);
138
+ resolved.createAuthCookie = createCookieGetter(resolved.options);
139
+ }
140
+ return resolved;
141
+ }
83
142
  async function getAwaitableValue(arr, item) {
84
143
  if (!arr) return void 0;
85
144
  for (const val of arr) {
@@ -94,4 +153,4 @@ async function getTrustedProviders(options, request) {
94
153
  return (await trustedProviders(request) ?? []).filter((v) => Boolean(v));
95
154
  }
96
155
  //#endregion
97
- export { getAwaitableValue, getInternalPlugins, getTrustedOrigins, getTrustedProviders, runPluginInit };
156
+ export { getAwaitableValue, getInternalPlugins, getTrustedOrigins, getTrustedProviders, pickSource, resolveDynamicTrustedProxyHeaders, resolveRequestContext, runPluginInit };