better-auth 1.6.11 → 1.6.13
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 +12 -48
- package/dist/api/routes/account.d.mts +2 -23
- package/dist/api/routes/account.mjs +94 -73
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +39 -42
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.mjs +15 -10
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +3 -2
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +7 -7
- package/dist/client/fetch-plugins.mjs +2 -1
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/create-context.mjs +10 -14
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +56 -50
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +3 -3
- package/dist/oauth2/link-account.d.mts +27 -1
- package/dist/oauth2/link-account.mjs +27 -4
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +11 -6
- package/dist/plugins/admin/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- package/dist/plugins/admin/routes.mjs +3 -3
- package/dist/plugins/anonymous/index.mjs +2 -2
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +37 -34
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.mjs +0 -1
- package/dist/plugins/mcp/index.mjs +2 -5
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +45 -32
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +2 -5
- package/dist/plugins/one-tap/client.mjs +9 -2
- package/dist/plugins/one-tap/index.mjs +16 -39
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +2 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
- package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
- package/dist/plugins/organization/routes/crud-org.mjs +2 -2
- package/dist/plugins/organization/types.d.mts +3 -3
- package/dist/plugins/phone-number/routes.mjs +1 -1
- package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
- package/dist/plugins/two-factor/client.mjs +2 -1
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +36 -144
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
|
@@ -129,12 +129,8 @@ const getSession = () => createAuthEndpoint("/get-session", {
|
|
|
129
129
|
const updateAge = cookieRefreshCache.updateAge * 1e3;
|
|
130
130
|
const shouldSkipSessionRefresh = await getShouldSkipSessionRefresh();
|
|
131
131
|
if (timeUntilExpiry < updateAge && !shouldSkipSessionRefresh) {
|
|
132
|
-
const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec");
|
|
133
132
|
const refreshedSession = {
|
|
134
|
-
session: {
|
|
135
|
-
...session.session,
|
|
136
|
-
expiresAt: newExpiresAt
|
|
137
|
-
},
|
|
133
|
+
session: { ...session.session },
|
|
138
134
|
user: session.user,
|
|
139
135
|
updatedAt: Date.now()
|
|
140
136
|
};
|
|
@@ -276,17 +272,26 @@ const getSessionFromCtx = async (ctx, config) => {
|
|
|
276
272
|
method: "GET",
|
|
277
273
|
asResponse: false,
|
|
278
274
|
headers: ctx.headers,
|
|
279
|
-
returnHeaders:
|
|
275
|
+
returnHeaders: true,
|
|
280
276
|
returnStatus: false,
|
|
281
277
|
query: {
|
|
282
278
|
...config,
|
|
283
279
|
...ctx.query
|
|
284
280
|
}
|
|
285
|
-
}).catch((
|
|
281
|
+
}).catch(() => {
|
|
286
282
|
return null;
|
|
287
283
|
});
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
if (!session) {
|
|
285
|
+
ctx.context.session = null;
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
if (session.headers) session.headers.forEach((value, key) => {
|
|
289
|
+
if (!ctx.context.responseHeaders) ctx.context.responseHeaders = new Headers({ [key]: value });
|
|
290
|
+
else if (key.toLowerCase() === "set-cookie") ctx.context.responseHeaders.append(key, value);
|
|
291
|
+
else ctx.context.responseHeaders.set(key, value);
|
|
292
|
+
});
|
|
293
|
+
ctx.context.session = session.response;
|
|
294
|
+
return session.response;
|
|
290
295
|
};
|
|
291
296
|
/**
|
|
292
297
|
* The middleware forces the endpoint to require a valid session.
|
|
@@ -440,7 +445,7 @@ const revokeSessions = createAuthEndpoint("/revoke-sessions", {
|
|
|
440
445
|
} }
|
|
441
446
|
}, async (ctx) => {
|
|
442
447
|
try {
|
|
443
|
-
await ctx.context.internalAdapter.
|
|
448
|
+
await ctx.context.internalAdapter.deleteUserSessions(ctx.context.session.user.id);
|
|
444
449
|
} catch (error) {
|
|
445
450
|
ctx.context.logger.error(error && typeof error === "object" && "name" in error ? error.name : "", error);
|
|
446
451
|
throw APIError.from("INTERNAL_SERVER_ERROR", {
|
|
@@ -114,6 +114,7 @@ declare const signInEmail: <O extends BetterAuthOptions>() => better_call0.Stric
|
|
|
114
114
|
method: "POST";
|
|
115
115
|
operationId: string;
|
|
116
116
|
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
|
|
117
|
+
cloneRequest: true;
|
|
117
118
|
body: z.ZodObject<{
|
|
118
119
|
email: z.ZodString;
|
|
119
120
|
password: z.ZodString;
|
|
@@ -3,8 +3,8 @@ import { parseUserOutput } from "../../db/schema.mjs";
|
|
|
3
3
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
4
4
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
5
5
|
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
6
|
-
import { generateState } from "../../oauth2/state.mjs";
|
|
7
6
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
|
+
import { generateState } from "../../oauth2/state.mjs";
|
|
8
8
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
9
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
10
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -144,6 +144,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
144
144
|
method: "POST",
|
|
145
145
|
operationId: "signInEmail",
|
|
146
146
|
use: [formCsrfMiddleware],
|
|
147
|
+
cloneRequest: true,
|
|
147
148
|
body: z.object({
|
|
148
149
|
email: z.string().meta({ description: "Email of the user" }),
|
|
149
150
|
password: z.string().meta({ description: "Password of the user" }),
|
|
@@ -236,7 +237,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
236
237
|
user: user.user,
|
|
237
238
|
url,
|
|
238
239
|
token
|
|
239
|
-
}, ctx.request));
|
|
240
|
+
}, ctx.request?.clone()));
|
|
240
241
|
}
|
|
241
242
|
throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
|
|
242
243
|
}
|
|
@@ -16,6 +16,7 @@ declare const signUpEmail: <O extends BetterAuthOptions>() => better_call0.Stric
|
|
|
16
16
|
callbackURL: z.ZodOptional<z.ZodString>;
|
|
17
17
|
rememberMe: z.ZodOptional<z.ZodBoolean>;
|
|
18
18
|
}, z.core.$strip>, z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
19
|
+
cloneRequest: true;
|
|
19
20
|
metadata: {
|
|
20
21
|
allowedMediaTypes: string[];
|
|
21
22
|
$Infer: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
2
|
import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
|
|
3
|
-
import { parseUserInput, parseUserOutput } from "../../db/schema.mjs";
|
|
3
|
+
import { buildSyntheticUserOutput, parseUserInput, parseUserOutput } from "../../db/schema.mjs";
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
6
6
|
import { runWithTransaction } from "@better-auth/core/context";
|
|
@@ -23,6 +23,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
23
23
|
operationId: "signUpWithEmailAndPassword",
|
|
24
24
|
use: [formCsrfMiddleware],
|
|
25
25
|
body: signUpEmailBodySchema,
|
|
26
|
+
cloneRequest: true,
|
|
26
27
|
metadata: {
|
|
27
28
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
28
29
|
$Infer: {
|
|
@@ -170,14 +171,14 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
170
171
|
* between existing and non-existing emails.
|
|
171
172
|
*/
|
|
172
173
|
await ctx.context.password.hash(password);
|
|
173
|
-
if (ctx.context.options.emailAndPassword?.onExistingUserSignUp) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailAndPassword.onExistingUserSignUp({ user: dbUser.user }, ctx.request));
|
|
174
|
+
if (ctx.context.options.emailAndPassword?.onExistingUserSignUp) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailAndPassword.onExistingUserSignUp({ user: dbUser.user }, ctx.request?.clone()));
|
|
174
175
|
const now = /* @__PURE__ */ new Date();
|
|
175
176
|
const generatedId = ctx.context.generateId({ model: "user" }) || generateId();
|
|
176
177
|
const coreFields = {
|
|
177
178
|
name,
|
|
178
179
|
email: normalizedEmail,
|
|
179
180
|
emailVerified: false,
|
|
180
|
-
image: image
|
|
181
|
+
image: image ?? null,
|
|
181
182
|
createdAt: now,
|
|
182
183
|
updatedAt: now
|
|
183
184
|
};
|
|
@@ -187,16 +188,17 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
187
188
|
const additionalFieldKeys = Object.keys(ctx.context.options.user?.additionalFields ?? {});
|
|
188
189
|
const additionalFields = {};
|
|
189
190
|
for (const key of additionalFieldKeys) if (key in additionalUserFields) additionalFields[key] = additionalUserFields[key];
|
|
190
|
-
|
|
191
|
+
const customResult = customSyntheticUser({
|
|
191
192
|
coreFields,
|
|
192
193
|
additionalFields,
|
|
193
194
|
id: generatedId
|
|
194
195
|
});
|
|
195
|
-
|
|
196
|
+
syntheticUser = buildSyntheticUserOutput(ctx.context.options, customResult);
|
|
197
|
+
} else syntheticUser = buildSyntheticUserOutput(ctx.context.options, {
|
|
196
198
|
...coreFields,
|
|
197
199
|
...additionalUserFields,
|
|
198
200
|
id: generatedId
|
|
199
|
-
};
|
|
201
|
+
});
|
|
200
202
|
return ctx.json({
|
|
201
203
|
token: null,
|
|
202
204
|
user: parseUserOutput(ctx.context.options, syntheticUser)
|
|
@@ -244,7 +246,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
244
246
|
user: createdUser,
|
|
245
247
|
url,
|
|
246
248
|
token
|
|
247
|
-
}, ctx.request));
|
|
249
|
+
}, ctx.request?.clone()));
|
|
248
250
|
}
|
|
249
251
|
if (shouldSkipAutoSignIn) return ctx.json({
|
|
250
252
|
token: null,
|
|
@@ -168,7 +168,7 @@ const changePassword = createAuthEndpoint("/change-password", {
|
|
|
168
168
|
await ctx.context.internalAdapter.updateAccount(account.id, { password: passwordHash });
|
|
169
169
|
let token = null;
|
|
170
170
|
if (revokeOtherSessions) {
|
|
171
|
-
await ctx.context.internalAdapter.
|
|
171
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
172
172
|
const newSession = await ctx.context.internalAdapter.createSession(session.user.id);
|
|
173
173
|
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
174
174
|
await setSessionCookie(ctx, {
|
|
@@ -309,7 +309,7 @@ const deleteUser = createAuthEndpoint("/delete-user", {
|
|
|
309
309
|
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
310
310
|
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
311
311
|
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
312
|
-
await ctx.context.internalAdapter.
|
|
312
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
313
313
|
deleteSessionCookie(ctx);
|
|
314
314
|
const afterDelete = ctx.context.options.user.deleteUser?.afterDelete;
|
|
315
315
|
if (afterDelete) await afterDelete(session.user, ctx.request);
|
|
@@ -362,7 +362,7 @@ const deleteUserCallback = createAuthEndpoint("/delete-user/callback", {
|
|
|
362
362
|
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
363
363
|
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
364
364
|
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
365
|
-
await ctx.context.internalAdapter.
|
|
365
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
366
366
|
await ctx.context.internalAdapter.deleteAccounts(session.user.id);
|
|
367
367
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`delete-account-${ctx.query.token}`);
|
|
368
368
|
deleteSessionCookie(ctx);
|
|
@@ -424,8 +424,8 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
424
424
|
* email would later throw 400, leaking email existence.
|
|
425
425
|
*/
|
|
426
426
|
const canUpdateWithoutVerification = ctx.context.session.user.emailVerified !== true && ctx.context.options.user.changeEmail.updateEmailWithoutVerification;
|
|
427
|
-
const canSendConfirmation = ctx.context.session.user.emailVerified && ctx.context.options.user.changeEmail.sendChangeEmailConfirmation;
|
|
428
427
|
const canSendVerification = ctx.context.options.emailVerification?.sendVerificationEmail;
|
|
428
|
+
const canSendConfirmation = canSendVerification && ctx.context.session.user.emailVerified && ctx.context.options.user.changeEmail.sendChangeEmailConfirmation;
|
|
429
429
|
if (!canUpdateWithoutVerification && !canSendConfirmation && !canSendVerification) {
|
|
430
430
|
ctx.context.logger.error("Verification email isn't enabled.");
|
|
431
431
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
@@ -449,7 +449,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
449
449
|
});
|
|
450
450
|
if (canSendVerification) {
|
|
451
451
|
const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
452
|
-
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
452
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
453
453
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
454
454
|
user: {
|
|
455
455
|
...ctx.context.session.user,
|
|
@@ -466,7 +466,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
466
466
|
*/
|
|
467
467
|
if (canSendConfirmation) {
|
|
468
468
|
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-confirmation" });
|
|
469
|
-
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
469
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
470
470
|
await ctx.context.runInBackgroundOrAwait(canSendConfirmation({
|
|
471
471
|
user: ctx.context.session.user,
|
|
472
472
|
newEmail,
|
|
@@ -480,7 +480,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
480
480
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
481
481
|
}
|
|
482
482
|
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-verification" });
|
|
483
|
-
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
483
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
484
484
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
485
485
|
user: {
|
|
486
486
|
...ctx.context.session.user,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
1
2
|
//#region src/client/fetch-plugins.ts
|
|
2
3
|
const redirectPlugin = {
|
|
3
4
|
id: "redirect",
|
|
4
5
|
name: "Redirect",
|
|
5
6
|
hooks: { onSuccess(context) {
|
|
6
|
-
if (context.data?.url && context.data?.redirect) {
|
|
7
|
+
if (context.data?.url && context.data?.redirect && isSafeUrlScheme(context.data.url)) {
|
|
7
8
|
if (typeof window !== "undefined" && window.location) {
|
|
8
9
|
if (window.location) try {
|
|
9
10
|
window.location.href = context.data.url;
|
package/dist/client/parser.mjs
CHANGED
|
@@ -34,7 +34,6 @@ function betterJSONParse(value, options = {}) {
|
|
|
34
34
|
const { strict = false, warnings = false, reviver, parseDates = true } = options;
|
|
35
35
|
if (typeof value !== "string") return value;
|
|
36
36
|
const trimmed = value.trim();
|
|
37
|
-
if (trimmed.length > 0 && trimmed[0] === "\"" && trimmed.endsWith("\"") && !trimmed.slice(1, -1).includes("\"")) return trimmed.slice(1, -1);
|
|
38
37
|
const lowerValue = trimmed.toLowerCase();
|
|
39
38
|
if (lowerValue.length <= 9 && lowerValue in SPECIAL_VALUES) return SPECIAL_VALUES[lowerValue];
|
|
40
39
|
if (!JSON_SIGNATURE.test(trimmed)) {
|
|
@@ -31,7 +31,7 @@ import { USERNAME_ERROR_CODES } from "../../plugins/username/error-codes.mjs";
|
|
|
31
31
|
import { ORGANIZATION_ERROR_CODES } from "../../plugins/organization/error-codes.mjs";
|
|
32
32
|
import { inferAdditionalFields } from "../../plugins/additional-fields/client.mjs";
|
|
33
33
|
import { ADMIN_ERROR_CODES } from "../../plugins/admin/error-codes.mjs";
|
|
34
|
-
import { adminClient } from "../../plugins/admin/client.mjs";
|
|
34
|
+
import { AdminClientOptions, adminClient } from "../../plugins/admin/client.mjs";
|
|
35
35
|
import { ANONYMOUS_ERROR_CODES } from "../../plugins/anonymous/error-codes.mjs";
|
|
36
36
|
import { anonymousClient } from "../../plugins/anonymous/client.mjs";
|
|
37
37
|
import { customSessionClient } from "../../plugins/custom-session/client.mjs";
|
|
@@ -47,10 +47,10 @@ import { multiSessionClient } from "../../plugins/multi-session/client.mjs";
|
|
|
47
47
|
import { OidcClientPlugin, oidcClient } from "../../plugins/oidc-provider/client.mjs";
|
|
48
48
|
import { GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, oneTapClient } from "../../plugins/one-tap/client.mjs";
|
|
49
49
|
import { oneTimeTokenClient } from "../../plugins/one-time-token/client.mjs";
|
|
50
|
-
import { clientSideHasPermission, inferOrgAdditionalFields, organizationClient } from "../../plugins/organization/client.mjs";
|
|
50
|
+
import { OrganizationClientOptions, clientSideHasPermission, inferOrgAdditionalFields, organizationClient } from "../../plugins/organization/client.mjs";
|
|
51
51
|
import { PHONE_NUMBER_ERROR_CODES } from "../../plugins/phone-number/error-codes.mjs";
|
|
52
52
|
import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
|
|
53
53
|
import { siweClient } from "../../plugins/siwe/client.mjs";
|
|
54
54
|
import { usernameClient } from "../../plugins/username/client.mjs";
|
|
55
55
|
import { InferServerPlugin } from "./infer-plugin.mjs";
|
|
56
|
-
export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, type FieldAttributeToObject, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, type RemoveFieldsWithReturnedFalse, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
|
|
56
|
+
export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminClientOptions, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, type FieldAttributeToObject, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationClientOptions, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, type RemoveFieldsWithReturnedFalse, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
|
package/dist/client/proxy.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAtom } from "../utils/is-atom.mjs";
|
|
2
|
+
import { toKebabCase } from "@better-auth/core/utils/string";
|
|
2
3
|
//#region src/client/proxy.ts
|
|
3
4
|
function getMethod(path, knownPathMethods, args) {
|
|
4
5
|
const method = knownPathMethods[path];
|
|
@@ -26,7 +27,7 @@ function createDynamicPathProxy(routes, client, knownPathMethods, atoms, atomLis
|
|
|
26
27
|
return createProxy(fullPath);
|
|
27
28
|
},
|
|
28
29
|
apply: async (_, __, args) => {
|
|
29
|
-
const routePath = "/" + path.map(
|
|
30
|
+
const routePath = "/" + path.map(toKebabCase).join("/");
|
|
30
31
|
const arg = args[0] || {};
|
|
31
32
|
const fetchOptions = args[1] || {};
|
|
32
33
|
const { query, fetchOptions: argFetchOptions, ...body } = arg;
|
|
@@ -42,18 +42,14 @@ function validateSecret(secret, logger) {
|
|
|
42
42
|
if (estimateEntropy(secret) < 120) logger.warn("[better-auth] Warning: your BETTER_AUTH_SECRET appears low-entropy. Use a randomly generated secret for production.");
|
|
43
43
|
}
|
|
44
44
|
async function createAuthContext(adapter, options, getDatabaseType) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
storeStateStrategy: "cookie",
|
|
54
|
-
storeAccountCookie: true
|
|
55
|
-
}
|
|
56
|
-
});
|
|
45
|
+
const isStateful = !!options.database || !!options.secondaryStorage;
|
|
46
|
+
if (!isStateful) options = defu$1(options, { session: { cookieCache: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
strategy: "jwe",
|
|
49
|
+
refreshCache: true,
|
|
50
|
+
maxAge: options.session?.expiresIn || 3600 * 24 * 7
|
|
51
|
+
} } });
|
|
52
|
+
if (!options.database) options = defu$1(options, { account: { storeAccountCookie: true } });
|
|
57
53
|
const plugins = options.plugins || [];
|
|
58
54
|
const internalPlugins = getInternalPlugins(options);
|
|
59
55
|
const logger = createLogger(options.logger);
|
|
@@ -130,7 +126,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
130
126
|
socialProviders: providers,
|
|
131
127
|
options,
|
|
132
128
|
oauthConfig: {
|
|
133
|
-
storeStateStrategy: options.account?.storeStateStrategy || (
|
|
129
|
+
storeStateStrategy: options.account?.storeStateStrategy || (isStateful ? "database" : "cookie"),
|
|
134
130
|
skipStateCookieCheck: !!options.account?.skipStateCookieCheck
|
|
135
131
|
},
|
|
136
132
|
tables,
|
|
@@ -146,7 +142,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
146
142
|
cookieRefreshCache: (() => {
|
|
147
143
|
const refreshCache = options.session?.cookieCache?.refreshCache;
|
|
148
144
|
const maxAge = options.session?.cookieCache?.maxAge || 300;
|
|
149
|
-
if (
|
|
145
|
+
if (isStateful && refreshCache) {
|
|
150
146
|
logger.warn("[better-auth] `session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured. `refreshCache` is meant for stateless (DB-less) setups. Disabling `refreshCache` — remove it from your config to silence this warning.");
|
|
151
147
|
return false;
|
|
152
148
|
}
|
package/dist/context/helpers.mjs
CHANGED
|
@@ -61,9 +61,10 @@ async function getTrustedOrigins(options, request) {
|
|
|
61
61
|
const trustedOrigins = [];
|
|
62
62
|
if (isDynamicBaseURLConfig(options.baseURL)) {
|
|
63
63
|
const allowedHosts = options.baseURL.allowedHosts;
|
|
64
|
+
const proto = options.baseURL.protocol;
|
|
64
65
|
for (const host of allowedHosts) if (!host.includes("://")) {
|
|
65
|
-
trustedOrigins.push(`https://${host}`);
|
|
66
|
-
if (isLoopbackHost(host)) trustedOrigins.push(`http://${host}`);
|
|
66
|
+
if (!proto || proto === "https" || proto === "auto") trustedOrigins.push(`https://${host}`);
|
|
67
|
+
if (proto === "http" || proto === "auto" || isLoopbackHost(host)) trustedOrigins.push(`http://${host}`);
|
|
67
68
|
} else trustedOrigins.push(host);
|
|
68
69
|
if (options.baseURL.fallback) try {
|
|
69
70
|
trustedOrigins.push(new URL(options.baseURL.fallback).origin);
|
|
@@ -33,6 +33,20 @@ declare function stripSecureCookiePrefix(cookieName: string): string;
|
|
|
33
33
|
declare function splitSetCookieHeader(setCookie: string): string[];
|
|
34
34
|
declare function parseSetCookieHeader(setCookie: string): Map<string, CookieAttributes>;
|
|
35
35
|
declare function toCookieOptions(attributes: CookieAttributes): ParsedCookieOptions;
|
|
36
|
+
/**
|
|
37
|
+
* Cookie-name token char set per RFC 7230 §3.2.6.
|
|
38
|
+
*
|
|
39
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
|
|
40
|
+
*/
|
|
41
|
+
declare const cookieNameRegex: RegExp;
|
|
42
|
+
/**
|
|
43
|
+
* Tolerates `;` separators without the SP that RFC 6265 §4.2.1 mandates,
|
|
44
|
+
* since proxies and runtimes commonly strip it. Silently drops entries
|
|
45
|
+
* whose name violates RFC 7230 token or whose value violates RFC 6265
|
|
46
|
+
* cookie-octet (plus space and comma). Strips optional surrounding
|
|
47
|
+
* double-quotes per RFC 6265 §4.1.1.
|
|
48
|
+
*/
|
|
49
|
+
declare function parseCookies(cookie: string): Map<string, string>;
|
|
36
50
|
/**
|
|
37
51
|
* Add or replace a cookie in the request `Cookie` header.
|
|
38
52
|
*
|
|
@@ -42,8 +56,17 @@ declare function toCookieOptions(attributes: CookieAttributes): ParsedCookieOpti
|
|
|
42
56
|
* parse-mutate-serialize.
|
|
43
57
|
*/
|
|
44
58
|
declare function setRequestCookie(headers: Headers, name: string, value: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Merge `Set-Cookie` header values into the target's `Cookie` header.
|
|
61
|
+
* Mutates `target`.
|
|
62
|
+
*
|
|
63
|
+
* Name/value-level merge only. RFC 6265 §5 user-agent semantics
|
|
64
|
+
* (expiration, domain/path scoping, ordering) are out of scope. Suitable
|
|
65
|
+
* for single-request proxy, middleware, and test contexts.
|
|
66
|
+
*/
|
|
67
|
+
declare function applySetCookies(target: Headers, setCookieValues: Iterable<string>): void;
|
|
45
68
|
declare function setCookieToHeader(headers: Headers): (context: {
|
|
46
69
|
response: Response;
|
|
47
70
|
}) => void;
|
|
48
71
|
//#endregion
|
|
49
|
-
export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
72
|
+
export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/cookies/cookie-utils.ts
|
|
2
2
|
function tryDecode(str) {
|
|
3
|
+
if (str.indexOf("%") === -1) return str;
|
|
3
4
|
try {
|
|
4
5
|
return decodeURIComponent(str);
|
|
5
6
|
} catch {
|
|
@@ -49,9 +50,9 @@ function parseSetCookieHeader(setCookie) {
|
|
|
49
50
|
splitSetCookieHeader(setCookie).forEach((cookieString) => {
|
|
50
51
|
const [nameValue, ...attributes] = cookieString.split(";").map((part) => part.trim());
|
|
51
52
|
const [name, ...valueParts] = (nameValue || "").split("=");
|
|
52
|
-
const value = valueParts.join("=");
|
|
53
|
-
if (!name
|
|
54
|
-
const attrObj = { value:
|
|
53
|
+
const value = unquoteCookieValue(valueParts.join("="));
|
|
54
|
+
if (!name) return;
|
|
55
|
+
const attrObj = { value: tryDecode(value) };
|
|
55
56
|
attributes.forEach((attribute) => {
|
|
56
57
|
const [attrName, ...attrValueParts] = attribute.split("=");
|
|
57
58
|
const attrValue = attrValueParts.join("=");
|
|
@@ -103,6 +104,69 @@ function toCookieOptions(attributes) {
|
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
107
|
+
* Cookie-name token char set per RFC 7230 §3.2.6.
|
|
108
|
+
*
|
|
109
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
|
|
110
|
+
*/
|
|
111
|
+
const cookieNameRegex = /^[\w!#$%&'*.^`|~+-]+$/;
|
|
112
|
+
/**
|
|
113
|
+
* Cookie-value char set per RFC 6265 §4.1.1, plus space and comma.
|
|
114
|
+
*
|
|
115
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
|
|
116
|
+
* @see https://github.com/golang/go/issues/7243
|
|
117
|
+
*/
|
|
118
|
+
const cookieValueRegex = /^[ !#-:<-[\]-~]*$/;
|
|
119
|
+
/**
|
|
120
|
+
* Strip surrounding double-quotes per RFC 6265 §4.1.1 quoted-string form.
|
|
121
|
+
*
|
|
122
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
|
|
123
|
+
*/
|
|
124
|
+
function unquoteCookieValue(value) {
|
|
125
|
+
if (value.length < 2 || !value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
126
|
+
return value.slice(1, -1);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Trim leading/trailing OWS (space / horizontal tab) per RFC 7230 §3.2.3.
|
|
130
|
+
* Narrower than `String.prototype.trim()`, which strips CR/LF and other
|
|
131
|
+
* whitespace and would let CTLs escape `cookieValueRegex`.
|
|
132
|
+
*
|
|
133
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
|
|
134
|
+
*/
|
|
135
|
+
function trimOWS(s) {
|
|
136
|
+
let start = 0;
|
|
137
|
+
let end = s.length;
|
|
138
|
+
while (start < end) {
|
|
139
|
+
const c = s.charCodeAt(start);
|
|
140
|
+
if (c !== 32 && c !== 9) break;
|
|
141
|
+
start++;
|
|
142
|
+
}
|
|
143
|
+
while (end > start) {
|
|
144
|
+
const c = s.charCodeAt(end - 1);
|
|
145
|
+
if (c !== 32 && c !== 9) break;
|
|
146
|
+
end--;
|
|
147
|
+
}
|
|
148
|
+
return start === 0 && end === s.length ? s : s.slice(start, end);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Tolerates `;` separators without the SP that RFC 6265 §4.2.1 mandates,
|
|
152
|
+
* since proxies and runtimes commonly strip it. Silently drops entries
|
|
153
|
+
* whose name violates RFC 7230 token or whose value violates RFC 6265
|
|
154
|
+
* cookie-octet (plus space and comma). Strips optional surrounding
|
|
155
|
+
* double-quotes per RFC 6265 §4.1.1.
|
|
156
|
+
*/
|
|
157
|
+
function parseCookies(cookie) {
|
|
158
|
+
const cookieMap = /* @__PURE__ */ new Map();
|
|
159
|
+
if (cookie.length < 2) return cookieMap;
|
|
160
|
+
for (const chunk of cookie.split(";")) {
|
|
161
|
+
const eq = chunk.indexOf("=");
|
|
162
|
+
if (eq === -1) continue;
|
|
163
|
+
const key = trimOWS(chunk.slice(0, eq));
|
|
164
|
+
const val = unquoteCookieValue(trimOWS(chunk.slice(eq + 1)));
|
|
165
|
+
if (cookieNameRegex.test(key) && cookieValueRegex.test(val)) cookieMap.set(key, tryDecode(val));
|
|
166
|
+
}
|
|
167
|
+
return cookieMap;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
106
170
|
* Add or replace a cookie in the request `Cookie` header.
|
|
107
171
|
*
|
|
108
172
|
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
|
|
@@ -111,30 +175,29 @@ function toCookieOptions(attributes) {
|
|
|
111
175
|
* parse-mutate-serialize.
|
|
112
176
|
*/
|
|
113
177
|
function setRequestCookie(headers, name, value) {
|
|
114
|
-
const cookieMap =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
const cookieMap = parseCookies(headers.get("cookie") || "");
|
|
179
|
+
if (cookieNameRegex.test(name)) cookieMap.set(name, value);
|
|
180
|
+
headers.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Merge `Set-Cookie` header values into the target's `Cookie` header.
|
|
184
|
+
* Mutates `target`.
|
|
185
|
+
*
|
|
186
|
+
* Name/value-level merge only. RFC 6265 §5 user-agent semantics
|
|
187
|
+
* (expiration, domain/path scoping, ordering) are out of scope. Suitable
|
|
188
|
+
* for single-request proxy, middleware, and test contexts.
|
|
189
|
+
*/
|
|
190
|
+
function applySetCookies(target, setCookieValues) {
|
|
191
|
+
const cookieMap = parseCookies(target.get("cookie") || "");
|
|
192
|
+
for (const setCookie of setCookieValues) for (const [name, attr] of parseSetCookieHeader(setCookie)) if (cookieNameRegex.test(name)) cookieMap.set(name, attr.value);
|
|
193
|
+
target.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
|
|
122
194
|
}
|
|
123
195
|
function setCookieToHeader(headers) {
|
|
124
196
|
return (context) => {
|
|
125
197
|
const setCookieHeader = context.response.headers.get("set-cookie");
|
|
126
198
|
if (!setCookieHeader) return;
|
|
127
|
-
|
|
128
|
-
(headers.get("cookie") || "").split(";").forEach((cookie) => {
|
|
129
|
-
const [name, ...rest] = cookie.trim().split("=");
|
|
130
|
-
if (name && rest.length > 0) cookieMap.set(name, rest.join("="));
|
|
131
|
-
});
|
|
132
|
-
parseSetCookieHeader(setCookieHeader).forEach((value, name) => {
|
|
133
|
-
cookieMap.set(name, value.value);
|
|
134
|
-
});
|
|
135
|
-
const updatedCookies = Array.from(cookieMap.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
136
|
-
headers.set("cookie", updatedCookies);
|
|
199
|
+
applySetCookies(headers, [setCookieHeader]);
|
|
137
200
|
};
|
|
138
201
|
}
|
|
139
202
|
//#endregion
|
|
140
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
203
|
+
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
package/dist/cookies/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Session, User } from "../types/models.mjs";
|
|
2
|
-
import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
2
|
+
import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
3
3
|
import { createSessionStore, getAccountCookie, getChunkedCookie } from "./session-store.mjs";
|
|
4
4
|
import { BetterAuthCookie, BetterAuthCookies, BetterAuthOptions, GenericEndpointContext } from "@better-auth/core";
|
|
5
5
|
import * as better_call0 from "better-call";
|
|
@@ -95,7 +95,6 @@ declare function setSessionCookie(ctx: GenericEndpointContext, session: {
|
|
|
95
95
|
*/
|
|
96
96
|
declare function expireCookie(ctx: GenericEndpointContext, cookie: BetterAuthCookie): void;
|
|
97
97
|
declare function deleteSessionCookie(ctx: GenericEndpointContext, skipDontRememberMe?: boolean | undefined): void;
|
|
98
|
-
declare function parseCookies(cookieHeader: string): Map<string, string>;
|
|
99
98
|
type EligibleCookies = (string & {}) | (keyof BetterAuthCookies & {});
|
|
100
99
|
declare const getSessionCookie: (request: Request | Headers, config?: {
|
|
101
100
|
cookiePrefix?: string;
|
|
@@ -116,4 +115,4 @@ declare const getCookieCache: <S extends {
|
|
|
116
115
|
version?: string | ((session: Session & Record<string, any>, user: User & Record<string, any>) => string) | ((session: Session & Record<string, any>, user: User & Record<string, any>) => Promise<string>);
|
|
117
116
|
} | undefined) => Promise<S | null>;
|
|
118
117
|
//#endregion
|
|
119
|
-
export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
118
|
+
export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
package/dist/cookies/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { parseUserOutput } from "../db/schema.mjs";
|
|
|
4
4
|
import { getDate } from "../utils/date.mjs";
|
|
5
5
|
import { isPromise } from "../utils/is-promise.mjs";
|
|
6
6
|
import { sec } from "../utils/time.mjs";
|
|
7
|
-
import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
7
|
+
import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
8
8
|
import { createAccountStore, createSessionStore, getAccountCookie, getChunkedCookie, setAccountCookie } from "./session-store.mjs";
|
|
9
9
|
import { env, isProduction } from "@better-auth/core/env";
|
|
10
10
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
@@ -134,9 +134,46 @@ async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
|
|
|
134
134
|
ctx.context.setNewSession(session);
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
|
+
* Remove any prior `Set-Cookie` entries on the current response whose cookie
|
|
138
|
+
* name matches `cookieName` or any chunked variant (`${cookieName}.0`, etc.).
|
|
139
|
+
*
|
|
140
|
+
* Prevents a valid cookie value from leaking on the wire when the same cookie
|
|
141
|
+
* is set and then expired within a single request (e.g. `/sign-in/email`
|
|
142
|
+
* writes credential session cookies and the 2FA after-hook expires them).
|
|
143
|
+
* Browsers honor the expiring entry, but anything reading the raw response
|
|
144
|
+
* headers — proxy/LB logs, server-side SDK consumers, observability tools —
|
|
145
|
+
* sees the earlier valid value and could replay it (bypassing the 2FA gate
|
|
146
|
+
* when the cookie cache is enabled).
|
|
147
|
+
*
|
|
148
|
+
* Scrubs both the local middleware scope's `responseHeaders` and the outer
|
|
149
|
+
* endpoint scope's `ctx.context.responseHeaders`, because plugin after-hooks
|
|
150
|
+
* run in a fresh local scope while accumulated response headers live on the
|
|
151
|
+
* outer one. `scoped.context` is required by {@link GenericEndpointContext}
|
|
152
|
+
* but unit-test mocks pass a minimal object via `as any`, so we use optional
|
|
153
|
+
* chaining defensively. The `Set` collapses the case where both scopes
|
|
154
|
+
* reference the same `Headers`.
|
|
155
|
+
*/
|
|
156
|
+
function removeSetCookieEntries(ctx, cookieName) {
|
|
157
|
+
const scoped = ctx;
|
|
158
|
+
const targets = /* @__PURE__ */ new Set();
|
|
159
|
+
if (scoped.responseHeaders) targets.add(scoped.responseHeaders);
|
|
160
|
+
if (scoped.context?.responseHeaders) targets.add(scoped.context.responseHeaders);
|
|
161
|
+
const exact = `${cookieName}=`;
|
|
162
|
+
const chunk = `${cookieName}.`;
|
|
163
|
+
for (const headers of targets) {
|
|
164
|
+
const existing = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitSetCookieHeader(headers.get("set-cookie") || "");
|
|
165
|
+
if (!existing.length) continue;
|
|
166
|
+
const survivors = existing.filter((entry) => !entry.startsWith(exact) && !entry.startsWith(chunk));
|
|
167
|
+
if (survivors.length === existing.length) continue;
|
|
168
|
+
headers.delete("set-cookie");
|
|
169
|
+
for (const entry of survivors) headers.append("set-cookie", entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
137
173
|
* Expires a cookie by setting `maxAge: 0` while preserving its attributes
|
|
138
174
|
*/
|
|
139
175
|
function expireCookie(ctx, cookie) {
|
|
176
|
+
removeSetCookieEntries(ctx, cookie.name);
|
|
140
177
|
ctx.setCookie(cookie.name, "", {
|
|
141
178
|
...cookie.attributes,
|
|
142
179
|
maxAge: 0
|
|
@@ -157,15 +194,6 @@ function deleteSessionCookie(ctx, skipDontRememberMe) {
|
|
|
157
194
|
sessionStore.setCookies(cleanCookies);
|
|
158
195
|
if (!skipDontRememberMe) expireCookie(ctx, ctx.context.authCookies.dontRememberToken);
|
|
159
196
|
}
|
|
160
|
-
function parseCookies(cookieHeader) {
|
|
161
|
-
const cookies = cookieHeader.split("; ");
|
|
162
|
-
const cookieMap = /* @__PURE__ */ new Map();
|
|
163
|
-
cookies.forEach((cookie) => {
|
|
164
|
-
const [name, value] = cookie.split(/=(.*)/s);
|
|
165
|
-
cookieMap.set(name, value);
|
|
166
|
-
});
|
|
167
|
-
return cookieMap;
|
|
168
|
-
}
|
|
169
197
|
const getSessionCookie = (request, config) => {
|
|
170
198
|
const cookies = (request instanceof Headers || !("headers" in request) ? request : request.headers).get("cookie");
|
|
171
199
|
if (!cookies) return null;
|
|
@@ -258,4 +286,4 @@ const getCookieCache = async (request, config) => {
|
|
|
258
286
|
return null;
|
|
259
287
|
};
|
|
260
288
|
//#endregion
|
|
261
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
289
|
+
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|