better-auth 1.6.15 → 1.6.17

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 (105) hide show
  1. package/dist/api/index.d.mts +2 -2
  2. package/dist/api/index.mjs +3 -4
  3. package/dist/api/middlewares/origin-check.mjs +6 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +31 -11
  6. package/dist/api/routes/callback.mjs +3 -3
  7. package/dist/api/routes/index.d.mts +1 -1
  8. package/dist/api/routes/password.mjs +3 -4
  9. package/dist/api/routes/session.d.mts +12 -1
  10. package/dist/api/routes/session.mjs +16 -2
  11. package/dist/api/routes/sign-in.mjs +5 -5
  12. package/dist/api/routes/sign-up.mjs +2 -2
  13. package/dist/api/routes/update-session.mjs +9 -4
  14. package/dist/api/routes/update-user.mjs +10 -12
  15. package/dist/auth/base.mjs +11 -7
  16. package/dist/client/equality.d.mts +19 -0
  17. package/dist/client/equality.mjs +42 -0
  18. package/dist/client/index.d.mts +5 -4
  19. package/dist/client/index.mjs +2 -1
  20. package/dist/client/lynx/index.d.mts +6 -5
  21. package/dist/client/path-to-object.d.mts +5 -2
  22. package/dist/client/plugins/index.d.mts +4 -1
  23. package/dist/client/plugins/index.mjs +4 -1
  24. package/dist/client/query.d.mts +4 -3
  25. package/dist/client/query.mjs +27 -17
  26. package/dist/client/react/index.d.mts +6 -5
  27. package/dist/client/session-atom.mjs +129 -4
  28. package/dist/client/session-refresh.d.mts +3 -18
  29. package/dist/client/session-refresh.mjs +38 -49
  30. package/dist/client/solid/index.d.mts +6 -5
  31. package/dist/client/svelte/index.d.mts +6 -5
  32. package/dist/client/types.d.mts +2 -2
  33. package/dist/client/vanilla.d.mts +6 -5
  34. package/dist/client/vue/index.d.mts +6 -5
  35. package/dist/context/create-context.mjs +3 -2
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +30 -2
  38. package/dist/db/internal-adapter.mjs +56 -0
  39. package/dist/oauth2/link-account.d.mts +13 -0
  40. package/dist/oauth2/link-account.mjs +1 -1
  41. package/dist/package.mjs +1 -1
  42. package/dist/plugins/access/access.mjs +49 -19
  43. package/dist/plugins/admin/access/statement.d.mts +10 -10
  44. package/dist/plugins/admin/access/statement.mjs +2 -0
  45. package/dist/plugins/admin/admin.d.mts +6 -3
  46. package/dist/plugins/admin/client.d.mts +6 -4
  47. package/dist/plugins/admin/error-codes.d.mts +2 -0
  48. package/dist/plugins/admin/error-codes.mjs +3 -1
  49. package/dist/plugins/admin/routes.mjs +73 -5
  50. package/dist/plugins/admin/schema.d.mts +1 -0
  51. package/dist/plugins/admin/schema.mjs +2 -1
  52. package/dist/plugins/captcha/constants.mjs +8 -1
  53. package/dist/plugins/captcha/index.mjs +8 -2
  54. package/dist/plugins/captcha/types.d.mts +21 -0
  55. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  56. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  57. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  58. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  59. package/dist/plugins/device-authorization/routes.mjs +16 -9
  60. package/dist/plugins/email-otp/routes.mjs +23 -53
  61. package/dist/plugins/generic-oauth/index.mjs +7 -2
  62. package/dist/plugins/generic-oauth/routes.mjs +20 -9
  63. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  64. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  65. package/dist/plugins/index.d.mts +5 -1
  66. package/dist/plugins/index.mjs +4 -1
  67. package/dist/plugins/jwt/index.mjs +2 -2
  68. package/dist/plugins/mcp/client/index.mjs +1 -0
  69. package/dist/plugins/mcp/index.mjs +8 -0
  70. package/dist/plugins/multi-session/index.mjs +7 -5
  71. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  72. package/dist/plugins/oauth-popup/client.mjs +203 -0
  73. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  74. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  75. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  76. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  77. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  78. package/dist/plugins/oauth-popup/index.mjs +227 -0
  79. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  80. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  81. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  82. package/dist/plugins/oidc-provider/index.mjs +10 -0
  83. package/dist/plugins/one-tap/client.mjs +12 -6
  84. package/dist/plugins/one-tap/index.d.mts +1 -0
  85. package/dist/plugins/one-tap/index.mjs +9 -5
  86. package/dist/plugins/one-time-token/index.mjs +1 -3
  87. package/dist/plugins/open-api/generator.mjs +7 -4
  88. package/dist/plugins/organization/adapter.d.mts +29 -1
  89. package/dist/plugins/organization/adapter.mjs +66 -6
  90. package/dist/plugins/organization/organization.mjs +2 -0
  91. package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
  92. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  93. package/dist/plugins/organization/routes/crud-team.mjs +51 -5
  94. package/dist/plugins/organization/schema.d.mts +2 -0
  95. package/dist/plugins/phone-number/routes.mjs +41 -36
  96. package/dist/plugins/siwe/index.mjs +30 -3
  97. package/dist/plugins/siwe/parse-message.mjs +60 -0
  98. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  99. package/dist/plugins/two-factor/index.mjs +9 -1
  100. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  101. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  102. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  103. package/dist/plugins/username/index.mjs +6 -6
  104. package/dist/test-utils/test-instance.d.mts +6 -5
  105. package/package.json +10 -10
