better-auth 1.6.16 → 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 +5 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +22 -7
- package/dist/api/routes/callback.mjs +2 -2
- 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 +13 -1
- 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 +2 -3
- 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/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/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/types.d.mts +2 -2
- package/dist/context/create-context.mjs +2 -1
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +25 -2
- package/dist/db/internal-adapter.mjs +51 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/routes.mjs +10 -3
- 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 +22 -52
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +16 -12
- 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/routes/crud-invites.mjs +49 -34
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +36 -3
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +2 -3
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -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/package.json +9 -9
package/dist/api/index.d.mts
CHANGED
|
@@ -11,7 +11,7 @@ import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEm
|
|
|
11
11
|
import { error } from "./routes/error.mjs";
|
|
12
12
|
import { ok } from "./routes/ok.mjs";
|
|
13
13
|
import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./routes/password.mjs";
|
|
14
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
14
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
15
15
|
import { signInEmail, signInSocial } from "./routes/sign-in.mjs";
|
|
16
16
|
import { signOut } from "./routes/sign-out.mjs";
|
|
17
17
|
import { signUpEmail } from "./routes/sign-up.mjs";
|
|
@@ -3961,4 +3961,4 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3961
3961
|
} extends infer T_2 ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K] } : never) & T_1> : never : never : never;
|
|
3962
3962
|
};
|
|
3963
3963
|
//#endregion
|
|
3964
|
-
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
3964
|
+
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
package/dist/api/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { isAPIError } from "../utils/is-api-error.mjs";
|
|
|
2
2
|
import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
|
|
3
3
|
import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
|
|
4
4
|
import { getIp } from "../utils/get-request-ip.mjs";
|
|
5
|
-
import { onRequestRateLimit
|
|
5
|
+
import { onRequestRateLimit } from "./rate-limiter/index.mjs";
|
|
6
6
|
import { getOAuthState } from "./state/oauth.mjs";
|
|
7
7
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
8
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
8
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
9
9
|
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
|
|
10
10
|
import { callbackOAuth } from "./routes/callback.mjs";
|
|
11
11
|
import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
|
|
@@ -178,7 +178,6 @@ const router = (ctx, options) => {
|
|
|
178
178
|
return currentRequest;
|
|
179
179
|
},
|
|
180
180
|
async onResponse(res, req) {
|
|
181
|
-
await onResponseRateLimit(req, ctx);
|
|
182
181
|
for (const plugin of ctx.options.plugins || []) if (plugin.onResponse) {
|
|
183
182
|
const response = await withSpan(`onResponse ${plugin.id}`, {
|
|
184
183
|
[ATTR_HOOK_TYPE]: "onResponse",
|
|
@@ -214,4 +213,4 @@ const router = (ctx, options) => {
|
|
|
214
213
|
});
|
|
215
214
|
};
|
|
216
215
|
//#endregion
|
|
217
|
-
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
216
|
+
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
@@ -23,7 +23,10 @@ function shouldSkipOriginCheck(ctx) {
|
|
|
23
23
|
if (Array.isArray(skipOriginCheck) && ctx.request) try {
|
|
24
24
|
const basePath = new URL(ctx.context.baseURL).pathname;
|
|
25
25
|
const currentPath = normalizePathname(ctx.request.url, basePath);
|
|
26
|
-
return skipOriginCheck.some((skipPath) =>
|
|
26
|
+
return skipOriginCheck.some((skipPath) => {
|
|
27
|
+
const normalizedSkipPath = skipPath.replace(/\/+$/, "");
|
|
28
|
+
return currentPath === normalizedSkipPath || currentPath.startsWith(`${normalizedSkipPath}/`);
|
|
29
|
+
});
|
|
27
30
|
} catch {}
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
@@ -47,6 +50,7 @@ const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
|
|
|
47
50
|
const newUserCallbackURL = body?.newUserCallbackURL;
|
|
48
51
|
const validateURL = (url, label) => {
|
|
49
52
|
if (!url) return;
|
|
53
|
+
if (typeof url !== "string") throw APIError.fromStatus("BAD_REQUEST", { message: `Invalid ${label}: expected a string` });
|
|
50
54
|
if (!ctx.context.isTrustedOrigin(url, { allowRelativePaths: label !== "origin" })) {
|
|
51
55
|
ctx.context.logger.error(`Invalid ${label}: ${url}`);
|
|
52
56
|
ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`, `Current list of trustedOrigins: ${ctx.context.trustedOrigins}`);
|
|
@@ -5,10 +5,62 @@ import { normalizePathname } from "@better-auth/core/utils/url";
|
|
|
5
5
|
import { createRateLimitKey } from "@better-auth/core/utils/ip";
|
|
6
6
|
//#region src/api/rate-limiter/index.ts
|
|
7
7
|
const memory = /* @__PURE__ */ new Map();
|
|
8
|
-
|
|
8
|
+
const MEMORY_STORE_MAX_ENTRIES = 1e5;
|
|
9
|
+
function pruneMemoryStore() {
|
|
9
10
|
const now = Date.now();
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
for (const [key, entry] of memory) if (now >= entry.expiresAt) memory.delete(key);
|
|
12
|
+
if (memory.size <= MEMORY_STORE_MAX_ENTRIES) return;
|
|
13
|
+
const overflow = memory.size - MEMORY_STORE_MAX_ENTRIES;
|
|
14
|
+
let removed = 0;
|
|
15
|
+
for (const key of memory.keys()) {
|
|
16
|
+
memory.delete(key);
|
|
17
|
+
if (++removed >= overflow) break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide an atomic rate-limit step against an in-memory `RateLimit` snapshot
|
|
22
|
+
* for the rolling `window` (seconds) and `max`. Shared by the memory backend
|
|
23
|
+
* (read-decide-write is atomic under single-threaded JS) and as the fallback
|
|
24
|
+
* for storages lacking an atomic primitive.
|
|
25
|
+
*/
|
|
26
|
+
function decideConsume(data, rule, now) {
|
|
27
|
+
const windowInMs = rule.window * 1e3;
|
|
28
|
+
if (!data) return {
|
|
29
|
+
next: {
|
|
30
|
+
key: "",
|
|
31
|
+
count: 1,
|
|
32
|
+
lastRequest: now
|
|
33
|
+
},
|
|
34
|
+
update: false,
|
|
35
|
+
allowed: true,
|
|
36
|
+
retryAfter: null
|
|
37
|
+
};
|
|
38
|
+
if (now - data.lastRequest > windowInMs) return {
|
|
39
|
+
next: {
|
|
40
|
+
...data,
|
|
41
|
+
count: 1,
|
|
42
|
+
lastRequest: now
|
|
43
|
+
},
|
|
44
|
+
update: true,
|
|
45
|
+
allowed: true,
|
|
46
|
+
retryAfter: null
|
|
47
|
+
};
|
|
48
|
+
if (data.count >= rule.max) return {
|
|
49
|
+
next: data,
|
|
50
|
+
update: true,
|
|
51
|
+
allowed: false,
|
|
52
|
+
retryAfter: getRetryAfter(data.lastRequest, rule.window)
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
next: {
|
|
56
|
+
...data,
|
|
57
|
+
count: data.count + 1,
|
|
58
|
+
lastRequest: now
|
|
59
|
+
},
|
|
60
|
+
update: true,
|
|
61
|
+
allowed: true,
|
|
62
|
+
retryAfter: null
|
|
63
|
+
};
|
|
12
64
|
}
|
|
13
65
|
function rateLimitResponse(retryAfter) {
|
|
14
66
|
return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), {
|
|
@@ -25,18 +77,109 @@ function getRetryAfter(lastRequest, window) {
|
|
|
25
77
|
function createDatabaseStorageWrapper(ctx) {
|
|
26
78
|
const model = "rateLimit";
|
|
27
79
|
const db = ctx.adapter;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
const readRow = async (key) => {
|
|
81
|
+
const data = (await db.findMany({
|
|
82
|
+
model,
|
|
83
|
+
where: [{
|
|
84
|
+
field: "key",
|
|
85
|
+
value: key
|
|
86
|
+
}]
|
|
87
|
+
}))[0];
|
|
88
|
+
if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest);
|
|
89
|
+
return data;
|
|
90
|
+
};
|
|
91
|
+
const consume = async (key, rule) => {
|
|
92
|
+
const windowInMs = rule.window * 1e3;
|
|
93
|
+
const data = await readRow(key);
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (!data) try {
|
|
96
|
+
await db.create({
|
|
97
|
+
model,
|
|
98
|
+
data: {
|
|
99
|
+
key,
|
|
100
|
+
count: 1,
|
|
101
|
+
lastRequest: now
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
allowed: true,
|
|
106
|
+
retryAfter: null
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!await readRow(key)) throw error;
|
|
110
|
+
return consume(key, rule);
|
|
111
|
+
}
|
|
112
|
+
if (now - data.lastRequest > windowInMs) {
|
|
113
|
+
if (await db.incrementOne({
|
|
31
114
|
model,
|
|
32
115
|
where: [{
|
|
33
116
|
field: "key",
|
|
34
117
|
value: key
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
118
|
+
}, {
|
|
119
|
+
field: "lastRequest",
|
|
120
|
+
operator: "lte",
|
|
121
|
+
value: data.lastRequest
|
|
122
|
+
}],
|
|
123
|
+
increment: {},
|
|
124
|
+
set: {
|
|
125
|
+
count: 1,
|
|
126
|
+
lastRequest: now
|
|
127
|
+
}
|
|
128
|
+
})) {
|
|
129
|
+
deleteExpiredRows(now);
|
|
130
|
+
return {
|
|
131
|
+
allowed: true,
|
|
132
|
+
retryAfter: null
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return consume(key, rule);
|
|
136
|
+
}
|
|
137
|
+
const windowStart = now - windowInMs;
|
|
138
|
+
if (await db.incrementOne({
|
|
139
|
+
model,
|
|
140
|
+
where: [
|
|
141
|
+
{
|
|
142
|
+
field: "key",
|
|
143
|
+
value: key
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
field: "lastRequest",
|
|
147
|
+
operator: "gt",
|
|
148
|
+
value: windowStart
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
field: "count",
|
|
152
|
+
operator: "lt",
|
|
153
|
+
value: rule.max
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
increment: { count: 1 },
|
|
157
|
+
set: { lastRequest: now }
|
|
158
|
+
})) return {
|
|
159
|
+
allowed: true,
|
|
160
|
+
retryAfter: null
|
|
161
|
+
};
|
|
162
|
+
const fresh = await readRow(key);
|
|
163
|
+
if (!fresh) return consume(key, rule);
|
|
164
|
+
if (now - fresh.lastRequest > windowInMs) return consume(key, rule);
|
|
165
|
+
return {
|
|
166
|
+
allowed: false,
|
|
167
|
+
retryAfter: getRetryAfter(fresh.lastRequest, rule.window)
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const deleteExpiredRows = (now) => {
|
|
171
|
+
const cutoff = now - Math.max(ctx.rateLimit.window, ...getDefaultSpecialRules().map((r) => r.window)) * 1e3;
|
|
172
|
+
ctx.runInBackground(db.deleteMany({
|
|
173
|
+
model,
|
|
174
|
+
where: [{
|
|
175
|
+
field: "lastRequest",
|
|
176
|
+
operator: "lt",
|
|
177
|
+
value: cutoff
|
|
178
|
+
}]
|
|
179
|
+
}).then(() => void 0).catch((e) => ctx.logger.error("Error pruning rate limit rows", e)));
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
get: readRow,
|
|
40
183
|
set: async (key, value, _update) => {
|
|
41
184
|
try {
|
|
42
185
|
if (_update) await db.updateMany({
|
|
@@ -61,58 +204,88 @@ function createDatabaseStorageWrapper(ctx) {
|
|
|
61
204
|
} catch (e) {
|
|
62
205
|
ctx.logger.error("Error setting rate limit", e);
|
|
63
206
|
}
|
|
64
|
-
}
|
|
207
|
+
},
|
|
208
|
+
consume
|
|
65
209
|
};
|
|
66
210
|
}
|
|
67
211
|
function getRateLimitStorage(ctx, rateLimitSettings) {
|
|
68
212
|
if (ctx.options.rateLimit?.customStorage) return ctx.options.rateLimit.customStorage;
|
|
69
213
|
const storage = ctx.rateLimit.storage;
|
|
70
|
-
if (storage === "secondary-storage")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
214
|
+
if (storage === "secondary-storage") {
|
|
215
|
+
const ttlFor = (window) => window ?? ctx.options.rateLimit?.window ?? 10;
|
|
216
|
+
return {
|
|
217
|
+
get: async (key) => {
|
|
218
|
+
const data = await ctx.options.secondaryStorage?.get(key);
|
|
219
|
+
return data ? safeJSONParse(data) : null;
|
|
220
|
+
},
|
|
221
|
+
set: async (key, value, _update) => {
|
|
222
|
+
await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value), ttlFor(rateLimitSettings.window));
|
|
223
|
+
},
|
|
224
|
+
consume: ctx.options.secondaryStorage?.increment ? async (key, rule) => {
|
|
225
|
+
if (await ctx.options.secondaryStorage.increment(key, ttlFor(rule.window)) <= rule.max) return {
|
|
226
|
+
allowed: true,
|
|
227
|
+
retryAfter: null
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
allowed: false,
|
|
231
|
+
retryAfter: rule.window
|
|
232
|
+
};
|
|
233
|
+
} : void 0
|
|
234
|
+
};
|
|
235
|
+
} else if (storage === "memory") {
|
|
236
|
+
const ttlFor = (window) => window ?? ctx.options.rateLimit?.window ?? 10;
|
|
237
|
+
return {
|
|
238
|
+
async get(key) {
|
|
239
|
+
const entry = memory.get(key);
|
|
240
|
+
if (!entry) return null;
|
|
241
|
+
if (Date.now() >= entry.expiresAt) {
|
|
242
|
+
memory.delete(key);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return entry.data;
|
|
246
|
+
},
|
|
247
|
+
async set(key, value, _update) {
|
|
248
|
+
const expiresAt = Date.now() + ttlFor(rateLimitSettings.window) * 1e3;
|
|
249
|
+
memory.set(key, {
|
|
250
|
+
data: value,
|
|
251
|
+
expiresAt
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async consume(key, rule) {
|
|
255
|
+
pruneMemoryStore();
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const entry = memory.get(key);
|
|
258
|
+
const decision = decideConsume(entry && now < entry.expiresAt ? entry.data : void 0, rule, now);
|
|
259
|
+
if (decision.allowed) memory.set(key, {
|
|
260
|
+
data: {
|
|
261
|
+
...decision.next,
|
|
262
|
+
key
|
|
263
|
+
},
|
|
264
|
+
expiresAt: now + ttlFor(rule.window) * 1e3
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
allowed: decision.allowed,
|
|
268
|
+
retryAfter: decision.retryAfter
|
|
269
|
+
};
|
|
87
270
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async set(key, value, _update) {
|
|
91
|
-
const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
|
|
92
|
-
const expiresAt = Date.now() + ttl * 1e3;
|
|
93
|
-
memory.set(key, {
|
|
94
|
-
data: value,
|
|
95
|
-
expiresAt
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
};
|
|
271
|
+
};
|
|
272
|
+
}
|
|
99
273
|
return createDatabaseStorageWrapper(ctx);
|
|
100
274
|
}
|
|
101
275
|
let ipWarningLogged = false;
|
|
276
|
+
const NO_TRUSTED_IP_KEY = "no-trusted-ip";
|
|
102
277
|
async function resolveRateLimitConfig(req, ctx) {
|
|
103
278
|
const basePath = new URL(ctx.baseURL).pathname;
|
|
104
279
|
const path = normalizePathname(req.url, basePath);
|
|
105
280
|
let currentWindow = ctx.rateLimit.window;
|
|
106
281
|
let currentMax = ctx.rateLimit.max;
|
|
107
282
|
const ip = getIp(req, ctx.options);
|
|
108
|
-
if (!ip)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
283
|
+
if (!ip && ctx.options.advanced?.ipAddress?.disableIpTracking) return null;
|
|
284
|
+
if (!ip && !ipWarningLogged) {
|
|
285
|
+
ctx.logger.warn("Rate limiting could not determine a client IP and is falling back to a single shared per-path bucket. Ensure your runtime forwards a trusted client IP header and configure `advanced.ipAddress.ipAddressHeaders` if needed.");
|
|
286
|
+
ipWarningLogged = true;
|
|
114
287
|
}
|
|
115
|
-
const key = createRateLimitKey(ip, path);
|
|
288
|
+
const key = createRateLimitKey(ip ?? NO_TRUSTED_IP_KEY, path);
|
|
116
289
|
const specialRule = getDefaultSpecialRules().find((rule) => rule.pathMatcher(path));
|
|
117
290
|
if (specialRule) {
|
|
118
291
|
currentWindow = specialRule.window;
|
|
@@ -150,37 +323,50 @@ async function resolveRateLimitConfig(req, ctx) {
|
|
|
150
323
|
currentMax
|
|
151
324
|
};
|
|
152
325
|
}
|
|
326
|
+
let legacyFallbackWarningLogged = false;
|
|
327
|
+
/**
|
|
328
|
+
* Decides the rate limit for the request in a single atomic step. The whole
|
|
329
|
+
* check-and-increment happens here in the request phase; there is no separate
|
|
330
|
+
* response-phase write-back, so concurrent requests cannot all pass a stale
|
|
331
|
+
* read before any increment lands.
|
|
332
|
+
*/
|
|
153
333
|
async function onRequestRateLimit(req, ctx) {
|
|
154
334
|
if (!ctx.rateLimit.enabled) return;
|
|
155
335
|
const config = await resolveRateLimitConfig(req, ctx);
|
|
156
336
|
if (!config) return;
|
|
157
337
|
const { key, currentWindow, currentMax } = config;
|
|
158
|
-
const data = await getRateLimitStorage(ctx, { window: currentWindow }).get(key);
|
|
159
|
-
if (data && shouldRateLimit(currentMax, currentWindow, data)) return rateLimitResponse(getRetryAfter(data.lastRequest, currentWindow));
|
|
160
|
-
}
|
|
161
|
-
async function onResponseRateLimit(req, ctx) {
|
|
162
|
-
if (!ctx.rateLimit.enabled) return;
|
|
163
|
-
const config = await resolveRateLimitConfig(req, ctx);
|
|
164
|
-
if (!config) return;
|
|
165
|
-
const { key, currentWindow } = config;
|
|
166
338
|
const storage = getRateLimitStorage(ctx, { window: currentWindow });
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
339
|
+
const rule = {
|
|
340
|
+
window: currentWindow,
|
|
341
|
+
max: currentMax
|
|
342
|
+
};
|
|
343
|
+
if (storage.consume) {
|
|
344
|
+
const { allowed, retryAfter } = await storage.consume(key, rule);
|
|
345
|
+
if (!allowed) return rateLimitResponse(retryAfter ?? currentWindow);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
return legacyConsume(ctx, storage, key, rule);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Non-atomic check-then-increment for storages that do not implement `consume`
|
|
352
|
+
* (custom storages, or secondary storages without `increment`). Under
|
|
353
|
+
* concurrency this is best-effort: simultaneous requests can each pass the
|
|
354
|
+
* check before either write lands.
|
|
355
|
+
*
|
|
356
|
+
* FIXME(rate-limit-consume-required): remove on `next` once `consume` is the
|
|
357
|
+
* sole required member of the storage contract.
|
|
358
|
+
*/
|
|
359
|
+
async function legacyConsume(ctx, storage, key, rule) {
|
|
360
|
+
if (!legacyFallbackWarningLogged) {
|
|
361
|
+
ctx.logger.warn("Rate limiting is best-effort: the configured storage has no atomic `consume`, so concurrent requests may bypass the limit. Provide a storage that implements `consume` for strict enforcement.");
|
|
362
|
+
legacyFallbackWarningLogged = true;
|
|
363
|
+
}
|
|
364
|
+
const decision = decideConsume(await storage.get(key), rule, Date.now());
|
|
365
|
+
if (!decision.allowed) return rateLimitResponse(decision.retryAfter ?? rule.window);
|
|
366
|
+
await storage.set(key, {
|
|
367
|
+
...decision.next,
|
|
368
|
+
key
|
|
369
|
+
}, decision.update);
|
|
184
370
|
}
|
|
185
371
|
function getDefaultSpecialRules() {
|
|
186
372
|
return [{
|
|
@@ -198,4 +384,4 @@ function getDefaultSpecialRules() {
|
|
|
198
384
|
}];
|
|
199
385
|
}
|
|
200
386
|
//#endregion
|
|
201
|
-
export { onRequestRateLimit
|
|
387
|
+
export { onRequestRateLimit };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
|
|
1
2
|
import { parseAccountOutput } from "../../db/schema.mjs";
|
|
2
3
|
import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
|
|
3
4
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
@@ -5,7 +6,7 @@ import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
|
5
6
|
import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils.mjs";
|
|
6
7
|
import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
|
|
7
8
|
import { generateState } from "../../oauth2/state.mjs";
|
|
8
|
-
import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "./session.mjs";
|
|
9
|
+
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
11
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
11
12
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -121,7 +122,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
121
122
|
}
|
|
122
123
|
const { token, nonce } = c.body.idToken;
|
|
123
124
|
if (!await provider.verifyIdToken(token, nonce)) {
|
|
124
|
-
c.context.logger.
|
|
125
|
+
c.context.logger.warn("Invalid id token", { provider: c.body.provider });
|
|
125
126
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
126
127
|
}
|
|
127
128
|
const linkingUserInfo = await provider.getUserInfo({
|
|
@@ -233,7 +234,7 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
|
|
|
233
234
|
* so the cache is left in place for them.
|
|
234
235
|
*/
|
|
235
236
|
async function resolveUserId(ctx, userId) {
|
|
236
|
-
const session = await getSessionFromCtx(ctx, { disableCookieCache:
|
|
237
|
+
const session = await getSessionFromCtx(ctx, { disableCookieCache: isStateful(ctx) });
|
|
237
238
|
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
|
|
238
239
|
const resolvedUserId = session?.user?.id || userId;
|
|
239
240
|
if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
|
|
@@ -242,6 +243,9 @@ async function resolveUserId(ctx, userId) {
|
|
|
242
243
|
});
|
|
243
244
|
return resolvedUserId;
|
|
244
245
|
}
|
|
246
|
+
function matchesAccountSelection(ctx, account, { resolvedUserId, providerId, accountId }) {
|
|
247
|
+
return (!shouldBindAccountCookieToSessionUser(ctx.context.options) || account.userId === resolvedUserId) && (!providerId || providerId === account.providerId) && (!accountId || account.accountId === accountId);
|
|
248
|
+
}
|
|
245
249
|
/**
|
|
246
250
|
* Fetches a currently-valid access token for a user's provider account,
|
|
247
251
|
* refreshing and persisting it when it is within five seconds of expiry.
|
|
@@ -257,7 +261,11 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
|
|
|
257
261
|
let account = resolvedAccount;
|
|
258
262
|
if (!account) {
|
|
259
263
|
const accountData = await getAccountCookie(ctx);
|
|
260
|
-
if (accountData &&
|
|
264
|
+
if (accountData && matchesAccountSelection(ctx, accountData, {
|
|
265
|
+
resolvedUserId,
|
|
266
|
+
providerId,
|
|
267
|
+
accountId
|
|
268
|
+
})) account = accountData;
|
|
261
269
|
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
262
270
|
}
|
|
263
271
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
@@ -388,7 +396,11 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
388
396
|
});
|
|
389
397
|
let account = void 0;
|
|
390
398
|
const accountData = await getAccountCookie(ctx);
|
|
391
|
-
const usedAccountCookie = !!accountData &&
|
|
399
|
+
const usedAccountCookie = !!accountData && matchesAccountSelection(ctx, accountData, {
|
|
400
|
+
resolvedUserId,
|
|
401
|
+
providerId,
|
|
402
|
+
accountId
|
|
403
|
+
});
|
|
392
404
|
if (usedAccountCookie) account = accountData;
|
|
393
405
|
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
394
406
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
@@ -484,7 +496,10 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
484
496
|
if (!providedAccountId) {
|
|
485
497
|
if (ctx.context.options.account?.storeAccountCookie) {
|
|
486
498
|
const accountData = await getAccountCookie(ctx);
|
|
487
|
-
if (accountData
|
|
499
|
+
if (accountData && matchesAccountSelection(ctx, accountData, {
|
|
500
|
+
resolvedUserId,
|
|
501
|
+
providerId: providedProviderId
|
|
502
|
+
})) account = accountData;
|
|
488
503
|
}
|
|
489
504
|
} else {
|
|
490
505
|
const matchingAccounts = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).filter((acc) => acc.accountId === providedAccountId && (!providedProviderId || acc.providerId === providedProviderId));
|
|
@@ -494,7 +509,7 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
494
509
|
});
|
|
495
510
|
account = matchingAccounts[0];
|
|
496
511
|
}
|
|
497
|
-
if (!account || account
|
|
512
|
+
if (!account || !matchesAccountSelection(ctx, account, { resolvedUserId })) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
498
513
|
const provider = await getAwaitableValue(ctx.context.socialProviders, { value: account.providerId });
|
|
499
514
|
if (!provider) throw APIError.from("BAD_REQUEST", {
|
|
500
515
|
message: "Account is not associated with a configured social provider.",
|
|
@@ -55,12 +55,12 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
55
55
|
const resolvedErrorURL = errorURL ?? defaultErrorURL;
|
|
56
56
|
if (error) redirectOnError(c, resolvedErrorURL, error, error_description);
|
|
57
57
|
if (!code) {
|
|
58
|
-
c.context.logger.
|
|
58
|
+
c.context.logger.warn("Code not found");
|
|
59
59
|
redirectOnError(c, resolvedErrorURL, "no_code");
|
|
60
60
|
}
|
|
61
61
|
const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
|
|
62
62
|
if (!provider) {
|
|
63
|
-
c.context.logger.
|
|
63
|
+
c.context.logger.warn("OAuth provider not found", { providerId: c.params.id });
|
|
64
64
|
redirectOnError(c, resolvedErrorURL, "oauth_provider_not_found");
|
|
65
65
|
}
|
|
66
66
|
let tokens;
|
|
@@ -4,7 +4,7 @@ import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEm
|
|
|
4
4
|
import { error } from "./error.mjs";
|
|
5
5
|
import { ok } from "./ok.mjs";
|
|
6
6
|
import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./password.mjs";
|
|
7
|
-
import { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
7
|
+
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
8
8
|
import { signInEmail, signInSocial } from "./sign-in.mjs";
|
|
9
9
|
import { signOut } from "./sign-out.mjs";
|
|
10
10
|
import { signUpEmail } from "./sign-up.mjs";
|
|
@@ -55,7 +55,7 @@ const requestPasswordReset = createAuthEndpoint("/request-password-reset", {
|
|
|
55
55
|
*/
|
|
56
56
|
generateId(24);
|
|
57
57
|
await ctx.context.internalAdapter.findVerificationValue("dummy-verification-token");
|
|
58
|
-
ctx.context.logger.
|
|
58
|
+
ctx.context.logger.warn("Reset Password: User not found");
|
|
59
59
|
return ctx.json({
|
|
60
60
|
status: true,
|
|
61
61
|
message: "If this email exists in our system, check your email for the reset link"
|
|
@@ -145,8 +145,8 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
145
145
|
if (newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
146
146
|
if (newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
147
147
|
const id = `reset-password:${token}`;
|
|
148
|
-
const verification = await ctx.context.internalAdapter.
|
|
149
|
-
if (!verification
|
|
148
|
+
const verification = await ctx.context.internalAdapter.consumeVerificationValue(id);
|
|
149
|
+
if (!verification) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
150
150
|
const userId = verification.value;
|
|
151
151
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
152
152
|
if (!(await ctx.context.internalAdapter.findAccounts(userId)).find((ac) => ac.providerId === "credential")) await ctx.context.internalAdapter.createAccount({
|
|
@@ -156,7 +156,6 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
156
156
|
accountId: userId
|
|
157
157
|
});
|
|
158
158
|
else await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
159
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(id);
|
|
160
159
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) {
|
|
161
160
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
162
161
|
if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
@@ -45,6 +45,17 @@ declare const getSession: <Option extends BetterAuthOptions>() => better_call0.S
|
|
|
45
45
|
session: Session$1<Option["session"], Option["plugins"]>;
|
|
46
46
|
user: User$1<Option["user"], Option["plugins"]>;
|
|
47
47
|
} | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the deployment keeps sessions in a durable server-side store
|
|
50
|
+
* (a database or secondary storage) rather than only in the signed cookie.
|
|
51
|
+
*
|
|
52
|
+
* Sensitive operations use this to decide whether the cookie cache is merely an
|
|
53
|
+
* optimization that must be bypassed for an authoritative read (`true`), or the
|
|
54
|
+
* only place the session lives and therefore the authority itself (`false`, for
|
|
55
|
+
* stateless / DB-less deployments). Pass the result as `disableCookieCache` so a
|
|
56
|
+
* revoked-but-cached session cannot authorize a sensitive action.
|
|
57
|
+
*/
|
|
58
|
+
declare const isStateful: (ctx: GenericEndpointContext) => boolean;
|
|
48
59
|
declare const getSessionFromCtx: <U extends Record<string, any> = Record<string, any>, S extends Record<string, any> = Record<string, any>>(ctx: GenericEndpointContext, config?: {
|
|
49
60
|
disableCookieCache?: boolean;
|
|
50
61
|
disableRefresh?: boolean;
|
|
@@ -409,4 +420,4 @@ declare const revokeOtherSessions: better_call0.StrictEndpoint<"/revoke-other-se
|
|
|
409
420
|
status: boolean;
|
|
410
421
|
}>;
|
|
411
422
|
//#endregion
|
|
412
|
-
export { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
|
|
423
|
+
export { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
|
+
import { hasServerSessionStore } from "../../context/store-capabilities.mjs";
|
|
2
3
|
import { symmetricDecodeJWT, verifyJWT } from "../../crypto/jwt.mjs";
|
|
3
4
|
import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
|
|
4
5
|
import { getDate } from "../../utils/date.mjs";
|
|
@@ -265,6 +266,17 @@ const getSession = () => createAuthEndpoint("/get-session", {
|
|
|
265
266
|
throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
266
267
|
}
|
|
267
268
|
});
|
|
269
|
+
/**
|
|
270
|
+
* Whether the deployment keeps sessions in a durable server-side store
|
|
271
|
+
* (a database or secondary storage) rather than only in the signed cookie.
|
|
272
|
+
*
|
|
273
|
+
* Sensitive operations use this to decide whether the cookie cache is merely an
|
|
274
|
+
* optimization that must be bypassed for an authoritative read (`true`), or the
|
|
275
|
+
* only place the session lives and therefore the authority itself (`false`, for
|
|
276
|
+
* stateless / DB-less deployments). Pass the result as `disableCookieCache` so a
|
|
277
|
+
* revoked-but-cached session cannot authorize a sensitive action.
|
|
278
|
+
*/
|
|
279
|
+
const isStateful = (ctx) => hasServerSessionStore(ctx.context.options);
|
|
268
280
|
const getSessionFromCtx = async (ctx, config) => {
|
|
269
281
|
if (ctx.context.session) return ctx.context.session;
|
|
270
282
|
const session = await getSession()({
|
|
@@ -488,4 +500,4 @@ const revokeOtherSessions = createAuthEndpoint("/revoke-other-sessions", {
|
|
|
488
500
|
return ctx.json({ status: true });
|
|
489
501
|
});
|
|
490
502
|
//#endregion
|
|
491
|
-
export { freshSessionMiddleware, getSession, getSessionFromCtx, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
|
|
503
|
+
export { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
|