better-auth 1.6.16 → 1.6.18

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 (93) 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 +5 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +22 -7
  6. package/dist/api/routes/callback.mjs +2 -2
  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 +13 -1
  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 +2 -3
  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 +4 -2
  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 +4 -2
  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 +4 -2
  31. package/dist/client/svelte/index.d.mts +4 -2
  32. package/dist/client/types.d.mts +27 -16
  33. package/dist/client/vanilla.d.mts +4 -2
  34. package/dist/client/vue/index.d.mts +4 -2
  35. package/dist/context/create-context.mjs +2 -1
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +25 -2
  38. package/dist/db/internal-adapter.mjs +51 -0
  39. package/dist/package.mjs +1 -1
  40. package/dist/plugins/access/access.mjs +49 -19
  41. package/dist/plugins/admin/routes.mjs +10 -3
  42. package/dist/plugins/captcha/constants.mjs +8 -1
  43. package/dist/plugins/captcha/index.mjs +8 -2
  44. package/dist/plugins/captcha/types.d.mts +21 -0
  45. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  46. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  47. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  48. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  49. package/dist/plugins/device-authorization/routes.mjs +16 -9
  50. package/dist/plugins/email-otp/routes.mjs +22 -52
  51. package/dist/plugins/generic-oauth/index.mjs +7 -2
  52. package/dist/plugins/generic-oauth/routes.mjs +16 -12
  53. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  54. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  55. package/dist/plugins/index.d.mts +6 -2
  56. package/dist/plugins/index.mjs +4 -1
  57. package/dist/plugins/jwt/index.mjs +2 -2
  58. package/dist/plugins/mcp/client/index.mjs +1 -0
  59. package/dist/plugins/mcp/index.mjs +8 -0
  60. package/dist/plugins/multi-session/index.mjs +7 -5
  61. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  62. package/dist/plugins/oauth-popup/client.mjs +203 -0
  63. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  64. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  65. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  66. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  67. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  68. package/dist/plugins/oauth-popup/index.mjs +227 -0
  69. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  70. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  71. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  72. package/dist/plugins/oidc-provider/index.mjs +10 -0
  73. package/dist/plugins/one-tap/client.mjs +12 -6
  74. package/dist/plugins/one-tap/index.d.mts +1 -0
  75. package/dist/plugins/one-tap/index.mjs +9 -5
  76. package/dist/plugins/one-time-token/index.mjs +1 -3
  77. package/dist/plugins/open-api/generator.d.mts +66 -57
  78. package/dist/plugins/open-api/generator.mjs +185 -67
  79. package/dist/plugins/open-api/index.d.mts +2 -2
  80. package/dist/plugins/organization/adapter.d.mts +29 -1
  81. package/dist/plugins/organization/adapter.mjs +66 -6
  82. package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
  83. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  84. package/dist/plugins/organization/routes/crud-team.mjs +36 -3
  85. package/dist/plugins/phone-number/routes.mjs +41 -36
  86. package/dist/plugins/siwe/index.mjs +2 -3
  87. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  88. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  89. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  90. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  91. package/dist/plugins/username/index.mjs +6 -6
  92. package/dist/test-utils/test-instance.d.mts +26 -23
  93. package/package.json +9 -9
@@ -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,
@@ -1,6 +1,6 @@
1
1
  import { ExtractPluginField, HasRequiredKeys, InferPluginFieldFromTuple, IsAny, OverrideMerge, Prettify, PrettifyDeep, RequiredKeysOf, StripEmptyObjects, UnionToIntersection } from "../../types/helper.mjs";
2
2
  import { InferActions, InferClientAPI, InferErrorCodes, IsSignal, SessionQueryParams } from "../types.mjs";
3
- import { BetterAuthClientOptions, BetterAuthClientPlugin } from "@better-auth/core";
3
+ import { BetterAuthClientOptions } from "@better-auth/core";
4
4
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
6
6
  import { BetterFetchError } from "@better-fetch/fetch";
@@ -11,7 +11,9 @@ export * from "@better-fetch/fetch";
11
11
  //#region src/client/solid/index.d.ts
12
12
  type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
13
13
  plugins: Array<infer Plugin>;
14
- } ? UnionToIntersection<Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Accessor<ReturnType<Atoms[key]["get"]>> } : {} : {} : {}> : {};
14
+ } ? UnionToIntersection<Plugin extends {
15
+ getAtoms?: infer GetAtoms;
16
+ } ? GetAtoms extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Accessor<ReturnType<Atoms[key]["get"]>> } : {} : {} : {}> : {};
15
17
  declare function createAuthClient<Option extends BetterAuthClientOptions>(options?: Option | undefined): UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
