@better-auth/core 1.6.18 → 1.6.20

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.
@@ -2,7 +2,7 @@
2
2
  const symbol = Symbol.for("better-auth:global");
3
3
  let bind = null;
4
4
  const __context = {};
5
- const __betterAuthVersion = "1.6.18";
5
+ const __betterAuthVersion = "1.6.20";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -1,10 +1,13 @@
1
1
  import { DBAdapter, DBTransactionAdapter } from "../db/adapter/index.mjs";
2
+ import { BetterAuthOptions } from "../types/init-options.mjs";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
4
 
4
5
  //#region src/context/transaction.d.ts
6
+ type StoredAdapter = DBTransactionAdapter<BetterAuthOptions>;
5
7
  type HookContext = {
6
- adapter: DBTransactionAdapter;
8
+ adapter: StoredAdapter;
7
9
  pendingHooks: Array<() => Promise<void>>;
10
+ isTransactionActive: boolean;
8
11
  };
9
12
  /**
10
13
  * This is for internal use only. Most users should use `getCurrentAdapter` instead.
@@ -12,9 +15,9 @@ type HookContext = {
12
15
  * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
13
16
  */
14
17
  declare const getCurrentDBAdapterAsyncLocalStorage: () => Promise<AsyncLocalStorage<HookContext>>;
