better-auth 1.6.11 → 1.6.13

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.
Files changed (84) hide show
  1. package/dist/api/index.d.mts +12 -48
  2. package/dist/api/routes/account.d.mts +2 -23
  3. package/dist/api/routes/account.mjs +94 -73
  4. package/dist/api/routes/callback.d.mts +1 -1
  5. package/dist/api/routes/callback.mjs +39 -42
  6. package/dist/api/routes/email-verification.d.mts +1 -0
  7. package/dist/api/routes/email-verification.mjs +4 -3
  8. package/dist/api/routes/password.mjs +1 -1
  9. package/dist/api/routes/session.mjs +15 -10
  10. package/dist/api/routes/sign-in.d.mts +1 -0
  11. package/dist/api/routes/sign-in.mjs +3 -2
  12. package/dist/api/routes/sign-up.d.mts +1 -0
  13. package/dist/api/routes/sign-up.mjs +9 -7
  14. package/dist/api/routes/update-user.mjs +7 -7
  15. package/dist/client/fetch-plugins.mjs +2 -1
  16. package/dist/client/parser.mjs +0 -1
  17. package/dist/client/plugins/index.d.mts +3 -3
  18. package/dist/client/proxy.mjs +2 -1
  19. package/dist/context/create-context.mjs +10 -14
  20. package/dist/context/helpers.mjs +3 -2
  21. package/dist/cookies/cookie-utils.d.mts +24 -1
  22. package/dist/cookies/cookie-utils.mjs +85 -22
  23. package/dist/cookies/index.d.mts +2 -3
  24. package/dist/cookies/index.mjs +39 -11
  25. package/dist/cookies/session-store.mjs +4 -23
  26. package/dist/db/get-migration.mjs +4 -4
  27. package/dist/db/index.d.mts +2 -2
  28. package/dist/db/index.mjs +3 -2
  29. package/dist/db/internal-adapter.mjs +56 -50
  30. package/dist/db/schema.d.mts +15 -2
  31. package/dist/db/schema.mjs +26 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.mjs +2 -2
  34. package/dist/oauth2/errors.mjs +16 -1
  35. package/dist/oauth2/index.d.mts +2 -2
  36. package/dist/oauth2/index.mjs +3 -3
  37. package/dist/oauth2/link-account.d.mts +27 -1
  38. package/dist/oauth2/link-account.mjs +27 -4
  39. package/dist/oauth2/state.mjs +8 -2
  40. package/dist/package.mjs +1 -1
  41. package/dist/plugins/access/access.mjs +11 -6
  42. package/dist/plugins/admin/admin.mjs +0 -4
  43. package/dist/plugins/admin/client.d.mts +1 -1
  44. package/dist/plugins/admin/routes.mjs +3 -3
  45. package/dist/plugins/anonymous/index.mjs +2 -2
  46. package/dist/plugins/bearer/index.mjs +4 -9
  47. package/dist/plugins/captcha/index.mjs +2 -2
  48. package/dist/plugins/email-otp/routes.mjs +1 -1
  49. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  50. package/dist/plugins/generic-oauth/index.mjs +6 -6
  51. package/dist/plugins/generic-oauth/routes.mjs +37 -34
  52. package/dist/plugins/generic-oauth/types.d.mts +7 -0
  53. package/dist/plugins/last-login-method/client.mjs +2 -2
  54. package/dist/plugins/magic-link/index.mjs +0 -1
  55. package/dist/plugins/mcp/index.mjs +2 -5
  56. package/dist/plugins/multi-session/index.mjs +2 -2
  57. package/dist/plugins/oauth-proxy/index.mjs +45 -32
  58. package/dist/plugins/oauth-proxy/utils.mjs +3 -10
  59. package/dist/plugins/oidc-provider/index.mjs +2 -5
  60. package/dist/plugins/one-tap/client.mjs +9 -2
  61. package/dist/plugins/one-tap/index.mjs +16 -39
  62. package/dist/plugins/open-api/generator.mjs +16 -5
  63. package/dist/plugins/organization/adapter.mjs +61 -56
  64. package/dist/plugins/organization/client.d.mts +2 -1
  65. package/dist/plugins/organization/error-codes.d.mts +1 -0
  66. package/dist/plugins/organization/error-codes.mjs +2 -1
  67. package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
  68. package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
  69. package/dist/plugins/organization/routes/crud-org.mjs +2 -2
  70. package/dist/plugins/organization/types.d.mts +3 -3
  71. package/dist/plugins/phone-number/routes.mjs +1 -1
  72. package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
  73. package/dist/plugins/two-factor/client.mjs +2 -1
  74. package/dist/plugins/two-factor/index.mjs +3 -2
  75. package/dist/plugins/username/index.d.mts +24 -2
  76. package/dist/plugins/username/index.mjs +49 -3
  77. package/dist/state.d.mts +2 -2
  78. package/dist/state.mjs +18 -4
  79. package/dist/test-utils/headers.mjs +2 -7
  80. package/dist/test-utils/test-instance.d.mts +36 -144
  81. package/dist/utils/index.d.mts +1 -1
  82. package/dist/utils/url.d.mts +2 -1
  83. package/dist/utils/url.mjs +9 -3
  84. package/package.json +15 -14
