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.
- package/dist/api/dispatch.d.mts +34 -0
- package/dist/api/dispatch.mjs +272 -0
- package/dist/api/index.d.mts +2 -1
- package/dist/api/index.mjs +2 -1
- package/dist/api/middlewares/origin-check.mjs +1 -0
- package/dist/api/routes/account.mjs +11 -6
- package/dist/api/routes/callback.mjs +1 -1
- package/dist/api/routes/session.mjs +4 -2
- package/dist/api/routes/update-session.mjs +9 -3
- package/dist/api/to-auth-endpoints.mjs +14 -265
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +1 -1
- package/dist/cookies/cookie-utils.mjs +2 -2
- package/dist/cookies/index.mjs +6 -1
- package/dist/db/internal-adapter.mjs +5 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +66 -2
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/routes.mjs +9 -2
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +10 -1
- package/dist/plugins/organization/routes/crud-team.mjs +15 -2
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/siwe/index.mjs +28 -0
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/test-utils/test-instance.d.mts +6 -5
- 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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
42
|
+
return dispatchAuthEndpoint(endpoint, {
|
|
57
43
|
...context,
|
|
58
|
-
context:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
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 = /^[\
|
|
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/cookies/index.mjs
CHANGED
|
@@ -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,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
|
],
|