@better-auth/core 1.7.0-beta.5 → 1.7.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +40 -1
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +57 -31
  7. package/dist/db/adapter/index.d.mts +54 -10
  8. package/dist/db/adapter/types.d.mts +1 -1
  9. package/dist/db/type.d.mts +12 -7
  10. package/dist/instrumentation/tracer.mjs +1 -1
  11. package/dist/oauth2/create-authorization-url.d.mts +3 -1
  12. package/dist/oauth2/create-authorization-url.mjs +3 -1
  13. package/dist/oauth2/dpop.d.mts +142 -0
  14. package/dist/oauth2/dpop.mjs +246 -0
  15. package/dist/oauth2/index.d.mts +4 -3
  16. package/dist/oauth2/index.mjs +3 -2
  17. package/dist/oauth2/oauth-provider.d.mts +37 -3
  18. package/dist/oauth2/refresh-access-token.mjs +15 -1
  19. package/dist/oauth2/verify.d.mts +74 -15
  20. package/dist/oauth2/verify.mjs +172 -20
  21. package/dist/social-providers/apple.d.mts +2 -0
  22. package/dist/social-providers/atlassian.d.mts +2 -0
  23. package/dist/social-providers/cognito.d.mts +2 -0
  24. package/dist/social-providers/discord.d.mts +2 -0
  25. package/dist/social-providers/dropbox.d.mts +2 -0
  26. package/dist/social-providers/facebook.d.mts +2 -0
  27. package/dist/social-providers/figma.d.mts +2 -0
  28. package/dist/social-providers/github.d.mts +2 -0
  29. package/dist/social-providers/gitlab.d.mts +2 -0
  30. package/dist/social-providers/google.d.mts +2 -0
  31. package/dist/social-providers/huggingface.d.mts +2 -0
  32. package/dist/social-providers/index.d.mts +71 -0
  33. package/dist/social-providers/kakao.d.mts +2 -0
  34. package/dist/social-providers/kick.d.mts +2 -0
  35. package/dist/social-providers/line.d.mts +2 -0
  36. package/dist/social-providers/linear.d.mts +2 -0
  37. package/dist/social-providers/linkedin.d.mts +2 -0
  38. package/dist/social-providers/microsoft-entra-id.d.mts +12 -0
  39. package/dist/social-providers/microsoft-entra-id.mjs +17 -2
  40. package/dist/social-providers/naver.d.mts +2 -0
  41. package/dist/social-providers/notion.d.mts +2 -0
  42. package/dist/social-providers/paybin.d.mts +2 -0
  43. package/dist/social-providers/paypal.d.mts +2 -0
  44. package/dist/social-providers/polar.d.mts +2 -0
  45. package/dist/social-providers/railway.d.mts +2 -0
  46. package/dist/social-providers/reddit.d.mts +2 -0
  47. package/dist/social-providers/reddit.mjs +1 -1
  48. package/dist/social-providers/roblox.d.mts +2 -0
  49. package/dist/social-providers/salesforce.d.mts +2 -0
  50. package/dist/social-providers/slack.d.mts +2 -0
  51. package/dist/social-providers/spotify.d.mts +2 -0
  52. package/dist/social-providers/tiktok.d.mts +2 -0
  53. package/dist/social-providers/twitch.d.mts +2 -0
  54. package/dist/social-providers/twitter.d.mts +2 -0
  55. package/dist/social-providers/vercel.d.mts +2 -0
  56. package/dist/social-providers/vk.d.mts +2 -0
  57. package/dist/social-providers/wechat.d.mts +2 -0
  58. package/dist/social-providers/wechat.mjs +1 -1
  59. package/dist/social-providers/zoom.d.mts +2 -0
  60. package/dist/types/context.d.mts +17 -0
  61. package/dist/types/init-options.d.mts +45 -5
  62. package/dist/types/plugin-client.d.mts +12 -2
  63. package/dist/utils/host.d.mts +1 -1
  64. package/dist/utils/host.mjs +7 -0
  65. package/dist/utils/url.mjs +4 -3
  66. package/package.json +5 -5
  67. package/src/api/index.ts +82 -0
  68. package/src/context/transaction.ts +45 -12
  69. package/src/db/adapter/factory.ts +127 -72
  70. package/src/db/adapter/index.ts +54 -9
  71. package/src/db/adapter/types.ts +1 -0
  72. package/src/db/type.ts +12 -7
  73. package/src/oauth2/create-authorization-url.ts +4 -0
  74. package/src/oauth2/dpop.ts +568 -0
  75. package/src/oauth2/index.ts +45 -1
  76. package/src/oauth2/oauth-provider.ts +40 -2
  77. package/src/oauth2/refresh-access-token.ts +27 -3
  78. package/src/oauth2/verify-id-token.ts +2 -0
  79. package/src/oauth2/verify.ts +329 -66
  80. package/src/social-providers/microsoft-entra-id.ts +44 -1
  81. package/src/social-providers/reddit.ts +5 -1
  82. package/src/social-providers/wechat.ts +8 -1
  83. package/src/types/context.ts +18 -0
  84. package/src/types/init-options.ts +40 -8
  85. package/src/types/plugin-client.ts +16 -2
  86. package/src/utils/host.ts +25 -1
  87. package/src/utils/url.ts +10 -4