15
- declare const getCurrentAdapter: (fallback: DBTransactionAdapter) => Promise<DBTransactionAdapter>;
16
- declare const runWithAdapter: <R>(adapter: DBAdapter, fn: () => R) => Promise<R>;
17
- declare const runWithTransaction: <R>(adapter: DBAdapter, fn: () => R) => Promise<R>;
18
+ declare const getCurrentAdapter: <Options extends BetterAuthOptions = BetterAuthOptions>(fallback: DBTransactionAdapter<Options>) => Promise<DBTransactionAdapter<Options>>;
19
+ declare const runWithAdapter: <R, Options extends BetterAuthOptions = BetterAuthOptions>(adapter: DBAdapter<Options>, fn: () => R) => Promise<R>;
20
+ declare const runWithTransaction: <R, Options extends BetterAuthOptions = BetterAuthOptions>(adapter: DBAdapter<Options>, fn: () => R) => Promise<R>;
18
21
  /**
19
22
  * Queue a hook to be executed after the current transaction commits.
20
23
  * If not in a transaction, the hook will execute immediately.
@@ -35,7 +35,8 @@ const runWithAdapter = async (adapter, fn) => {
35
35
  try {
36
36
  result = await als.run({
37
37
  adapter,
38
- pendingHooks
38
+ pendingHooks,
39
+ isTransactionActive: false
39
40
  }, fn);
40
41
  } catch (err) {
41
42
  error = err;
@@ -50,9 +51,10 @@ const runWithAdapter = async (adapter, fn) => {
50
51
  });
51
52
  };
52
53
  const runWithTransaction = async (adapter, fn) => {
53
- let called = true;
54
+ let called = false;
54
55
  return ensureAsyncStorage().then(async (als) => {
55
56
  called = true;
57
+ if (als.getStore()?.isTransactionActive) return fn();
56
58
  const pendingHooks = [];
57
59
  let result;
58
60
  let error;
@@ -61,7 +63,8 @@ const runWithTransaction = async (adapter, fn) => {
61
63
  result = await adapter.transaction(async (trx) => {
62
64
  return als.run({
63
65
  adapter: trx,
64
- pendingHooks
66
+ pendingHooks,
67
+ isTransactionActive: true
65
68
  }, fn);
66
69
  });
67
70
  } catch (e) {
@@ -1,3 +1,4 @@
1
+ import { getCurrentAdapter, runWithTransaction } from "../../context/transaction.mjs";
1
2
  import { BetterAuthError } from "../../error/index.mjs";
2
3
  import { getAuthTables } from "../get-tables.mjs";
3
4
  import { getColorDepth } from "../../env/color-depth.mjs";
@@ -705,7 +706,8 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
705
706
  res = await withSpan(`db consumeOne ${model}`, {
706
707
  [ATTR_DB_OPERATION_NAME]: "consumeOne",
707
708
  [ATTR_DB_COLLECTION_NAME]: model
708
- }, () => adapter.transaction(async (trx) => {
709
+ }, () => runWithTransaction(adapter, async () => {
710
+ const trx = await getCurrentAdapter(adapter);
709
711
  const target = (await trx.findMany({
710
712
  model: unsafeModel,
711
713
  where: unsafeWhere,
@@ -740,6 +742,9 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
740
742
  return transformed;
741
743
  },
742
744
  incrementOne: async ({ model: unsafeModel, where: unsafeWhere, increment: unsafeIncrement, set: unsafeSet }) => {
745
+ const hasIncrement = Object.keys(unsafeIncrement).length > 0;
746
+ const hasSet = !!unsafeSet && Object.keys(unsafeSet).length > 0;
747
+ if (!hasIncrement && !hasSet) throw new BetterAuthError("incrementOne requires a non-empty `increment` or `set`; both were empty.");
743
748
  transactionId++;
744
749
  const thisTransactionId = transactionId;
745
750
  const model = getModelName(unsafeModel);
@@ -767,6 +772,7 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
767
772
  let set;
768
773
  if (unsafeSet && !config.disableTransformInput) set = await transformInput(unsafeSet, unsafeModel, "update");
769
774
  else set = unsafeSet;
775
+ if (Object.keys(increment).length === 0 && (!set || Object.keys(set).length === 0)) throw new BetterAuthError("incrementOne resolved to an empty update: every increment/set field was unknown to the schema or transformed away.");
770
776
  res = await withSpan(`db incrementOne ${model}`, {
771
777
  [ATTR_DB_OPERATION_NAME]: "incrementOne",
772
778
  [ATTR_DB_COLLECTION_NAME]: model
@@ -780,7 +786,8 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
780
786
  res = await withSpan(`db incrementOne ${model}`, {
781
787
  [ATTR_DB_OPERATION_NAME]: "incrementOne",
782
788
  [ATTR_DB_COLLECTION_NAME]: model
783
- }, () => adapter.transaction(async (trx) => {
789
+ }, () => runWithTransaction(adapter, async () => {
790
+ const trx = await getCurrentAdapter(adapter);
784
791
  const target = (await trx.findMany({
785
792
  model: unsafeModel,
786
793
  where: unsafeWhere,
@@ -7,7 +7,14 @@ declare class BetterAuthError extends Error {
7
7
  cause?: unknown | undefined;
8
8
  });
9
9
  }
10
+ type BaseAPIErrorInstance = InstanceType<typeof APIError$1>;
10
11
  declare class APIError extends APIError$1 {
12
+ status: BaseAPIErrorInstance["status"];
13
+ body: BaseAPIErrorInstance["body"];
14
+ headers: BaseAPIErrorInstance["headers"];
15
+ statusCode: BaseAPIErrorInstance["statusCode"];
16
+ message: string;
17
+ errorStack: BaseAPIErrorInstance["errorStack"];
11
18
  constructor(...args: ConstructorParameters<typeof APIError$1>);
12
19
  static fromStatus(status: ConstructorParameters<typeof APIError$1>[0], body?: ConstructorParameters<typeof APIError$1>[1]): APIError;
13
20
  static from(status: ConstructorParameters<typeof APIError$1>[0], error: {
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
2
2
  import { getOpenTelemetryAPI } from "./api.mjs";
3
3
  //#region src/instrumentation/tracer.ts
4
4
  const INSTRUMENTATION_SCOPE = "better-auth";
5
- const INSTRUMENTATION_VERSION = "1.6.18";
5
+ const INSTRUMENTATION_VERSION = "1.6.20";
6
6
  /**
7
7
  * Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
8
8
  * callbacks). These are APIErrors with 3xx status codes and should not be
@@ -1033,9 +1033,13 @@ type BetterAuthOptions = {
1033
1033
  */
