better-auth 1.6.14 → 1.6.16

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 (43) hide show
  1. package/dist/api/dispatch.d.mts +34 -0
  2. package/dist/api/dispatch.mjs +272 -0
  3. package/dist/api/index.d.mts +2 -1
  4. package/dist/api/index.mjs +2 -1
  5. package/dist/api/middlewares/origin-check.mjs +1 -0
  6. package/dist/api/routes/account.mjs +11 -6
  7. package/dist/api/routes/callback.mjs +1 -1
  8. package/dist/api/routes/session.mjs +4 -2
  9. package/dist/api/routes/update-session.mjs +9 -3
  10. package/dist/api/to-auth-endpoints.mjs +14 -265
  11. package/dist/client/lynx/index.d.mts +6 -5
  12. package/dist/client/react/index.d.mts +6 -5
  13. package/dist/client/solid/index.d.mts +6 -5
  14. package/dist/client/svelte/index.d.mts +6 -5
  15. package/dist/client/vanilla.d.mts +6 -5
  16. package/dist/client/vue/index.d.mts +6 -5
  17. package/dist/context/create-context.mjs +1 -1
  18. package/dist/cookies/cookie-utils.mjs +2 -2
  19. package/dist/cookies/index.mjs +6 -1
  20. package/dist/db/internal-adapter.mjs +5 -0
  21. package/dist/oauth2/link-account.d.mts +13 -0
  22. package/dist/oauth2/link-account.mjs +1 -1
  23. package/dist/package.mjs +1 -1
  24. package/dist/plugins/admin/access/statement.d.mts +10 -10
  25. package/dist/plugins/admin/access/statement.mjs +2 -0
  26. package/dist/plugins/admin/admin.d.mts +6 -3
  27. package/dist/plugins/admin/client.d.mts +6 -4
  28. package/dist/plugins/admin/error-codes.d.mts +2 -0
  29. package/dist/plugins/admin/error-codes.mjs +3 -1
  30. package/dist/plugins/admin/routes.mjs +66 -2
  31. package/dist/plugins/admin/schema.d.mts +1 -0
  32. package/dist/plugins/admin/schema.mjs +2 -1
  33. package/dist/plugins/email-otp/routes.mjs +1 -1
  34. package/dist/plugins/generic-oauth/routes.mjs +9 -2
  35. package/dist/plugins/organization/organization.mjs +2 -0
  36. package/dist/plugins/organization/routes/crud-invites.mjs +10 -1
  37. package/dist/plugins/organization/routes/crud-team.mjs +15 -2
  38. package/dist/plugins/organization/schema.d.mts +2 -0
  39. package/dist/plugins/siwe/index.mjs +28 -0
  40. package/dist/plugins/siwe/parse-message.mjs +60 -0
  41. package/dist/plugins/two-factor/index.mjs +9 -1
  42. package/dist/test-utils/test-instance.d.mts +6 -5
  43. package/package.json +10 -10
@@ -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 };
@@ -141,6 +141,7 @@ async function validateFormCsrf(ctx) {
141
141
  }
142
142
  return await validateOrigin(ctx, true);
143
143
  }
144
+ if (headers.get("origin") || headers.get("referer")) return await validateOrigin(ctx, true);
144
145
  }
145
146
  //#endregion
146
147
  export { formCsrfMiddleware, originCheck, originCheckMiddleware };
