better-auth 1.6.14 → 1.6.15

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.
@@ -0,0 +1,34 @@
1
+ import { AuthContext } from "@better-auth/core";
2
+ import { Endpoint, EndpointContext, InputContext } from "better-call";
3
+
4
+ //#region src/api/dispatch.d.ts
5
+ /**
6
+ * Input accepted by {@link dispatchAuthEndpoint}. `context` must already be a
7
+ * resolved `AuthContext`; the caller owns `baseURL` resolution. A fresh
8
+ * dispatch carries no `session` (the shared context has none), while a resumed
9
+ * dispatch carries the in-flight request's `session` through.
10
+ */
11
+ type DispatchContext = Partial<InputContext<string, any> & EndpointContext<string, any>> & {
12
+ context: AuthContext & {
13
+ returned?: unknown | undefined;
14
+ responseHeaders?: Headers | undefined;
15
+ };
16
+ operationId?: string | undefined;
17
+ };
18
+ /**
19
+ * Run a single endpoint through the configured `hooks.before` / `hooks.after`
20
+ * pipeline, normalizing the response, headers, and `APIError`s the same way a
21
+ * router or `auth.api.*` dispatch does.
22
+ *
23
+ * This is the canonical hook runner. The HTTP router and `auth.api.*` reach it
24
+ * through {@link toAuthEndpoints}. Plugins call it directly when they need to
25
+ * re-enter the pipeline on purpose, such as resuming `/oauth2/authorize` after
26
+ * a fresh sign-in. Calling an endpoint as a plain function deliberately skips
27
+ * hooks; `dispatchAuthEndpoint` is the supported way to opt back in.
28
+ *
29
+ * @param endpoint The endpoint to dispatch.
30
+ * @param input Input context whose `context` is an already-resolved `AuthContext`.
31
+ */
32
+ declare function dispatchAuthEndpoint(endpoint: Endpoint, input: DispatchContext): Promise<unknown>;
33
+ //#endregion
34
+ export { DispatchContext, dispatchAuthEndpoint };
@@ -0,0 +1,272 @@
1
+ import { isAPIError } from "../utils/is-api-error.mjs";
2
+ import { isRequestLike } from "../utils/url.mjs";
3
+ import { runWithEndpointContext } from "@better-auth/core/context";
4
+ import { shouldPublishLog } from "@better-auth/core/env";
5
+ import { APIError } from "@better-auth/core/error";
6
+ import { createDefu } from "defu";
7
+ import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_ROUTE, ATTR_OPERATION_ID, withSpan } from "@better-auth/core/instrumentation";
8
+ import { kAPIErrorHeaderSymbol, toResponse } from "better-call";
9
+ //#region src/api/dispatch.ts
10
+ const defuReplaceArrays = createDefu((obj, key, value) => {
11
+ if (Array.isArray(obj[key]) && Array.isArray(value)) {
12
+ obj[key] = value;
13
+ return true;
14
+ }
15
+ });
16
+ const hooksSourceWeakMap = /* @__PURE__ */ new WeakMap();
17
+ /**
18
+ * Resolves the operation id used for spans, preferring an explicit
19
+ * `operationId`, then the OpenAPI one, then the caller's `fallback` (the
20
+ * `auth.api.*` map key), and finally the route path.
21
+ */
22
+ function getOperationId(endpoint, fallback) {
23
+ const opts = endpoint.options;
24
+ return opts?.operationId ?? opts?.metadata?.openapi?.operationId ?? fallback ?? endpoint.path ?? "/:virtual";
25
+ }
26
+ /**
27
+ * Merge a set of response headers onto the dispatch's accumulator, appending
28
+ * `set-cookie` (multiple cookies are legal) and replacing everything else.
29
+ */
30
+ function mergeResponseHeaders(context, headers) {
31
+ if (!headers) return;
32
+ headers.forEach((value, key) => {
33
+ if (!context.responseHeaders) context.responseHeaders = new Headers({ [key]: value });
34
+ else if (key.toLowerCase() === "set-cookie") context.responseHeaders.append(key, value);
35
+ else context.responseHeaders.set(key, value);
36
+ });
37
+ }
38
+ /**
39
+ * Combine the two header sources an `APIError` can carry into one set:
40
+ * - `kAPIErrorHeaderSymbol`: `ctx.responseHeaders` accumulated via
41
+ * `c.setCookie` / `c.setHeader` before the throw.
42
+ * - `e.headers`: explicit headers on the error (e.g. `location` from
43
+ * `c.redirect`).
44
+ *
45
+ * `c.redirect()` reuses `ctx.responseHeaders` as `e.headers`, so when both
46
+ * point at the same object iterating each would duplicate every `set-cookie`;
47
+ * the identity check skips that copy. Explicit error headers override
48
+ * accumulated ones, while cookies from both accumulate.
49
+ */
50
+ function mergeAPIErrorHeaders(error) {
51
+ const ctxHeaders = error[kAPIErrorHeaderSymbol];
52
+ const errHeaders = error.headers && error.headers !== ctxHeaders ? new Headers(error.headers) : null;
53
+ if (!ctxHeaders && !errHeaders) return null;
54
+ const headers = new Headers();
55
+ ctxHeaders?.forEach((value, key) => {
56
+ headers.append(key, value);
57
+ });
58
+ errHeaders?.forEach((value, key) => {
59
+ if (key.toLowerCase() === "set-cookie") headers.append(key, value);
60
+ else headers.set(key, value);
61
+ });
62
+ return headers;
63
+ }
64
+ async function runBeforeHooks(context, hooks, endpoint, operationId) {
65
+ let modifiedContext = {};
66
+ for (const hook of hooks) {
67
+ let matched = false;
68
+ try {
69
+ matched = hook.matcher(context);
70
+ } catch (error) {
71
+ const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
72
+ context.context.logger.error(`An error occurred during ${hookSource} hook matcher execution:`, error);
73
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: "An error occurred during hook matcher execution. Check the logs for more details." });
74
+ }
75
+ if (!matched) continue;
76
+ const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
77
+ const route = endpoint.path ?? "/:virtual";
78
+ const result = await withSpan(`hook before ${route} ${hookSource}`, {
79
+ [ATTR_HOOK_TYPE]: "before",
80
+ [ATTR_HTTP_ROUTE]: route,
81
+ [ATTR_CONTEXT]: hookSource,
82
+ [ATTR_OPERATION_ID]: operationId
83
+ }, () => hook.handler({
84
+ ...context,
85
+ returnHeaders: true
86
+ })).catch((e) => {
87
+ if (isAPIError(e) && shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
88
+ throw e;
89
+ });
90
+ mergeResponseHeaders(context.context, result?.headers);
91
+ const hookReturn = result?.response;
92
+ if (hookReturn && typeof hookReturn === "object") {
93
+ if ("context" in hookReturn && typeof hookReturn.context === "object") {
94
+ const { headers, ...rest } = hookReturn.context;
95
+ if (headers instanceof Headers) if (modifiedContext.headers) headers.forEach((value, key) => {
96
+ modifiedContext.headers?.set(key, value);
97
+ });
98
+ else modifiedContext.headers = headers;
99
+ modifiedContext = defuReplaceArrays(rest, modifiedContext);
100
+ continue;
101
+ }
102
+ return hookReturn;
103
+ }
104
+ }
105
+ return { context: modifiedContext };
106
+ }
107
+ async function runAfterHooks(context, hooks, endpoint, operationId) {
108
+ for (const hook of hooks) {
109
+ if (!hook.matcher(context)) continue;
110
+ const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
111
+ const route = endpoint.path ?? "/:virtual";
112
+ const result = await withSpan(`hook after ${route} ${hookSource}`, {
113
+ [ATTR_HOOK_TYPE]: "after",
114
+ [ATTR_HTTP_ROUTE]: route,
115
+ [ATTR_CONTEXT]: hookSource,
116
+ [ATTR_OPERATION_ID]: operationId
117
+ }, () => hook.handler(context)).catch((e) => {
118
+ if (isAPIError(e)) {
119
+ if (shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
120
+ return {
121
+ response: e,
122
+ headers: mergeAPIErrorHeaders(e)
123
+ };
124
+ }
125
+ throw e;
126
+ });
127
+ mergeResponseHeaders(context.context, result.headers);
128
+ if (result.response !== void 0) context.context.returned = result.response;
129
+ }
130
+ return {
131
+ response: context.context.returned,
132
+ headers: context.context.responseHeaders
133
+ };
134
+ }
135
+ function getHooks(authContext) {
136
+ const plugins = authContext.options.plugins || [];
137
+ const beforeHooks = [];
138
+ const afterHooks = [];
139
+ const beforeHookHandler = authContext.options.hooks?.before;
140
+ if (beforeHookHandler) {
141
+ hooksSourceWeakMap.set(beforeHookHandler, "user");
142
+ beforeHooks.push({
143
+ matcher: () => true,
144
+ handler: beforeHookHandler
145
+ });
146
+ }
147
+ const afterHookHandler = authContext.options.hooks?.after;
148
+ if (afterHookHandler) {
149
+ hooksSourceWeakMap.set(afterHookHandler, "user");
150
+ afterHooks.push({
151
+ matcher: () => true,
152
+ handler: afterHookHandler
153
+ });
154
+ }
155
+ const pluginBeforeHooks = plugins.flatMap((plugin) => (plugin.hooks?.before ?? []).map((h) => {
156
+ hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
157
+ return h;
158
+ }));
159
+ const pluginAfterHooks = plugins.flatMap((plugin) => (plugin.hooks?.after ?? []).map((h) => {
160
+ hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
161
+ return h;
162
+ }));
163
+ if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks);
164
+ if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks);
165
+ return {
166
+ beforeHooks,
167
+ afterHooks
168
+ };
169
+ }
170
+ /**
171
+ * Run a single endpoint through the configured `hooks.before` / `hooks.after`
172
+ * pipeline, normalizing the response, headers, and `APIError`s the same way a
173
+ * router or `auth.api.*` dispatch does.
174
+ *
175
+ * This is the canonical hook runner. The HTTP router and `auth.api.*` reach it
176
+ * through {@link toAuthEndpoints}. Plugins call it directly when they need to
177
+ * re-enter the pipeline on purpose, such as resuming `/oauth2/authorize` after
178
+ * a fresh sign-in. Calling an endpoint as a plain function deliberately skips
179
+ * hooks; `dispatchAuthEndpoint` is the supported way to opt back in.
180
+ *
181
+ * @param endpoint The endpoint to dispatch.
182
+ * @param input Input context whose `context` is an already-resolved `AuthContext`.
183
+ */
184
+ async function dispatchAuthEndpoint(endpoint, input) {
185
+ const operationId = input.operationId ?? getOperationId(endpoint);
186
+ const route = endpoint.path ?? "/:virtual";
187
+ const endpointMethod = endpoint.options?.method;
188
+ const defaultMethod = Array.isArray(endpointMethod) ? endpointMethod[0] : endpointMethod;
189
+ const methodName = input.method ?? input.request?.method ?? defaultMethod ?? "?";
190
+ const shouldReturnResponse = input.asResponse ?? isRequestLike(input.request);
191
+ let internalContext = {
192
+ ...input,
193
+ context: {
194
+ ...input.context,
195
+ returned: void 0,
196
+ responseHeaders: void 0,
197
+ session: input.context.session ?? null
198
+ },
199
+ path: endpoint.path,
200
+ headers: input.headers ? new Headers(input.headers) : void 0
201
+ };
202
+ return withSpan(`${methodName} ${route}`, {
203
+ [ATTR_HTTP_ROUTE]: route,
204
+ [ATTR_OPERATION_ID]: operationId
205
+ }, async () => runWithEndpointContext(internalContext, async () => {
206
+ const { beforeHooks, afterHooks } = getHooks(internalContext.context);
207
+ const before = await runBeforeHooks(internalContext, beforeHooks, endpoint, operationId);
208
+ if ("context" in before && before.context && typeof before.context === "object") {
209
+ const { headers, ...rest } = before.context;
210
+ if (headers) {
211
+ if (!internalContext.headers) internalContext.headers = new Headers();
212
+ const requestHeaders = internalContext.headers;
213
+ headers.forEach((value, key) => {
214
+ requestHeaders.set(key, value);
215
+ });
216
+ }
217
+ internalContext = defuReplaceArrays(rest, internalContext);
218
+ } else if (before) {
219
+ const responseHeaders = internalContext.context.responseHeaders;
220
+ return shouldReturnResponse ? toResponse(before, { headers: responseHeaders }) : input.returnHeaders ? {
221
+ headers: responseHeaders,
222
+ response: before
223
+ } : before;
224
+ }
225
+ internalContext.asResponse = false;
226
+ internalContext.returnHeaders = true;
227
+ internalContext.returnStatus = true;
228
+ const result = await runWithEndpointContext(internalContext, () => withSpan(`handler ${route}`, {
229
+ [ATTR_HTTP_ROUTE]: route,
230
+ [ATTR_OPERATION_ID]: operationId
231
+ }, () => endpoint(internalContext))).catch((e) => {
232
+ if (isAPIError(e)) return {
233
+ response: e,
234
+ status: e.statusCode,
235
+ headers: mergeAPIErrorHeaders(e)
236
+ };
237
+ throw e;
238
+ });
239
+ if (result instanceof Response) return result;
240
+ internalContext.context.returned = result.response;
241
+ internalContext.context.responseHeaders = result.headers ?? void 0;
242
+ const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
243
+ if (after.response !== void 0) result.response = after.response;
244
+ result.headers = after.headers ?? result.headers;
245
+ if (isAPIError(result.response) && shouldPublishLog(internalContext.context.logger.level, "debug")) result.response.stack = result.response.errorStack;
246
+ if (isAPIError(result.response) && !shouldReturnResponse) {
247
+ if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
248
+ enumerable: false,
249
+ configurable: true,
250
+ writable: false,
251
+ value: result.headers
252
+ });
253
+ throw result.response;
254
+ }
255
+ return shouldReturnResponse ? toResponse(result.response, {
256
+ headers: result.headers ?? void 0,
257
+ status: result.status
258
+ }) : input.returnHeaders ? input.returnStatus ? {
259
+ headers: result.headers,
260
+ response: result.response,
261
+ status: result.status
262
+ } : {
263
+ headers: result.headers,
264
+ response: result.response
265
+ } : input.returnStatus ? {
266
+ response: result.response,
267
+ status: result.status
268
+ } : result.response;
269
+ }));
270
+ }
271
+ //#endregion
272
+ export { dispatchAuthEndpoint, getOperationId };
@@ -2,6 +2,7 @@ import { OverrideMerge, Prettify as Prettify$1, UnionToIntersection } from "../t
2
2
  import { AdditionalSessionFieldsInput, AdditionalUserFieldsInput } from "../types/models.mjs";