@@ -1,21 +1,146 @@
1
- import { useAuthQuery } from "./query.mjs";
1
+ import { isJsonEqual, withEquality } from "./equality.mjs";
2
2
  import { createSessionRefreshManager } from "./session-refresh.mjs";
3
3
  import { atom, onMount } from "nanostores";
4
4
  //#region src/client/session-atom.ts
5
+ const isServer = () => typeof window === "undefined";
6
+ /**
7
+ * Normalize $fetch response: `throw: true` returns data directly,
8
+ * otherwise `{ data, error }`.
9
+ */
10
+ function normalizeSessionResponse(res) {
11
+ if (typeof res === "object" && res !== null && "data" in res && "error" in res) return res;
12
+ return {
13
+ data: res,
14
+ error: null
15
+ };
16
+ }
17
+ function normalizeSessionData(data) {
18
+ if (!data) return null;
19
+ if (data.session === null && data.user === null) return null;
20
+ return data;
21
+ }
22
+ function isSessionAtomEqual(a, b) {
23
+ return isJsonEqual(a.data, b.data) && a.error === b.error && a.isPending === b.isPending && a.isRefetching === b.isRefetching && a.refetch === b.refetch;
24
+ }
5
25
  function getSessionAtom($fetch, options) {
6
26
  const $signal = atom(false);
7
- const session = useAuthQuery($signal, "/get-session", $fetch, { method: "GET" });
27
+ let abortController;
28
+ const refetch = (queryParams) => fetchSession(queryParams);
29
+ const session = atom({
30
+ data: null,
31
+ error: null,
32
+ isPending: true,
33
+ isRefetching: false,
34
+ refetch
35
+ });
36
+ withEquality(session, isSessionAtomEqual);
37
+ const settleAbortedFetch = (controller) => {
38
+ if (abortController !== controller) return;
39
+ const current = session.get();
40
+ abortController = void 0;
41
+ if (!current.isPending && !current.isRefetching) return;
42
+ session.set({
43
+ ...current,
44
+ isPending: false,
45
+ isRefetching: false,
46
+ refetch
47
+ });
48
+ };
49
+ const fetchSession = async (queryParams) => {
50
+ abortController?.abort();
51
+ const controller = new AbortController();
52
+ abortController = controller;
53
+ const current = session.get();
54
+ session.set({
55
+ ...current,
56
+ isPending: current.data === null,
57
+ isRefetching: true,
58
+ error: null,
59
+ refetch
60
+ });
61
+ try {
62
+ const res = await $fetch("/get-session", {
63
+ method: "GET",
64
+ query: queryParams?.query,
65
+ signal: controller.signal
66
+ });
67
+ if (controller.signal.aborted) {
68
+ settleAbortedFetch(controller);
69
+ return;
70
+ }
71
+ let { data, error } = normalizeSessionResponse(res);
72
+ if (data?.needsRefresh) try {
73
+ const refreshRes = await $fetch("/get-session", {
74
+ method: "POST",
75
+ signal: controller.signal
76
+ });
77
+ if (controller.signal.aborted) {
78
+ settleAbortedFetch(controller);
79
+ return;
80
+ }
81
+ ({data, error} = normalizeSessionResponse(refreshRes));
82
+ } catch {
83
+ if (controller.signal.aborted) {
84
+ settleAbortedFetch(controller);
85
+ return;
86
+ }
87
+ }
88
+ if (error) {
89
+ const latest = session.get();
90
+ const isUnauthorized = error?.status === 401;
91
+ session.set({
92
+ data: isUnauthorized ? null : latest.data,
93
+ error,
94
+ isPending: false,
95
+ isRefetching: false,
96
+ refetch
97
+ });
98
+ return;
99
+ }
100
+ const sessionData = normalizeSessionData(data);
101
+ const current = session.get();
102
+ const stableData = current.data != null && sessionData != null && isJsonEqual(current.data, sessionData) ? current.data : sessionData;
103
+ session.set({
104
+ data: stableData,
105
+ error: null,
106
+ isPending: false,
107
+ isRefetching: false,
108
+ refetch
109
+ });
110
+ } catch (fetchError) {
111
+ if (controller.signal.aborted) {
112
+ settleAbortedFetch(controller);
113
+ return;
114
+ }
115
+ const latest = session.get();
116
+ session.set({
117
+ data: latest.data,
118
+ error: fetchError,
119
+ isPending: false,
120
+ isRefetching: false,
121
+ refetch
122
+ });
123
+ }
124
+ };
8
125
  let broadcastSessionUpdate = () => {};
9
126
  onMount(session, () => {
127
+ let timeoutId;
128
+ if (!isServer()) timeoutId = setTimeout(() => {
129
+ fetchSession();
130
+ }, 0);
10
131
  const refreshManager = createSessionRefreshManager({
11
- sessionAtom: session,
132
+ fetchSession,
133
+ shouldPollSession: () => session.get().data != null,
12
134
  sessionSignal: $signal,
13
- $fetch,
14
135
  options
15
136
  });
16
137
  refreshManager.init();
17
138
  broadcastSessionUpdate = refreshManager.broadcastSessionUpdate;
18
139
  return () => {
140
+ if (timeoutId) clearTimeout(timeoutId);
141
+ const controller = abortController;
142
+ controller?.abort();
143
+ if (controller) settleAbortedFetch(controller);
19
144
  refreshManager.cleanup();
20
145
  };
21
146
  });
@@ -1,28 +1,13 @@
1
- import { Session, User } from "../types/models.mjs";
2
- import { AuthQueryAtom } from "./query.mjs";
3
1
  import { BetterAuthClientOptions } from "@better-auth/core";
4
2
  import { WritableAtom } from "nanostores";
5
- import { BetterFetch } from "@better-fetch/fetch";
6
3
 
7
4
  //#region src/client/session-refresh.d.ts
8
5
  interface SessionRefreshOptions {
9
- sessionAtom: AuthQueryAtom<{
10
- user: User;
11
- session: Session;
12
- } & Record<string, any>>;
6
+ fetchSession: () => Promise<void>;
7
+ shouldPollSession?: () => boolean;
13
8
  sessionSignal: WritableAtom<boolean>;
14
- $fetch: BetterFetch;
15
9
  options?: BetterAuthClientOptions | undefined;
16
10
  }
17
- type SessionResponse = ({
18
- session: null;
19
- user: null;
20
- needsRefresh?: boolean;
21
- } | {
22
- session: Session;
23
- user: User;
24
- needsRefresh?: boolean;
25
- }) & Record<string, any>;
26
11
  declare function createSessionRefreshManager(opts: SessionRefreshOptions): {
27
12
  init: () => void;
28
13
  cleanup: () => void;
@@ -32,4 +17,4 @@ declare function createSessionRefreshManager(opts: SessionRefreshOptions): {
32
17
  broadcastSessionUpdate: (trigger: "signout" | "getSession" | "updateUser") => void;
33
18
  };
34
19
  //#endregion
35
- export { SessionRefreshOptions, SessionResponse, createSessionRefreshManager };
20
+ export { SessionRefreshOptions, createSessionRefreshManager };
@@ -4,28 +4,17 @@ import { getGlobalOnlineManager } from "./online-manager.mjs";
4
4
  //#region src/client/session-refresh.ts
5
5
  const now = () => Math.floor(Date.now() / 1e3);
6
6
  /**
7
- * Normalize $fetch response: `throw: true` returns data directly, otherwise `{ data, error }`.
8
- */
9
- function normalizeSessionResponse(res) {
10
- if (typeof res === "object" && res !== null && "data" in res && "error" in res) return res;
11
- return {
12
- data: res,
13
- error: null
14
- };
15
- }
16
- /**
17
7
  * Rate limit: don't refetch on focus if a session request was made within this many seconds
18
8
  */
19
9
  const FOCUS_REFETCH_RATE_LIMIT_SECONDS = 5;
20
10
  function createSessionRefreshManager(opts) {
21
- const { sessionAtom, sessionSignal, $fetch, options = {} } = opts;
11
+ const { fetchSession, shouldPollSession = () => true, sessionSignal, options = {} } = opts;
22
12
  const refetchInterval = options.sessionOptions?.refetchInterval ?? 0;
23
13
  const refetchOnWindowFocus = options.sessionOptions?.refetchOnWindowFocus ?? true;
24
14
  const refetchWhenOffline = options.sessionOptions?.refetchWhenOffline ?? false;
25
15
  const state = {
26
- lastSync: 0,
27
- lastSessionRequest: 0,
28
- cachedSession: void 0
16
+ isInitialized: false,
17
+ lastSessionRequest: 0
29
18
  };
30
19
  const shouldRefetch = () => {
31
20
  return refetchWhenOffline || getGlobalOnlineManager().isOnline;
@@ -33,45 +22,21 @@ function createSessionRefreshManager(opts) {
33
22
  const triggerRefetch = (event) => {
34
23
  if (!shouldRefetch()) return;
35
24
  if (event?.event === "storage") {
36
- state.lastSync = now();
37
- sessionSignal.set(!sessionSignal.get());
25
+ fetchSession();
38
26
  return;
39
27
  }
40
- const currentSession = sessionAtom.get();
41
- const fetchSessionWithRefresh = () => {
42
- state.lastSessionRequest = now();
43
- $fetch("/get-session").then(async (res) => {
44
- let { data, error } = normalizeSessionResponse(res);
45
- if (data?.needsRefresh) try {
46
- const refreshRes = await $fetch("/get-session", { method: "POST" });
47
- ({data, error} = normalizeSessionResponse(refreshRes));
48
- } catch {}
49
- const sessionData = data?.session && data?.user ? data : null;
50
- sessionAtom.set({
51
- ...currentSession,
52
- data: sessionData,
53
- error
54
- });
55
- state.lastSync = now();
56
- sessionSignal.set(!sessionSignal.get());
57
- }).catch(() => {});
58
- };
59
28
  if (event?.event === "poll") {
60
- fetchSessionWithRefresh();
29
+ state.lastSessionRequest = now();
30
+ fetchSession();
61
31
  return;
62
32
  }
63
33
  if (event?.event === "visibilitychange") {
64
34
  if (now() - state.lastSessionRequest < FOCUS_REFETCH_RATE_LIMIT_SECONDS) return;
65
35
  state.lastSessionRequest = now();
66
- }
67
- if (event?.event === "visibilitychange") {
68
- fetchSessionWithRefresh();
36
+ fetchSession();
69
37
  return;
70
38
  }
71
- if (currentSession?.data === null || currentSession?.data === void 0) {
72
- state.lastSync = now();
73
- sessionSignal.set(!sessionSignal.get());
74
- }
39
+ fetchSession();
75
40
  };
76
41
  const broadcastSessionUpdate = (trigger) => {
77
42
  getGlobalBroadcastChannel().post({
@@ -82,7 +47,7 @@ function createSessionRefreshManager(opts) {
82
47
  };
83
48
  const setupPolling = () => {
84
49
  if (refetchInterval && refetchInterval > 0) state.pollInterval = setInterval(() => {
85
- if (sessionAtom.get()?.data) triggerRefetch({ event: "poll" });
50
+ if (shouldPollSession()) triggerRefetch({ event: "poll" });
86
51
  }, refetchInterval * 1e3);
87
52
  };
88
53
  const setupBroadcast = () => {
@@ -101,16 +66,25 @@ function createSessionRefreshManager(opts) {
101
66
  if (online) triggerRefetch({ event: "visibilitychange" });
102
67
  });
103
68
  };
69
+ const setupSignalSubscription = () => {
70
+ state.unsubscribeSignal = sessionSignal.listen(() => {
71
+ fetchSession();
72
+ });
73
+ };
104
74
  const init = () => {
75
+ if (state.isInitialized) return;
76
+ state.isInitialized = true;
105
77
  setupPolling();
106
78
  setupBroadcast();
107
79
  setupFocusRefetch();
108
80
  setupOnlineRefetch();
109
- getGlobalBroadcastChannel().setup();
110
- getGlobalFocusManager().setup();
111
- getGlobalOnlineManager().setup();
81
+ setupSignalSubscription();
82
+ state.cleanupBroadcastSetup = getGlobalBroadcastChannel().setup();
83
+ state.cleanupFocusSetup = getGlobalFocusManager().setup();
84
+ state.cleanupOnlineSetup = getGlobalOnlineManager().setup();
112
85
  };
113
86
  const cleanup = () => {
87
+ if (!state.isInitialized) return;
114
88
  if (state.pollInterval) {
115
89
  clearInterval(state.pollInterval);
116
90
  state.pollInterval = void 0;
@@ -127,9 +101,24 @@ function createSessionRefreshManager(opts) {
127
101
  state.unsubscribeOnline();
128
102
  state.unsubscribeOnline = void 0;
129
103
  }
130
- state.lastSync = 0;
104
+ if (state.unsubscribeSignal) {
105
+ state.unsubscribeSignal();
106
+ state.unsubscribeSignal = void 0;
107
+ }
108
+ if (state.cleanupBroadcastSetup) {
109
+ state.cleanupBroadcastSetup();
110
+ state.cleanupBroadcastSetup = void 0;
111
+ }
112
+ if (state.cleanupFocusSetup) {
113
+ state.cleanupFocusSetup();
114
+ state.cleanupFocusSetup = void 0;
115
+ }
116
+ if (state.cleanupOnlineSetup) {
117
+ state.cleanupOnlineSetup();
118
+ state.cleanupOnlineSetup = void 0;
119
+ }
120
+ state.isInitialized = false;
131
121
  state.lastSessionRequest = 0;
132
- state.cachedSession = void 0;
133
122
  };
134
123
  return {
135
124
  init,
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
69
69
  priority?: RequestPriority | undefined;
70
70
  cache?: RequestCache | undefined;
71
71
  credentials?: RequestCredentials;
72
- headers?: (HeadersInit & (HeadersInit | {
73
- accept: "application/json" | "text/plain" | "application/octet-stream";
74
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
75
- authorization: "Bearer" | "Basic";
76
- })) | undefined;
77
72
  integrity?: string | undefined;
78
73
  keepalive?: boolean | undefined;
79
74
  method: string;
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
103
98
  prefix: string | (() => string | undefined) | undefined;
104
99
  value: string | (() => string | undefined) | undefined;
105
100
  }) | undefined;
101
+ headers?: {} | {
102
+ [x: string]: string | undefined;
103
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
104
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
105
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
106
+ } | undefined;
106
107
  body?: any;
107
108
  query?: any;
108
109
  params?: any;
@@ -55,11 +55,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
55
55
  priority?: RequestPriority | undefined;
56
56
  cache?: RequestCache | undefined;
57
57
  credentials?: RequestCredentials;
58
- headers?: (HeadersInit & (HeadersInit | {
59
- accept: "application/json" | "text/plain" | "application/octet-stream";
60
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
61
- authorization: "Bearer" | "Basic";
62
- })) | undefined;
63
58
  integrity?: string | undefined;
64
59
  keepalive?: boolean | undefined;
65
60
  method: string;
@@ -89,6 +84,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
89
84
  prefix: string | (() => string | undefined) | undefined;
90
85
  value: string | (() => string | undefined) | undefined;
91
86
  }) | undefined;
87
+ headers?: {} | {
88
+ [x: string]: string | undefined;
89
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
90
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
91
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
92
+ } | undefined;
92
93
  body?: any;
93
94
  query?: any;
94
95
  params?: any;
@@ -3,7 +3,7 @@ import { StripEmptyObjects, UnionToIntersection } from "../types/helper.mjs";
3
3
  import { InferRoutes } from "./path-to-object.mjs";
4
4
  import { Session as Session$1, User as User$1 } from "../types/models.mjs";
5
5
  import { Auth } from "../types/auth.mjs";
6
- import { BetterAuthClientOptions as BetterAuthClientOptions$1, BetterAuthClientPlugin as BetterAuthClientPlugin$1, ClientAtomListener, ClientStore } from "@better-auth/core";
6
+ import { BetterAuthClientOptions as BetterAuthClientOptions$1, BetterAuthClientPlugin as BetterAuthClientPlugin$1, ClientAtomListener, ClientStore as ClientStore$1 } from "@better-auth/core";
7
7
  import { BetterAuthPluginDBSchema, InferDBFieldsOutput } from "@better-auth/core/db";
8
8
  import { RawError } from "@better-auth/core/utils/error-codes";
9
9
 
@@ -37,4 +37,4 @@ type SessionQueryParams = {
37
37
  disableRefresh?: boolean | undefined;
38
38
  };
39
39
  //#endregion
40
- export { type BetterAuthClientOptions$1 as BetterAuthClientOptions, type BetterAuthClientPlugin$1 as BetterAuthClientPlugin, type ClientAtomListener, type ClientStore, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferSessionFromClient, InferUserFromClient, IsSignal, SessionQueryParams };
40
+ export { type BetterAuthClientOptions$1 as BetterAuthClientOptions, type BetterAuthClientPlugin$1 as BetterAuthClientPlugin, type ClientAtomListener, type ClientStore$1 as ClientStore, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferSessionFromClient, InferUserFromClient, IsSignal, SessionQueryParams };
@@ -53,11 +53,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
53
53
  priority?: RequestPriority | undefined;
54
54
  cache?: RequestCache | undefined;
55
55
  credentials?: RequestCredentials;
56
- headers?: (HeadersInit & (HeadersInit | {
57
- accept: "application/json" | "text/plain" | "application/octet-stream";
58
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
59
- authorization: "Bearer" | "Basic";
60
- })) | undefined;
61
56
  integrity?: string | undefined;
62
57
  keepalive?: boolean | undefined;
63
58
  method: string;
@@ -87,6 +82,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
87
82
  prefix: string | (() => string | undefined) | undefined;
88
83
  value: string | (() => string | undefined) | undefined;
89
84
  }) | undefined;
85
+ headers?: {} | {
86
+ [x: string]: string | undefined;
87
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
88
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
89
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
90
+ } | undefined;
90
91
  body?: any;
91
92
  query?: any;
92
93
  params?: any;
@@ -93,11 +93,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
93
93
  priority?: RequestPriority | undefined;
94
94
  cache?: RequestCache | undefined;
95
95
  credentials?: RequestCredentials;
96
- headers?: (HeadersInit & (HeadersInit | {
97
- accept: "application/json" | "text/plain" | "application/octet-stream";
98
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
99
- authorization: "Bearer" | "Basic";
100
- })) | undefined;
101
96
  integrity?: string | undefined;
102
97
  keepalive?: boolean | undefined;
103
98
  method: string;
@@ -127,6 +122,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
127
122
  prefix: string | (() => string | undefined) | undefined;
128
123
  value: string | (() => string | undefined) | undefined;
129
124
  }) | undefined;
125
+ headers?: {} | {
126
+ [x: string]: string | undefined;
127
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
128
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
129
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
130
+ } | undefined;
130
131
  body?: any;
131
132
  query?: any;
132
133
  params?: any;
@@ -1,5 +1,6 @@
1
1
  import { getBaseURL, isDynamicBaseURLConfig } from "../utils/url.mjs";
2
2
  import { matchesOriginPattern } from "../auth/trusted-origins.mjs";
3
+ import { hasServerSessionStore } from "./store-capabilities.mjs";
3
4
  import { isPromise } from "../utils/is-promise.mjs";
4
5
  import { hashPassword, verifyPassword } from "../crypto/password.mjs";
5
6
  import { createCookieGetter, getCookies } from "../cookies/index.mjs";
@@ -42,7 +43,7 @@ function validateSecret(secret, logger) {
42
43
  if (estimateEntropy(secret) < 120) logger.warn("[better-auth] Warning: your BETTER_AUTH_SECRET appears low-entropy. Use a randomly generated secret for production.");
43
44
  }
44
45
  async function createAuthContext(adapter, options, getDatabaseType) {
45
- const isStateful = !!options.database || !!options.secondaryStorage;
46
+ const isStateful = hasServerSessionStore(options);
46
47
  if (!isStateful) options = defu$1(options, { session: { cookieCache: {
47
48
  enabled: true,
48
49
  strategy: "jwe",
@@ -59,7 +60,7 @@ async function createAuthContext(adapter, options, getDatabaseType) {
59
60
  if (!allowedHosts || allowedHosts.length === 0) throw new BetterAuthError("baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"]).");
60
61
  }
61
62
  const baseURL = isDynamicConfig ? void 0 : getBaseURL(typeof options.baseURL === "string" ? options.baseURL : void 0, options.basePath);
62
- if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL could not be determined. Please set a valid base URL using the baseURL config option or the BETTER_AUTH_URL environment variable. Without this, callbacks and redirects may not work correctly.`);
63
+ if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL is not set. Set the baseURL option or BETTER_AUTH_URL env, or use a dynamic baseURL with allowedHosts for multi-host setups. Without it the origin is derived from the incoming request, and callbacks and redirects may not work correctly.`);
63
64
  if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger.error(`[better-auth] Misconfiguration detected.
64
65
  You are using the memory DB with generateId: false.
65
66
  This will cause no id to be generated for any model.
@@ -0,0 +1,12 @@
1
+ //#region src/context/store-capabilities.ts
2
+ function hasServerSessionStore(options) {
3
+ return !!options.database || !!options.secondaryStorage;
4
+ }
5
+ function hasServerAccountStore(options) {
6
+ return !!options.database;
7
+ }
8
+ function shouldBindAccountCookieToSessionUser(options) {
9
+ return hasServerAccountStore(options);
10
+ }
11
+ //#endregion
12
+ export { hasServerSessionStore, shouldBindAccountCookieToSessionUser };
@@ -1,4 +1,5 @@
1
1
  import { isDynamicBaseURLConfig } from "../utils/url.mjs";
2
+ import { shouldBindAccountCookieToSessionUser } from "../context/store-capabilities.mjs";
2
3
  import { signJWT, symmetricDecodeJWT, symmetricEncodeJWT, verifyJWT } from "../crypto/jwt.mjs";
3
4
  import { parseUserOutput } from "../db/schema.mjs";
4
5
  import { getDate } from "../utils/date.mjs";
@@ -114,9 +115,14 @@ async function setCookieCache(ctx, session, dontRememberMe) {
114
115
  }
115
116
  ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options);
116
117
  }
117
- if (ctx.context.options.account?.storeAccountCookie) {
118
+ if (ctx.context.options.account?.storeAccountCookie && !hasPendingSetCookie(ctx, ctx.context.authCookies.accountData.name)) {
118
119
  const accountData = await getAccountCookie(ctx);
119
- if (accountData) await setAccountCookie(ctx, accountData);
120
+ if (accountData) if (!shouldBindAccountCookieToSessionUser(ctx.context.options) || accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
121
+ else {
122
+ expireCookie(ctx, ctx.context.authCookies.accountData);
123
+ const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx);
124
+ accountStore.setCookies(accountStore.clean());
125
+ }
120
126
  }
121
127
  }
122
128
  async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
@@ -170,6 +176,20 @@ function removeSetCookieEntries(ctx, cookieName) {
170
176
  }
171
177
  }
172
178
  /**
179
+ * Whether the response already has a pending `Set-Cookie` for `cookieName`
180
+ * or a chunked variant.
181
+ */
182
+ function hasPendingSetCookie(ctx, cookieName) {
183
+ const scoped = ctx;
184
+ const targets = /* @__PURE__ */ new Set();
185
+ if (scoped.responseHeaders) targets.add(scoped.responseHeaders);
186
+ if (scoped.context?.responseHeaders) targets.add(scoped.context.responseHeaders);
187
+ const exact = `${cookieName}=`;
188
+ const chunk = `${cookieName}.`;
189
+ for (const headers of targets) if ((typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitSetCookieHeader(headers.get("set-cookie") || "")).some((entry) => entry.startsWith(exact) || entry.startsWith(chunk))) return true;
190
+ return false;
191
+ }
192
+ /**
173
193
  * Expires a cookie by setting `maxAge: 0` while preserving its attributes
174
194
  */
175
195
  function expireCookie(ctx, cookie) {
@@ -231,6 +251,10 @@ const getCookieCache = async (request, config) => {
231
251
  const secret = config?.secret || env.BETTER_AUTH_SECRET;
232
252
  if (!secret) throw new BetterAuthError("getCookieCache requires a secret to be provided. Either pass it as an option or set the BETTER_AUTH_SECRET environment variable");
233
253
  const strategy = config?.strategy || "compact";
254
+ const isEmbeddedSessionExpired = (payload) => {
255
+ const expiresAt = payload.session?.expiresAt;
256
+ return !!expiresAt && new Date(expiresAt).getTime() < Date.now();
257
+ };
234
258
  if (strategy === "jwe") {
235
259
  const payload = await symmetricDecodeJWT(sessionData, secret, "better-auth-session");
236
260
  if (payload && payload.session && payload.user) {
@@ -244,6 +268,7 @@ const getCookieCache = async (request, config) => {
244
268
  }
245
269
  if (cookieVersion !== expectedVersion) return null;
246
270
  }
271
+ if (isEmbeddedSessionExpired(payload)) return null;
247
272
  return payload;
248
273
  }
249
274
  return null;
@@ -260,6 +285,7 @@ const getCookieCache = async (request, config) => {
260
285
  }
261
286
  if (cookieVersion !== expectedVersion) return null;
262
287
  }
288
+ if (isEmbeddedSessionExpired(payload)) return null;
263
289
  return payload;
264
290
  }
265
291
  return null;
@@ -280,6 +306,8 @@ const getCookieCache = async (request, config) => {
280
306
  }
281
307
  if (cookieVersion !== expectedVersion) return null;
282
308
  }
309
+ if (typeof sessionDataPayload.expiresAt === "number" && sessionDataPayload.expiresAt < Date.now()) return null;
310
+ if (isEmbeddedSessionExpired(sessionDataPayload.session)) return null;
283
311
  return sessionDataPayload.session;
284
312
  }
285
313
  }
@@ -6,6 +6,8 @@ import { getWithHooks } from "./with-hooks.mjs";
6
6
  import { getCurrentAdapter, getCurrentAuthContext, runWithTransaction } from "@better-auth/core/context";
7
7
  import { generateId } from "@better-auth/core/utils/id";
8
8
  import { safeJSONParse } from "@better-auth/core/utils/json";
9
+ import { base64Url } from "@better-auth/utils/base64";
10
+ import { createHash } from "@better-auth/utils/hash";
9
11
  //#region src/db/internal-adapter.ts
10
12
  function getTTLSeconds(expiresAt, now = Date.now()) {
11
13
  const expiresMs = typeof expiresAt === "number" ? expiresAt : expiresAt.getTime();
@@ -16,6 +18,7 @@ const createInternalAdapter = (adapter, ctx) => {
16
18
  const options = ctx.options;
17
19
  const secondaryStorage = options.secondaryStorage;
18
20
  const verificationConsumeLocks = /* @__PURE__ */ new Map();
21
+ let warnedNonAtomicConsume = false;
19
22
  const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
20
23
  const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
21
24
  async function refreshUserSessions(user) {
@@ -653,6 +656,10 @@ const createInternalAdapter = (adapter, ctx) => {
653
656
  if (secondaryStorage && !options.verification?.storeInDatabase) {
654
657
  const consumeCacheKey = async (key) => {
655
658
  if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
659
+ if (!warnedNonAtomicConsume) {
660
+ warnedNonAtomicConsume = true;
661
+ logger.warn("Secondary storage does not implement `getAndDelete`, so single-use verification values cannot be consumed atomically across processes. Implement `getAndDelete` or use database-backed verification storage to guarantee single use.");
662
+ }
656
663
  return withVerificationConsumeLock(key, async () => {
657
664
  const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
658
665
  if (!parsed) return null;
@@ -712,6 +719,55 @@ const createInternalAdapter = (adapter, ctx) => {
712
719
  if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
713
720
  return consumed;
714
721
  },
722
+ reserveVerificationValue: async (data) => {
723
+ const reservationId = base64Url.encode(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), { padding: false });
724
+ const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier);
725
+ const storedIdentifier = await processIdentifier(data.identifier, storageOption);
726
+ if (secondaryStorage && !options.verification?.storeInDatabase) {
727
+ const cacheKey = `verification:${storedIdentifier}`;
728
+ if (await secondaryStorage.get(cacheKey)) return false;
729
+ await secondaryStorage.set(cacheKey, JSON.stringify({
730
+ id: reservationId,
731
+ identifier: storedIdentifier,
732
+ value: data.value,
733
+ expiresAt: data.expiresAt
734
+ }), getTTLSeconds(data.expiresAt));
735
+ return true;
736
+ }
737
+ try {
738
+ await adapter.create({
739
+ model: "verification",
740
+ data: {
741
+ id: reservationId,
742
+ identifier: storedIdentifier,
743
+ value: data.value,
744
+ expiresAt: data.expiresAt,
745
+ createdAt: /* @__PURE__ */ new Date(),
746
+ updatedAt: /* @__PURE__ */ new Date()
747
+ },
748
+ forceAllowId: true
749
+ });
750
+ } catch (error) {
751
+ if (await adapter.findOne({
752
+ model: "verification",
753
+ where: [{
754
+ field: "id",
755
+ value: reservationId
756
+ }]
757
+ })) return false;
758
+ throw error;
759
+ }
760
+ if (secondaryStorage) {
761
+ const ttl = getTTLSeconds(data.expiresAt);
762
+ if (ttl > 0) await secondaryStorage.set(`verification:${storedIdentifier}`, JSON.stringify({
763
+ id: reservationId,
764
+ identifier: storedIdentifier,
765
+ value: data.value,
766
+ expiresAt: data.expiresAt
767
+ }), ttl);
768
+ }
769
+ return true;
770
+ },
715
771
  updateVerificationByIdentifier: async (identifier, data) => {
716
772
  const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier));
717
773
  if (secondaryStorage) {