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
@@ -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 };
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
69
69
  priority?: RequestPriority | undefined;
70
70
  cache?: RequestCache | undefined;
71
71
  credentials?: RequestCredentials;
72
- headers?: (HeadersInit & (HeadersInit | {
73
- accept: "application/json" | "text/plain" | "application/octet-stream";
74
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
75
- authorization: "Bearer" | "Basic";
76
- })) | undefined;
77
72
  integrity?: string | undefined;
78
73
  keepalive?: boolean | undefined;
79
74
  method: string;
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
103
98
  prefix: string | (() => string | undefined) | undefined;
104
99
  value: string | (() => string | undefined) | undefined;
105
100
  }) | undefined;
101
+ headers?: {} | {
102
+ [x: string]: string | undefined;
103
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
104
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
105
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
106
+ } | undefined;
106
107
  body?: any;
107
108
  query?: any;
108
109
  params?: any;
@@ -70,11 +70,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
70
70
  priority?: RequestPriority | undefined;
71
71
  cache?: RequestCache | undefined;
72
72
  credentials?: RequestCredentials;
73
- headers?: (HeadersInit & (HeadersInit | {
74
- accept: "application/json" | "text/plain" | "application/octet-stream";
75
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
76
- authorization: "Bearer" | "Basic";
77
- })) | undefined;
78
73
  integrity?: string | undefined;
79
74
  keepalive?: boolean | undefined;
80
75
  method: string;
@@ -104,6 +99,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
104
99
  prefix: string | (() => string | undefined) | undefined;
105
100
  value: string | (() => string | undefined) | undefined;
106
101
  }) | undefined;
102
+ headers?: {} | {
103
+ [x: string]: string | undefined;
104
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
105
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
106
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
107
+ } | undefined;
107
108
  body?: any;
108
109
  query?: any;
109
110
  params?: any;
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
69
69
  priority?: RequestPriority | undefined;
70
70
  cache?: RequestCache | undefined;
71
71
  credentials?: RequestCredentials;
72
- headers?: (HeadersInit & (HeadersInit | {
73
- accept: "application/json" | "text/plain" | "application/octet-stream";
74
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
75
- authorization: "Bearer" | "Basic";
76
- })) | undefined;
77
72
  integrity?: string | undefined;
78
73
  keepalive?: boolean | undefined;
79
74
  method: string;
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
103
98
  prefix: string | (() => string | undefined) | undefined;
104
99
  value: string | (() => string | undefined) | undefined;
105
100
  }) | undefined;
101
+ headers?: {} | {
102
+ [x: string]: string | undefined;
103
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
104
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
105
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
106
+ } | undefined;
106
107
  body?: any;
107
108
  query?: any;
108
109
  params?: any;
@@ -55,11 +55,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
55
55
  priority?: RequestPriority | undefined;
56
56
  cache?: RequestCache | undefined;
57
57
  credentials?: RequestCredentials;
58
- headers?: (HeadersInit & (HeadersInit | {
59
- accept: "application/json" | "text/plain" | "application/octet-stream";
60
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
61
- authorization: "Bearer" | "Basic";
62
- })) | undefined;
63
58
  integrity?: string | undefined;
64
59
  keepalive?: boolean | undefined;
65
60
  method: string;
@@ -89,6 +84,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
89
84
  prefix: string | (() => string | undefined) | undefined;
90
85
  value: string | (() => string | undefined) | undefined;
91
86
  }) | undefined;
87
+ headers?: {} | {
88
+ [x: string]: string | undefined;
89
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
90
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
91
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
92
+ } | undefined;
92
93
  body?: any;
93
94
  query?: any;
94
95
  params?: any;
@@ -53,11 +53,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
53
53
  priority?: RequestPriority | undefined;
54
54
  cache?: RequestCache | undefined;
55
55
  credentials?: RequestCredentials;
56
- headers?: (HeadersInit & (HeadersInit | {
57
- accept: "application/json" | "text/plain" | "application/octet-stream";
58
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
59
- authorization: "Bearer" | "Basic";
60
- })) | undefined;
61
56
  integrity?: string | undefined;