3
3
  import { getIp } from "../utils/get-request-ip.mjs";
4
4
  import { isAPIError } from "../utils/is-api-error.mjs";
5
+ import { DispatchContext, dispatchAuthEndpoint } from "./dispatch.mjs";
5
6
  import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
6
7
  import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
7
8
  import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
@@ -3960,4 +3961,4 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
3960
3961
  } extends infer T_2 ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K] } : never) & T_1> : never : never : never;
3961
3962
  };
3962
3963
  //#endregion
3963
- export { APIError, type AuthEndpoint, type AuthMiddleware, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
3964
+ export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -17,6 +17,7 @@ import { signOut } from "./routes/sign-out.mjs";
17
17
  import { signUpEmail } from "./routes/sign-up.mjs";
18
18
  import { updateSession } from "./routes/update-session.mjs";
19
19
  import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./routes/update-user.mjs";
20
+ import { dispatchAuthEndpoint } from "./dispatch.mjs";
20
21
  import { toAuthEndpoints } from "./to-auth-endpoints.mjs";
21
22
  import { logger } from "@better-auth/core/env";
22
23
  import { APIError } from "@better-auth/core/error";
@@ -213,4 +214,4 @@ const router = (ctx, options) => {
213
214
  });
214
215
  };
215
216
  //#endregion
