@better-auth/core 1.7.1 → 1.7.3

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 (96) hide show
  1. package/dist/api/index.d.mts +3 -0
  2. package/dist/context/endpoint-context.d.mts +19 -5
  3. package/dist/context/endpoint-context.mjs +35 -16
  4. package/dist/context/global.mjs +5 -2
  5. package/dist/context/index.d.mts +2 -2
  6. package/dist/context/index.mjs +2 -2
  7. package/dist/context/transaction.mjs +3 -0
  8. package/dist/db/adapter/atomic-fallback.mjs +134 -0
  9. package/dist/db/adapter/factory.mjs +22 -4
  10. package/dist/db/adapter/index.d.mts +15 -11
  11. package/dist/db/get-tables.mjs +1 -9
  12. package/dist/db/index.d.mts +2 -2
  13. package/dist/db/index.mjs +2 -2
  14. package/dist/db/internal.d.mts +3 -1
  15. package/dist/db/internal.mjs +3 -1
  16. package/dist/db/schema/account.d.mts +2 -13
  17. package/dist/db/schema/account.mjs +1 -19
  18. package/dist/db/schema-check.d.mts +48 -0
  19. package/dist/db/schema-check.mjs +80 -0
  20. package/dist/db/schema-diff.d.mts +104 -0
  21. package/dist/db/schema-diff.mjs +154 -0
  22. package/dist/env/logger.mjs +16 -1
  23. package/dist/instrumentation/tracer.mjs +1 -1
  24. package/dist/oauth2/index.d.mts +2 -2
  25. package/dist/oauth2/oauth-provider.d.mts +0 -10
  26. package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
  27. package/dist/oauth2/token-endpoint-auth.mjs +11 -0
  28. package/dist/social-providers/apple.d.mts +0 -1
  29. package/dist/social-providers/apple.mjs +0 -1
  30. package/dist/social-providers/cloudflare.d.mts +132 -0
  31. package/dist/social-providers/cloudflare.mjs +85 -0
  32. package/dist/social-providers/cognito.d.mts +0 -1
  33. package/dist/social-providers/cognito.mjs +0 -1
  34. package/dist/social-providers/facebook.d.mts +0 -1
  35. package/dist/social-providers/facebook.mjs +0 -1
  36. package/dist/social-providers/google.d.mts +0 -1
  37. package/dist/social-providers/google.mjs +0 -1
  38. package/dist/social-providers/index.d.mts +53 -21
  39. package/dist/social-providers/index.mjs +3 -1
  40. package/dist/social-providers/line.d.mts +0 -1
  41. package/dist/social-providers/line.mjs +0 -1
  42. package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
  43. package/dist/social-providers/microsoft-entra-id.mjs +0 -1
  44. package/dist/social-providers/paybin.d.mts +0 -1
  45. package/dist/social-providers/paybin.mjs +0 -1
  46. package/dist/social-providers/paypal.d.mts +3 -11
  47. package/dist/social-providers/paypal.mjs +20 -47
  48. package/dist/social-providers/reddit.mjs +22 -23
  49. package/dist/social-providers/roblox.mjs +5 -1
  50. package/dist/social-providers/tiktok.d.mts +1 -0
  51. package/dist/social-providers/tiktok.mjs +19 -10
  52. package/dist/social-providers/twitter.mjs +5 -1
  53. package/dist/social-providers/wechat.mjs +6 -1
  54. package/dist/types/context.d.mts +11 -0
  55. package/dist/types/init-options.d.mts +11 -0
  56. package/dist/utils/ip.mjs +11 -9
  57. package/dist/utils/url.d.mts +10 -1
  58. package/dist/utils/url.mjs +21 -1
  59. package/package.json +3 -3
  60. package/src/context/endpoint-context.ts +46 -21
  61. package/src/context/global.ts +7 -0
  62. package/src/context/index.ts +2 -0
  63. package/src/context/transaction.ts +5 -0
  64. package/src/db/adapter/atomic-fallback.ts +237 -0
  65. package/src/db/adapter/factory.ts +33 -17
  66. package/src/db/adapter/index.ts +15 -11
  67. package/src/db/get-tables.ts +1 -14
  68. package/src/db/index.ts +0 -2
  69. package/src/db/internal.ts +19 -0
  70. package/src/db/schema/account.ts +3 -22
  71. package/src/db/schema/user.ts +1 -1
  72. package/src/db/schema-check.ts +107 -0
  73. package/src/db/schema-diff.ts +270 -0
  74. package/src/env/logger.ts +22 -1
  75. package/src/oauth2/index.ts +2 -0
  76. package/src/oauth2/oauth-provider.ts +0 -10
  77. package/src/oauth2/token-endpoint-auth.ts +39 -6
  78. package/src/social-providers/apple.ts +0 -1
  79. package/src/social-providers/cloudflare.ts +221 -0
  80. package/src/social-providers/cognito.ts +0 -1
  81. package/src/social-providers/facebook.ts +0 -1
  82. package/src/social-providers/google.ts +0 -1
  83. package/src/social-providers/index.ts +3 -0
  84. package/src/social-providers/line.ts +0 -1
  85. package/src/social-providers/microsoft-entra-id.ts +0 -1
  86. package/src/social-providers/paybin.ts +0 -1
  87. package/src/social-providers/paypal.ts +30 -71
  88. package/src/social-providers/reddit.ts +34 -37
  89. package/src/social-providers/roblox.ts +5 -3
  90. package/src/social-providers/tiktok.ts +25 -14
  91. package/src/social-providers/twitter.ts +8 -2
  92. package/src/social-providers/wechat.ts +6 -6
  93. package/src/types/context.ts +11 -0
  94. package/src/types/init-options.ts +11 -0
  95. package/src/utils/ip.ts +13 -9
  96. package/src/utils/url.ts +43 -0