62
57
  keepalive?: boolean | undefined;
63
58
  method: string;
@@ -87,6 +82,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
87
82
  prefix: string | (() => string | undefined) | undefined;
88
83
  value: string | (() => string | undefined) | undefined;
89
84
  }) | undefined;
85
+ headers?: {} | {
86
+ [x: string]: string | undefined;
87
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
88
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
89
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
90
+ } | undefined;
90
91
  body?: any;
91
92
  query?: any;
92
93
  params?: any;
@@ -93,11 +93,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
93
93
  priority?: RequestPriority | undefined;
94
94
  cache?: RequestCache | undefined;
95
95
  credentials?: RequestCredentials;
96
- headers?: (HeadersInit & (HeadersInit | {
97
- accept: "application/json" | "text/plain" | "application/octet-stream";
98
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
99
- authorization: "Bearer" | "Basic";
100
- })) | undefined;
101
96
  integrity?: string | undefined;
102
97
  keepalive?: boolean | undefined;
103
98
  method: string;
@@ -127,6 +122,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
127
122
  prefix: string | (() => string | undefined) | undefined;
128
123
  value: string | (() => string | undefined) | undefined;
129
124
  }) | undefined;
125
+ headers?: {} | {
126
+ [x: string]: string | undefined;
127
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
128
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
129
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
130
+ } | undefined;
130
131
  body?: any;
131
132
  query?: any;
132
133
  params?: any;
@@ -59,7 +59,7 @@ async function createAuthContext(adapter, options, getDatabaseType) {
59
59
  if (!allowedHosts || allowedHosts.length === 0) throw new BetterAuthError("baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"]).");
60
60
  }
61
61
  const baseURL = isDynamicConfig ? void 0 : getBaseURL(typeof options.baseURL === "string" ? options.baseURL : void 0, options.basePath);
62
- if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL could not be determined. Please set a valid base URL using the baseURL config option or the BETTER_AUTH_URL environment variable. Without this, callbacks and redirects may not work correctly.`);
62
+ if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL is not set. Set the baseURL option or BETTER_AUTH_URL env, or use a dynamic baseURL with allowedHosts for multi-host setups. Without it the origin is derived from the incoming request, and callbacks and redirects may not work correctly.`);
63
63
  if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger.error(`[better-auth] Misconfiguration detected.
64
64
  You are using the memory DB with generateId: false.
65
65
  This will cause no id to be generated for any model.
@@ -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
  *
@@ -116,7 +116,12 @@ async function setCookieCache(ctx, session, dontRememberMe) {
116
116
  }
117
117
  if (ctx.context.options.account?.storeAccountCookie) {
118
118
  const accountData = await getAccountCookie(ctx);
119
- if (accountData) await setAccountCookie(ctx, accountData);
119
+ if (accountData) if (accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
120
+ else {
121
+ expireCookie(ctx, ctx.context.authCookies.accountData);
122
+ const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx);
123
+ accountStore.setCookies(accountStore.clean());
124
+ }
120
125
  }
121
126
  }
122
127
  async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
@@ -16,6 +16,7 @@ const createInternalAdapter = (adapter, ctx) => {
16
16
  const options = ctx.options;
17
17
  const secondaryStorage = options.secondaryStorage;
18
18
  const verificationConsumeLocks = /* @__PURE__ */ new Map();
19
+ let warnedNonAtomicConsume = false;
19
20
  const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
20
21
  const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
21
22
  async function refreshUserSessions(user) {
@@ -653,6 +654,10 @@ const createInternalAdapter = (adapter, ctx) => {
653
654
  if (secondaryStorage && !options.verification?.storeInDatabase) {
654
655
  const consumeCacheKey = async (key) => {
655
656
  if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
657
+ if (!warnedNonAtomicConsume) {
658
+ warnedNonAtomicConsume = true;
659
+ logger.warn("Secondary storage does not implement `getAndDelete`, so single-use verification values cannot be consumed atomically across processes. Implement `getAndDelete` or use database-backed verification storage to guarantee single use.");
660
+ }
656
661
  return withVerificationConsumeLock(key, async () => {
657
662
  const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
658
663
  if (!parsed) return null;
@@ -9,6 +9,19 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
9
9
  disableSignUp?: boolean | undefined;
10
10
  overrideUserInfo?: boolean | undefined;
11
11
  isTrustedProvider?: boolean | undefined;
12
+ /**
13
+ * Whether `account.providerId` may be matched against the globally
14
+ * configured `accountLinking.trustedProviders` list to infer trust.
15
+ *
16
+ * Defaults to `true` for built-in social/OAuth providers, whose
17
+ * `providerId` namespace is controlled by the developer's config. Callers
18
+ * whose `providerId` is user-controlled (e.g. the SSO plugin, where any
19
+ * authenticated user can register a provider with an arbitrary id) must
20
+ * pass `false` so a provider named after a trusted social provider can't
21
+ * launder that trust. Such callers should supply their own
22
+ * `isTrustedProvider` signal instead.
23
+ */
24
+ trustProviderByName?: boolean | undefined;
12
25
  }): Promise<{
13
26
  error: string;
14
27
  data: null;
@@ -17,7 +17,7 @@ async function handleOAuthUserInfo(c, opts) {
17
17
  const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
18
18
  if (!linkedAccount) {
19
19
  const accountLinking = c.context.options.account?.accountLinking;
20
- const isTrustedProvider = opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId);
20
+ const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
21
21
  const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
22
22
  if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
23
23
  if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`);
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.16";
3
3
  //#endregion
