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
|
@@ -824,13 +824,13 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
824
824
|
$Infer: {
|
|
825
825
|
body: {
|
|
826
826
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
827
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
827
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
828
828
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
829
829
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
830
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
830
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
831
831
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
832
832
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
833
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
833
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
834
834
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
835
835
|
})[key]> : never)[] | undefined };
|
|
836
836
|
} & {
|
|
@@ -866,6 +866,8 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
866
866
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
867
867
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
868
868
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
869
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
870
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
869
871
|
};
|
|
870
872
|
schema: {
|
|
871
873
|
user: {
|
|
@@ -898,6 +900,7 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
898
900
|
impersonatedBy: {
|
|
899
901
|
type: "string";
|
|
900
902
|
required: false;
|
|
903
|
+
input: false;
|
|
901
904
|
};
|
|
902
905
|
};
|
|
903
906
|
};
|
|
@@ -14,7 +14,7 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
14
14
|
version: string;
|
|
15
15
|
$InferServerPlugin: ReturnType<typeof admin<{
|
|
16
16
|
ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<{
|
|
17
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
17
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
18
18
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
19
19
|
}>;
|
|
20
20
|
roles: O["roles"] extends Record<string, Role> ? O["roles"] : {
|
|
@@ -28,13 +28,13 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
28
28
|
roles: any;
|
|
29
29
|
} ? keyof O["roles"] : "admin" | "user")>(data: {
|
|
30
30
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
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
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
34
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
34
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
35
35
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
36
36
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
37
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
37
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
38
38
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
39
39
|
})[key]> : never)[] | undefined };
|
|
40
40
|
} & {
|
|
@@ -73,6 +73,8 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
73
73
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
74
74
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
75
75
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
76
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
77
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
76
78
|
};
|
|
77
79
|
};
|
|
78
80
|
//#endregion
|
|
@@ -23,6 +23,8 @@ declare const ADMIN_ERROR_CODES: {
|
|
|
23
23
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
24
24
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
25
25
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
26
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
27
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
26
28
|
};
|
|
27
29
|
//#endregion
|
|
28
30
|
export { ADMIN_ERROR_CODES };
|
|
@@ -21,7 +21,9 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
|
|
|
21
21
|
YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself",
|
|
22
22
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value",
|
|
23
23
|
YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
|
|
24
|
-
INVALID_ROLE_TYPE: "Invalid role type"
|
|
24
|
+
INVALID_ROLE_TYPE: "Invalid role type",
|
|
25
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: "You are not allowed to update users email",
|
|
26
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead"
|
|
25
27
|
});
|
|
26
28
|
//#endregion
|
|
27
29
|
export { ADMIN_ERROR_CODES };
|
|
@@ -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
|
});
|
|
@@ -155,14 +156,43 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
155
156
|
permissions: { user: ["create"] }
|
|
156
157
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS);
|
|
157
158
|
}
|
|
159
|
+
const { role: dataRole, ...userData } = ctx.body.data ?? {};
|
|
160
|
+
const requestedRole = ctx.body.role ?? dataRole;
|
|
161
|
+
if (requestedRole !== void 0) {
|
|
162
|
+
if (session) {
|
|
163
|
+
if (!hasPermission({
|
|
164
|
+
userId: session.user.id,
|
|
165
|
+
role: session.user.role,
|
|
166
|
+
options: opts,
|
|
167
|
+
permissions: { user: ["set-role"] }
|
|
168
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE);
|
|
169
|
+
}
|
|
170
|
+
const inputRoles = Array.isArray(requestedRole) ? requestedRole : [requestedRole];
|
|
171
|
+
for (const role of inputRoles) {
|
|
172
|
+
if (typeof role !== "string") throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.INVALID_ROLE_TYPE);
|
|
173
|
+
if (opts.roles && !opts.roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (session && [
|
|
177
|
+
"banned",
|
|
178
|
+
"banReason",
|
|
179
|
+
"banExpires"
|
|
180
|
+
].some((key) => Object.prototype.hasOwnProperty.call(userData, key))) {
|
|
181
|
+
if (!hasPermission({
|
|
182
|
+
userId: session.user.id,
|
|
183
|
+
role: session.user.role,
|
|
184
|
+
options: opts,
|
|
185
|
+
permissions: { user: ["ban"] }
|
|
186
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
187
|
+
}
|
|
158
188
|
const email = ctx.body.email.toLowerCase();
|
|
159
189
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
160
190
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
161
191
|
const user = await ctx.context.internalAdapter.createUser({
|
|
192
|
+
...userData,
|
|
162
193
|
email,
|
|
163
194
|
name: ctx.body.name,
|
|
164
|
-
role:
|
|
165
|
-
...ctx.body.data
|
|
195
|
+
role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
|
|
166
196
|
});
|
|
167
197
|
if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
|
|
168
198
|
if (ctx.body.password) {
|
|
@@ -219,6 +249,9 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
219
249
|
permissions: { user: ["update"] }
|
|
220
250
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS);
|
|
221
251
|
if (Object.keys(ctx.body.data).length === 0) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE);
|
|
252
|
+
const updateData = ctx.body.data;
|
|
253
|
+
const hasDataKey = (key) => Object.prototype.hasOwnProperty.call(updateData, key);
|
|
254
|
+
if (hasDataKey("password")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER);
|
|
222
255
|
if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
|
|
223
256
|
if (!hasPermission({
|
|
224
257
|
userId: ctx.context.session.user.id,
|
|
@@ -234,7 +267,37 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
234
267
|
}
|
|
235
268
|
ctx.body.data.role = parseRoles(inputRoles);
|
|
236
269
|
}
|
|
270
|
+
if ([
|
|
271
|
+
"banned",
|
|
272
|
+
"banReason",
|
|
273
|
+
"banExpires"
|
|
274
|
+
].some(hasDataKey)) {
|
|
275
|
+
if (!hasPermission({
|
|
276
|
+
userId: ctx.context.session.user.id,
|
|
277
|
+
role: ctx.context.session.user.role,
|
|
278
|
+
options: opts,
|
|
279
|
+
permissions: { user: ["ban"] }
|
|
280
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
281
|
+
if (updateData.banned === true && ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF);
|
|
282
|
+
}
|
|
283
|
+
if (hasDataKey("email") || hasDataKey("emailVerified")) {
|
|
284
|
+
if (!hasPermission({
|
|
285
|
+
userId: ctx.context.session.user.id,
|
|
286
|
+
role: ctx.context.session.user.role,
|
|
287
|
+
options: opts,
|
|
288
|
+
permissions: { user: ["set-email"] }
|
|
289
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL);
|
|
290
|
+
if (hasDataKey("email")) {
|
|
291
|
+
const email = String(updateData.email).toLowerCase();
|
|
292
|
+
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
293
|
+
const existUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
294
|
+
if (existUser && existUser.user.id !== ctx.body.userId) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
295
|
+
updateData.email = email;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
237
299
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
300
|
+
if (updateData.banned === true) await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
238
301
|
return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
|
|
239
302
|
});
|
|
240
303
|
const listUsersQuerySchema = z.object({
|
|
@@ -402,6 +465,7 @@ const unbanUser = (opts) => createAuthEndpoint("/admin/unban-user", {
|
|
|
402
465
|
options: opts,
|
|
403
466
|
permissions: { user: ["ban"] }
|
|
404
467
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
468
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
405
469
|
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
406
470
|
banned: false,
|
|
407
471
|
banExpires: null,
|
|
@@ -334,7 +334,7 @@ const verifyEmailOTP = (opts) => createAuthEndpoint("/email-otp/verify-email", {
|
|
|
334
334
|
});
|
|
335
335
|
}
|
|
336
336
|
const currentSession = await getSessionFromCtx(ctx);
|
|
337
|
-
if (currentSession && updatedUser.emailVerified) {
|
|
337
|
+
if (currentSession && updatedUser.emailVerified && currentSession.user.id === updatedUser.id) {
|
|
338
338
|
const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
|
|
339
339
|
await setCookieCache(ctx, {
|
|
340
340
|
session: currentSession.session,
|
|
@@ -209,7 +209,12 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
209
209
|
ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
|
|
210
210
|
redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
|
|
211
211
|
}
|
|
212
|
-
const
|
|
212
|
+
const rawId = mapUser.id !== void 0 && mapUser.id !== null && mapUser.id !== "" ? mapUser.id : userInfo.id;
|
|
213
|
+
const id = rawId !== void 0 && rawId !== null ? String(rawId) : "";
|
|
214
|
+
if (!id) {
|
|
215
|
+
ctx.context.logger.error("Provider did not return an account id (e.g. `sub`). Unable to sign in.", userInfo);
|
|
216
|
+
redirectOnError(ctx, resolvedErrorURL, "id_is_missing");
|
|
217
|
+
}
|
|
213
218
|
const name = mapUser.name ? mapUser.name : userInfo.name;
|
|
214
219
|
if (!name) {
|
|
215
220
|
ctx.context.logger.error("Unable to get user info", userInfo);
|
|
@@ -398,8 +403,10 @@ async function getUserInfo(tokens, finalUserInfoUrl) {
|
|
|
398
403
|
method: "GET",
|
|
399
404
|
headers: { Authorization: `Bearer ${tokens.accessToken}` }
|
|
400
405
|
});
|
|
406
|
+
const subjectId = userInfo.data?.sub ?? userInfo.data?.id;
|
|
407
|
+
if (subjectId === void 0 || subjectId === null || subjectId === "") return null;
|
|
401
408
|
return {
|
|
402
|
-
id:
|
|
409
|
+
id: subjectId,
|
|
403
410
|
emailVerified: userInfo.data?.email_verified ?? false,
|
|
404
411
|
email: userInfo.data?.email,
|
|
405
412
|
image: userInfo.data?.picture,
|
|
@@ -394,11 +394,13 @@ function organization(options) {
|
|
|
394
394
|
activeOrganizationId: {
|
|
395
395
|
type: "string",
|
|
396
396
|
required: false,
|
|
397
|
+
input: false,
|
|
397
398
|
fieldName: opts.schema?.session?.fields?.activeOrganizationId
|
|
398
399
|
},
|
|
399
400
|
...teamSupport ? { activeTeamId: {
|
|
400
401
|
type: "string",
|
|
401
402
|
required: false,
|
|
403
|
+
input: false,
|
|
402
404
|
fieldName: opts.schema?.session?.fields?.activeTeamId
|
|
403
405
|
} } : {}
|
|
404
406
|
} }
|
|
@@ -170,7 +170,12 @@ const createInvitation = (option) => {
|
|
|
170
170
|
}, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
|
|
171
171
|
if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
|
|
172
172
|
if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
|
|
173
|
-
|
|
173
|
+
const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
174
|
+
if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
|
|
175
|
+
for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({
|
|
176
|
+
teamId,
|
|
177
|
+
organizationId
|
|
178
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
174
179
|
}
|
|
175
180
|
if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
|
|
176
181
|
const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
@@ -285,6 +290,10 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
|
|
|
285
290
|
const teamIds = acceptedI.teamId.split(",");
|
|
286
291
|
const onlyOne = teamIds.length === 1;
|
|
287
292
|
for (const teamId of teamIds) {
|
|
293
|
+
if (!await adapter.findTeamById({
|
|
294
|
+
teamId,
|
|
295
|
+
organizationId: invitation.organizationId
|
|
296
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
288
297
|
await adapter.findOrCreateTeamMember({
|
|
289
298
|
teamId,
|
|
290
299
|
userId: session.user.id
|
|
@@ -441,8 +441,15 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
|
|
|
441
441
|
use: [orgMiddleware, orgSessionMiddleware]
|
|
442
442
|
}, async (ctx) => {
|
|
443
443
|
const session = ctx.context.session;
|
|
444
|
-
const
|
|
445
|
-
|
|
444
|
+
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
445
|
+
const teams = await adapter.listTeamsByUser({ userId: session.user.id });
|
|
446
|
+
const orgIds = [...new Set(teams.map((team) => team.organizationId))];
|
|
447
|
+
const memberships = await Promise.all(orgIds.map((organizationId) => adapter.checkMembership({
|
|
448
|
+
userId: session.user.id,
|
|
449
|
+
organizationId
|
|
450
|
+
})));
|
|
451
|
+
const memberOrgIds = new Set(orgIds.filter((_, index) => memberships[index]));
|
|
452
|
+
return ctx.json(teams.filter((team) => memberOrgIds.has(team.organizationId)));
|
|
446
453
|
});
|
|
447
454
|
const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
|
|
448
455
|
const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", {
|
|
@@ -494,6 +501,12 @@ const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team
|
|
|
494
501
|
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
495
502
|
const teamId = ctx.query?.teamId || session?.session.activeTeamId;
|
|
496
503
|
if (!teamId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM);
|
|
504
|
+
const team = await adapter.findTeamById({ teamId });
|
|
505
|
+
if (!team) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
506
|
+
if (!await adapter.checkMembership({
|
|
507
|
+
userId: session.user.id,
|
|
508
|
+
organizationId: team.organizationId
|
|
509
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM);
|
|
497
510
|
if (!await adapter.findTeamMember({
|
|
498
511
|
userId: session.user.id,
|
|
499
512
|
teamId
|
|
@@ -183,6 +183,7 @@ interface SessionDefaultFields {
|
|
|
183
183
|
activeOrganizationId: {
|
|
184
184
|
type: "string";
|
|
185
185
|
required: false;
|
|
186
|
+
input: false;
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessControl"] extends {
|
|
@@ -222,6 +223,7 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
|
|
|
222
223
|
activeTeamId: {
|
|
223
224
|
type: "string";
|
|
224
225
|
required: false;
|
|
226
|
+
input: false;
|
|
225
227
|
};
|
|
226
228
|
} : {});
|
|
227
229
|
};
|
|
@@ -5,6 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { APIError } from "../../api/index.mjs";
|
|
6
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
7
7
|
import { toChecksumAddress } from "../../utils/hashing.mjs";
|
|
8
|
+
import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
|
|
8
9
|
import { schema } from "./schema.mjs";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
11
|
import * as z from "zod";
|
|
@@ -74,6 +75,33 @@ const siwe = (options) => {
|
|
|
74
75
|
code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE"
|
|
75
76
|
});
|
|
76
77
|
const { value: nonce } = verification;
|
|
78
|
+
const parsedMessage = parseSiweMessage(message);
|
|
79
|
+
const nonceMatches = parsedMessage.nonce === nonce;
|
|
80
|
+
const addressMatches = !!parsedMessage.address && parsedMessage.address.toLowerCase() === walletAddress.toLowerCase();
|
|
81
|
+
const chainMatches = parsedMessage.chainId === chainId;
|
|
82
|
+
const domainMatches = !!parsedMessage.domain && normalizeSiweDomain(parsedMessage.domain) === normalizeSiweDomain(options.domain);
|
|
83
|
+
if (!nonceMatches || !addressMatches || !chainMatches || !domainMatches) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
84
|
+
message: "Unauthorized: SIWE message does not match the expected nonce, domain, address, or chain ID",
|
|
85
|
+
status: 401,
|
|
86
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_MISMATCH"
|
|
87
|
+
});
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (parsedMessage.expirationTime) {
|
|
90
|
+
const expiresAt = Date.parse(parsedMessage.expirationTime);
|
|
91
|
+
if (!Number.isNaN(expiresAt) && now >= expiresAt) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
92
|
+
message: "Unauthorized: SIWE message has expired",
|
|
93
|
+
status: 401,
|
|
94
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_EXPIRED"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (parsedMessage.notBefore) {
|
|
98
|
+
const notBefore = Date.parse(parsedMessage.notBefore);
|
|
99
|
+
if (!Number.isNaN(notBefore) && now < notBefore) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
100
|
+
message: "Unauthorized: SIWE message is not yet valid",
|
|
101
|
+
status: 401,
|
|
102
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_NOT_YET_VALID"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
77
105
|
if (!await options.verifyMessage({
|
|
78
106
|
message,
|
|
79
107
|
signature,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/plugins/siwe/parse-message.ts
|
|
2
|
+
const HEADER_REGEX = /^(?:([a-zA-Z][a-zA-Z0-9+.-]*):\/\/)?(\S+) wants you to sign in with your Ethereum account:$/;
|
|
3
|
+
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
4
|
+
const FIELD_REGEX = /^([A-Za-z ]+): (.*)$/;
|
|
5
|
+
function parseSiweMessage(message) {
|
|
6
|
+
const result = {};
|
|
7
|
+
const lines = message.split(/\r?\n/);
|
|
8
|
+
const headerMatch = lines[0]?.match(HEADER_REGEX);
|
|
9
|
+
if (headerMatch) {
|
|
10
|
+
if (headerMatch[1]) result.scheme = headerMatch[1];
|
|
11
|
+
result.domain = headerMatch[2];
|
|
12
|
+
}
|
|
13
|
+
const addressLine = lines[1]?.trim();
|
|
14
|
+
if (addressLine && ADDRESS_REGEX.test(addressLine)) result.address = addressLine;
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const match = line.match(FIELD_REGEX);
|
|
17
|
+
if (!match) continue;
|
|
18
|
+
const [, key, value] = match;
|
|
19
|
+
switch (key) {
|
|
20
|
+
case "URI":
|
|
21
|
+
result.uri = value;
|
|
22
|
+
break;
|
|
23
|
+
case "Version":
|
|
24
|
+
result.version = value;
|
|
25
|
+
break;
|
|
26
|
+
case "Chain ID": {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (Number.isInteger(parsed)) result.chainId = parsed;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "Nonce":
|
|
32
|
+
result.nonce = value;
|
|
33
|
+
break;
|
|
34
|
+
case "Issued At":
|
|
35
|
+
result.issuedAt = value;
|
|
36
|
+
break;
|
|
37
|
+
case "Expiration Time":
|
|
38
|
+
result.expirationTime = value;
|
|
39
|
+
break;
|
|
40
|
+
case "Not Before":
|
|
41
|
+
result.notBefore = value;
|
|
42
|
+
break;
|
|
43
|
+
case "Request ID":
|
|
44
|
+
result.requestId = value;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes a SIWE `domain` (RFC 3986 authority) for comparison: strips any
|
|
52
|
+
* scheme and path, lowercases, leaving `host[:port]`.
|
|
53
|
+
*/
|
|
54
|
+
function normalizeSiweDomain(domain) {
|
|
55
|
+
const withoutScheme = domain.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
56
|
+
const pathStart = withoutScheme.indexOf("/");
|
|
57
|
+
return pathStart === -1 ? withoutScheme : withoutScheme.slice(0, pathStart);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { normalizeSiweDomain, parseSiweMessage };
|
|
@@ -220,7 +220,15 @@ const twoFactor = (options) => {
|
|
|
220
220
|
expireCookie(ctx, trustDeviceCookieAttrs);
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
223
|
+
* Remove the session cookie set by the credential sign-in.
|
|
224
|
+
*
|
|
225
|
+
* The credential handler already created a session and set
|
|
226
|
+
* `ctx.context.newSession`. Since 2FA is still pending, that
|
|
227
|
+
* session is deleted here and `newSession` is reset to `null`
|
|
228
|
+
* so downstream hooks don't observe a session that no longer
|
|
229
|
+
* exists. Hooks that read `ctx.context.newSession` after a
|
|
230
|
+
* sign-in must therefore null-check it: it is `null` while a
|
|
231
|
+
* 2FA challenge is in flight (no authenticated session yet).
|
|
224
232
|
*/
|
|
225
233
|
deleteSessionCookie(ctx, true);
|
|
226
234
|
await ctx.context.internalAdapter.deleteSession(data.session.token);
|
|
@@ -7999,11 +7999,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
7999
7999
|
priority?: RequestPriority | undefined;
|
|
8000
8000
|
cache?: RequestCache | undefined;
|
|
8001
8001
|
credentials?: RequestCredentials;
|
|
8002
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
8003
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
8004
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
8005
|
-
authorization: "Bearer" | "Basic";
|
|
8006
|
-
})) | undefined;
|
|
8007
8002
|
integrity?: string | undefined;
|
|
8008
8003
|
keepalive?: boolean | undefined;
|
|
8009
8004
|
method: string;
|
|
@@ -8033,6 +8028,12 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
8033
8028
|
prefix: string | (() => string | undefined) | undefined;
|
|
8034
8029
|
value: string | (() => string | undefined) | undefined;
|
|
8035
8030
|
}) | undefined;
|
|
8031
|
+
headers?: {} | {
|
|
8032
|
+
[x: string]: string | undefined;
|
|
8033
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
8034
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
8035
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
8036
|
+
} | undefined;
|
|
8036
8037
|
body?: any;
|
|
8037
8038
|
query?: any;
|
|
8038
8039
|
params?: any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.16",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -480,22 +480,22 @@
|
|
|
480
480
|
},
|
|
481
481
|
"dependencies": {
|
|
482
482
|
"@better-auth/utils": "0.4.1",
|
|
483
|
-
"@better-fetch/fetch": "1.
|
|
483
|
+
"@better-fetch/fetch": "1.2.2",
|
|
484
484
|
"@noble/ciphers": "^2.1.1",
|
|
485
485
|
"@noble/hashes": "^2.0.1",
|
|
486
|
-
"better-call": "1.3.
|
|
486
|
+
"better-call": "1.3.6",
|
|
487
487
|
"defu": "^6.1.4",
|
|
488
488
|
"jose": "^6.1.3",
|
|
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.16",
|
|
493
|
+
"@better-auth/drizzle-adapter": "1.6.16",
|
|
494
|
+
"@better-auth/kysely-adapter": "1.6.16",
|
|
495
|
+
"@better-auth/memory-adapter": "1.6.16",
|
|
496
|
+
"@better-auth/mongo-adapter": "1.6.16",
|
|
497
|
+
"@better-auth/prisma-adapter": "1.6.16",
|
|
498
|
+
"@better-auth/telemetry": "1.6.16"
|
|
499
499
|
},
|
|
500
500
|
"devDependencies": {
|
|
501
501
|
"@lynx-js/react": "^0.116.3",
|