216
- export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
217
+ export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
@@ -355,7 +355,7 @@ const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
355
355
  const listSessions = () => createAuthEndpoint("/list-sessions", {
356
356
  method: "GET",
357
357
  operationId: "listUserSessions",
358
- use: [sessionMiddleware],
358
+ use: [freshSessionMiddleware],
359
359
  requireHeaders: true,
360
360
  metadata: { openapi: {
361
361
  operationId: "listUserSessions",
@@ -1,25 +1,9 @@
1
- import { isAPIError } from "../utils/is-api-error.mjs";
2
1
  import { isDynamicBaseURLConfig, isRequestLike } from "../utils/url.mjs";
3
2
  import { pickSource, resolveDynamicTrustedProxyHeaders, resolveRequestContext } from "../context/helpers.mjs";
4
- import { hasRequestState, runWithEndpointContext, runWithRequestState } from "@better-auth/core/context";
5
- import { shouldPublishLog } from "@better-auth/core/env";
3
+ import { dispatchAuthEndpoint, getOperationId } from "./dispatch.mjs";
4
+ import { hasRequestState, runWithRequestState } from "@better-auth/core/context";
6
5
  import { APIError, BetterAuthError } from "@better-auth/core/error";
7
- import { createDefu } from "defu";
8
- import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_ROUTE, ATTR_OPERATION_ID, withSpan } from "@better-auth/core/instrumentation";
9
- import { kAPIErrorHeaderSymbol, toResponse } from "better-call";
10
6
  //#region src/api/to-auth-endpoints.ts
11
- const defuReplaceArrays = createDefu((obj, key, value) => {
12
- if (Array.isArray(obj[key]) && Array.isArray(value)) {
13
- obj[key] = value;
14
- return true;
15
- }
16
- });
17
- const hooksSourceWeakMap = /* @__PURE__ */ new WeakMap();
18
- function getOperationId(endpoint, key) {
19
- if (!endpoint?.options) return key;
20
- const opts = endpoint.options;
21
- return opts.operationId ?? opts.metadata?.openapi?.operationId ?? key;
22
- }
23
7
  /**
24
8
  * Resolves the per-call `AuthContext` for endpoints with a dynamic `baseURL`.
25
9
  *
@@ -41,269 +25,34 @@ async function resolveDynamicContext(rawCtx, input) {
41
25
  throw err;
42
26
  }
43
27
  }
28
+ /**
29
+ * Wraps each raw endpoint so a router or `auth.api.*` call runs it through the
30
+ * configured hook pipeline. Per-call work that is specific to this entry point
31
+ * (dynamic `baseURL` resolution, request-state initialization) happens here;
32
+ * the hook pipeline itself lives in {@link dispatchAuthEndpoint}.
33
+ */
44
34
  function toAuthEndpoints(endpoints, ctx) {
45
35
  const api = {};
46
36
  for (const [key, endpoint] of Object.entries(endpoints)) {
47
37
  api[key] = async (context) => {
48
38
  const operationId = getOperationId(endpoint, key);
49
- const endpointMethod = endpoint?.options?.method;
50
- const defaultMethod = Array.isArray(endpointMethod) ? endpointMethod[0] : endpointMethod;
51
39
  const run = async () => {
52
40
  const rawContext = await ctx;
53
- const methodName = context?.method ?? context?.request?.method ?? defaultMethod ?? "?";
54
- const route = endpoint.path ?? "/:virtual";
55
41
  const authContext = isDynamicBaseURLConfig(rawContext.options.baseURL) ? await resolveDynamicContext(rawContext, context) : rawContext;
56
- let internalContext = {
42
+ return dispatchAuthEndpoint(endpoint, {
57
43
  ...context,
58
- context: {
59
- ...authContext,
60
- returned: void 0,
61
- responseHeaders: void 0,
62
- session: null
63
- },
64
- path: endpoint.path,
65
- headers: context?.headers ? new Headers(context?.headers) : void 0
66
- };
67
- const hasRequest = isRequestLike(context?.request);
68
- const shouldReturnResponse = context?.asResponse ?? hasRequest;
69
- return withSpan(`${methodName} ${route}`, {
70
- [ATTR_HTTP_ROUTE]: route,
71
- [ATTR_OPERATION_ID]: operationId
72
- }, async () => runWithEndpointContext(internalContext, async () => {
73
- const { beforeHooks, afterHooks } = getHooks(authContext);
74
- const before = await runBeforeHooks(internalContext, beforeHooks, endpoint, operationId);
75
- /**
76
- * If `before.context` is returned, it should
77
- * get merged with the original context
78
- */
79
- if ("context" in before && before.context && typeof before.context === "object") {
80
- const { headers, ...rest } = before.context;
81
- /**
82
- * Headers should be merged differently
83
- * so the hook doesn't override the whole
84
- * header
85
- */
86
- if (headers) headers.forEach((value, key) => {
87
- internalContext.headers.set(key, value);
88
- });
89
- internalContext = defuReplaceArrays(rest, internalContext);
90
- } else if (before) return shouldReturnResponse ? toResponse(before, { headers: context?.headers }) : context?.returnHeaders ? {
91
- headers: context?.headers,
92
- response: before
93
- } : before;
94
- internalContext.asResponse = false;
95
- internalContext.returnHeaders = true;
96
- internalContext.returnStatus = true;
97
- const result = await runWithEndpointContext(internalContext, () => withSpan(`handler ${route}`, {
98
- [ATTR_HTTP_ROUTE]: route,
99
- [ATTR_OPERATION_ID]: operationId
100
- }, () => endpoint(internalContext))).catch((e) => {
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
- /**
121
- * `c.redirect()` (and similar APIError throws) reuse
122
- * `ctx.responseHeaders` as `e.headers`, so when both sources
123
- * reference the same Headers, iterating both duplicates every
124
- * `set-cookie`. Skip the `errHeaders` copy in that case.
125
- */
126
- const errHeaders = e.headers && e.headers !== ctxHeaders ? new Headers(e.headers) : null;
127
- let headers = null;
128
- if (ctxHeaders || errHeaders) {
129
- headers = new Headers();
130
- ctxHeaders?.forEach((value, key) => {
131
- headers.append(key, value);
132
- });
133
- errHeaders?.forEach((value, key) => {
134
- if (key.toLowerCase() === "set-cookie") headers.append(key, value);
135
- else headers.set(key, value);
136
- });
137
- }
138
- return {
139
- response: e,
140
- status: e.statusCode,
141
- headers
142
- };
143
- }
144
- throw e;
145
- });
146
- if (result && result instanceof Response) return result;
147
- internalContext.context.returned = result.response;
148
- internalContext.context.responseHeaders = result.headers;
149
- const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
150
- if (after.response) result.response = after.response;
151
- if (isAPIError(result.response) && shouldPublishLog(authContext.logger.level, "debug")) result.response.stack = result.response.errorStack;
152
- if (isAPIError(result.response) && !shouldReturnResponse) {
153
- /**
154
- * Non-response path: we re-throw the raw APIError
155
- * to callers of `auth.api.*`. `result.headers`
156
- * holds the merged ctx + explicit headers (see
157
- * catch block above) — rewrite
158
- * `kAPIErrorHeaderSymbol` with the merged set so
159
- * downstream pipelines (e.g. better-call's
160
- * response builder, or an outer hook catch) see
161
- * the same headers we'd have written on the
162
- * response.
163
- */
164
- if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
165
- enumerable: false,
166
- configurable: true,
167
- writable: false,
168
- value: result.headers
169
- });
170
- throw result.response;
171
- }
172
- return shouldReturnResponse ? toResponse(result.response, {
173
- headers: result.headers,
174
- status: result.status
175
- }) : context?.returnHeaders ? context?.returnStatus ? {
176
- headers: result.headers,
177
- response: result.response,
178
- status: result.status
179
- } : {
180
- headers: result.headers,
181
- response: result.response
182
- } : context?.returnStatus ? {
183
- response: result.response,
184
- status: result.status
185
- } : result.response;
186
- }));
44
+ context: authContext,
45
+ operationId,
46
+ asResponse: context?.asResponse ?? isRequestLike(context?.request)
47
+ });
187
48
  };
188
49
  if (await hasRequestState()) return run();
189
- else return runWithRequestState(/* @__PURE__ */ new WeakMap(), run);
50
+ return runWithRequestState(/* @__PURE__ */ new WeakMap(), run);
190
51
  };
191
52
  api[key].path = endpoint.path;
192
53
  api[key].options = endpoint.options;
193
54
  }
194
55
  return api;
195
56
  }