@@ -1,6 +1,7 @@
1
1
  import { BetterAuthDBSchema, ModelNames, SecondaryStorage } from "../db/type.mjs";
2
2
  import { DBAdapter } from "../db/adapter/index.mjs";
3
3
  import { createLogger } from "../env/logger.mjs";
4
+ import { SchemaCheck } from "../db/schema-check.mjs";
4
5
  import { AuthContext } from "../types/context.mjs";
5
6
  import { OAuthProvider } from "../oauth2/oauth-provider.mjs";
6
7
  import * as _$better_call0 from "better-call";
@@ -112,6 +113,7 @@ declare const createAuthMiddleware: {
112
113
  storage: "memory" | "database" | "secondary-storage";
113
114
  } & Omit<_$_better_auth_core0.BetterAuthRateLimitOptions, "enabled" | "window" | "max" | "storage">;
114
115
  adapter: DBAdapter<_$_better_auth_core0.BetterAuthOptions>;
116
+ checkSchema?: SchemaCheck | undefined;
115
117
  internalAdapter: _$_better_auth_core0.InternalAdapter<_$_better_auth_core0.BetterAuthOptions>;
116
118
  createAuthCookie: (cookieName: string, overrideAttributes?: Partial<_$better_call0.CookieOptions> | undefined) => _$_better_auth_core0.BetterAuthCookie;
117
119
  secret: string;
@@ -242,6 +244,7 @@ declare const createAuthMiddleware: {
242
244
  storage: "memory" | "database" | "secondary-storage";
243
245
  } & Omit<_$_better_auth_core0.BetterAuthRateLimitOptions, "enabled" | "window" | "max" | "storage">;
244
246
  adapter: DBAdapter<_$_better_auth_core0.BetterAuthOptions>;
247
+ checkSchema?: SchemaCheck | undefined;
245
248
  internalAdapter: _$_better_auth_core0.InternalAdapter<_$_better_auth_core0.BetterAuthOptions>;
246
249
  createAuthCookie: (cookieName: string, overrideAttributes?: Partial<_$better_call0.CookieOptions> | undefined) => _$_better_auth_core0.BetterAuthCookie;
247
250
  secret: string;
@@ -6,13 +6,27 @@ import { AsyncLocalStorage } from "@better-auth/core/async_hooks";
6
6
  type AuthEndpointContext = Partial<InputContext<string, any> & EndpointContext<string, any>> & {
7
7
  context: AuthContext;
8
8
  };
9
+ type AuthEndpointContextStorage = AsyncLocalStorage<AuthEndpointContext>;
9
10
  /**
10
- * This is for internal use only. Most users should use `getCurrentAuthContext` instead.
11
+ * @deprecated Use `getCurrentAuthEndpointContext`,
12
+ * `tryGetCurrentAuthEndpointContext`, or `runWithEndpointContext` instead.
13
+ */
14
+ declare function getCurrentAuthContextAsyncLocalStorage(): Promise<AuthEndpointContextStorage>;
15
+ /**
16
+ * Returns the current auth endpoint context, or `undefined` when called outside
17
+ * of `runWithEndpointContext`.
18
+ */
19
+ declare function tryGetCurrentAuthEndpointContext(): AuthEndpointContext | undefined;
20
+ /**
21
+ * Returns the current auth endpoint context.
11
22
  *
12
- * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
23
+ * @throws When called outside of `runWithEndpointContext`.
24
+ */
25
+ declare function getCurrentAuthEndpointContext(): AuthEndpointContext;
26
+ /**
27
+ * @deprecated Use `getCurrentAuthEndpointContext` instead.
13
28
  */
14
- declare function getCurrentAuthContextAsyncLocalStorage(): Promise<AsyncLocalStorage<AuthEndpointContext>>;
15
29
  declare function getCurrentAuthContext(): Promise<AuthEndpointContext>;
16
- declare function runWithEndpointContext<T>(context: AuthEndpointContext, fn: () => T): Promise<T>;
30
+ declare function runWithEndpointContext<T>(authEndpointContext: AuthEndpointContext, fn: () => T): Promise<T>;
17
31
  //#endregion
18
- export { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext };
32
+ export { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext };
@@ -1,29 +1,48 @@
1
- import { __getBetterAuthGlobal } from "./global.mjs";
1
+ import { __getBetterAuthGlobal, __getCurrentEndpointContext } from "./global.mjs";
2
2
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
3
  //#region src/context/endpoint-context.ts