@@ -225,9 +225,15 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
225
225
  * `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
226
226
  * unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
227
227
  * nor a `userId` is available.
228
+ *
229
+ * When a durable store is authoritative, bypasses the cookie cache: these
230
+ * routes mint or refresh provider access tokens, so a server-side session
231
+ * revocation must take effect immediately rather than waiting for the cached
232
+ * cookie to expire. DB-less deployments keep the session in the cookie itself,
233
+ * so the cache is left in place for them.
228
234
  */
229
235
  async function resolveUserId(ctx, userId) {
230
- const session = await getSessionFromCtx(ctx);
236
+ const session = await getSessionFromCtx(ctx, { disableCookieCache: !!ctx.context.options.database || !!ctx.context.options.secondaryStorage });
231
237
  if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
232
238
  const resolvedUserId = session?.user?.id || userId;
233
239
  if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
@@ -382,12 +388,11 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
382
388
  });
383
389
  let account = void 0;
384
390
  const accountData = await getAccountCookie(ctx);
385
- if (accountData && accountData.userId === resolvedUserId && (!providerId || providerId === accountData?.providerId)) account = accountData;
391
+ const usedAccountCookie = !!accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId);
392
+ if (usedAccountCookie) account = accountData;
386
393
  else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
387
394
  if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
388
- let refreshToken = void 0;
389
- if (accountData && providerId === accountData.providerId) refreshToken = accountData.refreshToken ?? void 0;
390
- else refreshToken = account.refreshToken ?? void 0;
395
+ const refreshToken = account.refreshToken ?? void 0;
391
396
  if (!refreshToken) throw APIError.from("BAD_REQUEST", {
392
397
  message: "Refresh token not found",
393
398
  code: "REFRESH_TOKEN_NOT_FOUND"
@@ -409,7 +414,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
409
414
  };
410
415
  await ctx.context.internalAdapter.updateAccount(account.id, updateData);
411
416
  }
412
- if (accountData && providerId === accountData.providerId && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
417
+ if (usedAccountCookie && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
413
418
  ...accountData,
414
419
  accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
415
420
  refreshToken: resolvedRefreshToken,
@@ -81,7 +81,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
81
81
  ...tokens,
82
82
  user: parsedUserData ?? void 0
83
83
  }).then((res) => res?.user);
84
- if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
84
+ if (!userInfo || userInfo.id === void 0 || userInfo.id === null || userInfo.id === "") {
85
85
  c.context.logger.error("Unable to get user info");
86
86
  redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
87
87
  }
@@ -276,7 +276,9 @@ const getSessionFromCtx = async (ctx, config) => {
276
276
  returnStatus: false,
277
277
  query: {
278
278
  ...config,
279
- ...ctx.query
279
+ ...ctx.query,
280
+ disableCookieCache: config?.disableCookieCache || ctx.query?.disableCookieCache,
281
+ disableRefresh: config?.disableRefresh || ctx.query?.disableRefresh
280
282
  }
281
283
  }).catch(() => {
282
284
  return null;
@@ -355,7 +357,7 @@ const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
355
357
  const listSessions = () => createAuthEndpoint("/list-sessions", {
356
358
  method: "GET",
357
359
  operationId: "listUserSessions",
358
- use: [sessionMiddleware],
360
+ use: [freshSessionMiddleware],
359
361
  requireHeaders: true,
360
362
  metadata: { openapi: {
361
363
  operationId: "listUserSessions",
@@ -1,5 +1,5 @@
1
1
  import { parseSessionInput, parseSessionOutput } from "../../db/schema.mjs";
2
- import { setSessionCookie } from "../../cookies/index.mjs";
2
+ import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
3
3
  import { sessionMiddleware } from "./session.mjs";
4
4
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import { createAuthEndpoint } from "@better-auth/core/api";
@@ -34,10 +34,16 @@ const updateSession = () => createAuthEndpoint("/update-session", {
34
34
  const session = ctx.context.session;
35
35
  const additionalFields = parseSessionInput(ctx.context.options, body, "update");
36
36
  if (Object.keys(additionalFields).length === 0) throw APIError.fromStatus("BAD_REQUEST", { message: "No fields to update" });
37
- const newSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
37
+ const updatedSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
38
38
  ...additionalFields,
39
39
  updatedAt: /* @__PURE__ */ new Date()
40
- }) ?? {
40
+ });
41
+ const isStateful = !!ctx.context.options.database || !!ctx.context.options.secondaryStorage;
42
+ if (!updatedSession && isStateful) {
43
+ deleteSessionCookie(ctx);
44
+ throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
45
+ }
46
+ const newSession = updatedSession ?? {
41
47
  ...session.session,
42
48
  ...additionalFields,
43
49
  updatedAt: /* @__PURE__ */ new Date()