16
18
  useSession: () => Accessor<{
17
19
  data: InferClientAPI<Option> extends {
@@ -1,6 +1,6 @@
1
1
  import { ExtractPluginField, HasRequiredKeys, InferPluginFieldFromTuple, IsAny, OverrideMerge, Prettify, PrettifyDeep, RequiredKeysOf, StripEmptyObjects, UnionToIntersection } from "../../types/helper.mjs";
2
2
  import { InferActions, InferClientAPI, InferErrorCodes, IsSignal, SessionQueryParams } from "../types.mjs";
3
- import { BetterAuthClientOptions, BetterAuthClientPlugin } from "@better-auth/core";
3
+ import { BetterAuthClientOptions } from "@better-auth/core";
4
4
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import * as nanostores from "nanostores";
6
6
  import { Atom } from "nanostores";
@@ -12,7 +12,9 @@ export * from "@better-fetch/fetch";
12
12
  //#region src/client/svelte/index.d.ts
13
13
  type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
14
14
  plugins: Array<infer Plugin>;
15
- } ? UnionToIntersection<Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Atoms[key] } : {} : {} : {}> : {};
15
+ } ? UnionToIntersection<Plugin extends {
16
+ getAtoms?: infer GetAtoms;
17
+ } ? GetAtoms extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Atoms[key] } : {} : {} : {}> : {};
16
18
  declare function createAuthClient<Option extends BetterAuthClientOptions>(options?: Option | undefined): UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
