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
|
@@ -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 };
|
package/dist/api/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/api/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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: [
|
|
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
|
|
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()
|