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.
Files changed (84) hide show
  1. package/dist/api/index.d.mts +12 -48
  2. package/dist/api/routes/account.d.mts +2 -23
  3. package/dist/api/routes/account.mjs +94 -73
  4. package/dist/api/routes/callback.d.mts +1 -1
  5. package/dist/api/routes/callback.mjs +39 -42
  6. package/dist/api/routes/email-verification.d.mts +1 -0
  7. package/dist/api/routes/email-verification.mjs +4 -3
  8. package/dist/api/routes/password.mjs +1 -1
  9. package/dist/api/routes/session.mjs +15 -10
  10. package/dist/api/routes/sign-in.d.mts +1 -0
  11. package/dist/api/routes/sign-in.mjs +3 -2
  12. package/dist/api/routes/sign-up.d.mts +1 -0
  13. package/dist/api/routes/sign-up.mjs +9 -7
  14. package/dist/api/routes/update-user.mjs +7 -7
  15. package/dist/client/fetch-plugins.mjs +2 -1
  16. package/dist/client/parser.mjs +0 -1
  17. package/dist/client/plugins/index.d.mts +3 -3
  18. package/dist/client/proxy.mjs +2 -1
  19. package/dist/context/create-context.mjs +10 -14
  20. package/dist/context/helpers.mjs +3 -2
  21. package/dist/cookies/cookie-utils.d.mts +24 -1
  22. package/dist/cookies/cookie-utils.mjs +85 -22
  23. package/dist/cookies/index.d.mts +2 -3
  24. package/dist/cookies/index.mjs +39 -11
  25. package/dist/cookies/session-store.mjs +4 -23
  26. package/dist/db/get-migration.mjs +4 -4
  27. package/dist/db/index.d.mts +2 -2
  28. package/dist/db/index.mjs +3 -2
  29. package/dist/db/internal-adapter.mjs +56 -50
  30. package/dist/db/schema.d.mts +15 -2
  31. package/dist/db/schema.mjs +26 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.mjs +2 -2
  34. package/dist/oauth2/errors.mjs +16 -1
  35. package/dist/oauth2/index.d.mts +2 -2
  36. package/dist/oauth2/index.mjs +3 -3
  37. package/dist/oauth2/link-account.d.mts +27 -1
  38. package/dist/oauth2/link-account.mjs +27 -4
  39. package/dist/oauth2/state.mjs +8 -2
  40. package/dist/package.mjs +1 -1
  41. package/dist/plugins/access/access.mjs +11 -6
  42. package/dist/plugins/admin/admin.mjs +0 -4
  43. package/dist/plugins/admin/client.d.mts +1 -1
  44. package/dist/plugins/admin/routes.mjs +3 -3
  45. package/dist/plugins/anonymous/index.mjs +2 -2
  46. package/dist/plugins/bearer/index.mjs +4 -9
  47. package/dist/plugins/captcha/index.mjs +2 -2
  48. package/dist/plugins/email-otp/routes.mjs +1 -1
  49. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  50. package/dist/plugins/generic-oauth/index.mjs +6 -6
  51. package/dist/plugins/generic-oauth/routes.mjs +37 -34
  52. package/dist/plugins/generic-oauth/types.d.mts +7 -0
  53. package/dist/plugins/last-login-method/client.mjs +2 -2
  54. package/dist/plugins/magic-link/index.mjs +0 -1
  55. package/dist/plugins/mcp/index.mjs +2 -5
  56. package/dist/plugins/multi-session/index.mjs +2 -2
  57. package/dist/plugins/oauth-proxy/index.mjs +45 -32
  58. package/dist/plugins/oauth-proxy/utils.mjs +3 -10
  59. package/dist/plugins/oidc-provider/index.mjs +2 -5
  60. package/dist/plugins/one-tap/client.mjs +9 -2
  61. package/dist/plugins/one-tap/index.mjs +16 -39
  62. package/dist/plugins/open-api/generator.mjs +16 -5
  63. package/dist/plugins/organization/adapter.mjs +61 -56
  64. package/dist/plugins/organization/client.d.mts +2 -1
  65. package/dist/plugins/organization/error-codes.d.mts +1 -0
  66. package/dist/plugins/organization/error-codes.mjs +2 -1
  67. package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
  68. package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
  69. package/dist/plugins/organization/routes/crud-org.mjs +2 -2
  70. package/dist/plugins/organization/types.d.mts +3 -3
  71. package/dist/plugins/phone-number/routes.mjs +1 -1
  72. package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
  73. package/dist/plugins/two-factor/client.mjs +2 -1
  74. package/dist/plugins/two-factor/index.mjs +3 -2
  75. package/dist/plugins/username/index.d.mts +24 -2
  76. package/dist/plugins/username/index.mjs +49 -3
  77. package/dist/state.d.mts +2 -2
  78. package/dist/state.mjs +18 -4
  79. package/dist/test-utils/headers.mjs +2 -7
  80. package/dist/test-utils/test-instance.d.mts +36 -144
  81. package/dist/utils/index.d.mts +1 -1
  82. package/dist/utils/url.d.mts +2 -1
  83. package/dist/utils/url.mjs +9 -3
  84. 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: false,
275
+ returnHeaders: true,
280
276
  returnStatus: false,
281
277
  query: {
282
278
  ...config,
283
279
  ...ctx.query
284
280
  }
285
- }).catch((e) => {
281
+ }).catch(() => {
286
282
  return null;
287
283
  });
288
- ctx.context.session = session;
289
- return session;
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.deleteSessions(ctx.context.session.user.id);
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 || null,
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
- syntheticUser = customSyntheticUser({
191
+ const customResult = customSyntheticUser({
191
192
  coreFields,
192
193
  additionalFields,
193
194
  id: generatedId
194
195
  });
195
- } else syntheticUser = {
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.deleteSessions(session.user.id);
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.deleteSessions(session.user.id);
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.deleteSessions(session.user.id);
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;
@@ -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 };
@@ -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((segment) => segment.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)).join("/");
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
- if (!options.database) options = defu$1(options, {
46
- session: { cookieCache: {
47
- enabled: true,
48
- strategy: "jwe",
49
- refreshCache: true,
50
- maxAge: options.session?.expiresIn || 3600 * 24 * 7
51
- } },
52
- account: {
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 || (options.database ? "database" : "cookie"),
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 ((!!options.database || !!options.secondaryStorage) && refreshCache) {
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
  }
@@ -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 || value === void 0) return;
54
- const attrObj = { value: value.includes("%") ? tryDecode(value) : 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 = /* @__PURE__ */ new Map();
115
- for (const pair of (headers.get("cookie") || "").split(";")) {
116
- const trimmed = pair.trim();
117
- const eq = trimmed.indexOf("=");
118
- if (eq > 0) cookieMap.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
119
- }
120
- cookieMap.set(name, value);
121
- headers.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${v}`).join("; "));
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
- const cookieMap = /* @__PURE__ */ new Map();
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 };
@@ -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 };
@@ -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 };