@@ -8,6 +8,24 @@ import { EndpointContext, EndpointOptions, StrictEndpoint } from "better-call";
8
8
  import * as _better_auth_core0 from "@better-auth/core";
9
9
 
10
10
  //#region src/api/index.d.ts
11
+ /**
12
+ * Response headers that forbid any intermediary (proxy, CDN, browser) from
13
+ * caching a response body. Credential-bearing responses (access/refresh tokens,
14
+ * ID tokens, client secrets, device codes) must carry them.
15
+ *
16
+ * Set `metadata: { noStore: true }` on an endpoint and {@link createAuthEndpoint}
17
+ * applies these to the responses its handler produces: the success body and any
18
+ * error the handler throws. A request rejected by schema or media-type
19
+ * validation before the handler runs is not covered, and carries no credentials
20
+ * to protect. Spread them into a hand-built `Response` or `APIError`'s headers
21
+ * for the rare endpoint that constructs its own response.
22
+ *
23
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
24
+ */
25
+ declare const NO_STORE_HEADERS: {
26
+ readonly "Cache-Control": "no-store";
27
+ readonly Pragma: "no-cache";
28
+ };
11
29
  declare const optionsMiddleware: <InputCtx extends better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>>(inputContext: InputCtx) => Promise<AuthContext>;