4
- const ensureAsyncStorage = async () => {
5
- const betterAuthGlobal = __getBetterAuthGlobal();
6
- const existing = betterAuthGlobal.context.endpointContextAsyncStorage;
4
+ const getExistingEndpointContextStorage = () => {
5
+ return __getBetterAuthGlobal().context.endpointContextAsyncStorage;
6
+ };
7
+ const getOrCreateEndpointContextStorage = async () => {
8
+ const existing = getExistingEndpointContextStorage();
7
9
  if (existing) return existing;
8
10
  const AsyncLocalStorage = await getAsyncLocalStorage();
9
- betterAuthGlobal.context.endpointContextAsyncStorage ??= new AsyncLocalStorage();
10
- return betterAuthGlobal.context.endpointContextAsyncStorage;
11
+ const globalContext = __getBetterAuthGlobal().context;
12
+ return globalContext.endpointContextAsyncStorage ??= new AsyncLocalStorage();
11
13
  };
12
14
  /**
13
- * This is for internal use only. Most users should use `getCurrentAuthContext` instead.
14
- *
15
- * It is exposed for advanced use cases where you need direct access to the AsyncLocalStorage instance.
15
+ * @deprecated Use `getCurrentAuthEndpointContext`,
16
+ * `tryGetCurrentAuthEndpointContext`, or `runWithEndpointContext` instead.
16
17
  */
17
18
  async function getCurrentAuthContextAsyncLocalStorage() {
18
- return ensureAsyncStorage();
19
+ return getOrCreateEndpointContextStorage();
20
+ }
21
+ /**
22
+ * Returns the current auth endpoint context, or `undefined` when called outside
23
+ * of `runWithEndpointContext`.
24
+ */
25
+ function tryGetCurrentAuthEndpointContext() {
26
+ return __getCurrentEndpointContext();
19
27
  }
28
+ /**
29
+ * Returns the current auth endpoint context.
30
+ *
31
+ * @throws When called outside of `runWithEndpointContext`.
32
+ */
33
+ function getCurrentAuthEndpointContext() {
34
+ const authEndpointContext = tryGetCurrentAuthEndpointContext();
35
+ if (!authEndpointContext) throw new Error("No auth context found. Please make sure you are calling this function within a `runWithEndpointContext` callback.");
36
+ return authEndpointContext;
37
+ }
38
+ /**
39
+ * @deprecated Use `getCurrentAuthEndpointContext` instead.
40
+ */
20
41
  async function getCurrentAuthContext() {
21
- const context = (await ensureAsyncStorage()).getStore();
22
- if (!context) throw new Error("No auth context found. Please make sure you are calling this function within a `runWithEndpointContext` callback.");
23
- return context;
42
+ return getCurrentAuthEndpointContext();
24
43
  }
25
- async function runWithEndpointContext(context, fn) {
26
- return (await ensureAsyncStorage()).run(context, fn);
44
+ async function runWithEndpointContext(authEndpointContext, fn) {
45
+ return (await getOrCreateEndpointContextStorage()).run(authEndpointContext, fn);
27
46
  }
28
47
  //#endregion
29
- export { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext };
48
+ export { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext };
@@ -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.7.1";
5
+ const __betterAuthVersion = "1.7.3";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -29,8 +29,11 @@ function __getBetterAuthGlobal() {
29
29
  }
30
30
  return globalThis[symbol];
31
31
  }
32
+ function __getCurrentEndpointContext() {
33
+ return __getBetterAuthGlobal().context.endpointContextAsyncStorage?.getStore();
34
+ }
32
35
  function getBetterAuthVersion() {
33
36
  return __getBetterAuthGlobal().version;
34
37
  }
35
38
  //#endregion
36
- export { __getBetterAuthGlobal, getBetterAuthVersion };
39
+ export { __getBetterAuthGlobal, __getCurrentEndpointContext, getBetterAuthVersion };
@@ -1,5 +1,5 @@
1
- import { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext } from "./endpoint-context.mjs";
1
+ import { AuthEndpointContext, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext } from "./endpoint-context.mjs";
2
2
  import { getBetterAuthVersion } from "./global.mjs";
3
3
  import { RequestState, RequestStateWeakMap, defineRequestState, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, runWithRequestState } from "./request-state.mjs";
4
4
  import { getCurrentAdapter, getCurrentDBAdapterAsyncLocalStorage, queueAfterTransactionHook, runWithAdapter, runWithTransaction } from "./transaction.mjs";
5
- export { type AuthEndpointContext, type RequestState, type RequestStateWeakMap, defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction };
5
+ export { type AuthEndpointContext, type RequestState, type RequestStateWeakMap, defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction, tryGetCurrentAuthEndpointContext };
@@ -1,5 +1,5 @@
1
1
  import { getBetterAuthVersion } from "./global.mjs";
2
- import { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, runWithEndpointContext } from "./endpoint-context.mjs";
2
+ import { getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, runWithEndpointContext, tryGetCurrentAuthEndpointContext } from "./endpoint-context.mjs";
3
3
  import { defineRequestState, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, runWithRequestState } from "./request-state.mjs";
4
4
  import { getCurrentAdapter, getCurrentDBAdapterAsyncLocalStorage, queueAfterTransactionHook, runWithAdapter, runWithTransaction } from "./transaction.mjs";
5
- export { defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction };
5
+ export { defineRequestState, getBetterAuthVersion, getCurrentAdapter, getCurrentAuthContext, getCurrentAuthContextAsyncLocalStorage, getCurrentAuthEndpointContext, getCurrentDBAdapterAsyncLocalStorage, getCurrentRequestState, getRequestStateAsyncLocalStorage, hasRequestState, queueAfterTransactionHook, runWithAdapter, runWithEndpointContext, runWithRequestState, runWithTransaction, tryGetCurrentAuthEndpointContext };
@@ -1,4 +1,5 @@
1
1
  import { __getBetterAuthGlobal } from "./global.mjs";
2
+ import { schemaCheckFor } from "../db/schema-check.mjs";
2
3
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
4
  //#region src/context/transaction.ts
4
5
  const ensureAsyncStorage = async () => {
@@ -55,6 +56,8 @@ const runWithTransaction = async (adapter, fn, options) => {
55
56
  return ensureAsyncStorage().then(async (als) => {
56
57
  called = true;
57
58
  if (als.getStore()?.isTransactionActive) return fn();
59
+ const pendingSchemaCheck = schemaCheckFor(adapter)?.();
60
+ if (pendingSchemaCheck) await pendingSchemaCheck;
58
61
  const pendingHooks = [];
59
62
  let result;
60
63
  let error;
@@ -0,0 +1,134 @@
1
+ import { BetterAuthError } from "../../error/index.mjs";
2
+ import * as z from "zod";
3
+ //#region src/db/adapter/atomic-fallback.ts
4
+ const MAX_ATTEMPTS = 5;
5
+ const scalar = z.union([
6
+ z.string(),
7
+ z.number(),
8
+ z.boolean(),
9
+ z.date()
10
+ ]).nullable();
11
+ const readSchema = z.record(z.string(), z.unknown()).nullish();
12
+ const setSchema = z.record(z.string(), z.unknown()).transform((values) => {
13
+ const assignments = {};
14
+ for (const [field, value] of Object.entries(values)) if (value !== void 0) assignments[field] = value;
15
+ return assignments;
16
+ });
17
+ const mutationSchema = z.object({
18
+ increment: z.record(z.string(), z.number()),
19
+ set: setSchema.optional()
20
+ });
21
+ const counterSchema = z.number().nullish();
22
+ function createAtomicFallbacks(context) {
23
+ const { adapter, adapterId } = context;
24
+ const outputId = Object.entries(context.mapKeysTransformOutput ?? {}).find(([, field]) => field === "id")?.[0] ?? "id";
25
+ async function idWhere(row, model, action) {
26
+ const mappedId = context.mapKeysTransformInput?.id || context.getFieldName({
27
+ model,
28
+ field: "id"
29
+ });
30
+ if (row[mappedId] === void 0 || row[mappedId] === null) throw new BetterAuthError(`Adapter "${context.adapterId}" must return the row id for atomic fallbacks.`);
31
+ const id = (await context.transformOutput(row, model, [outputId], void 0))?.id;
32
+ if (typeof id !== "string" && typeof id !== "number") throw new BetterAuthError(`Adapter "${context.adapterId}" must expose a logical string or number id through its output transform.`);
33
+ const [condition] = context.transformWhereClause({
34
+ model,
35
+ where: [{
36
+ field: "id",
37
+ value: id
38
+ }],
39
+ action
40
+ });
41
+ if (!condition) throw new BetterAuthError("The atomic fallback id condition was transformed away.");
42
+ return condition;
43
+ }
44
+ async function readRow({ model, where }) {
45
+ const result = readSchema.safeParse(await adapter.findOne({
46
+ model,
47
+ where
48
+ }));
49
+ if (!result.success) throw new BetterAuthError(`Adapter "${adapterId}" must return a row snapshot or null.`);
50
+ return result.data ?? null;
51
+ }
52
+ async function snapshotGuard(row, fields, request, action) {
53
+ const id = await idWhere(row, request.logicalModel, action);
54
+ const hasOr = request.where.some((clause) => clause.connector === "OR");
55
+ const guard = hasOr ? [id] : [...request.where, id];
56
+ const keys = new Set(fields);
57
+ for (const field of keys) {
58
+ if (field === id.field) continue;
59
+ const value = scalar.safeParse(row[field] ?? null);
60
+ if (!value.success) {
61
+ if (hasOr && request.where.some((clause) => clause.field === field)) throw new BetterAuthError(`Adapter "${adapterId}" must implement native atomic methods for OR predicates on structured values.`);
62
+ continue;
63
+ }
64
+ guard.push({
65
+ field,
66
+ value: value.data,
67
+ operator: "eq",
68
+ connector: "AND",
69
+ mode: "sensitive"
70
+ });
71
+ }
72
+ return guard;
73
+ }
74
+ function changedOne(count) {
75
+ if (count !== 0 && count !== 1) throw new BetterAuthError(`Adapter "${adapterId}" must return an affected row count of 0 or 1 from an atomic fallback.`);
76
+ return count === 1;
77
+ }
78
+ async function consumeOne(request) {
79
+ const { model, where } = request;
80
+ const row = await readRow(request);
81
+ if (row === null) return null;
82
+ const guard = await snapshotGuard(row, [...Object.keys(row), ...where.map(({ field }) => field)], request, "consumeOne");
83
+ return changedOne(await adapter.deleteMany({
84
+ model,
85
+ where: guard
86
+ })) ? row : null;
87
+ }
88
+ async function incrementOne(request) {
89
+ const { model, where } = request;
90
+ const mutation = mutationSchema.safeParse(request);
91
+ if (!mutation.success) throw new BetterAuthError("incrementOne requires finite increments and a set object for the atomic fallback.");
92
+ const { increment, set } = mutation.data;
93
+ const deltas = Object.entries(increment);
94
+ const fields = [
95
+ ...where.map(({ field }) => field),
96
+ ...Object.keys(increment),
97
+ ...Object.keys(set ?? {})
98
+ ];
99
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
100
+ const row = await readRow(request);
101
+ if (row === null) return null;
102
+ const update = { ...set };
103
+ for (const [field, delta] of deltas) {
104
+ const previous = counterSchema.safeParse(row[field]);
105
+ if (!previous.success) throw new BetterAuthError(`Adapter "${adapterId}" must return finite numeric counter values or null for atomic increments.`);
106
+ const current = previous.data ?? 0;
107
+ const next = current + delta;
108
+ if (!Number.isFinite(next) || delta !== 0 && next === current) throw new BetterAuthError(`Adapter "${adapterId}" cannot represent the requested counter increment safely.`);
109
+ update[field] = next;
110
+ }
111
+ const guard = await snapshotGuard(row, fields, request, "incrementOne");
112
+ if (Object.entries(update).every(([field, value]) => {
113
+ const previous = row[field];
114
+ if (previous instanceof Date && value instanceof Date) return previous.getTime() === value.getTime();
115
+ return Object.is(previous, value);
116
+ })) return row;
117
+ if (changedOne(await adapter.updateMany({
118
+ model,
119
+ where: guard,
120
+ update
121
+ }))) return {
122
+ ...row,
123
+ ...update
124
+ };
125
+ }
126
+ throw new BetterAuthError(`Adapter "${adapterId}" could not complete an atomic increment due to contention. Retry the operation or implement incrementOne natively.`);
127
+ }
128
+ return {
129
+ consumeOne,
130
+ incrementOne
131
+ };
132
+ }
133
+ //#endregion
134
+ export { createAtomicFallbacks };
@@ -3,6 +3,7 @@ import { getAuthTables } from "../get-tables.mjs";
3
3
  import { getColorDepth } from "../../env/color-depth.mjs";
4
4
  import { TTY_COLORS, createLogger } from "../../env/logger.mjs";
5
5
  import { safeJSONParse } from "../../utils/json.mjs";
6
+ import { createAtomicFallbacks } from "./atomic-fallback.mjs";
6
7
  import { initGetDefaultModelName } from "./get-default-model-name.mjs";
7
8
  import { initGetDefaultFieldName } from "./get-default-field-name.mjs";
8
9
  import { initGetIdField } from "./get-id-field.mjs";
@@ -401,6 +402,15 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
401
402
  transformWhereClause
402
403
  });
403
404
  let lazyLoadTransaction = null;
405
+ const atomicFallbacks = createAtomicFallbacks({
406
+ adapter: adapterInstance,
407
+ adapterId: config.adapterId,
408
+ mapKeysTransformInput: config.mapKeysTransformInput,
409
+ mapKeysTransformOutput: config.mapKeysTransformOutput,
410
+ getFieldName,
411
+ transformOutput,
412
+ transformWhereClause
413
+ });
404
414
  const adapter = {
405
415
  transaction: async (cb) => {
406
416
  if (!lazyLoadTransaction) if (!config.transaction) lazyLoadTransaction = createAsIsTransaction(adapter);
@@ -696,12 +706,15 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
696
706
  model,
697
707
  where
698
708
  });
699
- if (typeof adapterInstance.consumeOne !== "function") throw new BetterAuthError(`Adapter "${config.adapterId}" must implement consumeOne for atomic single-use credential consumption.`);
700
709
  const res = await withSpan(`db consumeOne ${model}`, {
701
710
  [ATTR_DB_OPERATION_NAME]: "consumeOne",
702
711
  [ATTR_DB_COLLECTION_NAME]: model
703
- }, () => adapterInstance.consumeOne({
712
+ }, () => adapterInstance.consumeOne ? adapterInstance.consumeOne({
713
+ model,
714
+ where
715
+ }) : atomicFallbacks.consumeOne({
704
716
  model,
717
+ logicalModel: unsafeModel,
705
718
  where
706
719
  }));
707
720
  debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`, `${formatMethod("consumeOne")} ${formatAction("DB Result")}:`, {
@@ -735,7 +748,6 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
735
748
  increment: unsafeIncrement,
736
749
  set: unsafeSet
737
750
  });
738
- if (typeof adapterInstance.incrementOne !== "function") throw new BetterAuthError(`Adapter "${config.adapterId}" must implement incrementOne for atomic guarded counter updates.`);
739
751
  const mappedKeys = config.mapKeysTransformInput ?? {};
740
752
  const increment = {};
741
753
  for (const [field, delta] of Object.entries(unsafeIncrement)) increment[mappedKeys[field] || getFieldName({
@@ -749,8 +761,14 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
749
761
  const res = await withSpan(`db incrementOne ${model}`, {
750
762
  [ATTR_DB_OPERATION_NAME]: "incrementOne",
751
763
  [ATTR_DB_COLLECTION_NAME]: model
752
- }, () => adapterInstance.incrementOne({
764
+ }, () => adapterInstance.incrementOne ? adapterInstance.incrementOne({
765
+ model,
766
+ where,
767
+ increment,
768
+ set
769
+ }) : atomicFallbacks.incrementOne({
753
770
  model,
771
+ logicalModel: unsafeModel,
754
772
  where,
755
773
  increment,
756
774
  set
@@ -434,9 +434,9 @@ type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
434
434
  * race-safe primitive for consuming single-use credentials
435
435
  * (verification tokens, authorization codes, one-time tokens).
436
436
  *
437
- * Always defined on the factory-wrapped adapter. The underlying
438
- * `CustomAdapter` must implement this natively; there is no portable
439
- * fallback that can guarantee cross-process single-use semantics.
437
+ * Always defined on the factory-wrapped adapter. Without a native method,
438
+ * the factory uses a snapshot-guarded delete and requires an exact affected
439
+ * row count. The adapter must evaluate the condition and deletion atomically.
440
440
  */
441
441
  consumeOne: <T>(data: {
442
442
  model: string;
@@ -461,9 +461,11 @@ type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
461
461
  * primitive for guarded counter updates (e.g. decrementing a remaining-uses
462
462
  * counter only while it is still positive).
463
463
  *
464
- * Always defined on the factory-wrapped adapter. The underlying
465
- * `CustomAdapter` must implement this natively; there is no portable
466
- * fallback that can guarantee guarded counter semantics across runtimes.
464
+ * Always defined on the factory-wrapped adapter. Without a native method,
465
+ * the factory uses bounded compare-and-swap retries. Contention exhaustion
466
+ * throws rather than returning null. Conditional writes must be atomic.
467
+ * A no-op may return the read snapshot without writing. A non-null result
468
+ * alone does not establish exclusive ownership of the row.
467
469
  */
468
470
  incrementOne: <T>(data: {
469
471
  model: string;
@@ -553,19 +555,21 @@ interface CustomAdapter {
553
555
  where: CleanedWhere[];
554
556
  }) => Promise<number>;
555
557
  /**
556
- * Native atomic single-row consume.
558
+ * Optional native atomic single-row consume.
559
+ *
557
560
  * Implementing this method natively (e.g. `DELETE ... RETURNING *`,
558
561
  * `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
559
562
  * strongest race-safety guarantee. Implementations must delete at most
560
563
  * one matching row.
561
564
  */
562
- consumeOne: <T>(data: {
565
+ consumeOne?: <T>(data: {
563
566
  model: string;
564
567
  where: CleanedWhere[];
565
568
  }) => Promise<T | null>;
566
569
  /**
567
- * Native atomic guarded counter mutation. Applies
568
- * `field = field + delta` for each entry in `increment` (negative deltas
570
+ * Optional native atomic guarded counter mutation.
571
+ *
572
+ * Applies `field = field + delta` for each entry in `increment` (negative deltas
569
573
  * decrement), with `where` acting as both selector and guard and `set`
570
574
  * assigning absolute values in the same operation. Returns the updated row,
571
575
  * or `null` when the guard matched no row.
@@ -574,7 +578,7 @@ interface CustomAdapter {
574
578
  * RETURNING *`) gives one round trip and the strongest race-safety
575
579
  * guarantee.
576
580
  */
577
- incrementOne: <T>(data: {
581
+ incrementOne?: <T>(data: {
578
582
  model: string;
579
583
  where: CleanedWhere[];
580
584
  increment: Record<string, number>;
@@ -197,16 +197,8 @@ const buildAuthTables = (options) => {
197
197
  ...!options.secondaryStorage || options.session?.storeSessionInDatabase ? sessionTable : {},
198
198
  account: {
199
199
  modelName: options.account?.modelName || "account",
200
- indexes: mergeTableIndexes([{
201
- fields: ["issuer", "accountId"],
202
- unique: true
203
- }], account?.indexes),
200
+ indexes: account?.indexes,
204
201
  fields: {
205
- issuer: {
206
- type: "string",
207
- required: true,
208
- fieldName: options.account?.fields?.issuer || "issuer"
209
- },
210
202
  accountId: {
211
203
  type: "string",
212
204
  required: true,
@@ -5,6 +5,6 @@ import { BaseRateLimit, RateLimit, rateLimitSchema } from "./schema/rate-limit.m
5
5
  import { BaseSession, Session, sessionSchema } from "./schema/session.mjs";
6
6
  import { BaseUser, User, userSchema } from "./schema/user.mjs";
7
7
  import { BaseVerification, Verification, verificationSchema } from "./schema/verification.mjs";
8
- import { Account, AccountKey, BaseAccount, accountSchema, createLocalAccountIssuer, createOAuthAccountIssuer } from "./schema/account.mjs";
8
+ import { Account, AccountKey, BaseAccount, accountSchema } from "./schema/account.mjs";
9
9
  import { coreSchema } from "./schema/shared.mjs";
10
- export { type Account, type AccountKey, type BaseAccount, type BaseModelNames, type BaseRateLimit, type BaseSession, type BaseUser, type BaseVerification, type BetterAuthDBSchema, type BetterAuthPluginDBSchema, type DBFieldAttribute, type DBFieldAttributeConfig, type DBFieldType, type DBPrimitive, type DBTableIndex, type InferDBFieldInput, type InferDBFieldOutput, type InferDBFieldsFromOptions, type InferDBFieldsFromOptionsInput, type InferDBFieldsFromPlugins, type InferDBFieldsFromPluginsInput, type InferDBFieldsInput, type InferDBFieldsOutput, type InferDBValueType, type ModelNames, type RateLimit, type SecondaryStorage, type Session, type User, type Verification, accountSchema, coreSchema, createLocalAccountIssuer, createOAuthAccountIssuer, getAuthTables, rateLimitSchema, sessionSchema, userSchema, verificationSchema };
10
+ export { type Account, type AccountKey, type BaseAccount, type BaseModelNames, type BaseRateLimit, type BaseSession, type BaseUser, type BaseVerification, type BetterAuthDBSchema, type BetterAuthPluginDBSchema, type DBFieldAttribute, type DBFieldAttributeConfig, type DBFieldType, type DBPrimitive, type DBTableIndex, type InferDBFieldInput, type InferDBFieldOutput, type InferDBFieldsFromOptions, type InferDBFieldsFromOptionsInput, type InferDBFieldsFromPlugins, type InferDBFieldsFromPluginsInput, type InferDBFieldsInput, type InferDBFieldsOutput, type InferDBValueType, type ModelNames, type RateLimit, type SecondaryStorage, type Session, type User, type Verification, accountSchema, coreSchema, getAuthTables, rateLimitSchema, sessionSchema, userSchema, verificationSchema };
package/dist/db/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { getAuthTables } from "./get-tables.mjs";
2
2
  import { coreSchema } from "./schema/shared.mjs";
3
- import { accountSchema, createLocalAccountIssuer, createOAuthAccountIssuer } from "./schema/account.mjs";
3
+ import { accountSchema } from "./schema/account.mjs";
4
4
  import { rateLimitSchema } from "./schema/rate-limit.mjs";
5
5
  import { sessionSchema } from "./schema/session.mjs";
6
6
  import { userSchema } from "./schema/user.mjs";
7
7
  import { verificationSchema } from "./schema/verification.mjs";
8
- export { accountSchema, coreSchema, createLocalAccountIssuer, createOAuthAccountIssuer, getAuthTables, rateLimitSchema, sessionSchema, userSchema, verificationSchema };
8
+ export { accountSchema, coreSchema, getAuthTables, rateLimitSchema, sessionSchema, userSchema, verificationSchema };
@@ -1,3 +1,5 @@
1
1
  import { BoundedDatabaseIndexDialect, DBTableIndexSource, ResolvedDBTableIndex, getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes } from "./database-index.mjs";
2
2
  import { getAuthTablesWithResolvedIndexes } from "./get-tables.mjs";
3
- export { type BoundedDatabaseIndexDialect, type DBTableIndexSource, type ResolvedDBTableIndex, getAuthTablesWithResolvedIndexes, getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes };
3
+ import { ExpectedSchema, IntrospectedColumn, IntrospectedTable, SchemaFinding, SchemaMismatchError, SchemaSource, diffSchema, formatSchemaFinding, getExpectedSchema } from "./schema-diff.mjs";
4
+ import { SchemaCheck, checksSchema, createSchemaCheck, invalidateSchemaChecks, registerSchemaCheck, schemaCheckFor } from "./schema-check.mjs";
5
+ export { type BoundedDatabaseIndexDialect, type DBTableIndexSource, type ExpectedSchema, type IntrospectedColumn, type IntrospectedTable, type ResolvedDBTableIndex, type SchemaCheck, type SchemaFinding, SchemaMismatchError, type SchemaSource, checksSchema, createSchemaCheck, diffSchema, formatSchemaFinding, getAuthTablesWithResolvedIndexes, getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getExpectedSchema, getPortableDatabaseIdentifierKey, invalidateSchemaChecks, registerSchemaCheck, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes, schemaCheckFor };
@@ -1,3 +1,5 @@
1
1
  import { getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes } from "./database-index.mjs";
2
2
  import { getAuthTablesWithResolvedIndexes } from "./get-tables.mjs";
3
- export { getAuthTablesWithResolvedIndexes, getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes };
3
+ import { SchemaMismatchError, diffSchema, formatSchemaFinding, getExpectedSchema } from "./schema-diff.mjs";
4
+ import { checksSchema, createSchemaCheck, invalidateSchemaChecks, registerSchemaCheck, schemaCheckFor } from "./schema-check.mjs";
5
+ export { SchemaMismatchError, checksSchema, createSchemaCheck, diffSchema, formatSchemaFinding, getAuthTablesWithResolvedIndexes, getDatabaseFieldIndexName, getDatabaseIndexName, getDatabaseIndexStringLength, getExpectedSchema, getPortableDatabaseIdentifierKey, invalidateSchemaChecks, registerSchemaCheck, resolveDatabaseSchemaIndexes, resolveDatabaseTableIndexes, schemaCheckFor };
@@ -9,7 +9,6 @@ declare const accountSchema: z.ZodObject<{
9
9
  createdAt: z.ZodDefault<z.ZodDate>;
10
10
  updatedAt: z.ZodDefault<z.ZodDate>;
11
11
  providerId: z.ZodString;
12
- issuer: z.ZodString;
13
12
  accountId: z.ZodString;
14
13
  userId: z.ZodCoercedString<unknown>;
15
14
  accessToken: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -22,20 +21,10 @@ declare const accountSchema: z.ZodObject<{
22
21
  }, z.core.$strip>;
23
22
  type BaseAccount = z.infer<typeof accountSchema>;
24
23
  /** The stable provider-side key used to recognize an account. */
25
- type AccountKey = Readonly<Pick<BaseAccount, "issuer" | "accountId">>;
26
- /**
27
- * Creates the synthetic issuer used by providers without an issuer of their own.
28
- */
29
- declare function createLocalAccountIssuer(providerId: string): string;
30
- /**
31
- * Creates the synthetic issuer used by OAuth providers without an issuer of
32
- * their own. OAuth identities use a distinct namespace so a provider ID
33
- * cannot collide with an internal local authentication method.
34
- */
35
- declare function createOAuthAccountIssuer(providerId: string): string;
24
+ type AccountKey = Readonly<Pick<BaseAccount, "providerId" | "accountId">>;
36
25
  /**
37
26
  * Account schema type used by better-auth, note that it's possible that account could have additional fields
38
27
  */
39
28
  type Account<DBOptions extends BetterAuthOptions["account"] = BetterAuthOptions["account"], Plugins extends BetterAuthOptions["plugins"] = BetterAuthOptions["plugins"]> = Prettify<BaseAccount & InferDBFieldsFromOptions<DBOptions> & InferDBFieldsFromPlugins<"account", Plugins>>;
40
29
  //#endregion
41
- export { Account, AccountKey, BaseAccount, accountSchema, createLocalAccountIssuer, createOAuthAccountIssuer };
30
+ export { Account, AccountKey, BaseAccount, accountSchema };
@@ -3,7 +3,6 @@ import * as z from "zod";
3
3
  //#region src/db/schema/account.ts
4
4
  const accountSchema = coreSchema.extend({
5
5
  providerId: z.string(),
6
- issuer: z.string(),
7
6
  accountId: z.string(),
8
7
  userId: z.coerce.string(),
9
8
  accessToken: z.string().nullish(),
@@ -29,22 +28,5 @@ const accountSchema = coreSchema.extend({
29
28
  */
30
29
  password: z.string().nullish()
31
30
  });
32
- function encodeAccountIssuerProviderId(providerId) {
33
- return encodeURIComponent(providerId);
34
- }
35
- /**
36
- * Creates the synthetic issuer used by providers without an issuer of their own.
37
- */
38
- function createLocalAccountIssuer(providerId) {
39
- return `local:${encodeAccountIssuerProviderId(providerId)}`;
40
- }
41
- /**
42
- * Creates the synthetic issuer used by OAuth providers without an issuer of
43
- * their own. OAuth identities use a distinct namespace so a provider ID
44
- * cannot collide with an internal local authentication method.
45
- */
46
- function createOAuthAccountIssuer(providerId) {
47
- return `local:oauth:${encodeAccountIssuerProviderId(providerId)}`;
48
- }
49
31
  //#endregion
50
- export { accountSchema, createLocalAccountIssuer, createOAuthAccountIssuer };
32
+ export { accountSchema };
@@ -0,0 +1,48 @@
1
+ import { BetterAuthOptions } from "../types/init-options.mjs";
2
+ import { SchemaFinding, SchemaSource } from "./schema-diff.mjs";
3
+ //#region src/db/schema-check.d.ts
4
+ /**
5
+ * Whether the adapter validates its schema. Enabled in every environment
6
+ * unless explicitly disabled.
7
+ */
8
+ declare function checksSchema(options: BetterAuthOptions): boolean;
9
+ /**
10
+ * Resolves when the schema can hold what Better Auth writes. Returns nothing
11
+ * once that is known and the database schema revision is unchanged.
12
+ */
13
+ type SchemaCheck = () => Promise<void> | undefined;
14
+ /** Invalidates cached checks after Better Auth changes this database's schema. */
15
+ declare function invalidateSchemaChecks(database: object): void;
16
+ /**
17
+ * Attaches a check to the adapter it verifies. The adapter object itself is
18
+ * left untouched, so this works for adapters Better Auth does not own.
19
+ */
20
+ declare function registerSchemaCheck(adapter: object, check: SchemaCheck): void;
21
+ /**
22
+ * The check registered for an adapter, if its store is checked at all.
23
+ */
24
+ declare function schemaCheckFor(adapter: object): SchemaCheck | undefined;
25
+ /**
26
+ * Turns a schema comparison into a check shared by one adapter instance.
27
+ *
28
+ * The first call runs `find` and every concurrent call shares that promise. A
29
+ * clean result is cached until invalidation. A mismatch is kept as one
30
+ * {@link SchemaMismatchError} and rethrown on every later call without asking
31
+ * the store again, until a migration invalidates it. When a database identity is supplied,
32
+ * checks for that identity share its schema revision. Pending callers follow
33
+ * the new check if their revision is invalidated. A failure to reach
34
+ * the store is not kept, so the next call asks again.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const checkSchema = createSchemaCheck(
39
+ * () => findSchemaProblems(db, "postgres", expected),
40
+ * "database",
41
+ * );
42
+ * const pending = checkSchema();
43
+ * if (pending) await pending;
44
+ * ```
45
+ */
46
+ declare function createSchemaCheck(find: () => Promise<SchemaFinding[]>, source: SchemaSource, database?: object): SchemaCheck;
47
+ //#endregion
48
+ export { SchemaCheck, checksSchema, createSchemaCheck, invalidateSchemaChecks, registerSchemaCheck, schemaCheckFor };