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.
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +6 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +31 -11
- package/dist/api/routes/callback.mjs +3 -3
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +16 -2
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +9 -4
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/types.d.mts +2 -2
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +3 -2
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +30 -2
- package/dist/db/internal-adapter.mjs +56 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +73 -5
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +23 -53
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +20 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +5 -1
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.mjs +7 -4
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +51 -5
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +30 -3
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +6 -5
- package/package.json +10 -10
|
@@ -1,21 +1,146 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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.
|
|
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;
|
package/dist/client/types.d.mts
CHANGED
|
@@ -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 =
|
|
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
|
|
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 };
|
package/dist/cookies/index.mjs
CHANGED
|
@@ -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) {
|