196
- async function runBeforeHooks(context, hooks, endpoint, operationId) {
197
- let modifiedContext = {};
198
- for (const hook of hooks) {
199
- let matched = false;
200
- try {
201
- matched = hook.matcher(context);
202
- } catch (error) {
203
- const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
204
- context.context.logger.error(`An error occurred during ${hookSource} hook matcher execution:`, error);
205
- throw new APIError("INTERNAL_SERVER_ERROR", { message: `An error occurred during hook matcher execution. Check the logs for more details.` });
206
- }
207
- if (matched) {
208
- const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
209
- const route = endpoint.path ?? "/:virtual";
210
- const result = await withSpan(`hook before ${route} ${hookSource}`, {
211
- [ATTR_HOOK_TYPE]: "before",
212
- [ATTR_HTTP_ROUTE]: route,
213
- [ATTR_CONTEXT]: hookSource,
214
- [ATTR_OPERATION_ID]: operationId
215
- }, () => hook.handler({
216
- ...context,
217
- returnHeaders: false
218
- })).catch((e) => {
219
- if (isAPIError(e) && shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
220
- throw e;
221
- });
222
- if (result && typeof result === "object") {
223
- if ("context" in result && typeof result.context === "object") {
224
- const { headers, ...rest } = result.context;
225
- if (headers instanceof Headers) if (modifiedContext.headers) headers.forEach((value, key) => {
226
- modifiedContext.headers?.set(key, value);
227
- });
228
- else modifiedContext.headers = headers;
229
- modifiedContext = defuReplaceArrays(rest, modifiedContext);
230
- continue;
231
- }
232
- return result;
233
- }
234
- }
235
- }
236
- return { context: modifiedContext };
237
- }
238
- async function runAfterHooks(context, hooks, endpoint, operationId) {
239
- for (const hook of hooks) if (hook.matcher(context)) {
240
- const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown";
241
- const route = endpoint.path ?? "/:virtual";
242
- const result = await withSpan(`hook after ${route} ${hookSource}`, {
243
- [ATTR_HOOK_TYPE]: "after",
244
- [ATTR_HTTP_ROUTE]: route,
245
- [ATTR_CONTEXT]: hookSource,
246
- [ATTR_OPERATION_ID]: operationId
247
- }, () => hook.handler(context)).catch((e) => {
248
- if (isAPIError(e)) {
249
- const headers = e[kAPIErrorHeaderSymbol];
250
- if (shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack;
251
- return {
252
- response: e,
253
- headers: headers ? headers : e.headers ? new Headers(e.headers) : null
254
- };
255
- }
256
- throw e;
257
- });
258
- if (result.headers) result.headers.forEach((value, key) => {
259
- if (!context.context.responseHeaders) context.context.responseHeaders = new Headers({ [key]: value });
260
- else if (key.toLowerCase() === "set-cookie") context.context.responseHeaders.append(key, value);
261
- else context.context.responseHeaders.set(key, value);
262
- });
263
- if (result.response) context.context.returned = result.response;
264
- }
265
- return {
266
- response: context.context.returned,
267
- headers: context.context.responseHeaders
268
- };
269
- }
270
- function getHooks(authContext) {
271
- const plugins = authContext.options.plugins || [];
272
- const beforeHooks = [];
273
- const afterHooks = [];
274
- const beforeHookHandler = authContext.options.hooks?.before;
275
- if (beforeHookHandler) {
276
- hooksSourceWeakMap.set(beforeHookHandler, "user");
277
- beforeHooks.push({
278
- matcher: () => true,
279
- handler: beforeHookHandler
280
- });
281
- }
282
- const afterHookHandler = authContext.options.hooks?.after;
283
- if (afterHookHandler) {
284
- hooksSourceWeakMap.set(afterHookHandler, "user");
285
- afterHooks.push({
286
- matcher: () => true,
287
- handler: afterHookHandler
288
- });
289
- }
290
- const pluginBeforeHooks = plugins.flatMap((plugin) => (plugin.hooks?.before ?? []).map((h) => {
291
- hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
292
- return h;
293
- }));
294
- const pluginAfterHooks = plugins.flatMap((plugin) => (plugin.hooks?.after ?? []).map((h) => {
295
- hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
296
- return h;
297
- }));
298
- /**
299
- * Add plugin added hooks at last
300
- */
301
- if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks);
302
- if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks);
303
- return {
304
- beforeHooks,
305
- afterHooks
306
- };
307
- }
308
57
  //#endregion
