better-auth 1.6.13 → 1.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/routes/session.mjs +1 -1
- package/dist/api/to-auth-endpoints.mjs +14 -265
- package/dist/cookies/cookie-utils.mjs +2 -2
- package/dist/cookies/index.mjs +1 -1
- package/dist/db/to-zod.d.mts +2 -2
- package/dist/db/to-zod.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +3 -0
- package/dist/plugins/organization/adapter.mjs +2 -0
- package/dist/plugins/organization/routes/crud-access-control.d.mts +1 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +30 -4
- package/dist/plugins/organization/types.d.mts +18 -11
- package/package.json +8 -8
|
@@ -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 };
|
|
@@ -355,7 +355,7 @@ const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
|
|
|
355
355
|
const listSessions = () => createAuthEndpoint("/list-sessions", {
|
|
356
356
|
method: "GET",
|
|
357
357
|
operationId: "listUserSessions",
|
|
358
|
-
use: [
|
|
358
|
+
use: [freshSessionMiddleware],
|
|
359
359
|
requireHeaders: true,
|
|
360
360
|
metadata: { openapi: {
|
|
361
361
|
operationId: "listUserSessions",
|
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import { isAPIError } from "../utils/is-api-error.mjs";
|
|
2
1
|
import { isDynamicBaseURLConfig, isRequestLike } from "../utils/url.mjs";
|
|
3
2
|
import { pickSource, resolveDynamicTrustedProxyHeaders, resolveRequestContext } from "../context/helpers.mjs";
|
|
4
|
-
import {
|
|
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 };
|
|
@@ -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
|
@@ -199,7 +199,7 @@ const getSessionCookie = (request, config) => {
|
|
|
199
199
|
if (!cookies) return null;
|
|
200
200
|
const { cookieName = "session_token", cookiePrefix = "better-auth" } = config || {};
|
|
201
201
|
const parsedCookie = parseCookies(cookies);
|
|
202
|
-
const getCookie = (name) => parsedCookie.get(name)
|
|
202
|
+
const getCookie = (name) => parsedCookie.get(`__Secure-${name}`) ?? parsedCookie.get(name);
|
|
203
203
|
const sessionToken = getCookie(`${cookiePrefix}.${cookieName}`) || getCookie(`${cookiePrefix}-${cookieName}`);
|
|
204
204
|
if (sessionToken) return sessionToken;
|
|
205
205
|
return null;
|
package/dist/db/to-zod.d.mts
CHANGED
|
@@ -25,8 +25,8 @@ type GetType<F extends DBFieldAttribute> = F extends {
|
|
|
25
25
|
type: "date";
|
|
26
26
|
} ? z.ZodDate : z.ZodAny;
|
|
27
27
|
type GetRequired<F extends DBFieldAttribute, Schema extends z.core.SomeType> = F extends {
|
|
28
|
-
required:
|
|
29
|
-
} ?
|
|
28
|
+
required: false;
|
|
29
|
+
} ? z.ZodOptional<z.ZodNullable<Schema>> : Schema;
|
|
30
30
|
type GetInput<isClientSide extends boolean, Field extends DBFieldAttribute, Schema extends z.core.SomeType> = Field extends {
|
|
31
31
|
input: false;
|
|
32
32
|
} ? isClientSide extends true ? never : Schema : Schema;
|
package/dist/db/to-zod.mjs
CHANGED
|
@@ -10,7 +10,7 @@ function toZodSchema({ fields, isClientSide }) {
|
|
|
10
10
|
else if (field.type === "string[]" || field.type === "number[]") schema = z.array(field.type === "string[]" ? z.string() : z.number());
|
|
11
11
|
else if (Array.isArray(field.type)) schema = z.any();
|
|
12
12
|
else schema = z[field.type]();
|
|
13
|
-
if (field?.required === false) schema = schema.
|
|
13
|
+
if (field?.required === false) schema = schema.nullish();
|
|
14
14
|
if (!isClientSide && field?.returned === false) return acc;
|
|
15
15
|
return {
|
|
16
16
|
...acc,
|
package/dist/package.mjs
CHANGED
|
@@ -72,6 +72,7 @@ const setRole = (opts) => createAuthEndpoint("/admin/set-role", {
|
|
|
72
72
|
const inputRoles = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role];
|
|
73
73
|
for (const role of inputRoles) if (!roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
|
|
74
74
|
}
|
|
75
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
75
76
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, { role: parseRoles(ctx.body.role) });
|
|
76
77
|
return ctx.json({ user: parseUserOutput(ctx.context.options, updatedUser) });
|
|
77
78
|
});
|
|
@@ -234,6 +235,7 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
234
235
|
}
|
|
235
236
|
ctx.body.data.role = parseRoles(inputRoles);
|
|
236
237
|
}
|
|
238
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
237
239
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
238
240
|
return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
|
|
239
241
|
});
|
|
@@ -402,6 +404,7 @@ const unbanUser = (opts) => createAuthEndpoint("/admin/unban-user", {
|
|
|
402
404
|
options: opts,
|
|
403
405
|
permissions: { user: ["ban"] }
|
|
404
406
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
407
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
405
408
|
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
406
409
|
banned: false,
|
|
407
410
|
banExpires: null,
|
|
@@ -550,9 +550,11 @@ const getOrgAdapter = (context, options) => {
|
|
|
550
550
|
createInvitation: async ({ invitation, user }) => {
|
|
551
551
|
const adapter = await getCurrentAdapter(baseAdapter);
|
|
552
552
|
const expiresAt = getDate(options?.invitationExpiresIn || 3600 * 48, "sec");
|
|
553
|
+
const invitationId = context.generateId({ model: "invitation" });
|
|
553
554
|
return await adapter.create({
|
|
554
555
|
model: "invitation",
|
|
555
556
|
data: {
|
|
557
|
+
...invitationId !== false ? { id: invitationId } : {},
|
|
556
558
|
status: "pending",
|
|
557
559
|
expiresAt,
|
|
558
560
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -14,7 +14,7 @@ declare const createOrgRole: <O extends OrganizationOptions>(options: O) => bett
|
|
|
14
14
|
role: z.ZodString;
|
|
15
15
|
permission: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
16
16
|
additionalFields: z.ZodOptional<z.ZodObject<{
|
|
17
|
-
[x: string]: z.
|
|
17
|
+
[x: string]: z.ZodAny;
|
|
18
18
|
}, z.core.$strip>>;
|
|
19
19
|
}, z.core.$strip>;
|
|
20
20
|
metadata: {
|
|
@@ -19,6 +19,20 @@ const baseInvitationSchema = z.object({
|
|
|
19
19
|
resend: z.boolean().meta({ description: "Resend the invitation email, if the user is already invited. Eg: true" }).optional(),
|
|
20
20
|
teamId: z.union([z.string().meta({ description: "The team ID to invite the user to" }).optional(), z.array(z.string()).meta({ description: "The team IDs to invite the user to" }).optional()])
|
|
21
21
|
});
|
|
22
|
+
const getAdvancedGenerateId = (advancedOptions) => {
|
|
23
|
+
if (typeof advancedOptions !== "object" || advancedOptions === null) return;
|
|
24
|
+
const generateId = advancedOptions.generateId;
|
|
25
|
+
if (typeof generateId !== "function") return;
|
|
26
|
+
return generateId;
|
|
27
|
+
};
|
|
28
|
+
const hasBuiltInOpaqueInvitationIdGeneration = ({ advancedGenerateId, databaseGenerateId }) => advancedGenerateId === void 0 && (databaseGenerateId === void 0 || databaseGenerateId === "uuid");
|
|
29
|
+
const shouldRequireVerifiedEmailForInvitationIdAction = ({ organizationOptions, advancedGenerateId, databaseGenerateId }) => {
|
|
30
|
+
if (organizationOptions.requireEmailVerificationOnInvitation !== void 0) return organizationOptions.requireEmailVerificationOnInvitation;
|
|
31
|
+
return !hasBuiltInOpaqueInvitationIdGeneration({
|
|
32
|
+
advancedGenerateId,
|
|
33
|
+
databaseGenerateId
|
|
34
|
+
});
|
|
35
|
+
};
|
|
22
36
|
const createInvitation = (option) => {
|
|
23
37
|
const additionalFieldsSchema = toZodSchema({
|
|
24
38
|
fields: option?.schema?.invitation?.additionalFields || {},
|
|
@@ -247,7 +261,11 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
|
|
|
247
261
|
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
|
248
262
|
if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND);
|
|
249
263
|
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION);
|
|
250
|
-
if ((
|
|
264
|
+
if (shouldRequireVerifiedEmailForInvitationIdAction({
|
|
265
|
+
organizationOptions: ctx.context.orgOptions,
|
|
266
|
+
advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced),
|
|
267
|
+
databaseGenerateId: ctx.context.options.advanced?.database?.generateId
|
|
268
|
+
}) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION);
|
|
251
269
|
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
|
|
252
270
|
const membersCount = await adapter.countMembers({ organizationId: invitation.organizationId });
|
|
253
271
|
const organization = await adapter.findOrganizationById(invitation.organizationId);
|
|
@@ -336,7 +354,11 @@ const rejectInvitation = (options) => createAuthEndpoint("/organization/reject-i
|
|
|
336
354
|
code: "INVITATION_NOT_FOUND"
|
|
337
355
|
});
|
|
338
356
|
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION);
|
|
339
|
-
if ((
|
|
357
|
+
if (shouldRequireVerifiedEmailForInvitationIdAction({
|
|
358
|
+
organizationOptions: ctx.context.orgOptions,
|
|
359
|
+
advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced),
|
|
360
|
+
databaseGenerateId: ctx.context.options.advanced?.database?.generateId
|
|
361
|
+
}) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION);
|
|
340
362
|
const organization = await adapter.findOrganizationById(invitation.organizationId);
|
|
341
363
|
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
|
|
342
364
|
if (options?.organizationHooks?.beforeRejectInvitation) await options?.organizationHooks.beforeRejectInvitation({
|
|
@@ -455,7 +477,11 @@ const getInvitation = (options) => createAuthEndpoint("/organization/get-invitat
|
|
|
455
477
|
const invitation = await adapter.findInvitationById(ctx.query.id);
|
|
456
478
|
if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) throw APIError.fromStatus("BAD_REQUEST", { message: "Invitation not found!" });
|
|
457
479
|
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION);
|
|
458
|
-
if ((
|
|
480
|
+
if (shouldRequireVerifiedEmailForInvitationIdAction({
|
|
481
|
+
organizationOptions: ctx.context.orgOptions,
|
|
482
|
+
advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced),
|
|
483
|
+
databaseGenerateId: ctx.context.options.advanced?.database?.generateId
|
|
484
|
+
}) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION);
|
|
459
485
|
const organization = await adapter.findOrganizationById(invitation.organizationId);
|
|
460
486
|
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
|
|
461
487
|
const member = await adapter.findMemberByOrgId({
|
|
@@ -541,7 +567,7 @@ const listUserInvitations = (options) => createAuthEndpoint("/organization/list-
|
|
|
541
567
|
}, async (ctx) => {
|
|
542
568
|
const session = await getSessionFromCtx(ctx);
|
|
543
569
|
if (ctx.request && ctx.query?.email) throw APIError.fromStatus("BAD_REQUEST", { message: "User email cannot be passed for client side API calls." });
|
|
544
|
-
if (session &&
|
|
570
|
+
if (session && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION);
|
|
545
571
|
const userEmail = session?.user.email || ctx.query?.email;
|
|
546
572
|
if (!userEmail) throw APIError.fromStatus("BAD_REQUEST", { message: "Missing session headers, or email query parameter." });
|
|
547
573
|
const pendingInvitations = (await getOrgAdapter(ctx.context, options).listUserInvitations(userEmail)).filter((inv) => inv.status === "pending");
|
|
@@ -165,19 +165,26 @@ interface OrganizationOptions {
|
|
|
165
165
|
*/
|
|
166
166
|
cancelPendingInvitationsOnReInvite?: boolean | undefined;
|
|
167
167
|
/**
|
|
168
|
-
* Require email verification
|
|
169
|
-
* calls (accept, reject, get
|
|
170
|
-
* accounts registered against a victim's email cannot accept, read, or
|
|
171
|
-
* enumerate invitations targeted at that email. Server-side
|
|
172
|
-
* `listUserInvitations` calls without a session (caller passes
|
|
173
|
-
* `ctx.query.email`) continue to bypass the gate because the caller is
|
|
174
|
-
* trusted. Set to `false` for backward compatibility on apps that do not
|
|
175
|
-
* require email verification; understand the takeover risk before doing so.
|
|
168
|
+
* Require email verification before session-authenticated recipient
|
|
169
|
+
* invitation calls that carry an invitation ID (accept, reject, get).
|
|
176
170
|
*
|
|
177
|
-
*
|
|
171
|
+
* When unset, Better Auth preserves the normal emailed-invitation flow for
|
|
172
|
+
* built-in opaque invitation IDs, including the default generator and
|
|
173
|
+
* `advanced.database.generateId: "uuid"`. It requires verification for
|
|
174
|
+
* externally controlled or predictable invitation IDs, such as
|
|
175
|
+
* `advanced.database.generateId: "serial"` / `false` or custom ID
|
|
176
|
+
* generation.
|
|
177
|
+
*
|
|
178
|
+
* Set this option to `true` when invitation IDs may be visible outside the
|
|
179
|
+
* invited user's mailbox, when organization invitation lists are exposed to
|
|
180
|
+
* members, or when verified email should be the ownership proof for by-ID
|
|
181
|
+
* invitation actions. Client-side `listUserInvitations` calls always require
|
|
182
|
+
* a verified session email because they enumerate invitation IDs from
|
|
183
|
+
* `session.user.email`. Server-side `listUserInvitations` calls without a
|
|
184
|
+
* session (caller passes `ctx.query.email`) continue to bypass the gate
|
|
185
|
+
* because the caller is trusted.
|
|
178
186
|
*
|
|
179
|
-
* @
|
|
180
|
-
* become unconditional. Plan to verify emails before invitation acceptance.
|
|
187
|
+
* @default undefined
|
|
181
188
|
*/
|
|
182
189
|
requireEmailVerificationOnInvitation?: boolean | undefined;
|
|
183
190
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.15",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -489,13 +489,13 @@
|
|
|
489
489
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
490
490
|
"nanostores": "^1.1.1",
|
|
491
491
|
"zod": "^4.3.6",
|
|
492
|
-
"@better-auth/core": "1.6.
|
|
493
|
-
"@better-auth/drizzle-adapter": "1.6.
|
|
494
|
-
"@better-auth/kysely-adapter": "1.6.
|
|
495
|
-
"@better-auth/memory-adapter": "1.6.
|
|
496
|
-
"@better-auth/mongo-adapter": "1.6.
|
|
497
|
-
"@better-auth/prisma-adapter": "1.6.
|
|
498
|
-
"@better-auth/telemetry": "1.6.
|
|
492
|
+
"@better-auth/core": "1.6.15",
|
|
493
|
+
"@better-auth/drizzle-adapter": "1.6.15",
|
|
494
|
+
"@better-auth/kysely-adapter": "1.6.15",
|
|
495
|
+
"@better-auth/memory-adapter": "1.6.15",
|
|
496
|
+
"@better-auth/mongo-adapter": "1.6.15",
|
|
497
|
+
"@better-auth/prisma-adapter": "1.6.15",
|
|
498
|
+
"@better-auth/telemetry": "1.6.15"
|
|
499
499
|
},
|
|
500
500
|
"devDependencies": {
|
|
501
501
|
"@lynx-js/react": "^0.116.3",
|