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.
@@ -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 IPV4_HOST_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
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(":")[0];
17
+ const [patternHost = ""] = patternAuthority.split(":");
17
18
  if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
18
- return IPV4_HOST_PATTERN.test(authority.split(":")[0]);
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
@@ -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 = getMethod(routePath, knownPathMethods, arg);
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 === "GET" ? void 0 : {
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) {
@@ -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 { token } of sessionReferences) await secondaryStorage.delete(token);
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
- const remainingSessionReferences = currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token));
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
- const sorted = [...list, {
315
+ await setActiveSessionReferences(userId, [...list, {
313
316
  token: data.token,
314
317
  expiresAt: data.expiresAt.getTime()
315
- }].sort((a, b) => a.expiresAt - b.expiresAt);
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 sorted = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now).concat([{
470
+ const filtered = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now).concat([{
470
471
  token: sessionToken,
471
472
  expiresAt: expiresMs
472
- }]).sort((a, b) => a.expiresAt - b.expiresAt);
473
- const furthestSessionExp = sorted.at(-1)?.expiresAt;
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
- const filtered = list.filter((session) => session.expiresAt > now && session.token !== token);
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
- if (!options.session?.storeSessionInDatabase) {
538
- await queueCachedUserSessionDeletion(userId, sessionReferences);
539
- return;
540
- }
541
- if (ctx.options.session?.preserveSessionInDatabase) {
542
- if (await endPreservedSessions([{
543
- field: "userId",
544
- value: userId
545
- }]) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences);
546
- return;
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
- if (await deleteManyWithHooks([{
551
+ await deleteManyWithHooks([{
550
552
  field: "userId",
551
553
  value: userId
552
- }], "session", void 0) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences);
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) {
@@ -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 newModelName = newSchema[table]?.modelName;
154
- if (newModelName) schema[table].modelName = newModelName;
155
- for (const field in schema[table].fields) {
156
- const newField = newSchema[table]?.fields?.[field];
157
- if (!newField) continue;
158
- schema[table].fields[field].fieldName = newField;
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 schema;
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
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.8.0-beta.5";
2
+ var version = "1.8.0-beta.7";
3
3
  //#endregion
4
4
  export { version };
@@ -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 user = await ctx.context.internalAdapter.createUser({
197
- ...userData,
198
- email,
199
- name: ctx.body.name,
200
- role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
201
- }, { method: "admin" });
202
- if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
203
- if (ctx.body.password) {
204
- const hashedPassword = await ctx.context.password.hash(ctx.body.password);
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: "invalid_grant",
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
- if (!options.metadata?.openapi?.parameters && options.query instanceof z.ZodObject) Object.entries(options.query.shape).forEach(([key, value]) => {
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.fromStatus("BAD_REQUEST", { message: "Invitation not found!" });
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 (!data[opts.phoneNumber] || data[opts.phoneNumberVerified] === true) return;
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 || !data[opts.phoneNumber] || data[opts.phoneNumberVerified] === true || ctx.path === "/admin/update-user") return;
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.internalAdapter.createUser({
190
- name: name ?? walletAddress,
191
- email,
192
- image: avatar ?? ""
193
- }, { method: "siwe" });
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
- if (!await ctx.context.internalAdapter.findUserByEmail(normalizedEmail)) throw error;
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 = /* @__PURE__ */ new Date();
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/promise");
53
- return new Kysely({ dialect: new MysqlDialect({ pool: createPool("mysql://user:password@localhost:3306/better_auth") }) });
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: testWith === "postgres" ? {
80
- db: await getPostgres(),
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)) : testWith === "mysql" ? {
84
- db: await getMysql(),
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
- } : await getSqlite(),
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.5",
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.5",
507
- "@better-auth/drizzle-adapter": "1.8.0-beta.5",
508
- "@better-auth/kysely-adapter": "1.8.0-beta.5",
509
- "@better-auth/memory-adapter": "1.8.0-beta.5",
510
- "@better-auth/mongo-adapter": "1.8.0-beta.5",
511
- "@better-auth/prisma-adapter": "1.8.0-beta.5",
512
- "@better-auth/telemetry": "1.8.0-beta.5"
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.2.7",
533
- "react-dom": "^19.2.7",
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",