309
58
  export { toAuthEndpoints };
@@ -108,14 +108,14 @@ function toCookieOptions(attributes) {
108
108
  *
109
109
  * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
110
110
  */
111
- const cookieNameRegex = /^[\w!#$%&'*.^`|~+-]+$/;
111
+ const cookieNameRegex = /^[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E\x5F\x60\x61-\x7A\x7C\x7E]+$/;
112
112
  /**
113
113
  * Cookie-value char set per RFC 6265 §4.1.1, plus space and comma.
114
114
  *
115
115
  * @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
116
116
  * @see https://github.com/golang/go/issues/7243
117
117
  */
118
- const cookieValueRegex = /^[ !#-:<-[\]-~]*$/;
118
+ const cookieValueRegex = /^[\x20\x21\x23-\x3A\x3C-\x5B\x5D-\x7E]*$/;
119
119
  /**
120
120
  * Strip surrounding double-quotes per RFC 6265 §4.1.1 quoted-string form.
121
121
  *
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.14";
2
+ var version = "1.6.15";
3
3
  //#endregion
4
4
  export { version };
@@ -72,6 +72,7 @@ const setRole = (opts) => createAuthEndpoint("/admin/set-role", {
72
72
  const inputRoles = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role];
73
73
  for (const role of inputRoles) if (!roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
74
74
  }
75
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
75
76
  const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, { role: parseRoles(ctx.body.role) });
76
77
  return ctx.json({ user: parseUserOutput(ctx.context.options, updatedUser) });
77
78
  });
@@ -234,6 +235,7 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
234
235
  }
235
236
  ctx.body.data.role = parseRoles(inputRoles);
236
237
  }
238
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
237
239
  const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
238
240
  return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
239
241
  });
@@ -402,6 +404,7 @@ const unbanUser = (opts) => createAuthEndpoint("/admin/unban-user", {
402
404
  options: opts,
403
405
  permissions: { user: ["ban"] }
404
406
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
407
+ if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
405
408
  const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
406
409
  banned: false,
407
410
  banExpires: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.6.14",
3
+ "version": "1.6.15",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -489,13 +489,13 @@
489
489
  "kysely": "^0.28.17 || ^0.29.0",
490
490
  "nanostores": "^1.1.1",
491
491
  "zod": "^4.3.6",
492
- "@better-auth/core": "1.6.14",
493
- "@better-auth/drizzle-adapter": "1.6.14",
494
- "@better-auth/kysely-adapter": "1.6.14",
495
- "@better-auth/memory-adapter": "1.6.14",
496
- "@better-auth/mongo-adapter": "1.6.14",
497
- "@better-auth/prisma-adapter": "1.6.14",
498
- "@better-auth/telemetry": "1.6.14"
492
+ "@better-auth/core": "1.6.15",
493
+ "@better-auth/drizzle-adapter": "1.6.15",
494
+ "@better-auth/kysely-adapter": "1.6.15",
495
+ "@better-auth/memory-adapter": "1.6.15",
496
+ "@better-auth/mongo-adapter": "1.6.15",
497
+ "@better-auth/prisma-adapter": "1.6.15",
498
+ "@better-auth/telemetry": "1.6.15"
499
499
  },
500
500
  "devDependencies": {
501
501
  "@lynx-js/react": "^0.116.3",