12
30
  declare const createAuthMiddleware: {
13
31
  <Options extends better_call0.MiddlewareOptions, R>(options: Options, handler: (ctx: better_call0.MiddlewareContext<Options, {
@@ -272,7 +290,32 @@ declare const createAuthMiddleware: {
272
290
  type EndpointHandler<Path extends string, Options extends EndpointOptions, R> = (context: EndpointContext<Path, Options, AuthContext>) => Promise<R>;
273
291
  declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(path: Path, options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
274
292
  declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
293
+ declare namespace createAuthEndpoint {
294
+ /**
295
+ * Declare a **server-only** endpoint.
296
+ *
297
+ * The endpoint is callable through `auth.api.*` from trusted server code but is
298
+ * never registered on the HTTP router and never emitted into the OpenAPI
299
+ * schema. It takes no path because it has no URL to be reached at.
300
+ *
301
+ * Prefer this over the path-less `createAuthEndpoint({ ... }, handler)` form.
302
+ * Setting `metadata.SERVER_ONLY` makes the intent explicit at the call site and
303
+ * keeps the endpoint off the HTTP surface even if a path is later added by
304
+ * mistake: better-call's router skips an endpoint when its path is missing *or*
305
+ * when `SERVER_ONLY` is set, so the two together are defense in depth. Relying
306
+ * on path omission alone is invisible and one keystroke away from exposure.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * viewBackupCodes: createAuthEndpoint.serverOnly(
311
+ * { method: "POST", body: schema },
312
+ * async (ctx) => { ... },
313
+ * )
314
+ * ```
315
+ */
316
+ function serverOnly<Path extends string, Options extends EndpointOptions, R>(options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
317
+ }
275
318
  type AuthEndpoint<Path extends string, Opts extends EndpointOptions, R> = ReturnType<typeof createAuthEndpoint<Path, Opts, R>>;
276
319
  type AuthMiddleware = ReturnType<typeof createAuthMiddleware>;
277
320
  //#endregion
278
- export { AuthEndpoint, AuthMiddleware, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
321
+ export { AuthEndpoint, AuthMiddleware, NO_STORE_HEADERS, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
@@ -3,6 +3,24 @@ import { isAPIError } from "../utils/is-api-error.mjs";
3
3
  import { createEndpoint, createMiddleware, kAPIErrorHeaderSymbol } from "better-call";
4
4
  //#region src/api/index.ts
5
5
  /**
6
+ * Response headers that forbid any intermediary (proxy, CDN, browser) from
7
+ * caching a response body. Credential-bearing responses (access/refresh tokens,
8
+ * ID tokens, client secrets, device codes) must carry them.
9
+ *
10
+ * Set `metadata: { noStore: true }` on an endpoint and {@link createAuthEndpoint}
11
+ * applies these to the responses its handler produces: the success body and any
12
+ * error the handler throws. A request rejected by schema or media-type
13
+ * validation before the handler runs is not covered, and carries no credentials
14
+ * to protect. Spread them into a hand-built `Response` or `APIError`'s headers
15
+ * for the rare endpoint that constructs its own response.
16
+ *
17
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
18
+ */
19
+ const NO_STORE_HEADERS = {
20
+ "Cache-Control": "no-store",
21
+ Pragma: "no-cache"
22
+ };
23
+ /**
6
24
  * Better-call's createEndpoint re-throws APIError without exposing the headers
7
25
  * accumulated on ctx.responseHeaders (e.g. Set-Cookie from deleteSessionCookie
8
26
  * before throw). Attach them to the error via kAPIErrorHeaderSymbol — matching
@@ -34,7 +52,9 @@ function createAuthEndpoint(pathOrOptions, handlerOrOptions, handlerOrNever) {
34
52
  const path = typeof pathOrOptions === "string" ? pathOrOptions : void 0;
35
53
  const options = typeof handlerOrOptions === "object" ? handlerOrOptions : pathOrOptions;
36
54
  const handler = typeof handlerOrOptions === "function" ? handlerOrOptions : handlerOrNever;
55
+ const noStore = options.metadata?.noStore === true;
37
56
  const wrapped = async (ctx) => {
57
+ if (noStore) for (const [name, value] of Object.entries(NO_STORE_HEADERS)) ctx.setHeader(name, value);
38
58
  const runtimeCtx = ctx;
39
59
  try {
40
60
  return await runWithEndpointContext(ctx, () => handler(ctx));
@@ -52,5 +72,24 @@ function createAuthEndpoint(pathOrOptions, handlerOrOptions, handlerOrNever) {
52
72
  use: [...options?.use || [], ...use]
53
73
  }, wrapped);
54
74
  }
75
+ /**
76
+ * Set `metadata.SERVER_ONLY` while preserving any existing metadata
77
+ * (`$Infer`, `openapi`, ...).
78
+ */
79
+ function withServerOnly(options) {
80
+ return {
81
+ ...options,
82
+ metadata: {
83
+ ...options.metadata,
84
+ SERVER_ONLY: true
85
+ }
86
+ };
87
+ }
88
+ (function(_createAuthEndpoint) {
89
+ function serverOnly(options, handler) {
90
+ return createAuthEndpoint(withServerOnly(options), handler);
91
+ }
92
+ _createAuthEndpoint.serverOnly = serverOnly;
93
+ })(createAuthEndpoint || (createAuthEndpoint = {}));
55
94
  //#endregion
56
- export { createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
95
+ export { NO_STORE_HEADERS, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
@@ -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.0-beta.5";
5
+ const __betterAuthVersion = "1.7.0-beta.7";
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,7 +1,7 @@
1
1
  import { BetterAuthError } from "../../error/index.mjs";
2
- import { getAuthTables } from "../get-tables.mjs";
3
2
  import { getColorDepth } from "../../env/color-depth.mjs";
4
3
  import { TTY_COLORS, createLogger } from "../../env/logger.mjs";
4
+ import { getAuthTables } from "../get-tables.mjs";
5
5
  import { safeJSONParse } from "../../utils/json.mjs";
6
6
  import { initGetDefaultModelName } from "./get-default-model-name.mjs";
7
7
  import { initGetDefaultFieldName } from "./get-default-field-name.mjs";
@@ -58,6 +58,7 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
58
58
  else if (method === "delete" && !config.debugLogs.delete) return;
59
59
  else if (method === "deleteMany" && !config.debugLogs.deleteMany) return;
60
60
  else if (method === "consumeOne" && !config.debugLogs.consumeOne) return;
61
+ else if (method === "incrementOne" && !config.debugLogs.incrementOne) return;
61
62
  else if (method === "count" && !config.debugLogs.count) return;
62
63
  }
63
64
  logger.info(`[${config.adapterName}]`, ...args);
@@ -515,6 +516,7 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
515
516
  where,
516
517
  update: data
517
518
  }));
519
+ if (typeof updatedCount !== "number" || !Number.isFinite(updatedCount)) throw new BetterAuthError(`Adapter "${config.adapterId}" updateMany must return a finite number affected row count.`);
518
520
  debugLog({ method: "updateMany" }, `${formatTransactionId(thisTransactionId)} ${formatStep(3, 4)}`, `${formatMethod("updateMany")} ${formatAction("DB Result")}:`, {
519
521
  model,
520
522
  data: updatedCount
@@ -691,53 +693,77 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
691
693
  model,
692
694
  where
693
695
  });
694
- let res;
695
- let resultNeedsOutputTransform = true;
696
- if (adapterInstance.consumeOne) res = await withSpan(`db consumeOne ${model}`, {
696
+ if (typeof adapterInstance.consumeOne !== "function") throw new BetterAuthError(`Adapter "${config.adapterId}" must implement consumeOne for atomic single-use credential consumption.`);
697
+ const res = await withSpan(`db consumeOne ${model}`, {
697
698
  [ATTR_DB_OPERATION_NAME]: "consumeOne",
698
699
  [ATTR_DB_COLLECTION_NAME]: model
699
700
  }, () => adapterInstance.consumeOne({
700
701
  model,
701
702
  where
702
703
  }));
703
- else {
704
- res = await withSpan(`db consumeOne ${model}`, {
705
- [ATTR_DB_OPERATION_NAME]: "consumeOne",
706
- [ATTR_DB_COLLECTION_NAME]: model
707
- }, () => adapter.transaction(async (trx) => {
708
- const target = (await trx.findMany({
709
- model: unsafeModel,
710
- where: unsafeWhere,
711
- limit: 1
712
- }))[0];
713
- if (!target) return null;
714
- const deleted = await trx.deleteMany({
715
- model: unsafeModel,
716
- where: [...unsafeWhere, {
717
- field: "id",
718
- value: target.id,
719
- operator: "eq",
720
- connector: "AND",
721
- mode: "sensitive"
722
- }]
723
- });
724
- if (typeof deleted !== "number") throw new BetterAuthError(`Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`);
725
- return Number.isFinite(deleted) && deleted > 0 ? target : null;
726
- }));
727
- resultNeedsOutputTransform = false;
728
- }
729
704
  debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`, `${formatMethod("consumeOne")} ${formatAction("DB Result")}:`, {
730
705
  model,
731
706
  data: res
732
707
  });
733
708
  let transformed = res;
734
- if (!config.disableTransformOutput && resultNeedsOutputTransform && res) transformed = await transformOutput(res, unsafeModel, void 0, void 0);
709
+ if (!config.disableTransformOutput && res) transformed = await transformOutput(res, unsafeModel, void 0, void 0);
735
710
  debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`, `${formatMethod("consumeOne")} ${formatAction("Parsed Result")}:`, {
736
711
  model,
737
712
  data: transformed
738
713
  });
739
714
  return transformed;
740
715
  },
716
+ incrementOne: async ({ model: unsafeModel, where: unsafeWhere, increment: unsafeIncrement, set: unsafeSet }) => {
717
+ const hasIncrement = Object.keys(unsafeIncrement).length > 0;
718
+ const hasSet = !!unsafeSet && Object.keys(unsafeSet).length > 0;
719
+ if (!hasIncrement && !hasSet) throw new BetterAuthError("incrementOne requires a non-empty `increment` or `set`; both were empty.");
720
+ transactionId++;
721
+ const thisTransactionId = transactionId;
722
+ const model = getModelName(unsafeModel);
723
+ const where = transformWhereClause({
724
+ model: unsafeModel,
725
+ where: unsafeWhere,
726
+ action: "incrementOne"
727
+ });
728
+ unsafeModel = getDefaultModelName(unsafeModel);
729
+ debugLog({ method: "incrementOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`, `${formatMethod("incrementOne")} ${formatAction("IncrementOne")}:`, {
730
+ model,
731
+ where,
732
+ increment: unsafeIncrement,
733
+ set: unsafeSet
734
+ });
735
+ if (typeof adapterInstance.incrementOne !== "function") throw new BetterAuthError(`Adapter "${config.adapterId}" must implement incrementOne for atomic guarded counter updates.`);
736
+ const mappedKeys = config.mapKeysTransformInput ?? {};
737
+ const increment = {};
738
+ for (const [field, delta] of Object.entries(unsafeIncrement)) increment[mappedKeys[field] || getFieldName({
739
+ model: unsafeModel,
740
+ field
741
+ })] = delta;
742
+ let set;
743
+ if (unsafeSet && !config.disableTransformInput) set = await transformInput(unsafeSet, unsafeModel, "update");
744
+ else set = unsafeSet;
745
+ 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.");
746
+ const res = await withSpan(`db incrementOne ${model}`, {
747
+ [ATTR_DB_OPERATION_NAME]: "incrementOne",
748
+ [ATTR_DB_COLLECTION_NAME]: model
749
+ }, () => adapterInstance.incrementOne({
750
+ model,
751
+ where,
752
+ increment,
753
+ set
754
+ }));
755
+ debugLog({ method: "incrementOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`, `${formatMethod("incrementOne")} ${formatAction("DB Result")}:`, {
756
+ model,
757
+ data: res
758
+ });
759
+ let transformed = res;
760
+ if (!config.disableTransformOutput && res) transformed = await transformOutput(res, unsafeModel, void 0, void 0);
761
+ debugLog({ method: "incrementOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`, `${formatMethod("incrementOne")} ${formatAction("Parsed Result")}:`, {
762
+ model,
763
+ data: transformed
764
+ });
765
+ return transformed;
766
+ },
741
767
  count: async ({ model: unsafeModel, where: unsafeWhere }) => {
742
768
  transactionId++;
743
769
  const thisTransactionId = transactionId;
@@ -23,6 +23,7 @@ type DBAdapterDebugLogOption = boolean | {
23
23
  delete?: boolean | undefined;
24
24
  deleteMany?: boolean | undefined;
25
25
  consumeOne?: boolean | undefined;
26
+ incrementOne?: boolean | undefined;
26
27
  count?: boolean | undefined;
27
28
  } | {
28
29
  /**
@@ -198,7 +199,7 @@ interface DBAdapterFactoryConfig<Options extends BetterAuthOptions = BetterAuthO
198
199
  /**
199
200
  * The action which was called from the adapter.
200
201
  */
201
- action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "count";
202
+ action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "incrementOne" | "count";
202
203
  /**
203
204
  * The model name.
204
205
  */
@@ -427,15 +428,43 @@ type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
427
428
  * race-safe primitive for consuming single-use credentials
428
429
  * (verification tokens, authorization codes, one-time tokens).
429
430
  *
430
- * Always defined on the factory-wrapped adapter. When the underlying
431
- * `CustomAdapter` does not implement `consumeOne`, the factory provides
432
- * a fallback that wraps `findMany + deleteMany` in `transaction(...)`
433
- * and returns the row only when the delete reports an affected row.
431
+ * Always defined on the factory-wrapped adapter. The underlying
432
+ * `CustomAdapter` must implement this natively; there is no portable
433
+ * fallback that can guarantee cross-process single-use semantics.
434
434
  */
435
435
  consumeOne: <T>(data: {
436
436
  model: string;
437
437
  where: Where[];
438
438
  }) => Promise<T | null>;
439
+ /**
440
+ * Atomically apply signed numeric deltas to a single row matching the where
441
+ * clause. For each entry in `increment`, the operation applies
442
+ * `field = field + delta` in one atomic step; a negative delta decrements.
443
+ *
444
+ * The `where` clause is both the selector AND the guard: comparison
445
+ * operators are honored, so passing `{ field: "remaining", operator: "gt",
446
+ * value: 0 }` only mutates the row while `remaining` is still above zero.
447
+ * When the guard matches no row, the operation makes no change and returns
448
+ * `null`.
449
+ *
450
+ * The optional `set` map assigns absolute values to fields in the same
451
+ * atomic operation, alongside the increments.
452
+ *
453
+ * Returns the updated row, or `null` when the guard matched no row. Under
454
+ * concurrent invocation against the same row, this is the race-safe
455
+ * primitive for guarded counter updates (e.g. decrementing a remaining-uses
456
+ * counter only while it is still positive).
457
+ *
458
+ * Always defined on the factory-wrapped adapter. The underlying
459
+ * `CustomAdapter` must implement this natively; there is no portable
460
+ * fallback that can guarantee guarded counter semantics across runtimes.
461
+ */
462
+ incrementOne: <T>(data: {
463
+ model: string;
464
+ where: Where[];
465
+ increment: Record<string, number>;
466
+ set?: Record<string, unknown> | undefined;
467
+ }) => Promise<T | null>;
439
468
  /**
440
469
  * Execute multiple operations in a transaction.
441
470
  * If the adapter doesn't support transactions, operations will be executed sequentially.
@@ -518,17 +547,32 @@ interface CustomAdapter {
518
547
  where: CleanedWhere[];
519
548
  }) => Promise<number>;
520
549
  /**
521
- * Optional native atomic single-row consume. When omitted, the adapter
522
- * factory falls back to `transaction(findMany + deleteMany)`.
550
+ * Native atomic single-row consume.
523
551
  * Implementing this method natively (e.g. `DELETE ... RETURNING *`,
524
552
  * `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
525
553
  * strongest race-safety guarantee. Implementations must delete at most
526
- * one matching row. TODO(consume-one-required): tighten to required in the
527
- * next minor on `next`.
554
+ * one matching row.
555
+ */
556
+ consumeOne: <T>(data: {
557
+ model: string;
558
+ where: CleanedWhere[];
559
+ }) => Promise<T | null>;
560
+ /**
561
+ * Native atomic guarded counter mutation. Applies
562
+ * `field = field + delta` for each entry in `increment` (negative deltas
563
+ * decrement), with `where` acting as both selector and guard and `set`
564
+ * assigning absolute values in the same operation. Returns the updated row,
565
+ * or `null` when the guard matched no row.
566
+ *
567
+ * Implementing this natively (e.g. `UPDATE ... SET n = n + $delta WHERE ...
568
+ * RETURNING *`) gives one round trip and the strongest race-safety
569
+ * guarantee.
528
570
  */
529
- consumeOne?: <T>(data: {
571
+ incrementOne: <T>(data: {
530
572
  model: string;
531
573
  where: CleanedWhere[];
574
+ increment: Record<string, number>;
575
+ set?: Record<string, unknown> | undefined;
532
576
  }) => Promise<T | null>;
533
577
  count: ({
534
578
  model,
@@ -94,7 +94,7 @@ type AdapterFactoryCustomizeAdapterCreator = (config: {
94
94
  }: {
95
95
  where: W;
96
96
  model: string;
97
- action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "count";
97
+ action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "incrementOne" | "count";
98
98
  }) => W extends undefined ? undefined : CleanedWhere[];
99
99
  }) => CustomAdapter;
100
100
  type AdapterTestDebugLogs = {
@@ -143,16 +143,21 @@ interface SecondaryStorage {
143
143
  get: (key: string) => Awaitable<unknown>;
144
144
  /**
145
145
  * Atomically get a value and delete it from storage.
146
+ */
147
+ getAndDelete: (key: string) => Awaitable<unknown>;
148
+ /**
149
+ * Atomically increment the counter at `key` by one, returning the
150
+ * post-increment value.
146
151
  *
147
- * This is optional for backwards compatibility with existing secondary
148
- * storage implementations. Single-use credential consumers use it when
149
- * present to avoid a read-then-delete race.
152
+ * When the key is absent, it is created with a value of `1` and the given
153
+ * `ttl` (in SECONDS). The TTL is applied only on creation; later increments
154
+ * never extend it, so the counter expires a fixed window after it was first
155
+ * created.
150
156
  *
151
- * TODO(secondary-storage-atomic-consume): make this required in the next
152
- * breaking release, or require database-backed verification storage for
153
- * security-sensitive consume paths.
157
+ * Required so secondary-storage-backed rate limiting can enforce the limit
158
+ * in one distributed-safe operation.
154
159
  */
155
- getAndDelete?: (key: string) => Awaitable<unknown>;
160
+ increment: (key: string, ttl: number) => Awaitable<number>;
156
161
  set: (
157
162
  /**
158
163
  * Key to store
@@ -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.7.0-beta.5";
5
+ const INSTRUMENTATION_VERSION = "1.7.0-beta.7";
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
@@ -7,7 +7,7 @@ import { ProviderOptions } from "./oauth-provider.mjs";
7
7
  * `additionalParams`. Overriding `state`, PKCE, or `redirect_uri` would
8
8
  * break the callback correlation and session pinning guarantees.
9
9
  */
10
- declare const RESERVED_AUTHORIZATION_PARAMS: readonly ["state", "client_id", "redirect_uri", "response_type", "code_challenge", "code_challenge_method", "scope"];
10
+ declare const RESERVED_AUTHORIZATION_PARAMS: readonly ["state", "client_id", "redirect_uri", "response_type", "code_challenge", "code_challenge_method", "nonce", "scope"];
11
11
  declare const RESERVED_AUTHORIZATION_PARAMS_SET: ReadonlySet<string>;
12
12
  declare function createAuthorizationURL({
13
13
  id,
@@ -24,6 +24,7 @@ declare function createAuthorizationURL({
24
24
  responseType,
25
25
  display,
26
26
  loginHint,
27
+ nonce,
27
28
  hd,
28
29
  responseMode,
29
30
  additionalParams,
@@ -43,6 +44,7 @@ declare function createAuthorizationURL({
43
44
  responseType?: string | undefined;
44
45
  display?: string | undefined;
45
46
  loginHint?: string | undefined;
47
+ nonce?: string | undefined;
46
48
  hd?: string | undefined;
47
49
  responseMode?: string | undefined;
48
50
  additionalParams?: Record<string, string> | undefined;
@@ -13,10 +13,11 @@ const RESERVED_AUTHORIZATION_PARAMS = [
13
13
  "response_type",
14
14
  "code_challenge",
15
15
  "code_challenge_method",
16
+ "nonce",
16
17
  "scope"
17
18
  ];
18
19
  const RESERVED_AUTHORIZATION_PARAMS_SET = new Set(RESERVED_AUTHORIZATION_PARAMS);
19
- async function createAuthorizationURL({ id, options, authorizationEndpoint, state, codeVerifier, scopes, claims, redirectURI, duration, prompt, accessType, responseType, display, loginHint, hd, responseMode, additionalParams, scopeJoiner }) {
20
+ async function createAuthorizationURL({ id, options, authorizationEndpoint, state, codeVerifier, scopes, claims, redirectURI, duration, prompt, accessType, responseType, display, loginHint, nonce, hd, responseMode, additionalParams, scopeJoiner }) {
20
21
  options = typeof options === "function" ? await options() : options;
21
22
  const url = new URL(options.authorizationEndpoint || authorizationEndpoint);
22
23
  url.searchParams.set("response_type", responseType || "code");
@@ -29,6 +30,7 @@ async function createAuthorizationURL({ id, options, authorizationEndpoint, stat
29
30
  duration && url.searchParams.set("duration", duration);
30
31
  display && url.searchParams.set("display", display);
31
32
  loginHint && url.searchParams.set("login_hint", loginHint);
33
+ nonce && url.searchParams.set("nonce", nonce);
32
34
  prompt && url.searchParams.set("prompt", prompt);
33
35
  hd && url.searchParams.set("hd", hd);
34
36
  accessType && url.searchParams.set("access_type", accessType);