@@ -1,4 +1,5 @@
1
1
  import { symmetricDecodeJWT, symmetricEncodeJWT } from "../crypto/jwt.mjs";
2
+ import { parseCookies } from "./cookie-utils.mjs";
2
3
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
4
  import * as z from "zod";
4
5
  //#region src/cookies/session-store.ts
@@ -6,20 +7,6 @@ const ALLOWED_COOKIE_SIZE = 4096;
6
7
  const ESTIMATED_EMPTY_COOKIE_SIZE = 200;
7
8
  const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
8
9
  /**
9
- * Parse cookies from the request headers
10
- */
11
- function parseCookiesFromContext(ctx) {
12
- const cookieHeader = ctx.headers?.get("cookie");
13
- if (!cookieHeader) return {};
14
- const cookies = {};
15
- const pairs = cookieHeader.split("; ");
16
- for (const pair of pairs) {
17
- const [name, ...valueParts] = pair.split("=");
18
- if (name && valueParts.length > 0) cookies[name] = valueParts.join("=");
19
- }
20
- return cookies;
21
- }
22
- /**
23
10
  * Extract the chunk index from a cookie name
24
11
  */
25
12
  function getChunkIndex(cookieName) {
@@ -33,8 +20,8 @@ function getChunkIndex(cookieName) {
33
20
  */
34
21
  function readExistingChunks(cookieName, ctx) {
35
22
  const chunks = {};
36
- const cookies = parseCookiesFromContext(ctx);
37
- for (const [name, value] of Object.entries(cookies)) if (name.startsWith(cookieName)) chunks[name] = value;
23
+ const cookies = parseCookies(ctx.headers?.get("cookie") || "");
24
+ for (const [name, value] of cookies) if (name.startsWith(cookieName)) chunks[name] = value;
38
25
  return chunks;
39
26
  }
40
27
  /**
@@ -140,13 +127,7 @@ function getChunkedCookie(ctx, cookieName) {
140
127
  const chunks = [];
141
128
  const cookieHeader = ctx.headers?.get("cookie");
142
129
  if (!cookieHeader) return null;
143
- const cookies = {};
144
- const pairs = cookieHeader.split("; ");
145
- for (const pair of pairs) {
146
- const [name, ...valueParts] = pair.split("=");
147
- if (name && valueParts.length > 0) cookies[name] = valueParts.join("=");
148
- }
149
- for (const [name, val] of Object.entries(cookies)) if (name.startsWith(cookieName + ".")) {
130
+ for (const [name, val] of parseCookies(cookieHeader)) if (name.startsWith(cookieName + ".")) {
150
131
  const indexStr = name.split(".").at(-1);
151
132
  const index = parseInt(indexStr || "0", 10);
152
133
  if (!isNaN(index)) chunks.push({
@@ -269,13 +269,14 @@ async function getMigrations(config) {
269
269
  return `${model}.${field}`;
270
270
  }
271
271
  }
272
+ const deferredIndexes = [];
272
273
  if (toBeAdded.length) for (const table of toBeAdded) for (const [fieldName, field] of Object.entries(table.fields)) {
273
274
  const type = getType(field, fieldName);
274
275
  const builder = db.schema.alterTable(table.table);
275
276
  if (field.index) {
276
277
  const indexName = `${table.table}_${fieldName}_${field.unique ? "uidx" : "idx"}`;
277
278
  const indexBuilder = db.schema.createIndex(indexName).on(table.table).columns([fieldName]);
278
- migrations.push(field.unique ? indexBuilder.unique() : indexBuilder);
279
+ deferredIndexes.push(field.unique ? indexBuilder.unique() : indexBuilder);
279
280
  }
280
281
  const built = builder.addColumn(fieldName, type, (col) => {
281
282
  col = field.required !== false ? col.notNull() : col;
@@ -287,7 +288,6 @@ async function getMigrations(config) {
287
288
  });
288
289
  migrations.push(built);
289
290
  }
290
- const toBeIndexed = [];
291
291
  if (toBeCreated.length) for (const table of toBeCreated) {
292
292
  const idType = getType({ type: useNumberId ? "number" : "string" }, "id");
293
293
  let dbT = db.schema.createTable(table.table).addColumn("id", idType, (col) => {
@@ -315,12 +315,12 @@ async function getMigrations(config) {
315
315
  });
316
316
  if (field.index) {
317
317
  const builder = db.schema.createIndex(`${table.table}_${fieldName}_${field.unique ? "uidx" : "idx"}`).on(table.table).columns([fieldName]);
318
- toBeIndexed.push(field.unique ? builder.unique() : builder);
318
+ deferredIndexes.push(field.unique ? builder.unique() : builder);
319
319
  }
320
320
  }
321
321
  migrations.push(dbT);
322
322
  }
323
- if (toBeIndexed.length) for (const index of toBeIndexed) migrations.push(index);
323
+ for (const index of deferredIndexes) migrations.push(index);
324
324
  async function runMigrations() {
325
325
  for (const migration of migrations) await migration.execute();
326
326
  }
@@ -3,7 +3,7 @@ import { convertFromDB, convertToDB } from "./field-converter.mjs";
3
3
  import { getSchema } from "./get-schema.mjs";
4
4
  import { DatabaseHooksEntry, getWithHooks } from "./with-hooks.mjs";
5
5
  import { createInternalAdapter } from "./internal-adapter.mjs";
6
- import { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
6
+ import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
7
7
  import { FieldAttributeToSchema, toZodSchema } from "./to-zod.mjs";
8
8
  export * from "@better-auth/core/db";
9
- export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
9
+ export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
package/dist/db/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { __exportAll, __reExport } from "../_virtual/_rolldown/runtime.mjs";
2
2
  import { getSchema } from "./get-schema.mjs";
3
- import { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
3
+ import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
4
4
  import { convertFromDB, convertToDB } from "./field-converter.mjs";
5
5
  import { getWithHooks } from "./with-hooks.mjs";
6
6
  import { createInternalAdapter } from "./internal-adapter.mjs";
@@ -8,6 +8,7 @@ import { toZodSchema } from "./to-zod.mjs";
8
8
  export * from "@better-auth/core/db";
9
9
  //#region src/db/index.ts
10
10
  var db_exports = /* @__PURE__ */ __exportAll({
11
+ buildSyntheticUserOutput: () => buildSyntheticUserOutput,
11
12
  convertFromDB: () => convertFromDB,
12
13
  convertToDB: () => convertToDB,
13
14
  createInternalAdapter: () => createInternalAdapter,
@@ -28,4 +29,4 @@ var db_exports = /* @__PURE__ */ __exportAll({
28
29
  import * as import__better_auth_core_db from "@better-auth/core/db";
29
30
  __reExport(db_exports, import__better_auth_core_db);
30
31
  //#endregion
31
- export { convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
32
+ export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
@@ -388,21 +388,29 @@ const createInternalAdapter = (adapter, ctx) => {
388
388
  value: id
389
389
  }], "account", void 0);
390
390
  },
391
- deleteSessions: async (userIdOrSessionTokens) => {
391
+ deleteUserSessions: async (userId) => {
392
392
  if (secondaryStorage) {
393
- if (typeof userIdOrSessionTokens === "string") {
394
- const activeSession = await secondaryStorage.get(`active-sessions-${userIdOrSessionTokens}`);
395
- const sessions = activeSession ? safeJSONParse(activeSession) : [];
396
- if (!sessions) return;
397
- for (const session of sessions) await secondaryStorage.delete(session.token);
398
- await secondaryStorage.delete(`active-sessions-${userIdOrSessionTokens}`);
399
- } else for (const sessionToken of userIdOrSessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken);
393
+ const activeSession = await secondaryStorage.get(`active-sessions-${userId}`);
394
+ const sessions = activeSession ? safeJSONParse(activeSession) : [];
395
+ if (!sessions) return;
396
+ for (const session of sessions) await secondaryStorage.delete(session.token);
397
+ await secondaryStorage.delete(`active-sessions-${userId}`);
400
398
  if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return;
401
399
  }
402
400
  await deleteManyWithHooks([{
403
- field: Array.isArray(userIdOrSessionTokens) ? "token" : "userId",
404
- value: userIdOrSessionTokens,
405
- operator: Array.isArray(userIdOrSessionTokens) ? "in" : void 0
401
+ field: "userId",
402
+ value: userId
403
+ }], "session", void 0);
404
+ },
405
+ deleteSessions: async (sessionTokens) => {
406
+ if (secondaryStorage) {
407
+ for (const sessionToken of sessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken);
408
+ if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return;
409
+ }
410
+ await deleteManyWithHooks([{
411
+ field: "token",
412
+ value: sessionTokens,
413
+ operator: "in"
406
414
  }], "session", void 0);
407
415
  },
408
416
  findOAuthUser: async (email, accountId, providerId) => {
@@ -532,15 +540,6 @@ const createInternalAdapter = (adapter, ctx) => {
532
540
  }]
533
541
  });
534
542
  },
535
- findAccount: async (accountId) => {
536
- return await (await getCurrentAdapter(adapter)).findOne({
537
- model: "account",
538
- where: [{
539
- field: "accountId",
540
- value: accountId
541
- }]
542
- });
543
- },
544
543
  findAccountByProviderId: async (accountId, providerId) => {
545
544
  return await (await getCurrentAdapter(adapter)).findOne({
546
545
  model: "account",
@@ -639,17 +638,23 @@ const createInternalAdapter = (adapter, ctx) => {
639
638
  const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier);
640
639
  const storedIdentifier = await processIdentifier(identifier, storageOption);
641
640
  const identifiersToTry = storageOption && storageOption !== "plain" ? [storedIdentifier, identifier] : [storedIdentifier];
642
- if (secondaryStorage && !options.verification?.storeInDatabase) {
643
- const parseCachedVerification = (raw) => {
644
- if (!raw) return null;
645
- if (typeof raw === "string") return safeJSONParse(raw);
646
- if (typeof raw === "object") return raw;
647
- return null;
641
+ const hydrateCachedVerification = (raw) => {
642
+ if (!raw) return null;
643
+ const candidate = typeof raw === "string" ? safeJSONParse(raw) : typeof raw === "object" ? raw : null;
644
+ if (!candidate) return null;
645
+ const expiresAt = new Date(candidate.expiresAt);
646
+ if (!Number.isFinite(expiresAt.getTime())) return null;
647
+ return {
648
+ ...candidate,
649
+ expiresAt
648
650
  };
651
+ };
652
+ let consumed = null;
653
+ if (secondaryStorage && !options.verification?.storeInDatabase) {
649
654
  const consumeCacheKey = async (key) => {
650
- if (secondaryStorage.getAndDelete) return parseCachedVerification(await secondaryStorage.getAndDelete(key));
655
+ if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
651
656
  return withVerificationConsumeLock(key, async () => {
652
- const parsed = parseCachedVerification(await secondaryStorage.get(key));
657
+ const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
653
658
  if (!parsed) return null;
654
659
  await secondaryStorage.delete(key);
655
660
  return parsed;
@@ -659,17 +664,16 @@ const createInternalAdapter = (adapter, ctx) => {
659
664
  const cached = await consumeCacheKey(`verification:${stored}`);
660
665
  if (!cached) continue;
661
666
  await Promise.all(identifiersToTry.filter((candidate) => candidate !== stored).map((candidate) => secondaryStorage.delete(`verification:${candidate}`)));
662
- return cached;
667
+ consumed = cached;
668
+ break;
663
669
  }
664
- return null;
665
- }
666
- async function consumeByIdentifier(id) {
667
- const where = [{
668
- field: "identifier",
669
- value: id
670
- }];
671
- return withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
670
+ } else {
671
+ const consumeByIdentifier = async (id) => withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
672
672
  const txAdapter = await getCurrentAdapter(adapter);
673
+ const where = [{
674
+ field: "identifier",
675
+ value: id
676
+ }];
673
677
  const latest = (await txAdapter.findMany({
674
678
  model: "verification",
675
679
  where,
@@ -680,30 +684,32 @@ const createInternalAdapter = (adapter, ctx) => {
680
684
  limit: 1
681
685
  }))[0] ?? null;
682
686
  if (!latest) return null;
683
- const hookWhere = [{
687
+ return consumeOneWithHooks("verification", [{
684
688
  field: "id",
685
689
  value: latest.id
686
- }];
687
- return consumeOneWithHooks("verification", hookWhere, async () => {
688
- const consumed = await txAdapter.consumeOne({
690
+ }], async () => {
691
+ const row = await txAdapter.consumeOne({
689
692
  model: "verification",
690
- where: hookWhere
693
+ where: [{
694
+ field: "id",
695
+ value: latest.id
696
+ }]
691
697
  });
692
- if (!consumed) return null;
698
+ if (!row) return null;
693
699
  await txAdapter.deleteMany({
694
700
  model: "verification",
695
701
  where
696
702
  });
697
- return consumed;
703
+ return row;
698
704
  }, latest);
699
705
  }));
706
+ for (const stored of identifiersToTry) {
707
+ consumed = await consumeByIdentifier(stored);
708
+ if (consumed) break;
709
+ }
710
+ if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
700
711
  }
701
- let consumed = null;
702
- for (const stored of identifiersToTry) {
703
- consumed = await consumeByIdentifier(stored);
704
- if (consumed) break;
705
- }
706
- if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
712
+ if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
707
713
  return consumed;
708
714
  },
709
715
  updateVerificationByIdentifier: async (identifier, data) => {
@@ -4,8 +4,21 @@ import { BetterAuthPluginDBSchema, DBFieldAttribute } from "@better-auth/core/db
4
4
 
5
5
  //#region src/db/schema.d.ts
6
6
  declare function parseUserOutput<T extends User$1>(options: BetterAuthOptions, user: T): T;
7
+ /**
8
+ * Builds a synthetic user object that matches the shape of a real user
9
+ * returned from the database. This ensures enumeration protection works
10
+ * correctly by making synthetic and real user responses indistinguishable.
11
+ *
12
+ * The function iterates over the user output schema and:
13
+ * - Includes all fields that should be returned (returned !== false)
14
+ * - Uses provided values when available
15
+ * - Sets optional fields to null when no value is provided
16
+ * - Applies default values where defined
17
+ * - Always includes the 'id' field (not part of schema but always present)
18
+ */
19
+ declare function buildSyntheticUserOutput(options: BetterAuthOptions, data: Record<string, unknown>): Record<string, unknown>;
7
20
  declare function parseSessionOutput<T extends Session$1>(options: BetterAuthOptions, session: T): T;
8
- declare function parseAccountOutput<T extends Account>(options: BetterAuthOptions, account: T): Omit<T, "idToken" | "accessToken" | "refreshToken" | "accessTokenExpiresAt" | "refreshTokenExpiresAt" | "password">;
21
+ declare function parseAccountOutput<T extends Account>(options: BetterAuthOptions, account: T): Omit<T, "idToken" | "accessToken" | "refreshToken" | "password" | "accessTokenExpiresAt" | "refreshTokenExpiresAt">;
9
22
  declare function parseInputData<T extends Record<string, any>>(data: T, schema: {
10
23
  fields: Record<string, DBFieldAttribute>;
11
24
  action?: ("create" | "update") | undefined;
@@ -45,4 +58,4 @@ declare function mergeSchema<S extends BetterAuthPluginDBSchema>(schema: S, newS
45
58
  } | undefined;
46
59
  } | undefined } | undefined): S;
47
60
  //#endregion
48
- export { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
61
+ export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
@@ -24,6 +24,31 @@ function getFields(options, modelName, mode) {
24
24
  function parseUserOutput(options, user) {
25
25
  return filterOutputFields(user, getFields(options, "user", "output"));
26
26
  }
27
+ /**
28
+ * Builds a synthetic user object that matches the shape of a real user
29
+ * returned from the database. This ensures enumeration protection works
30
+ * correctly by making synthetic and real user responses indistinguishable.
31
+ *
32
+ * The function iterates over the user output schema and:
33
+ * - Includes all fields that should be returned (returned !== false)
34
+ * - Uses provided values when available
35
+ * - Sets optional fields to null when no value is provided
36
+ * - Applies default values where defined
37
+ * - Always includes the 'id' field (not part of schema but always present)
38
+ */
39
+ function buildSyntheticUserOutput(options, data) {
40
+ const schema = getFields(options, "user", "output");
41
+ const result = {};
42
+ for (const key in schema) {
43
+ const fieldAttr = schema[key];
44
+ if (fieldAttr.returned === false) continue;
45
+ if (key in data && data[key] !== void 0) result[key] = data[key];
46
+ else if (fieldAttr.defaultValue !== void 0) result[key] = typeof fieldAttr.defaultValue === "function" ? fieldAttr.defaultValue() : fieldAttr.defaultValue;
47
+ else if (!fieldAttr.required) result[key] = null;
48
+ }
49
+ if ("id" in data) result.id = data.id;
50
+ return result;
51
+ }
27
52
  function parseSessionOutput(options, session) {
28
53
  return filterOutputFields(session, getFields(options, "session", "output"));
29
54
  }
@@ -121,4 +146,4 @@ function mergeSchema(schema, newSchema) {
121
146
  return schema;
122
147
  }
123
148
  //#endregion
124
- export { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
149
+ export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
package/dist/index.d.mts CHANGED
@@ -10,7 +10,7 @@ import { betterAuth } from "./auth/full.mjs";
10
10
  import { generateState, parseState } from "./oauth2/state.mjs";
11
11
  import { StateData, generateGenericState, parseGenericState } from "./state.mjs";
12
12
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
13
- import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./utils/url.mjs";
13
+ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
14
14
  import { APIError } from "./api/index.mjs";
15
15
  import { StandardSchemaV1 } from "@better-auth/core";
16
16
  import { getCurrentAdapter } from "@better-auth/core/context";
@@ -27,4 +27,4 @@ export * from "@better-auth/core/utils/json";
27
27
  export * from "@better-auth/core/social-providers";
28
28
  export * from "better-call";
29
29
  export * from "zod";
30
- export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL };
30
+ export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./utils/url.mjs";
1
+ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
2
2
  import { generateGenericState, parseGenericState } from "./state.mjs";
3
3
  import { generateState, parseState } from "./oauth2/state.mjs";
4
4
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
14
14
  export * from "@better-auth/core/utils/error-codes";
15
15
  export * from "@better-auth/core/utils/id";
16
16
  export * from "@better-auth/core/utils/json";
17
- export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL };
17
+ export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
@@ -1,6 +1,21 @@
1
1
  //#region src/oauth2/errors.ts
2
2
  const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handling-providers-without-email";
3
3
  /**
4
+ * Redirect the user to the OAuth error page with a machine-readable `error`
5
+ * code (and optional `error_description`).
6
+ *
7
+ * Every OAuth callback path routes its failures through this helper so the
8
+ * query parameter name, the `?`/`&` separator, and URL encoding are decided in
9
+ * one place. The error page reads the `error` query parameter, so callers must
10
+ * never hand-build the redirect with a different parameter name.
11
+ */
12
+ function redirectOnError(ctx, errorURL, error, description) {
13
+ const params = new URLSearchParams({ error });
14
+ if (description) params.set("error_description", description);
15
+ const sep = errorURL.includes("?") ? "&" : "?";
16
+ throw ctx.redirect(`${errorURL}${sep}${params.toString()}`);
17
+ }
18
+ /**
4
19
  * Build the logger message shown when an OAuth provider does not return an
5
20
  * email address. Kept in one place so every rejection site points users at
6
21
  * the same workaround docs.
@@ -9,4 +24,4 @@ function missingEmailLogMessage(providerId, options) {
9
24
  return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or synthesize one via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
10
25
  }
11
26
  //#endregion
12
- export { missingEmailLogMessage };
27
+ export { missingEmailLogMessage, redirectOnError };
@@ -1,5 +1,5 @@
1
1
  import { generateState, parseState } from "./state.mjs";
2
- import { handleOAuthUserInfo } from "./link-account.mjs";
2
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
3
3
  import { decryptOAuthToken, setTokenUtil } from "./utils.mjs";
4
4
  export * from "@better-auth/core/oauth2";
5
- export { decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
5
+ export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
@@ -1,5 +1,5 @@
1
- import { generateState, parseState } from "./state.mjs";
2
1
  import { decryptOAuthToken, setTokenUtil } from "./utils.mjs";
3
- import { handleOAuthUserInfo } from "./link-account.mjs";
2
+ import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
3
+ import { generateState, parseState } from "./state.mjs";
4
4
  export * from "@better-auth/core/oauth2";
5
- export { decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
5
+ export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
@@ -42,5 +42,31 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
42
42
  error: null;
43
43
  isRegister: boolean;
44
44
  }>;
45
+ /**
46
+ * Provider profile a freshly linked account may copy onto the local user.
47
+ * `id` is the provider's account id (never the local user id), and `email`/
48
+ * `emailVerified` are identity anchors; all three are stripped before the
49
+ * remaining fields are written.
50
+ */
51
+ type LinkedProviderProfile = {
52
+ id: string | number;
53
+ name?: string | undefined;
54
+ email?: string | null | undefined;
55
+ emailVerified?: boolean | undefined;
56
+ image?: string | null | undefined;
57
+ };
58
+ /**
59
+ * Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
60
+ * copy the freshly linked provider's profile onto the local user, matching the
61
+ * field set persisted on sign-up. The local `email` and `emailVerified` are
62
+ * never changed, so a link can't rebind the account's identity, and
63
+ * `updateUser` drops `undefined` fields, so a provider that omits one leaves
64
+ * the existing column intact.
65
+ *
66
+ * Returns the updated user so a caller that issues a session can seed the
67
+ * cookie cache with the fresh row. Returns `undefined` when the policy is
68
+ * disabled or the update fails: a failed profile sync must not abort the link.
69
+ */
70
+ declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
45
71
  //#endregion
46
- export { handleOAuthUserInfo };
72
+ export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
@@ -1,5 +1,6 @@
1
1
  import { isAPIError } from "../utils/is-api-error.mjs";
2
2
  import { setAccountCookie } from "../cookies/session-store.mjs";
3
+ import { redirectOnError } from "./errors.mjs";
3
4
  import { setTokenUtil } from "./utils.mjs";
4
5
  import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
5
6
  import { isDevelopment, logger } from "@better-auth/core/env";
@@ -8,8 +9,7 @@ async function handleOAuthUserInfo(c, opts) {
8
9
  const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
9
10
  const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
10
11
  logger.error("Better auth was unable to query your database.\nError: ", e);
11
- const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
12
- throw c.redirect(`${errorURL}?error=internal_server_error`);
12
+ redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
13
13
  });
14
14
  let user = dbUser?.user;
15
15
  const isRegister = !user;
@@ -46,6 +46,7 @@ async function handleOAuthUserInfo(c, opts) {
46
46
  };
47
47
  }
48
48
  if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
49
+ user = await applyUpdateUserInfoOnLink(c, dbUser.user.id, userInfo) ?? user;
49
50
  } else {
50
51
  const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({
51
52
  idToken: account.idToken,
@@ -96,7 +97,7 @@ async function handleOAuthUserInfo(c, opts) {
96
97
  if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
97
98
  if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp && c.context.options.emailVerification?.sendVerificationEmail) {
98
99
  const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
99
- const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
100
+ const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
100
101
  await c.context.runInBackgroundOrAwait(c.context.options.emailVerification.sendVerificationEmail({
101
102
  user,
102
103
  url,
@@ -137,5 +138,27 @@ async function handleOAuthUserInfo(c, opts) {
137
138
  isRegister
138
139
  };
139
140
  }
141
+ /**
142
+ * Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
143
+ * copy the freshly linked provider's profile onto the local user, matching the
144
+ * field set persisted on sign-up. The local `email` and `emailVerified` are
145
+ * never changed, so a link can't rebind the account's identity, and
146
+ * `updateUser` drops `undefined` fields, so a provider that omits one leaves
147
+ * the existing column intact.
148
+ *
149
+ * Returns the updated user so a caller that issues a session can seed the
150
+ * cookie cache with the fresh row. Returns `undefined` when the policy is
151
+ * disabled or the update fails: a failed profile sync must not abort the link.
152
+ */
153
+ async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
154
+ if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
155
+ const { id: _id, email: _email, emailVerified: _emailVerified, ...profile } = userInfo;
156
+ try {
157
+ return await c.context.internalAdapter.updateUser(userId, profile);
158
+ } catch (e) {
159
+ c.context.logger.warn("Could not update user info on account link", e);
160
+ return;
161
+ }
162
+ }
140
163
  //#endregion
141
- export { handleOAuthUserInfo };
164
+ export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
@@ -1,4 +1,5 @@
1
1
  import { generateRandomString } from "../crypto/random.mjs";
2
+ import { redirectOnError } from "./errors.mjs";
2
3
  import { setOAuthState } from "../api/state/oauth.mjs";
3
4
  import { StateError, generateGenericState, parseGenericState } from "../state.mjs";
4
5
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -36,8 +37,13 @@ async function parseState(c) {
36
37
  parsedData = await parseGenericState(c, state);
37
38
  } catch (error) {
38
39
  c.context.logger.error("Failed to parse state", error);
39
- if (error instanceof StateError && error.code === "state_security_mismatch") throw c.redirect(`${errorURL}?error=state_mismatch`);
40
- throw c.redirect(`${errorURL}?error=please_restart_the_process`);
40
+ let code = "internal_server_error";
41
+ let redirectErrorURL = errorURL;
42
+ if (error instanceof StateError) {
43
+ code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
44
+ redirectErrorURL = error.errorURL ?? errorURL;
45
+ }
46
+ redirectOnError(c, redirectErrorURL, code);
41
47
  }
42
48
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
43
49
  if (parsedData) await setOAuthState(parsedData);
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.11";
2
+ var version = "1.6.13";
3
3
  //#endregion
4
4
  export { version };
@@ -6,14 +6,19 @@ function role(statements) {
6
6
  let success = false;
7
7
  for (const [requestedResource, requestedActions] of Object.entries(request)) {
8
8
  const allowedActions = statements[requestedResource];
9
- if (!allowedActions) return {
10
- success: false,
11
- error: `You are not allowed to access resource: ${requestedResource}`
12
- };
13
- if (Array.isArray(requestedActions)) success = requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
9
+ if (!allowedActions) {
10
+ if (connector === "AND") return {
11
+ success: false,
12
+ error: `You are not allowed to access resource: ${requestedResource}`
13
+ };
14
+ success = false;
15
+ continue;
16
+ }
17
+ if (Array.isArray(requestedActions)) success = requestedActions.length > 0 && requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
14
18
  else if (typeof requestedActions === "object") {
15
19
  const actions = requestedActions;
16
- if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
20
+ if (!Array.isArray(actions.actions) || actions.actions.length === 0) success = false;
21
+ else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
17
22
  else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
18
23
  } else throw new BetterAuthError("Invalid access control request");
19
24
  if (success && connector === "OR") return { success };
@@ -42,10 +42,6 @@ const admin = (options) => {
42
42
  });
43
43
  return;
44
44
  }
45
- if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) {
46
- const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
47
- throw ctx.redirect(`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`);
48
- }
49
45
  throw APIError.from("FORBIDDEN", {
50
46
  message: opts.bannedUserMessage,
51
47
  code: "BANNED_USER"
@@ -76,4 +76,4 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
76
76
  };
77
77
  };
78
78
  //#endregion
79
- export { adminClient };
79
+ export { AdminClientOptions, adminClient };
@@ -462,7 +462,7 @@ const banUser = (opts) => createAuthEndpoint("/admin/ban-user", {
462
462
  banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : void 0,
463
463
  updatedAt: /* @__PURE__ */ new Date()
464
464
  });
465
- await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
465
+ await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
466
466
  return ctx.json({ user: parseUserOutput(ctx.context.options, user) });
467
467
  });
468
468
  const impersonateUserBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
@@ -658,7 +658,7 @@ const revokeUserSessions = (opts) => createAuthEndpoint("/admin/revoke-user-sess
658
658
  options: opts,
659
659
  permissions: { session: ["revoke"] }
660
660
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS);
661
- await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
661
+ await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
662
662
  return ctx.json({ success: true });
663
663
  });
664
664
  const removeUserBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
@@ -703,7 +703,7 @@ const removeUser = (opts) => createAuthEndpoint("/admin/remove-user", {
703
703
  })) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS);
704
704
  if (ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF);
705
705
  if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
706
- await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
706
+ await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
707
707
  await ctx.context.internalAdapter.deleteUser(ctx.body.userId);
708
708
  return ctx.json({ success: true });
709
709
  });