4
4
  export { version };
@@ -1,49 +1,49 @@
1
1
  import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/types.mjs";
2
2
  //#region src/plugins/admin/access/statement.d.ts
3
3
  declare const defaultStatements: {
4
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
4
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
5
5
  readonly session: readonly ["list", "revoke", "delete"];
6
6
  };
7
7
  declare const defaultAc: {
8
8
  newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
9
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
9
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
10
10
  readonly session: readonly ["list", "revoke", "delete"];
11
11
  }, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
12
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
12
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
13
13
  readonly session: readonly ["list", "revoke", "delete"];
14
14
  }>;
15
15
  statements: {
16
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
16
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
17
17
  readonly session: readonly ["list", "revoke", "delete"];
18
18
  };
19
19
  };
20
20
  declare const adminAc: Role<ExactRoleStatements<{
21
- readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
21
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
22
22
  readonly session: ["list", "revoke", "delete"];
23
23
  }>, {
24
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
24
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
25
25
  readonly session: readonly ["list", "revoke", "delete"];
26
26
  }>;
27
27
  declare const userAc: Role<ExactRoleStatements<{
28
28
  readonly user: [];
29
29
  readonly session: [];
30
30
  }>, {
31
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
31
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
32
32
  readonly session: readonly ["list", "revoke", "delete"];
33
33
  }>;
34
34
  declare const defaultRoles: {
35
35
  admin: Role<ExactRoleStatements<{
36
- readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
36
+ readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
37
37
  readonly session: ["list", "revoke", "delete"];
38
38
  }>, {
39
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
39
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
40
40
  readonly session: readonly ["list", "revoke", "delete"];
41
41
  }>;
42
42
  user: Role<ExactRoleStatements<{
43
43
  readonly user: [];
44
44
  readonly session: [];
45
45
  }>, {
46
- readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
46
+ readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
47
47
  readonly session: readonly ["list", "revoke", "delete"];
48
48
  }>;
49
49
  };
@@ -10,6 +10,7 @@ const defaultStatements = {
10
10
  "impersonate-admins",
11
11
  "delete",
12
12
  "set-password",
13
+ "set-email",
13
14
  "get",
14
15
  "update"
15
16
  ],
@@ -29,6 +30,7 @@ const adminAc = defaultAc.newRole({
29
30
  "impersonate",
30
31
  "delete",
31
32
  "set-password",
33
+ "set-email",
32
34
  "get",
33
35
  "update"
34
36
  ],