better-auth 1.8.0-beta.5 → 1.8.0-beta.7
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/auth/trusted-origins.mjs +5 -3
- package/dist/client/proxy.mjs +4 -11
- package/dist/cookies/index.mjs +1 -0
- package/dist/db/internal-adapter.mjs +50 -38
- package/dist/db/schema.mjs +17 -8
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +13 -11
- package/dist/plugins/device-authorization/index.d.mts +1 -0
- package/dist/plugins/device-authorization/routes.mjs +3 -1
- package/dist/plugins/open-api/generator.mjs +2 -1
- package/dist/plugins/organization/adapter.mjs +4 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +1 -1
- package/dist/plugins/phone-number/index.mjs +2 -2
- package/dist/plugins/siwe/index.mjs +40 -23
- package/dist/plugins/username/index.mjs +3 -2
- package/dist/state.mjs +1 -2
- package/dist/test-utils/test-instance.mjs +43 -33
- package/package.json +10 -10
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { wildcardMatch } from "../utils/wildcard.mjs";
|
|
2
2
|
import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
|
|
3
|
+
import * as z from "zod";
|
|
3
4
|
//#region src/auth/trusted-origins.ts
|
|
4
5
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
6
|
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
6
7
|
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
7
8
|
const IP_WILDCARD_HOST_PATTERN = /^(?=.*\d)[\d*?.]+$/;
|
|
8
|
-
const
|
|
9
|
+
const ipv4Schema = z.ipv4();
|
|
9
10
|
/**
|
|
10
11
|
* A wildcard host made only of digits, dots and wildcards (`192.168.*.*`,
|
|
11
12
|
* `10.*`) names an address range, so it admits only an IPv4 address. A plain
|
|
@@ -13,9 +14,10 @@ const IPV4_HOST_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
|
13
14
|
* hosts keep the documented subdomain matching.
|
|
14
15
|
*/
|
|
15
16
|
const isAllowedIPv4Host = (patternAuthority, authority) => {
|
|
16
|
-
const patternHost = patternAuthority.split(":")
|
|
17
|
+
const [patternHost = ""] = patternAuthority.split(":");
|
|
17
18
|
if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
|
|
18
|
-
|
|
19
|
+
const [host = ""] = authority.split(":");
|
|
20
|
+
return z.validate(ipv4Schema, host);
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
23
|
* Resolves `.` and `..` segments in a path after percent-decoding so a
|
package/dist/client/proxy.mjs
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { isAtom } from "../utils/is-atom.mjs";
|
|
2
2
|
import { toKebabCase } from "@better-auth/core/utils/string";
|
|
3
|
+
import { isPayloadMethod } from "@better-fetch/fetch";
|
|
3
4
|
//#region src/client/proxy.ts
|
|
4
|
-
function getMethod(path, knownPathMethods, args) {
|
|
5
|
-
const method = knownPathMethods[path];
|
|
6
|
-
const { fetchOptions, query: _query, ...body } = args || {};
|
|
7
|
-
if (method) return method;
|
|
8
|
-
if (fetchOptions?.method) return fetchOptions.method;
|
|
9
|
-
if (body && Object.keys(body).length > 0) return "POST";
|
|
10
|
-
return "GET";
|
|
11
|
-
}
|
|
12
5
|
function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomListeners) {
|
|
13
6
|
function createProxy(path = []) {
|
|
14
7
|
return new Proxy(function() {}, {
|
|
@@ -35,13 +28,13 @@ function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomLis
|
|
|
35
28
|
...fetchOptions,
|
|
36
29
|
...argFetchOptions
|
|
37
30
|
};
|
|
38
|
-
const method =
|
|
31
|
+
const method = knownPathMethods[routePath] || options.method || (Object.keys(body).length > 0 ? "POST" : "GET");
|
|
39
32
|
return await client(routePath, {
|
|
40
33
|
...options,
|
|
41
|
-
body: method
|
|
34
|
+
body: isPayloadMethod(method) ? {
|
|
42
35
|
...body,
|
|
43
36
|
...options?.body || {}
|
|
44
|
-
},
|
|
37
|
+
} : void 0,
|
|
45
38
|
query: query || options?.query,
|
|
46
39
|
method,
|
|
47
40
|
async onSuccess(context) {
|
package/dist/cookies/index.mjs
CHANGED
|
@@ -177,6 +177,7 @@ async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
|
|
|
177
177
|
...overrides
|
|
178
178
|
});
|
|
179
179
|
if (dontRememberMe) await ctx.setSignedCookie(ctx.context.authCookies.dontRememberToken.name, "true", ctx.context.secret, ctx.context.authCookies.dontRememberToken.attributes);
|
|
180
|
+
else if (dontRememberMeCookie) expireCookie(ctx, ctx.context.authCookies.dontRememberToken);
|
|
180
181
|
await setCookieCache(ctx, session, dontRememberMe);
|
|
181
182
|
ctx.context.setNewSession(session);
|
|
182
183
|
}
|
|
@@ -38,7 +38,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
38
38
|
* or `deleteUserSessions` sweeping a user's accumulated preserved rows) would
|
|
39
39
|
* re-match it and re-fire the hooks, re-dispatching back-channel logout.
|
|
40
40
|
*/
|
|
41
|
-
const endPreservedSessions = (where) => {
|
|
41
|
+
const endPreservedSessions = (where, afterEnd) => {
|
|
42
42
|
const liveSessions = [...where, {
|
|
43
43
|
field: "expiresAt",
|
|
44
44
|
value: /* @__PURE__ */ new Date(),
|
|
@@ -51,6 +51,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
51
51
|
where: liveSessions,
|
|
52
52
|
update: { expiresAt: /* @__PURE__ */ new Date() }
|
|
53
53
|
});
|
|
54
|
+
await afterEnd?.();
|
|
54
55
|
},
|
|
55
56
|
executeMainFn: false
|
|
56
57
|
});
|
|
@@ -78,6 +79,16 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
78
79
|
const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
|
|
79
80
|
return activeSessions ? safeJSONParse(activeSessions) || [] : [];
|
|
80
81
|
}
|
|
82
|
+
async function setActiveSessionReferences(userId, sessionReferences, now) {
|
|
83
|
+
if (!secondaryStorage) return;
|
|
84
|
+
const sorted = [...sessionReferences].sort((a, b) => a.expiresAt - b.expiresAt);
|
|
85
|
+
const ttl = getTTLSeconds(sorted.at(-1)?.expiresAt ?? 0, now);
|
|
86
|
+
if (ttl > 0) {
|
|
87
|
+
await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), ttl);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await secondaryStorage.delete(`active-sessions-${userId}`);
|
|
91
|
+
}
|
|
81
92
|
const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
|
|
82
93
|
const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
|
|
83
94
|
async function isRevokedSession(session, revokedBeforeByUser) {
|
|
@@ -91,18 +102,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
91
102
|
const previousRevokedBefore = Number(await secondaryStorage.get(revokedBeforeKey(userId)));
|
|
92
103
|
await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
|
|
93
104
|
const deletedTokens = new Set(sessionReferences.map((session) => session.token));
|
|
94
|
-
for (const
|
|
95
|
-
const activeSessionsKey = `active-sessions-${userId}`;
|
|
105
|
+
for (const token of deletedTokens) await secondaryStorage.delete(token);
|
|
96
106
|
const currentSessionReferences = await getActiveSessionReferences(userId);
|
|
97
107
|
const now = Date.now();
|
|
98
|
-
|
|
99
|
-
remainingSessionReferences.sort((a, b) => a.expiresAt - b.expiresAt);
|
|
100
|
-
const furthestExpiration = remainingSessionReferences.at(-1)?.expiresAt;
|
|
101
|
-
if (furthestExpiration) {
|
|
102
|
-
await secondaryStorage.set(activeSessionsKey, JSON.stringify(remainingSessionReferences), getTTLSeconds(furthestExpiration, now));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
await secondaryStorage.delete(activeSessionsKey);
|
|
108
|
+
await setActiveSessionReferences(userId, currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token)), now);
|
|
106
109
|
}
|
|
107
110
|
async function queueCachedUserSessionDeletion(userId, sessionReferences) {
|
|
108
111
|
if (!secondaryStorage) return;
|
|
@@ -309,12 +312,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
309
312
|
list = safeJSONParse(currentList) || [];
|
|
310
313
|
list = list.filter((session) => session.expiresAt > now && session.token !== data.token);
|
|
311
314
|
}
|
|
312
|
-
|
|
315
|
+
await setActiveSessionReferences(userId, [...list, {
|
|
313
316
|
token: data.token,
|
|
314
317
|
expiresAt: data.expiresAt.getTime()
|
|
315
|
-
}]
|
|
316
|
-
const furthestSessionTTL = getTTLSeconds(sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime(), now);
|
|
317
|
-
if (furthestSessionTTL > 0) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), furthestSessionTTL);
|
|
318
|
+
}], now);
|
|
318
319
|
const user = await (await getCurrentAdapter(adapter)).findOne({
|
|
319
320
|
model: "user",
|
|
320
321
|
where: [{
|
|
@@ -466,13 +467,11 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
466
467
|
}), sessionTTL);
|
|
467
468
|
const listKey = `active-sessions-${updatedSession.userId}`;
|
|
468
469
|
const listRaw = await secondaryStorage.get(listKey);
|
|
469
|
-
const
|
|
470
|
+
const filtered = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now).concat([{
|
|
470
471
|
token: sessionToken,
|
|
471
472
|
expiresAt: expiresMs
|
|
472
|
-
}])
|
|
473
|
-
|
|
474
|
-
if (furthestSessionExp && furthestSessionExp > now) await secondaryStorage.set(listKey, JSON.stringify(sorted), getTTLSeconds(furthestSessionExp, now));
|
|
475
|
-
else await secondaryStorage.delete(listKey);
|
|
473
|
+
}]);
|
|
474
|
+
await setActiveSessionReferences(updatedSession.userId, filtered, now);
|
|
476
475
|
}
|
|
477
476
|
return updatedSession;
|
|
478
477
|
},
|
|
@@ -493,10 +492,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
493
492
|
if (currentList) {
|
|
494
493
|
const list = safeJSONParse(currentList) || [];
|
|
495
494
|
const now = Date.now();
|
|
496
|
-
|
|
497
|
-
const furthestSessionExp = filtered.sort((a, b) => a.expiresAt - b.expiresAt).at(-1)?.expiresAt;
|
|
498
|
-
if (filtered.length > 0 && furthestSessionExp && furthestSessionExp > Date.now()) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(filtered), getTTLSeconds(furthestSessionExp, now));
|
|
499
|
-
else await secondaryStorage.delete(`active-sessions-${userId}`);
|
|
495
|
+
await setActiveSessionReferences(userId, list.filter((session) => session.expiresAt > now && session.token !== token), now);
|
|
500
496
|
} else logger.error("Active sessions list not found in secondary storage");
|
|
501
497
|
}
|
|
502
498
|
await secondaryStorage.delete(token);
|
|
@@ -533,23 +529,39 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
533
529
|
},
|
|
534
530
|
deleteUserSessions: async (userId) => {
|
|
535
531
|
const sessionReferences = await getActiveSessionReferences(userId);
|
|
536
|
-
if (secondaryStorage) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
532
|
+
if (!secondaryStorage) {
|
|
533
|
+
await deleteManyWithHooks([{
|
|
534
|
+
field: "userId",
|
|
535
|
+
value: userId
|
|
536
|
+
}], "session", void 0);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!options.session?.storeSessionInDatabase) {
|
|
540
|
+
await queueCachedUserSessionDeletion(userId, sessionReferences);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const revokeCachedSessions = async () => queueCachedUserSessionDeletion(userId, [...sessionReferences, ...await getActiveSessionReferences(userId)]);
|
|
544
|
+
if (ctx.options.session?.preserveSessionInDatabase) {
|
|
545
|
+
await endPreservedSessions([{
|
|
546
|
+
field: "userId",
|
|
547
|
+
value: userId
|
|
548
|
+
}], revokeCachedSessions);
|
|
549
|
+
return;
|
|
548
550
|
}
|
|
549
|
-
|
|
551
|
+
await deleteManyWithHooks([{
|
|
550
552
|
field: "userId",
|
|
551
553
|
value: userId
|
|
552
|
-
}], "session",
|
|
554
|
+
}], "session", {
|
|
555
|
+
fn: async (where) => {
|
|
556
|
+
const deleted = await (await getCurrentAdapter(adapter)).deleteMany({
|
|
557
|
+
model: "session",
|
|
558
|
+
where
|
|
559
|
+
});
|
|
560
|
+
await revokeCachedSessions();
|
|
561
|
+
return deleted;
|
|
562
|
+
},
|
|
563
|
+
executeMainFn: false
|
|
564
|
+
});
|
|
553
565
|
},
|
|
554
566
|
deleteSessions: async (sessionTokens) => {
|
|
555
567
|
if (secondaryStorage) {
|
package/dist/db/schema.mjs
CHANGED
|
@@ -149,16 +149,25 @@ function getSessionDefaultFields(options) {
|
|
|
149
149
|
}
|
|
150
150
|
function mergeSchema(schema, newSchema) {
|
|
151
151
|
if (!newSchema) return schema;
|
|
152
|
+
const merged = { ...schema };
|
|
152
153
|
for (const table in newSchema) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
const base = schema[table];
|
|
155
|
+
const override = newSchema[table];
|
|
156
|
+
if (!base || !override) continue;
|
|
157
|
+
const fields = Object.fromEntries(Object.entries(base.fields).map(([key, field]) => {
|
|
158
|
+
const fieldName = override.fields?.[key];
|
|
159
|
+
return [key, fieldName ? {
|
|
160
|
+
...field,
|
|
161
|
+
fieldName
|
|
162
|
+
} : field];
|
|
163
|
+
}));
|
|
164
|
+
merged[table] = {
|
|
165
|
+
...base,
|
|
166
|
+
...override.modelName ? { modelName: override.modelName } : {},
|
|
167
|
+
fields
|
|
168
|
+
};
|
|
160
169
|
}
|
|
161
|
-
return
|
|
170
|
+
return merged;
|
|
162
171
|
}
|
|
163
172
|
//#endregion
|
|
164
173
|
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
|
package/dist/package.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/rou
|
|
|
5
5
|
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
6
6
|
import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
|
|
7
7
|
import { hasPermission } from "./has-permission.mjs";
|
|
8
|
+
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
10
|
import { whereOperators } from "@better-auth/core/db/adapter";
|
|
10
11
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
@@ -193,22 +194,23 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
193
194
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
194
195
|
if (ctx.body.password) assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
195
196
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
await ctx.context.internalAdapter.linkAccount({
|
|
197
|
+
const hashedPassword = ctx.body.password ? await ctx.context.password.hash(ctx.body.password) : void 0;
|
|
198
|
+
const user = await runWithTransaction(ctx.context.adapter, async () => {
|
|
199
|
+
const user = await ctx.context.internalAdapter.createUser({
|
|
200
|
+
...userData,
|
|
201
|
+
email,
|
|
202
|
+
name: ctx.body.name,
|
|
203
|
+
role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
|
|
204
|
+
}, { method: "admin" });
|
|
205
|
+
if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
|
|
206
|
+
if (hashedPassword !== void 0) await ctx.context.internalAdapter.linkAccount({
|
|
206
207
|
providerId: "credential",
|
|
207
208
|
accountId: user.id,
|
|
208
209
|
password: hashedPassword,
|
|
209
210
|
userId: user.id
|
|
210
211
|
});
|
|
211
|
-
|
|
212
|
+
return user;
|
|
213
|
+
});
|
|
212
214
|
return ctx.json({ user: parseUserOutput(ctx.context.options, user) });
|
|
213
215
|
});
|
|
214
216
|
const adminUpdateUserBodySchema = z.object({
|
|
@@ -305,6 +305,7 @@ export declare const deviceAuthorization: <Grant extends DeviceAuthorizationGran
|
|
|
305
305
|
invalid_request: "invalid_request";
|
|
306
306
|
expired_token: "expired_token";
|
|
307
307
|
access_denied: "access_denied";
|
|
308
|
+
invalid_client: "invalid_client";
|
|
308
309
|
authorization_pending: "authorization_pending";
|
|
309
310
|
slow_down: "slow_down";
|
|
310
311
|
invalid_grant: "invalid_grant";
|
|
@@ -369,6 +369,7 @@ const deviceTokenErrorSchema = z.object({
|
|
|
369
369
|
"expired_token",
|
|
370
370
|
"access_denied",
|
|
371
371
|
"invalid_request",
|
|
372
|
+
"invalid_client",
|
|
372
373
|
"invalid_grant"
|
|
373
374
|
]).meta({ description: "Error code" }),
|
|
374
375
|
error_description: z.string().meta({ description: "Detailed error description" })
|
|
@@ -504,6 +505,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
504
505
|
"expired_token",
|
|
505
506
|
"access_denied",
|
|
506
507
|
"invalid_request",
|
|
508
|
+
"invalid_client",
|
|
507
509
|
"invalid_grant"
|
|
508
510
|
]
|
|
509
511
|
},
|
|
@@ -518,7 +520,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
518
520
|
const { device_code, client_id } = ctx.body;
|
|
519
521
|
if (opts.validateClient) {
|
|
520
522
|
if (!await opts.validateClient(client_id)) throw new APIError("BAD_REQUEST", {
|
|
521
|
-
error: "
|
|
523
|
+
error: "invalid_client",
|
|
522
524
|
error_description: "Invalid client ID"
|
|
523
525
|
});
|
|
524
526
|
}
|
|
@@ -74,7 +74,8 @@ function getZodPipeSchema(zodType) {
|
|
|
74
74
|
function getParameters(options) {
|
|
75
75
|
const parameters = [];
|
|
76
76
|
if (options.metadata?.openapi?.parameters) parameters.push(...options.metadata.openapi.parameters);
|
|
77
|
-
|
|
77
|
+
const query = options.query instanceof z.core.$ZodOptional ? options.query._zod.def.innerType : options.query;
|
|
78
|
+
if (!options.metadata?.openapi?.parameters && query instanceof z.core.$ZodObject) Object.entries(query._zod.def.shape).forEach(([key, value]) => {
|
|
78
79
|
if (value instanceof z.ZodType) {
|
|
79
80
|
const parameterSchema = toOpenApiSchema(value);
|
|
80
81
|
parameters.push({
|
|
@@ -214,6 +214,10 @@ const getOrgAdapter = (context, options) => {
|
|
|
214
214
|
...data.filter.operator ? { operator: data.filter.operator } : {}
|
|
215
215
|
}] : []]
|
|
216
216
|
})]);
|
|
217
|
+
if (members[0].length === 0) return {
|
|
218
|
+
members: [],
|
|
219
|
+
total: members[1]
|
|
220
|
+
};
|
|
217
221
|
const users = await adapter.findMany({
|
|
218
222
|
model: "user",
|
|
219
223
|
where: [{
|
|
@@ -509,7 +509,7 @@ const getInvitation = (options) => createAuthEndpoint("/organization/get-invitat
|
|
|
509
509
|
if (!session) throw APIError.fromStatus("UNAUTHORIZED", { message: "Not authenticated" });
|
|
510
510
|
const adapter = getOrgAdapter(ctx.context, options);
|
|
511
511
|
const invitation = await adapter.findInvitationById(ctx.query.id);
|
|
512
|
-
if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) throw APIError.
|
|
512
|
+
if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND);
|
|
513
513
|
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION);
|
|
514
514
|
if (shouldRequireVerifiedEmailForInvitationIdAction({
|
|
515
515
|
organizationOptions: ctx.context.orgOptions,
|
|
@@ -22,7 +22,7 @@ const phoneNumber = (options) => {
|
|
|
22
22
|
init() {
|
|
23
23
|
return { options: { databaseHooks: { user: {
|
|
24
24
|
create: { async before(data, ctx) {
|
|
25
|
-
if (
|
|
25
|
+
if (data[opts.phoneNumber] == null || data[opts.phoneNumberVerified] === true) return;
|
|
26
26
|
if (ctx?.body?.[opts.phoneNumber]) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_CANNOT_BE_SET);
|
|
27
27
|
return { data: {
|
|
28
28
|
...data,
|
|
@@ -35,7 +35,7 @@ const phoneNumber = (options) => {
|
|
|
35
35
|
...data,
|
|
36
36
|
[opts.phoneNumberVerified]: false
|
|
37
37
|
} };
|
|
38
|
-
if (!ctx ||
|
|
38
|
+
if (!ctx || data[opts.phoneNumber] == null || data[opts.phoneNumberVerified] === true || ctx.path === "/admin/update-user") return;
|
|
39
39
|
return { data: {
|
|
40
40
|
...data,
|
|
41
41
|
[opts.phoneNumber]: void 0
|
|
@@ -6,6 +6,7 @@ import { PACKAGE_VERSION } from "../../version.mjs";
|
|
|
6
6
|
import { toChecksumAddress } from "../../utils/hashing.mjs";
|
|
7
7
|
import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
|
|
8
8
|
import { schema } from "./schema.mjs";
|
|
9
|
+
import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
11
|
import * as z from "zod";
|
|
11
12
|
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
@@ -186,38 +187,54 @@ const siwe = (options) => {
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
const { name, avatar } = await options.ensLookup?.({ walletAddress }) ?? {};
|
|
189
|
-
const createSIWEUser = (email) => ctx.context.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
const createSIWEUser = (email) => runWithTransaction(ctx.context.adapter, async () => {
|
|
191
|
+
const createdUser = await ctx.context.internalAdapter.createUser({
|
|
192
|
+
name: name ?? walletAddress,
|
|
193
|
+
email,
|
|
194
|
+
image: avatar ?? ""
|
|
195
|
+
}, { method: "siwe" });
|
|
196
|
+
await (await getCurrentAdapter(ctx.context.adapter)).create({
|
|
197
|
+
model: "walletAddress",
|
|
198
|
+
data: {
|
|
199
|
+
userId: createdUser.id,
|
|
200
|
+
address: walletAddress,
|
|
201
|
+
chainId,
|
|
202
|
+
isPrimary: true,
|
|
203
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
await ctx.context.internalAdapter.createAccount({
|
|
207
|
+
userId: createdUser.id,
|
|
208
|
+
providerId: "siwe",
|
|
209
|
+
accountId: `${walletAddress}:${chainId}`,
|
|
210
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
211
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
212
|
+
});
|
|
213
|
+
return createdUser;
|
|
214
|
+
});
|
|
194
215
|
try {
|
|
195
216
|
user = await createSIWEUser(userEmail);
|
|
196
217
|
} catch (error) {
|
|
197
218
|
if (userEmail !== normalizedEmail || !normalizedEmail) throw error;
|
|
198
|
-
|
|
219
|
+
const claimedUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail);
|
|
220
|
+
const registeredWallet = await ctx.context.adapter.findOne({
|
|
221
|
+
model: "walletAddress",
|
|
222
|
+
where: [{
|
|
223
|
+
field: "address",
|
|
224
|
+
operator: "eq",
|
|
225
|
+
value: walletAddress
|
|
226
|
+
}, {
|
|
227
|
+
field: "chainId",
|
|
228
|
+
operator: "eq",
|
|
229
|
+
value: chainId
|
|
230
|
+
}]
|
|
231
|
+
});
|
|
232
|
+
if (!claimedUser || registeredWallet) throw error;
|
|
199
233
|
userEmail = walletEmail;
|
|
200
234
|
user = await createSIWEUser(userEmail);
|
|
201
235
|
} finally {
|
|
202
236
|
if (emailClaimIdentifier) await ctx.context.internalAdapter.consumeVerificationValue(emailClaimIdentifier).catch(() => {});
|
|
203
237
|
}
|
|
204
|
-
await ctx.context.adapter.create({
|
|
205
|
-
model: "walletAddress",
|
|
206
|
-
data: {
|
|
207
|
-
userId: user.id,
|
|
208
|
-
address: walletAddress,
|
|
209
|
-
chainId,
|
|
210
|
-
isPrimary: true,
|
|
211
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
await ctx.context.internalAdapter.createAccount({
|
|
215
|
-
userId: user.id,
|
|
216
|
-
providerId: "siwe",
|
|
217
|
-
accountId: `${walletAddress}:${chainId}`,
|
|
218
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
219
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
220
|
-
});
|
|
221
238
|
} else if (!existingWalletAddress) {
|
|
222
239
|
await ctx.context.adapter.create({
|
|
223
240
|
model: "walletAddress",
|
|
@@ -6,6 +6,7 @@ import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
|
6
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
7
7
|
import { USERNAME_ERROR_CODES } from "./error-codes.mjs";
|
|
8
8
|
import { getSchema } from "./schema.mjs";
|
|
9
|
+
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
11
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
11
12
|
import * as z from "zod";
|
|
@@ -71,7 +72,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
71
72
|
const currentPath = context?.path;
|
|
72
73
|
const skipValidation = currentPath && pathsWithHttpHookValidation.includes(currentPath);
|
|
73
74
|
if (username) {
|
|
74
|
-
if (!skipValidation) await validateUsername(username, displayUsername, ctx.adapter);
|
|
75
|
+
if (!skipValidation) await validateUsername(username, displayUsername, await getCurrentAdapter(ctx.adapter));
|
|
75
76
|
return { data: {
|
|
76
77
|
...user,
|
|
77
78
|
username: normalizer(username),
|
|
@@ -91,7 +92,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
91
92
|
if (username) {
|
|
92
93
|
if (!skipValidation) {
|
|
93
94
|
const currentUserId = context?.context?.session?.user?.id || ("id" in user ? user.id : null);
|
|
94
|
-
await validateUsername(username, displayUsername, ctx.adapter, currentUserId);
|
|
95
|
+
await validateUsername(username, displayUsername, await getCurrentAdapter(ctx.adapter), currentUserId);
|
|
95
96
|
}
|
|
96
97
|
return { data: {
|
|
97
98
|
...user,
|
package/dist/state.mjs
CHANGED
|
@@ -80,8 +80,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
80
80
|
}
|
|
81
81
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state", { maxAge: 300 });
|
|
82
82
|
await c.setSignedCookie(stateCookie.name, state, c.context.secret, stateCookie.attributes);
|
|
83
|
-
const expiresAt =
|
|
84
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + 10);
|
|
83
|
+
const expiresAt = new Date(Date.now() + 6e5);
|
|
85
84
|
if (!await c.context.internalAdapter.createVerificationValue({
|
|
86
85
|
value: JSON.stringify({
|
|
87
86
|
...stateData,
|
|
@@ -32,6 +32,8 @@ const testPassword = {
|
|
|
32
32
|
async function getTestInstance(options, config) {
|
|
33
33
|
const testWith = config?.testWith || "sqlite";
|
|
34
34
|
const postgresSchema = testWith === "postgres" ? `ba_test_${randomUUID().replaceAll("-", "_")}` : void 0;
|
|
35
|
+
const mysqlServerURL = "mysql://root:root_password@127.0.0.1:3306";
|
|
36
|
+
const mysqlDatabase = testWith === "mysql" ? `ba_test_${randomUUID().replaceAll("-", "_")}` : void 0;
|
|
35
37
|
const quotePostgresIdentifier = (identifier) => `"${identifier.replaceAll("\"", "\"\"")}"`;
|
|
36
38
|
async function getPostgres() {
|
|
37
39
|
const { Kysely, PostgresDialect } = await import("kysely");
|
|
@@ -49,8 +51,14 @@ async function getTestInstance(options, config) {
|
|
|
49
51
|
}
|
|
50
52
|
async function getMysql() {
|
|
51
53
|
const { Kysely, MysqlDialect } = await import("kysely");
|
|
52
|
-
const { createPool } = await import("mysql2
|
|
53
|
-
|
|
54
|
+
const { createConnection, createPool } = await import("mysql2");
|
|
55
|
+
const admin = createConnection(mysqlServerURL).promise();
|
|
56
|
+
try {
|
|
57
|
+
await admin.query(`CREATE DATABASE \`${mysqlDatabase}\``);
|
|
58
|
+
} finally {
|
|
59
|
+
await admin.end();
|
|
60
|
+
}
|
|
61
|
+
return new Kysely({ dialect: new MysqlDialect({ pool: createPool(`${mysqlServerURL}/${mysqlDatabase}`) }) });
|
|
54
62
|
}
|
|
55
63
|
async function mongodbClient() {
|
|
56
64
|
const { MongoClient } = await import("mongodb");
|
|
@@ -64,6 +72,9 @@ async function getTestInstance(options, config) {
|
|
|
64
72
|
};
|
|
65
73
|
return await dbClient("mongodb://127.0.0.1:27017", "better-auth");
|
|
66
74
|
}
|
|
75
|
+
const postgres = testWith === "postgres" ? await getPostgres() : void 0;
|
|
76
|
+
const mysql = testWith === "mysql" ? await getMysql() : void 0;
|
|
77
|
+
const sqlite = testWith === "sqlite" ? await getSqlite() : void 0;
|
|
67
78
|
const opts = {
|
|
68
79
|
socialProviders: {
|
|
69
80
|
github: {
|
|
@@ -76,20 +87,45 @@ async function getTestInstance(options, config) {
|
|
|
76
87
|
}
|
|
77
88
|
},
|
|
78
89
|
secret: "better-auth-secret-that-is-long-enough-for-validation-test",
|
|
79
|
-
database:
|
|
80
|
-
db:
|
|
90
|
+
database: postgres ? {
|
|
91
|
+
db: postgres,
|
|
81
92
|
type: "postgres",
|
|
82
93
|
transaction: config?.transaction
|
|
83
|
-
} : testWith === "mongodb" ? await Promise.all([mongodbClient(), await import("../adapters/mongodb-adapter/index.mjs")]).then(([db, { mongodbAdapter }]) => mongodbAdapter(db)) :
|
|
84
|
-
db:
|
|
94
|
+
} : testWith === "mongodb" ? await Promise.all([mongodbClient(), await import("../adapters/mongodb-adapter/index.mjs")]).then(([db, { mongodbAdapter }]) => mongodbAdapter(db)) : mysql ? {
|
|
95
|
+
db: mysql,
|
|
85
96
|
type: "mysql",
|
|
86
97
|
transaction: config?.transaction
|
|
87
|
-
} :
|
|
98
|
+
} : sqlite,
|
|
88
99
|
emailAndPassword: { enabled: true },
|
|
89
100
|
rateLimit: { enabled: false },
|
|
90
101
|
advanced: { cookies: {} },
|
|
91
102
|
logger: { level: "debug" }
|
|
92
103
|
};
|
|
104
|
+
const cleanup = async () => {
|
|
105
|
+
if (testWith === "mongodb") {
|
|
106
|
+
await (await mongodbClient()).dropDatabase();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (postgres) {
|
|
110
|
+
try {
|
|
111
|
+
if (postgresSchema) await sql.raw(`DROP SCHEMA IF EXISTS ${quotePostgresIdentifier(postgresSchema)} CASCADE`).execute(postgres);
|
|
112
|
+
else await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(postgres);
|
|
113
|
+
} finally {
|
|
114
|
+
await postgres.destroy();
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (mysql) {
|
|
119
|
+
try {
|
|
120
|
+
await sql.raw(`DROP DATABASE IF EXISTS \`${mysqlDatabase}\``).execute(mysql);
|
|
121
|
+
} finally {
|
|
122
|
+
await mysql.destroy();
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (sqlite) sqlite.close();
|
|
127
|
+
};
|
|
128
|
+
cleanupSet.add(cleanup);
|
|
93
129
|
const authOptions = {
|
|
94
130
|
baseURL: "http://localhost:" + (config?.port || 3e3),
|
|
95
131
|
...opts,
|
|
@@ -126,32 +162,6 @@ async function getTestInstance(options, config) {
|
|
|
126
162
|
});
|
|
127
163
|
}
|
|
128
164
|
await createTestUser();
|
|
129
|
-
const cleanup = async () => {
|
|
130
|
-
if (testWith === "mongodb") {
|
|
131
|
-
await (await mongodbClient()).dropDatabase();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (testWith === "postgres") {
|
|
135
|
-
const postgres = await getPostgres();
|
|
136
|
-
if (postgresSchema) await sql.raw(`DROP SCHEMA IF EXISTS ${quotePostgresIdentifier(postgresSchema)} CASCADE`).execute(postgres);
|
|
137
|
-
else await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(postgres);
|
|
138
|
-
await postgres.destroy();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (testWith === "mysql") {
|
|
142
|
-
const mysql = await getMysql();
|
|
143
|
-
await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql);
|
|
144
|
-
const tables = await mysql.introspection.getTables();
|
|
145
|
-
for (const table of tables) await mysql.deleteFrom(table.name).execute();
|
|
146
|
-
await sql`SET FOREIGN_KEY_CHECKS = 1;`.execute(mysql);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (testWith === "sqlite") {
|
|
150
|
-
(await getSqlite()).close();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
cleanupSet.add(cleanup);
|
|
155
165
|
const customFetchImpl = async (url, init) => {
|
|
156
166
|
const headers = init?.headers || {};
|
|
157
167
|
const storageHeaders = currentUserContextStorage.getStore()?.headers;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.8.0-beta.
|
|
3
|
+
"version": "1.8.0-beta.7",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -503,13 +503,13 @@
|
|
|
503
503
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
504
504
|
"nanostores": "^1.3.0",
|
|
505
505
|
"zod": "^4.5.4",
|
|
506
|
-
"@better-auth/core": "1.8.0-beta.
|
|
507
|
-
"@better-auth/drizzle-adapter": "1.8.0-beta.
|
|
508
|
-
"@better-auth/kysely-adapter": "1.8.0-beta.
|
|
509
|
-
"@better-auth/memory-adapter": "1.8.0-beta.
|
|
510
|
-
"@better-auth/mongo-adapter": "1.8.0-beta.
|
|
511
|
-
"@better-auth/prisma-adapter": "1.8.0-beta.
|
|
512
|
-
"@better-auth/telemetry": "1.8.0-beta.
|
|
506
|
+
"@better-auth/core": "1.8.0-beta.7",
|
|
507
|
+
"@better-auth/drizzle-adapter": "1.8.0-beta.7",
|
|
508
|
+
"@better-auth/kysely-adapter": "1.8.0-beta.7",
|
|
509
|
+
"@better-auth/memory-adapter": "1.8.0-beta.7",
|
|
510
|
+
"@better-auth/mongo-adapter": "1.8.0-beta.7",
|
|
511
|
+
"@better-auth/prisma-adapter": "1.8.0-beta.7",
|
|
512
|
+
"@better-auth/telemetry": "1.8.0-beta.7"
|
|
513
513
|
},
|
|
514
514
|
"devDependencies": {
|
|
515
515
|
"@lynx-js/react": "^0.121.2",
|
|
@@ -529,8 +529,8 @@
|
|
|
529
529
|
"msw": "^2.14.6",
|
|
530
530
|
"next": "^16.4.2",
|
|
531
531
|
"oauth2-mock-server": "^9.0.0",
|
|
532
|
-
"react": "^19.
|
|
533
|
-
"react-dom": "^19.
|
|
532
|
+
"react": "^19.3.1",
|
|
533
|
+
"react-dom": "^19.3.1",
|
|
534
534
|
"solid-js": "^1.9.11",
|
|
535
535
|
"tsdown": "0.23.1",
|
|
536
536
|
"type-fest": "^5.7.0",
|