better-auth 1.8.0-beta.6 → 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,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) {
@@ -79,6 +79,16 @@ const createInternalAdapter = (adapter, ctx) => {
79
79
  const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
80
80
  return activeSessions ? safeJSONParse(activeSessions) || [] : [];
81
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
+ }
82
92
  const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
83
93
  const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
84
94
  async function isRevokedSession(session, revokedBeforeByUser) {
@@ -93,17 +103,9 @@ const createInternalAdapter = (adapter, ctx) => {
93
103
  await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
94
104
  const deletedTokens = new Set(sessionReferences.map((session) => session.token));
95
105
  for (const token of deletedTokens) await secondaryStorage.delete(token);
96
- const activeSessionsKey = `active-sessions-${userId}`;
97
106
  const currentSessionReferences = await getActiveSessionReferences(userId);
98
107
  const now = Date.now();
99
- const remainingSessionReferences = currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token));
100
- remainingSessionReferences.sort((a, b) => a.expiresAt - b.expiresAt);
101
- const furthestExpiration = remainingSessionReferences.at(-1)?.expiresAt;
102
- if (furthestExpiration) {
103
- await secondaryStorage.set(activeSessionsKey, JSON.stringify(remainingSessionReferences), getTTLSeconds(furthestExpiration, now));
104
- return;
105
- }
106
- await secondaryStorage.delete(activeSessionsKey);
108
+ await setActiveSessionReferences(userId, currentSessionReferences.filter((session) => session.expiresAt > now && !deletedTokens.has(session.token)), now);
107
109
  }
108
110
  async function queueCachedUserSessionDeletion(userId, sessionReferences) {
109
111
  if (!secondaryStorage) return;
@@ -310,12 +312,10 @@ const createInternalAdapter = (adapter, ctx) => {
310
312
  list = safeJSONParse(currentList) || [];
311
313
  list = list.filter((session) => session.expiresAt > now && session.token !== data.token);
312
314
  }
313
- const sorted = [...list, {
315
+ await setActiveSessionReferences(userId, [...list, {
314
316
  token: data.token,
315
317
  expiresAt: data.expiresAt.getTime()
316
- }].sort((a, b) => a.expiresAt - b.expiresAt);
317
- const furthestSessionTTL = getTTLSeconds(sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime(), now);
318
- if (furthestSessionTTL > 0) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), furthestSessionTTL);
318
+ }], now);
319
319
  const user = await (await getCurrentAdapter(adapter)).findOne({
320
320
  model: "user",
321
321
  where: [{
@@ -467,13 +467,11 @@ const createInternalAdapter = (adapter, ctx) => {
467
467
  }), sessionTTL);
468
468
  const listKey = `active-sessions-${updatedSession.userId}`;
469
469
  const listRaw = await secondaryStorage.get(listKey);
470
- 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([{
471
471
  token: sessionToken,
472
472
  expiresAt: expiresMs
473
- }]).sort((a, b) => a.expiresAt - b.expiresAt);
474
- const furthestSessionExp = sorted.at(-1)?.expiresAt;
475
- if (furthestSessionExp && furthestSessionExp > now) await secondaryStorage.set(listKey, JSON.stringify(sorted), getTTLSeconds(furthestSessionExp, now));
476
- else await secondaryStorage.delete(listKey);
473
+ }]);
474
+ await setActiveSessionReferences(updatedSession.userId, filtered, now);
477
475
  }
478
476
  return updatedSession;
479
477
  },
@@ -494,10 +492,7 @@ const createInternalAdapter = (adapter, ctx) => {
494
492
  if (currentList) {
495
493
  const list = safeJSONParse(currentList) || [];
496
494
  const now = Date.now();
497
- const filtered = list.filter((session) => session.expiresAt > now && session.token !== token);
498
- const furthestSessionExp = filtered.sort((a, b) => a.expiresAt - b.expiresAt).at(-1)?.expiresAt;
499
- if (filtered.length > 0 && furthestSessionExp && furthestSessionExp > Date.now()) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(filtered), getTTLSeconds(furthestSessionExp, now));
500
- else await secondaryStorage.delete(`active-sessions-${userId}`);
495
+ await setActiveSessionReferences(userId, list.filter((session) => session.expiresAt > now && session.token !== token), now);
501
496
  } else logger.error("Active sessions list not found in secondary storage");
502
497
  }
503
498
  await secondaryStorage.delete(token);
@@ -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.6";
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,
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.8.0-beta.6",
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.6",
507
- "@better-auth/drizzle-adapter": "1.8.0-beta.6",
508
- "@better-auth/kysely-adapter": "1.8.0-beta.6",
509
- "@better-auth/memory-adapter": "1.8.0-beta.6",
510
- "@better-auth/mongo-adapter": "1.8.0-beta.6",
511
- "@better-auth/prisma-adapter": "1.8.0-beta.6",
512
- "@better-auth/telemetry": "1.8.0-beta.6"
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",