17
19
  useSession: () => Atom<{
18
20
  data: InferClientAPI<Option> extends {
@@ -3,25 +3,32 @@ 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";
7
- import { BetterAuthPluginDBSchema, InferDBFieldsOutput } from "@better-auth/core/db";
8
- import { RawError } from "@better-auth/core/utils/error-codes";
6
+ import { BetterAuthClientOptions as BetterAuthClientOptions$1, BetterAuthClientPlugin, ClientAtomListener, ClientStore as ClientStore$1 } from "@better-auth/core";
7
+ import { DBFieldAttribute, InferDBFieldsOutput } from "@better-auth/core/db";
9
8
 
10
9
  //#region src/client/types.d.ts
10
+ type ClientPluginError<K extends string = string> = {
11
+ readonly code: K;
12
+ message: string;
13
+ };
11
14
  type InferPluginEndpoints<Plugins> = Plugins extends Array<infer Pl> ? UnionToIntersection<Pl extends {
12
- $InferServerPlugin: infer Plug;
15
+ $InferServerPlugin?: infer Plug;
13
16
  } ? Plug extends {
14
- endpoints: infer Endpoints;
15
- } ? Endpoints : {} : {}> : {};
17
+ endpoints?: infer Endpoints;
18
+ } ? Endpoints extends Record<string, unknown> ? Endpoints : {} : {} : {}> : {};
16
19
  type InferClientAPI<O extends BetterAuthClientOptions$1> = InferRoutes<O["plugins"] extends Array<any> ? Omit<Auth["api"], keyof InferPluginEndpoints<O["plugins"]>> & InferPluginEndpoints<O["plugins"]> : Auth["api"], O>;
17
- type InferActions<O extends BetterAuthClientOptions$1> = (O["plugins"] extends Array<infer Plugin> ? UnionToIntersection<Plugin extends BetterAuthClientPlugin$1 ? Plugin["getActions"] extends ((...args: any) => infer Actions) ? Actions : {} : {}> : {}) & InferRoutes<O["$InferAuth"] extends {
20
+ type InferActions<O extends BetterAuthClientOptions$1> = (O["plugins"] extends Array<infer Plugin> ? UnionToIntersection<Plugin extends {
21
+ getActions?: infer GetActions;
22
+ } ? GetActions extends ((...args: any) => infer Actions) ? Actions : {} : {}> : {}) & InferRoutes<O["$InferAuth"] extends {
18
23
  plugins: infer Plugins;
19
24
  } ? Plugins extends Array<infer Plugin> ? Plugin extends {
20
- endpoints: infer Endpoints;
21
- } ? Endpoints : {} : {} : {}, O>;
22
- type InferErrorCodes<O extends BetterAuthClientOptions$1> = O["plugins"] extends Array<infer Plugin> ? UnionToIntersection<Plugin extends BetterAuthClientPlugin$1 ? Plugin["$InferServerPlugin"] extends {
23
- $ERROR_CODES: infer E;
24
- } ? { [K in keyof E & string]: E[K] extends RawError ? RawError<K> : never } : {} : {}> : {};
25
+ endpoints?: infer Endpoints;
26
+ } ? Endpoints extends Record<string, unknown> ? Endpoints : {} : {} : {} : {}, O>;
27
+ type InferErrorCodes<O extends BetterAuthClientOptions$1> = O["plugins"] extends Array<infer Plugin> ? UnionToIntersection<Plugin extends {
28
+ $InferServerPlugin?: infer ServerPlugin;
29
+ } ? ServerPlugin extends {
30
+ $ERROR_CODES?: infer E;
31
+ } ? { [K in keyof E & string]: E[K] extends ClientPluginError ? ClientPluginError<K> : never } : {} : {}> : {};
25
32
  /**
26
33
  * signals are just used to recall a computed value.
27
34
  * as a convention they start with "$"
@@ -29,12 +36,16 @@ type InferErrorCodes<O extends BetterAuthClientOptions$1> = O["plugins"] extends
29
36
  type IsSignal<T> = T extends `$${infer _}` ? true : false;
30
37
  type InferSessionFromClient<O extends BetterAuthClientOptions$1> = StripEmptyObjects<Session$1 & UnionToIntersection<InferAdditionalFromClient<O, "session", "output">>>;
31
38
  type InferUserFromClient<O extends BetterAuthClientOptions$1> = StripEmptyObjects<User$1 & UnionToIntersection<InferAdditionalFromClient<O, "user", "output">>>;
32
- type InferAdditionalFromClient<Options extends BetterAuthClientOptions$1, Key extends string, Format extends "input" | "output" = "output"> = Options["plugins"] extends Array<infer Plugin> ? Plugin extends BetterAuthClientPlugin$1 ? Plugin["$InferServerPlugin"] extends {
33
- schema: infer Schema;
34
- } ? Schema extends BetterAuthPluginDBSchema ? Format extends "input" ? InferFieldsInputClient<Schema[Key]["fields"]> : InferDBFieldsOutput<Schema[Key]["fields"]> : {} : {} : {} : {};
39
+ type InferAdditionalFromClient<Options extends BetterAuthClientOptions$1, Key extends string, Format extends "input" | "output" = "output"> = Options["plugins"] extends Array<infer Plugin> ? Plugin extends {
40
+ $InferServerPlugin?: infer ServerPlugin;
41
+ } ? ServerPlugin extends {
42
+ schema?: infer Schema;
43
+ } ? Schema extends Record<Key, {
44
+ fields: infer Fields;
45
+ }> ? Fields extends Record<string, DBFieldAttribute> ? Format extends "input" ? InferFieldsInputClient<Fields> : InferDBFieldsOutput<Fields> : {} : {} : {} : {} : {};
35
46
  type SessionQueryParams = {
36
47
  disableCookieCache?: boolean | undefined;
37
48
  disableRefresh?: boolean | undefined;
38
49
  };
39
50
  //#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 };
51
+ export { type BetterAuthClientOptions$1 as BetterAuthClientOptions, type BetterAuthClientPlugin, type ClientAtomListener, type ClientStore$1 as ClientStore, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferSessionFromClient, InferUserFromClient, IsSignal, SessionQueryParams };
@@ -1,6 +1,6 @@
1
1
  import { PrettifyDeep, UnionToIntersection } from "../types/helper.mjs";
2
2
  import { InferActions, InferClientAPI, InferErrorCodes, IsSignal, SessionQueryParams } from "./types.mjs";
3
- import { BetterAuthClientOptions, BetterAuthClientPlugin } from "@better-auth/core";
3
+ import { BetterAuthClientOptions } from "@better-auth/core";
4
4
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import * as nanostores from "nanostores";
6
6
  import { Atom } from "nanostores";
@@ -10,7 +10,9 @@ import { BetterFetchError } from "@better-fetch/fetch";
10
10
  //#region src/client/vanilla.d.ts
11
11
  type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
12
12
  plugins: Array<infer Plugin>;
13
- } ? UnionToIntersection<Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: Atoms[key] } : {} : {} : {}> : {};
13
+ } ? UnionToIntersection<Plugin extends {
14
+ getAtoms?: infer GetAtoms;
15
+ } ? GetAtoms extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: Atoms[key] } : {} : {} : {}> : {};
14
16
  declare function createAuthClient<Option extends BetterAuthClientOptions>(options?: Option | undefined): UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
15
17
  useSession: Atom<{
16
18
  data: InferClientAPI<Option> extends {
@@ -1,6 +1,6 @@
1
1
  import { ExtractPluginField, HasRequiredKeys, InferPluginFieldFromTuple, IsAny, OverrideMerge, Prettify, PrettifyDeep, RequiredKeysOf, StripEmptyObjects, UnionToIntersection } from "../../types/helper.mjs";
2
2
  import { InferActions, InferClientAPI, InferErrorCodes, IsSignal, SessionQueryParams } from "../types.mjs";
3
- import { BetterAuthClientOptions, BetterAuthClientPlugin } from "@better-auth/core";
3
+ import { BetterAuthClientOptions } from "@better-auth/core";
4
4
  import { BASE_ERROR_CODES } from "@better-auth/core/error";
5
5
  import * as nanostores from "nanostores";
6
6
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
@@ -12,7 +12,9 @@ export * from "@better-fetch/fetch";
12
12
  //#region src/client/vue/index.d.ts
13
13
  type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
14
14
  plugins: Array<infer Plugin>;
15
- } ? UnionToIntersection<Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => DeepReadonly<Ref<ReturnType<Atoms[key]["get"]>>> } : {} : {} : {}> : {};
15
+ } ? UnionToIntersection<Plugin extends {
16
+ getAtoms?: infer GetAtoms;
17
+ } ? GetAtoms extends ((fetch: any) => infer Atoms) ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => DeepReadonly<Ref<ReturnType<Atoms[key]["get"]>>> } : {} : {} : {}> : {};
16
18
  declare function createAuthClient<Option extends BetterAuthClientOptions>(options?: Option | undefined): UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
17
19
  useSession: {
18
20
  (): DeepReadonly<Ref<{
@@ -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",
@@ -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,9 @@ 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) if (accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
120
+ if (accountData) if (!shouldBindAccountCookieToSessionUser(ctx.context.options) || accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
120
121
  else {
121
122
  expireCookie(ctx, ctx.context.authCookies.accountData);
122
123
  const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx);
@@ -175,6 +176,20 @@ function removeSetCookieEntries(ctx, cookieName) {
175
176
  }
176
177
  }
177
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
+ /**
178
193
  * Expires a cookie by setting `maxAge: 0` while preserving its attributes
179
194
  */
180
195
  function expireCookie(ctx, cookie) {
@@ -236,6 +251,10 @@ const getCookieCache = async (request, config) => {
236
251
  const secret = config?.secret || env.BETTER_AUTH_SECRET;
237
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");
238
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
+ };
239
258
  if (strategy === "jwe") {
240
259
  const payload = await symmetricDecodeJWT(sessionData, secret, "better-auth-session");
241
260
  if (payload && payload.session && payload.user) {
@@ -249,6 +268,7 @@ const getCookieCache = async (request, config) => {
249
268
  }
250
269
  if (cookieVersion !== expectedVersion) return null;
251
270
  }
271
+ if (isEmbeddedSessionExpired(payload)) return null;
252
272
  return payload;
253
273
  }
254
274
  return null;
@@ -265,6 +285,7 @@ const getCookieCache = async (request, config) => {
265
285
  }
266
286
  if (cookieVersion !== expectedVersion) return null;
267
287
  }
288
+ if (isEmbeddedSessionExpired(payload)) return null;
268
289
  return payload;
269
290
  }
270
291
  return null;
@@ -285,6 +306,8 @@ const getCookieCache = async (request, config) => {
285
306
  }
286
307
  if (cookieVersion !== expectedVersion) return null;
287
308
  }
309
+ if (typeof sessionDataPayload.expiresAt === "number" && sessionDataPayload.expiresAt < Date.now()) return null;
310
+ if (isEmbeddedSessionExpired(sessionDataPayload.session)) return null;
288
311
  return sessionDataPayload.session;
289
312
  }
290
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();
@@ -717,6 +719,55 @@ const createInternalAdapter = (adapter, ctx) => {
717
719
  if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
718
720
  return consumed;
719
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
+ },
720
771
  updateVerificationByIdentifier: async (identifier, data) => {
721
772
  const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier));
722
773
  if (secondaryStorage) {
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.16";
2
+ var version = "1.6.18";
3
3
  //#endregion
4
4
  export { version };