1034
1034
  storeStateStrategy?: "database" | "cookie";
1035
1035
  /**
1036
- * Store account data after oauth flow on a cookie
1036
+ * Store provider account data after an OAuth flow in an encrypted
1037
+ * cookie. This includes OAuth token material such as access tokens,
1038
+ * refresh tokens, ID tokens, scopes, and token expiry.
1037
1039
  *
1038
- * This is useful for database-less flow
1040
+ * This is useful for database-less flows, but large provider tokens can
1041
+ * still hit browser or proxy cookie/header limits even though Better Auth
1042
+ * chunks oversized account cookies.
1039
1043
  *
1040
1044
  * @default false
1041
1045
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.6.18",
3
+ "version": "1.6.20",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -152,8 +152,8 @@
152
152
  "zod": "^4.3.6"
153
153
  },
154
154
  "devDependencies": {
155
- "@better-auth/utils": "0.4.1",
156
- "@better-fetch/fetch": "1.3.0",
155
+ "@better-auth/utils": "0.4.2",
156
+ "@better-fetch/fetch": "1.3.1",
157
157
  "@opentelemetry/api": "^1.9.0",
158
158
  "@opentelemetry/sdk-trace-base": "^1.30.0",
159
159
  "@opentelemetry/sdk-trace-node": "^1.30.0",
@@ -165,8 +165,8 @@
165
165
  "tsdown": "0.21.1"
166
166
  },
167
167
  "peerDependencies": {
168
- "@better-auth/utils": "0.4.1",
169
- "@better-fetch/fetch": "1.3.0",
168
+ "@better-auth/utils": "0.4.2",
169
+ "@better-fetch/fetch": "1.3.1",
170
170
  "@opentelemetry/api": "^1.9.0",
171
171
  "better-call": "1.3.6",
172
172
  "@cloudflare/workers-types": ">=4",
@@ -1,11 +1,15 @@
1
1
  import type { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
3
  import type { DBAdapter, DBTransactionAdapter } from "../db/adapter";
4
+ import type { BetterAuthOptions } from "../types";
4
5
  import { __getBetterAuthGlobal } from "./global";
5
6
 
7
+ type StoredAdapter = DBTransactionAdapter<BetterAuthOptions>;
8
+
6
9
  type HookContext = {
7
- adapter: DBTransactionAdapter;
10
+ adapter: StoredAdapter;
8
11
  pendingHooks: Array<() => Promise<void>>;
12
+ isTransactionActive: boolean;
9
13
  };
10
14
 
11
15
  const ensureAsyncStorage = async () => {
@@ -27,21 +31,29 @@ export const getCurrentDBAdapterAsyncLocalStorage = async () => {
27
31
  return ensureAsyncStorage();
28
32
  };
29
33
 
30
- export const getCurrentAdapter = async (
31
- fallback: DBTransactionAdapter,
32
- ): Promise<DBTransactionAdapter> => {
34
+ export const getCurrentAdapter = async <
35
+ Options extends BetterAuthOptions = BetterAuthOptions,
36
+ >(
37
+ fallback: DBTransactionAdapter<Options>,
38
+ ): Promise<DBTransactionAdapter<Options>> => {
33
39
  return ensureAsyncStorage()
34
40
  .then((als) => {
35
41
  const store = als.getStore();
36
- return store?.adapter || fallback;
42
+ return (
43
+ (store?.adapter as DBTransactionAdapter<Options> | undefined) ||
44
+ fallback
45
+ );
37
46
  })
38
47
  .catch(() => {
39
48
  return fallback;
40
49
  });
41
50
  };
42
51
 
43
- export const runWithAdapter = async <R>(
44
- adapter: DBAdapter,
52
+ export const runWithAdapter = async <
53
+ R,
54
+ Options extends BetterAuthOptions = BetterAuthOptions,
55
+ >(
56
+ adapter: DBAdapter<Options>,
45
57
  fn: () => R,
46
58
  ): Promise<R> => {
47
59
  let called = false;
@@ -53,7 +65,14 @@ export const runWithAdapter = async <R>(
53
65
  let error: unknown;
54
66
  let hasError = false;
55
67
  try {
56
- result = await als.run({ adapter, pendingHooks }, fn);
68
+ result = await als.run(
69
+ {
70
+ adapter: adapter as unknown as StoredAdapter,
71
+ pendingHooks,
72
+ isTransactionActive: false,
73
+ },
74
+ fn,
75
+ );
57
76
  } catch (err) {
58
77
  error = err;
59
78
  hasError = true;
@@ -75,21 +94,35 @@ export const runWithAdapter = async <R>(
75
94
  });
76
95
  };
77
96
 
78
- export const runWithTransaction = async <R>(
79
- adapter: DBAdapter,
97
+ export const runWithTransaction = async <
98
+ R,
99
+ Options extends BetterAuthOptions = BetterAuthOptions,
100
+ >(
101
+ adapter: DBAdapter<Options>,
80
102
  fn: () => R,
81
103
  ): Promise<R> => {
82
- let called = true;
104
+ let called = false;
83
105
  return ensureAsyncStorage()
84
106
  .then(async (als) => {
85
107
  called = true;
108
+ const store = als.getStore();
109
+ if (store?.isTransactionActive) {
110
+ return fn();
111
+ }
86
112
  const pendingHooks: Array<() => Promise<void>> = [];
87
113
  let result: Awaited<R>;
88
114
  let error: unknown;
89
115
  let hasError = false;
90
116
  try {
91
117
  result = await adapter.transaction(async (trx) => {
92
- return als.run({ adapter: trx, pendingHooks }, fn);
118
+ return als.run(
119
+ {
120
+ adapter: trx as unknown as StoredAdapter,
121
+ pendingHooks,
122
+ isTransactionActive: true,
123
+ },
124
+ fn,
125
+ );
93
126
  });
94
127
  } catch (e) {
95
128
  hasError = true;
@@ -3,6 +3,10 @@ import {
3
3
  ATTR_DB_OPERATION_NAME,
4
4
  withSpan,
5
5
  } from "@better-auth/core/instrumentation";
6
+ import {
7
+ getCurrentAdapter,
8
+ runWithTransaction,
9
+ } from "../../context/transaction";
6
10
  import { createLogger, getColorDepth, TTY_COLORS } from "../../env";
7
11
  import { BetterAuthError } from "../../error";
8
12
  import type { BetterAuthOptions } from "../../types";
@@ -1364,11 +1368,9 @@ export const createAdapterFactory =
1364
1368
  // engines with real transaction isolation; race window narrows
1365
1369
  // (does not close) on adapters that fall through to sequential
1366
1370
  // execution. Remove this branch when consumeOne becomes required.
1367
- // FIXME(consume-one-nested-transaction): custom adapters without a
1368
- // native consumeOne have no portable signal for "already inside a
1369
- // transaction". First-party adapters mark transaction-scoped
1370
- // adapters as as-is; make that capability explicit in the next
1371
- // breaking adapter contract.
1371
+ // Use Better Auth's transaction context here, not a direct adapter
1372
+ // transaction, so callers already inside a transaction keep using
1373
+ // the active transaction adapter.
1372
1374
  res = await withSpan(
1373
1375
  `db consumeOne ${model}`,
1374
1376
  {
@@ -1376,7 +1378,8 @@ export const createAdapterFactory =
1376
1378
  [ATTR_DB_COLLECTION_NAME]: model,
1377
1379
  },
1378
1380
  () =>
1379
- adapter.transaction(async (trx) => {
1381
+ runWithTransaction(adapter, async () => {
1382
+ const trx = await getCurrentAdapter(adapter);
1380
1383
  const rows = await trx.findMany<Record<string, any>>({
1381
1384
  model: unsafeModel,
1382
1385
  where: unsafeWhere,
@@ -1447,6 +1450,16 @@ export const createAdapterFactory =
1447
1450
  increment: Record<string, number>;
1448
1451
  set?: Record<string, unknown> | undefined;
1449
1452
  }): Promise<T | null> => {
1453
+ const hasIncrement = Object.keys(unsafeIncrement).length > 0;
1454
+ const hasSet = !!unsafeSet && Object.keys(unsafeSet).length > 0;
1455
+ if (!hasIncrement && !hasSet) {
1456
+ // An empty `increment` and empty `set` compiles to `UPDATE ... SET `
1457
+ // with no assignments, which is a syntax error on kysely, drizzle, and
1458
+ // Prisma. Fail fast with an actionable message instead.
1459
+ throw new BetterAuthError(
1460
+ "incrementOne requires a non-empty `increment` or `set`; both were empty.",
1461
+ );
1462
+ }
1450
1463
  transactionId++;
1451
1464
  const thisTransactionId = transactionId;
1452
1465
  const model = getModelName(unsafeModel);
@@ -1483,6 +1496,17 @@ export const createAdapterFactory =
1483
1496
  } else {
1484
1497
  set = unsafeSet;
1485
1498
  }
1499
+ // `transformInput` drops unknown-to-schema and `undefined` fields, so
1500
+ // a `set` that was non-empty on input can resolve to nothing. Re-check
1501
+ // after mapping so this never reaches the adapter as an empty UPDATE.
1502
+ if (
1503
+ Object.keys(increment).length === 0 &&
1504
+ (!set || Object.keys(set).length === 0)
1505
+ ) {
1506
+ throw new BetterAuthError(
1507
+ "incrementOne resolved to an empty update: every increment/set field was unknown to the schema or transformed away.",
1508
+ );
1509
+ }
1486
1510
  res = await withSpan(
1487
1511
  `db incrementOne ${model}`,
1488
1512
  {
@@ -1508,7 +1532,8 @@ export const createAdapterFactory =
1508
1532
  [ATTR_DB_COLLECTION_NAME]: model,
1509
1533
  },
1510
1534
  () =>
1511
- adapter.transaction(async (trx) => {
1535
+ runWithTransaction(adapter, async () => {
1536
+ const trx = await getCurrentAdapter(adapter);
1512
1537
  const rows = await trx.findMany<Record<string, any>>({
1513
1538
  model: unsafeModel,
1514
1539
  where: unsafeWhere,
@@ -11,7 +11,16 @@ export class BetterAuthError extends Error {
11
11
 
12
12
  export { type APIErrorCode, BASE_ERROR_CODES } from "./codes";
13
13
 
14
+ type BaseAPIErrorInstance = InstanceType<typeof BaseAPIError>;
15
+
14
16
  export class APIError extends BaseAPIError {
17
+ declare status: BaseAPIErrorInstance["status"];
18
+ declare body: BaseAPIErrorInstance["body"];
19
+ declare headers: BaseAPIErrorInstance["headers"];
20
+ declare statusCode: BaseAPIErrorInstance["statusCode"];
21
+ declare message: string;
22
+ declare errorStack: BaseAPIErrorInstance["errorStack"];
23
+
15
24
  constructor(...args: ConstructorParameters<typeof BaseAPIError>) {
16
25
  super(...args);
17
26
  }
@@ -1160,9 +1160,13 @@ export type BetterAuthOptions = {
1160
1160
  */
1161
1161
  storeStateStrategy?: "database" | "cookie";
1162
1162
  /**
1163
- * Store account data after oauth flow on a cookie
1163
+ * Store provider account data after an OAuth flow in an encrypted
1164
+ * cookie. This includes OAuth token material such as access tokens,
1165
+ * refresh tokens, ID tokens, scopes, and token expiry.
1164
1166
  *
1165
- * This is useful for database-less flow
1167
+ * This is useful for database-less flows, but large provider tokens can
1168
+ * still hit browser or proxy cookie/header limits even though Better Auth
1169
+ * chunks oversized account cookies.
1166
1170
  *
1167
1171
  * @default false
1168
1172
  *