better-auth 1.6.9 → 1.6.11
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 +0 -2
- package/dist/api/routes/callback.mjs +6 -5
- package/dist/api/routes/email-verification.mjs +2 -2
- package/dist/api/routes/error.mjs +1 -1
- package/dist/api/routes/sign-in.d.mts +0 -1
- package/dist/api/routes/sign-in.mjs +4 -11
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +1 -1
- package/dist/api/to-auth-endpoints.mjs +7 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/plugins/index.d.mts +2 -1
- package/dist/cookies/cookie-utils.d.mts +10 -1
- package/dist/cookies/cookie-utils.mjs +19 -1
- package/dist/cookies/index.d.mts +2 -2
- package/dist/cookies/index.mjs +2 -2
- package/dist/db/internal-adapter.mjs +103 -7
- package/dist/db/with-hooks.d.mts +1 -0
- package/dist/db/with-hooks.mjs +58 -1
- package/dist/integrations/cookie-plugin-guard.mjs +18 -0
- package/dist/integrations/next-js.mjs +6 -0
- package/dist/integrations/svelte-kit.mjs +6 -0
- package/dist/integrations/tanstack-start-solid.mjs +6 -0
- package/dist/integrations/tanstack-start.mjs +6 -0
- package/dist/oauth2/link-account.mjs +3 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.d.mts +3 -15
- package/dist/plugins/access/index.d.mts +2 -2
- package/dist/plugins/access/types.d.mts +11 -4
- package/dist/plugins/admin/access/statement.d.mts +29 -93
- package/dist/plugins/admin/client.d.mts +5 -0
- package/dist/plugins/admin/client.mjs +5 -0
- package/dist/plugins/admin/routes.mjs +1 -0
- package/dist/plugins/anonymous/client.d.mts +1 -0
- package/dist/plugins/anonymous/error-codes.d.mts +1 -0
- package/dist/plugins/anonymous/error-codes.mjs +1 -0
- package/dist/plugins/anonymous/index.d.mts +1 -0
- package/dist/plugins/anonymous/index.mjs +16 -2
- package/dist/plugins/bearer/index.mjs +2 -4
- package/dist/plugins/captcha/index.mjs +14 -1
- package/dist/plugins/device-authorization/error-codes.mjs +1 -0
- package/dist/plugins/device-authorization/index.d.mts +1 -0
- package/dist/plugins/device-authorization/routes.mjs +34 -3
- package/dist/plugins/email-otp/routes.mjs +3 -3
- package/dist/plugins/generic-oauth/routes.mjs +3 -3
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.d.mts +8 -1
- package/dist/plugins/magic-link/index.mjs +5 -17
- package/dist/plugins/mcp/authorize.mjs +8 -2
- package/dist/plugins/mcp/index.mjs +73 -30
- package/dist/plugins/oidc-provider/authorize.mjs +8 -2
- package/dist/plugins/oidc-provider/index.mjs +63 -33
- package/dist/plugins/one-tap/index.mjs +16 -10
- package/dist/plugins/organization/access/statement.d.mts +68 -201
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/client.mjs +1 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
- package/dist/plugins/organization/routes/crud-invites.d.mts +8 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +5 -3
- package/dist/plugins/organization/routes/crud-team.mjs +7 -2
- package/dist/plugins/organization/types.d.mts +12 -2
- package/dist/plugins/siwe/client.d.mts +4 -0
- package/dist/plugins/siwe/client.mjs +5 -1
- package/dist/plugins/siwe/index.d.mts +13 -2
- package/dist/plugins/siwe/index.mjs +179 -165
- package/dist/plugins/username/index.d.mts +11 -0
- package/dist/plugins/username/index.mjs +18 -2
- package/dist/test-utils/test-instance.d.mts +1 -6
- package/dist/test-utils/test-instance.mjs +11 -2
- package/package.json +10 -10
package/dist/api/index.d.mts
CHANGED
|
@@ -178,7 +178,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
178
178
|
};
|
|
179
179
|
redirect: {
|
|
180
180
|
type: string;
|
|
181
|
-
enum: boolean[];
|
|
182
181
|
};
|
|
183
182
|
};
|
|
184
183
|
required: string[];
|
|
@@ -2167,7 +2166,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2167
2166
|
};
|
|
2168
2167
|
redirect: {
|
|
2169
2168
|
type: string;
|
|
2170
|
-
enum: boolean[];
|
|
2171
2169
|
};
|
|
2172
2170
|
};
|
|
2173
2171
|
required: string[];
|
|
@@ -91,10 +91,11 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
91
91
|
...tokens,
|
|
92
92
|
user: parsedUserData ?? void 0
|
|
93
93
|
}).then((res) => res?.user);
|
|
94
|
-
if (!userInfo) {
|
|
94
|
+
if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
|
|
95
95
|
c.context.logger.error("Unable to get user info");
|
|
96
96
|
return redirectOnError("unable_to_get_user_info");
|
|
97
97
|
}
|
|
98
|
+
const providerAccountId = String(userInfo.id);
|
|
98
99
|
if (!callbackURL) {
|
|
99
100
|
c.context.logger.error("No callback URL found");
|
|
100
101
|
throw redirectOnError("no_callback_url");
|
|
@@ -105,7 +106,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
105
106
|
return redirectOnError("unable_to_link_account");
|
|
106
107
|
}
|
|
107
108
|
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
|
|
108
|
-
const existingAccount = await c.context.internalAdapter.findAccountByProviderId(
|
|
109
|
+
const existingAccount = await c.context.internalAdapter.findAccountByProviderId(providerAccountId, provider.id);
|
|
109
110
|
if (existingAccount) {
|
|
110
111
|
if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
|
|
111
112
|
const updateData = Object.fromEntries(Object.entries({
|
|
@@ -120,7 +121,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
120
121
|
} else if (!await c.context.internalAdapter.createAccount({
|
|
121
122
|
userId: link.userId,
|
|
122
123
|
providerId: provider.id,
|
|
123
|
-
accountId:
|
|
124
|
+
accountId: providerAccountId,
|
|
124
125
|
...tokens,
|
|
125
126
|
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
126
127
|
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
@@ -140,14 +141,14 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
140
141
|
}
|
|
141
142
|
const accountData = {
|
|
142
143
|
providerId: provider.id,
|
|
143
|
-
accountId:
|
|
144
|
+
accountId: providerAccountId,
|
|
144
145
|
...tokens,
|
|
145
146
|
scope: tokens.scopes?.join(",")
|
|
146
147
|
};
|
|
147
148
|
const result = await handleOAuthUserInfo(c, {
|
|
148
149
|
userInfo: {
|
|
149
150
|
...userInfo,
|
|
150
|
-
id:
|
|
151
|
+
id: providerAccountId,
|
|
151
152
|
email: userInfo.email,
|
|
152
153
|
name: userInfo.name || ""
|
|
153
154
|
},
|
|
@@ -12,7 +12,7 @@ import { JWTExpired } from "jose/errors";
|
|
|
12
12
|
async function createEmailVerificationToken(secret, email, updateTo, expiresIn = 3600, extraPayload) {
|
|
13
13
|
return await signJWT({
|
|
14
14
|
email: email.toLowerCase(),
|
|
15
|
-
updateTo,
|
|
15
|
+
updateTo: updateTo?.toLowerCase(),
|
|
16
16
|
...extraPayload
|
|
17
17
|
}, secret, expiresIn);
|
|
18
18
|
}
|
|
@@ -101,7 +101,7 @@ const sendVerificationEmail = createAuthEndpoint("/send-verification-email", {
|
|
|
101
101
|
await sendVerificationEmailFn(ctx, user.user);
|
|
102
102
|
return ctx.json({ status: true });
|
|
103
103
|
}
|
|
104
|
-
if (session?.user.email !== email) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_MISMATCH);
|
|
104
|
+
if (session?.user.email.toLowerCase() !== email.toLowerCase()) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_MISMATCH);
|
|
105
105
|
if (session?.user.emailVerified) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_ALREADY_VERIFIED);
|
|
106
106
|
await sendVerificationEmailFn(ctx, session.user);
|
|
107
107
|
return ctx.json({ status: true });
|
|
@@ -282,7 +282,7 @@ ${custom?.disableBackgroundGrid ? "" : `
|
|
|
282
282
|
text-wrap: pretty;
|
|
283
283
|
"
|
|
284
284
|
>
|
|
285
|
-
${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find
|
|
285
|
+
${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find <a href='https://better-auth.com/docs/reference/errors/${encodeURIComponent(code)}' target='_blank' rel="noopener noreferrer" style='color: var(--foreground); text-decoration: underline;'>more information about the error</a>.` : description}
|
|
286
286
|
</p>
|
|
287
287
|
</div>
|
|
288
288
|
|
|
@@ -49,10 +49,10 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
49
49
|
description: "Sign in with a social provider",
|
|
50
50
|
operationId: "socialSignIn",
|
|
51
51
|
responses: { "200": {
|
|
52
|
-
description: "Success - Returns
|
|
52
|
+
description: "Success - Returns session details (idToken branch) or an authorize URL (redirect branch)",
|
|
53
53
|
content: { "application/json": { schema: {
|
|
54
54
|
type: "object",
|
|
55
|
-
description: "
|
|
55
|
+
description: "Returns session details when idToken is provided, or an authorize URL otherwise",
|
|
56
56
|
properties: {
|
|
57
57
|
token: { type: "string" },
|
|
58
58
|
user: {
|
|
@@ -60,16 +60,9 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
60
60
|
$ref: "#/components/schemas/User"
|
|
61
61
|
},
|
|
62
62
|
url: { type: "string" },
|
|
63
|
-
redirect: {
|
|
64
|
-
type: "boolean",
|
|
65
|
-
enum: [false]
|
|
66
|
-
}
|
|
63
|
+
redirect: { type: "boolean" }
|
|
67
64
|
},
|
|
68
|
-
required: [
|
|
69
|
-
"redirect",
|
|
70
|
-
"token",
|
|
71
|
-
"user"
|
|
72
|
-
]
|
|
65
|
+
required: ["redirect"]
|
|
73
66
|
} } }
|
|
74
67
|
} }
|
|
75
68
|
}
|
|
@@ -157,7 +157,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
157
157
|
ctx.context.logger.error("Password is too long");
|
|
158
158
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
159
159
|
}
|
|
160
|
-
const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification;
|
|
160
|
+
const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification || ctx.context.options.emailAndPassword.autoSignIn === false;
|
|
161
161
|
const shouldSkipAutoSignIn = ctx.context.options.emailAndPassword.autoSignIn === false || shouldReturnGenericDuplicateResponse;
|
|
162
162
|
const additionalUserFields = parseUserInput(ctx.context.options, rest, "create");
|
|
163
163
|
const normalizedEmail = email.toLowerCase();
|
|
@@ -410,7 +410,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
410
410
|
}, async (ctx) => {
|
|
411
411
|
if (!ctx.context.options.user?.changeEmail?.enabled) {
|
|
412
412
|
ctx.context.logger.error("Change email is disabled.");
|
|
413
|
-
throw APIError.
|
|
413
|
+
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CHANGE_EMAIL_DISABLED);
|
|
414
414
|
}
|
|
415
415
|
const newEmail = ctx.body.newEmail.toLowerCase();
|
|
416
416
|
if (newEmail === ctx.context.session.user.email) {
|
|
@@ -117,7 +117,13 @@ function toAuthEndpoints(endpoints, ctx) {
|
|
|
117
117
|
* headers override while cookies accumulate.
|
|
118
118
|
*/
|
|
119
119
|
const ctxHeaders = e[kAPIErrorHeaderSymbol];
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* `c.redirect()` (and similar APIError throws) reuse
|
|
122
|
+
* `ctx.responseHeaders` as `e.headers`, so when both sources
|
|
123
|
+
* reference the same Headers, iterating both duplicates every
|
|
124
|
+
* `set-cookie`. Skip the `errHeaders` copy in that case.
|
|
125
|
+
*/
|
|
126
|
+
const errHeaders = e.headers && e.headers !== ctxHeaders ? new Headers(e.headers) : null;
|
|
121
127
|
let headers = null;
|
|
122
128
|
if (ctxHeaders || errHeaders) {
|
|
123
129
|
headers = new Headers();
|
package/dist/client/index.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ import { parseJSON } from "./parser.mjs";
|
|
|
8
8
|
import { AuthQueryAtom, useAuthQuery } from "./query.mjs";
|
|
9
9
|
import { SessionRefreshOptions, SessionResponse, createSessionRefreshManager } from "./session-refresh.mjs";
|
|
10
10
|
import { AuthClient, createAuthClient } from "./vanilla.mjs";
|
|
11
|
-
import { AccessControl, ArrayElement, Role, Statements, SubArray, Subset } from "../plugins/access/types.mjs";
|
|
11
|
+
import { AccessControl, ArrayElement, ExactRoleStatements, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, Statements, SubArray, Subset } from "../plugins/access/types.mjs";
|
|
12
12
|
import { AuthorizeResponse, createAccessControl, role } from "../plugins/access/access.mjs";
|
|
13
13
|
import { OrganizationOptions } from "../plugins/organization/types.mjs";
|
|
14
14
|
import { InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferTeam, Invitation, InvitationInput, InvitationStatus, Member, MemberInput, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, Team, TeamInput, TeamMember, TeamMemberInput, defaultRolesSchema, invitationSchema, invitationStatus, memberSchema, organizationRoleSchema, organizationSchema, roleSchema, teamMemberSchema, teamSchema } from "../plugins/organization/schema.mjs";
|
|
@@ -31,4 +31,4 @@ declare function InferAuth<O extends {
|
|
|
31
31
|
options: BetterAuthOptions;
|
|
32
32
|
}>(): O["options"];
|
|
33
33
|
//#endregion
|
|
34
|
-
export { AccessControl, ArrayElement, AuthClient, AuthQueryAtom, AuthorizeResponse, BetterAuthClientOptions, BetterAuthClientPlugin, BroadcastChannel, BroadcastListener, BroadcastMessage, CamelCase, ClientAtomListener, ClientStore, type DBPrimitive, DefaultOrganizationPlugin, DynamicAccessControlEndpoints, ExtractPluginField, type FocusListener, type FocusManager, HasRequiredKeys, InferActions, InferAdditionalFromClient, InferAuth, InferClientAPI, InferCtx, InferErrorCodes, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPlugin, InferPluginFieldFromTuple, InferRoute, InferRoutes, InferSessionFromClient, InferSignUpEmailCtx, InferTeam, InferUserFromClient, InferUserUpdateCtx, Invitation, InvitationInput, InvitationStatus, IsAny, IsSignal, Member, MemberInput, MergeRoutes, type OnlineListener, type OnlineManager, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, OverrideMerge, PathToObject, Prettify, PrettifyDeep, ProxyRequest, RequiredKeysOf, Role, SessionQueryParams, SessionRefreshOptions, SessionResponse, Statements, StripEmptyObjects, SubArray, Subset, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, type UnionToIntersection, createAccessControl, createAuthClient, createSessionRefreshManager, defaultRolesSchema, getGlobalBroadcastChannel, getOrgAdapter, hasPermission, invitationSchema, invitationStatus, kBroadcastChannel, kFocusManager, kOnlineManager, memberSchema, organization, organizationRoleSchema, organizationSchema, parseJSON, parseRoles, role, roleSchema, teamMemberSchema, teamSchema, useAuthQuery };
|
|
34
|
+
export { AccessControl, ArrayElement, AuthClient, AuthQueryAtom, AuthorizeResponse, BetterAuthClientOptions, BetterAuthClientPlugin, BroadcastChannel, BroadcastListener, BroadcastMessage, CamelCase, ClientAtomListener, ClientStore, type DBPrimitive, DefaultOrganizationPlugin, DynamicAccessControlEndpoints, ExactRoleStatements, ExtractPluginField, type FocusListener, type FocusManager, HasRequiredKeys, InferActions, InferAdditionalFromClient, InferAuth, InferClientAPI, InferCtx, InferErrorCodes, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPlugin, InferPluginFieldFromTuple, InferRoute, InferRoutes, InferSessionFromClient, InferSignUpEmailCtx, InferTeam, InferUserFromClient, InferUserUpdateCtx, Invitation, InvitationInput, InvitationStatus, IsAny, IsSignal, Member, MemberInput, MergeRoutes, type OnlineListener, type OnlineManager, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, OverrideMerge, PathToObject, Prettify, PrettifyDeep, ProxyRequest, RequiredKeysOf, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SessionQueryParams, SessionRefreshOptions, SessionResponse, Statements, StripEmptyObjects, SubArray, Subset, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, type UnionToIntersection, createAccessControl, createAuthClient, createSessionRefreshManager, defaultRolesSchema, getGlobalBroadcastChannel, getOrgAdapter, hasPermission, invitationSchema, invitationStatus, kBroadcastChannel, kFocusManager, kOnlineManager, memberSchema, organization, organizationRoleSchema, organizationSchema, parseJSON, parseRoles, role, roleSchema, teamMemberSchema, teamSchema, useAuthQuery };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FieldAttributeToObject, RemoveFieldsWithReturnedFalse } from "../../db/field.mjs";
|
|
1
2
|
import { ExtractPluginField, HasRequiredKeys, InferPluginFieldFromTuple, IsAny, OverrideMerge, Prettify, PrettifyDeep, RequiredKeysOf, StripEmptyObjects, UnionToIntersection } from "../../types/helper.mjs";
|
|
2
3
|
import { InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferTeam, Invitation, InvitationInput, InvitationStatus, Member, MemberInput, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, Team, TeamInput, TeamMember, TeamMemberInput, defaultRolesSchema, invitationSchema, invitationStatus, memberSchema, organizationRoleSchema, organizationSchema, roleSchema, teamMemberSchema, teamSchema } from "../../plugins/organization/schema.mjs";
|
|
3
4
|
import { AdminOptions, InferAdminRolesFromOption, SessionWithImpersonatedBy, UserWithRole } from "../../plugins/admin/types.mjs";
|
|
@@ -52,4 +53,4 @@ import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
|
|
|
52
53
|
import { siweClient } from "../../plugins/siwe/client.mjs";
|
|
53
54
|
import { usernameClient } from "../../plugins/username/client.mjs";
|
|
54
55
|
import { InferServerPlugin } from "./infer-plugin.mjs";
|
|
55
|
-
export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, 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, 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, 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 };
|
|
@@ -33,8 +33,17 @@ 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
|
+
* Add or replace a cookie in the request `Cookie` header.
|
|
38
|
+
*
|
|
39
|
+
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
|
|
40
|
+
* joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
|
|
41
|
+
* breaks downstream cookie parsing. This builds the header value via
|
|
42
|
+
* parse-mutate-serialize.
|
|
43
|
+
*/
|
|
44
|
+
declare function setRequestCookie(headers: Headers, name: string, value: string): void;
|
|
36
45
|
declare function setCookieToHeader(headers: Headers): (context: {
|
|
37
46
|
response: Response;
|
|
38
47
|
}) => void;
|
|
39
48
|
//#endregion
|
|
40
|
-
export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
49
|
+
export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
@@ -102,6 +102,24 @@ function toCookieOptions(attributes) {
|
|
|
102
102
|
partitioned: attributes.partitioned
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Add or replace a cookie in the request `Cookie` header.
|
|
107
|
+
*
|
|
108
|
+
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
|
|
109
|
+
* joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
|
|
110
|
+
* breaks downstream cookie parsing. This builds the header value via
|
|
111
|
+
* parse-mutate-serialize.
|
|
112
|
+
*/
|
|
113
|
+
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("; "));
|
|
122
|
+
}
|
|
105
123
|
function setCookieToHeader(headers) {
|
|
106
124
|
return (context) => {
|
|
107
125
|
const setCookieHeader = context.response.headers.get("set-cookie");
|
|
@@ -119,4 +137,4 @@ function setCookieToHeader(headers) {
|
|
|
119
137
|
};
|
|
120
138
|
}
|
|
121
139
|
//#endregion
|
|
122
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
140
|
+
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, 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, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
2
|
+
import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, 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";
|
|
@@ -116,4 +116,4 @@ declare const getCookieCache: <S extends {
|
|
|
116
116
|
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
117
|
} | undefined) => Promise<S | null>;
|
|
118
118
|
//#endregion
|
|
119
|
-
export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
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 };
|
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, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
7
|
+
import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, 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";
|
|
@@ -258,4 +258,4 @@ const getCookieCache = async (request, config) => {
|
|
|
258
258
|
return null;
|
|
259
259
|
};
|
|
260
260
|
//#endregion
|
|
261
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
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 };
|
|
@@ -15,8 +15,9 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
15
15
|
const logger = ctx.logger;
|
|
16
16
|
const options = ctx.options;
|
|
17
17
|
const secondaryStorage = options.secondaryStorage;
|
|
18
|
+
const verificationConsumeLocks = /* @__PURE__ */ new Map();
|
|
18
19
|
const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
|
|
19
|
-
const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks } = getWithHooks(adapter, ctx);
|
|
20
|
+
const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
|
|
20
21
|
async function refreshUserSessions(user) {
|
|
21
22
|
if (!secondaryStorage) return;
|
|
22
23
|
const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`);
|
|
@@ -35,13 +36,30 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
35
36
|
}), Math.floor(sessionTTL));
|
|
36
37
|
}));
|
|
37
38
|
}
|
|
39
|
+
async function withVerificationConsumeLock(key, fn) {
|
|
40
|
+
const previous = verificationConsumeLocks.get(key) ?? Promise.resolve();
|
|
41
|
+
let release;
|
|
42
|
+
const current = new Promise((resolve) => {
|
|
43
|
+
release = resolve;
|
|
44
|
+
});
|
|
45
|
+
const next = previous.catch(() => {}).then(() => current);
|
|
46
|
+
verificationConsumeLocks.set(key, next);
|
|
47
|
+
await previous.catch(() => {});
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} finally {
|
|
51
|
+
release();
|
|
52
|
+
if (verificationConsumeLocks.get(key) === next) verificationConsumeLocks.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
38
55
|
return {
|
|
39
56
|
createOAuthUser: async (user, account) => {
|
|
40
57
|
return runWithTransaction(adapter, async () => {
|
|
41
58
|
const createdUser = await createWithHooks({
|
|
42
59
|
createdAt: /* @__PURE__ */ new Date(),
|
|
43
60
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
44
|
-
...user
|
|
61
|
+
...user,
|
|
62
|
+
email: user.email?.toLowerCase()
|
|
45
63
|
}, "user", void 0);
|
|
46
64
|
return {
|
|
47
65
|
user: createdUser,
|
|
@@ -364,10 +382,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
364
382
|
value: userId
|
|
365
383
|
}], "account", void 0);
|
|
366
384
|
},
|
|
367
|
-
deleteAccount: async (
|
|
385
|
+
deleteAccount: async (id) => {
|
|
368
386
|
await deleteWithHooks([{
|
|
369
387
|
field: "id",
|
|
370
|
-
value:
|
|
388
|
+
value: id
|
|
371
389
|
}], "account", void 0);
|
|
372
390
|
},
|
|
373
391
|
deleteSessions: async (userIdOrSessionTokens) => {
|
|
@@ -475,7 +493,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
475
493
|
}, "account", void 0);
|
|
476
494
|
},
|
|
477
495
|
updateUser: async (userId, data) => {
|
|
478
|
-
const user = await updateWithHooks(
|
|
496
|
+
const user = await updateWithHooks({
|
|
497
|
+
...data,
|
|
498
|
+
...data.email ? { email: data.email.toLowerCase() } : {}
|
|
499
|
+
}, [{
|
|
479
500
|
field: "id",
|
|
480
501
|
value: userId
|
|
481
502
|
}], "user", void 0);
|
|
@@ -483,7 +504,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
483
504
|
return user;
|
|
484
505
|
},
|
|
485
506
|
updateUserByEmail: async (email, data) => {
|
|
486
|
-
const user = await updateWithHooks(
|
|
507
|
+
const user = await updateWithHooks({
|
|
508
|
+
...data,
|
|
509
|
+
...data.email ? { email: data.email.toLowerCase() } : {}
|
|
510
|
+
}, [{
|
|
487
511
|
field: "email",
|
|
488
512
|
value: email.toLowerCase()
|
|
489
513
|
}], "user", void 0);
|
|
@@ -611,6 +635,77 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
611
635
|
value: storedIdentifier
|
|
612
636
|
}], "verification", void 0);
|
|
613
637
|
},
|
|
638
|
+
consumeVerificationValue: async (identifier) => {
|
|
639
|
+
const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier);
|
|
640
|
+
const storedIdentifier = await processIdentifier(identifier, storageOption);
|
|
641
|
+
const identifiersToTry = storageOption && storageOption !== "plain" ? [storedIdentifier, identifier] : [storedIdentifier];
|
|
642
|
+
if (secondaryStorage && !options.verification?.storeInDatabase) {
|
|
643
|
+
const parseCachedVerification = (raw) => {
|
|
644
|
+
if (!raw) return null;
|
|
645
|
+
if (typeof raw === "string") return safeJSONParse(raw);
|
|
646
|
+
if (typeof raw === "object") return raw;
|
|
647
|
+
return null;
|
|
648
|
+
};
|
|
649
|
+
const consumeCacheKey = async (key) => {
|
|
650
|
+
if (secondaryStorage.getAndDelete) return parseCachedVerification(await secondaryStorage.getAndDelete(key));
|
|
651
|
+
return withVerificationConsumeLock(key, async () => {
|
|
652
|
+
const parsed = parseCachedVerification(await secondaryStorage.get(key));
|
|
653
|
+
if (!parsed) return null;
|
|
654
|
+
await secondaryStorage.delete(key);
|
|
655
|
+
return parsed;
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
for (const stored of identifiersToTry) {
|
|
659
|
+
const cached = await consumeCacheKey(`verification:${stored}`);
|
|
660
|
+
if (!cached) continue;
|
|
661
|
+
await Promise.all(identifiersToTry.filter((candidate) => candidate !== stored).map((candidate) => secondaryStorage.delete(`verification:${candidate}`)));
|
|
662
|
+
return cached;
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
async function consumeByIdentifier(id) {
|
|
667
|
+
const where = [{
|
|
668
|
+
field: "identifier",
|
|
669
|
+
value: id
|
|
670
|
+
}];
|
|
671
|
+
return withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
|
|
672
|
+
const txAdapter = await getCurrentAdapter(adapter);
|
|
673
|
+
const latest = (await txAdapter.findMany({
|
|
674
|
+
model: "verification",
|
|
675
|
+
where,
|
|
676
|
+
sortBy: {
|
|
677
|
+
field: "createdAt",
|
|
678
|
+
direction: "desc"
|
|
679
|
+
},
|
|
680
|
+
limit: 1
|
|
681
|
+
}))[0] ?? null;
|
|
682
|
+
if (!latest) return null;
|
|
683
|
+
const hookWhere = [{
|
|
684
|
+
field: "id",
|
|
685
|
+
value: latest.id
|
|
686
|
+
}];
|
|
687
|
+
return consumeOneWithHooks("verification", hookWhere, async () => {
|
|
688
|
+
const consumed = await txAdapter.consumeOne({
|
|
689
|
+
model: "verification",
|
|
690
|
+
where: hookWhere
|
|
691
|
+
});
|
|
692
|
+
if (!consumed) return null;
|
|
693
|
+
await txAdapter.deleteMany({
|
|
694
|
+
model: "verification",
|
|
695
|
+
where
|
|
696
|
+
});
|
|
697
|
+
return consumed;
|
|
698
|
+
}, latest);
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
let consumed = null;
|
|
702
|
+
for (const stored of identifiersToTry) {
|
|
703
|
+
consumed = await consumeByIdentifier(stored);
|
|
704
|
+
if (consumed) break;
|
|
705
|
+
}
|
|
706
|
+
if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
|
|
707
|
+
return consumed;
|
|
708
|
+
},
|
|
614
709
|
updateVerificationByIdentifier: async (identifier, data) => {
|
|
615
710
|
const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier));
|
|
616
711
|
if (secondaryStorage) {
|
|
@@ -634,7 +729,8 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
634
729
|
value: storedIdentifier
|
|
635
730
|
}], "verification", void 0);
|
|
636
731
|
return data;
|
|
637
|
-
}
|
|
732
|
+
},
|
|
733
|
+
refreshUserSessions
|
|
638
734
|
};
|
|
639
735
|
};
|
|
640
736
|
//#endregion
|
package/dist/db/with-hooks.d.mts
CHANGED
|
@@ -31,6 +31,7 @@ declare function getWithHooks(adapter: DBAdapter<BetterAuthOptions>, ctx: {
|
|
|
31
31
|
fn: (where: Where[]) => void | Promise<any>;
|
|
32
32
|
executeMainFn?: boolean;
|
|
33
33
|
} | undefined) => Promise<any>;
|
|
34
|
+
consumeOneWithHooks: <T extends Record<string, any>>(model: BaseModelNames, hookWhere: Where[], consumeFn: () => Promise<T | null>, preSnapshot?: T | null) => Promise<T | null>;
|
|
34
35
|
};
|
|
35
36
|
//#endregion
|
|
36
37
|
export { DatabaseHooksEntry, getWithHooks };
|
package/dist/db/with-hooks.mjs
CHANGED
|
@@ -185,12 +185,69 @@ function getWithHooks(adapter, ctx) {
|
|
|
185
185
|
}
|
|
186
186
|
return deleted;
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Wraps an atomic consume operation in the plugin `delete.before` and
|
|
190
|
+
* `delete.after` hook lifecycle. The caller supplies a `consumeFn` that
|
|
191
|
+
* performs the actual single-row delete-and-return (typically the
|
|
192
|
+
* adapter's `consumeOne`). The first concurrent caller wins, subsequent
|
|
193
|
+
* racers resolve to `null` without firing `delete.after` hooks.
|
|
194
|
+
*
|
|
195
|
+
* `preSnapshot` lets the caller hand in a row it already fetched so
|
|
196
|
+
* `delete.before` hooks don't trigger a second read. Without it, the
|
|
197
|
+
* helper falls back to a best-effort `findMany` against `hookWhere`.
|
|
198
|
+
* The snapshot only feeds `delete.before`; the `consumeFn` return value
|
|
199
|
+
* is the race gate.
|
|
200
|
+
*
|
|
201
|
+
* Returning `false` from a `delete.before` hook aborts the consume and
|
|
202
|
+
* the helper resolves to `null` (no `consumeFn` call, no after hooks).
|
|
203
|
+
*/
|
|
204
|
+
async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) {
|
|
205
|
+
const context = await getCurrentAuthContext().catch(() => null);
|
|
206
|
+
const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => {
|
|
207
|
+
const fn = hooks[model]?.delete?.before;
|
|
208
|
+
return fn ? [{
|
|
209
|
+
source,
|
|
210
|
+
fn
|
|
211
|
+
}] : [];
|
|
212
|
+
});
|
|
213
|
+
let snapshot = preSnapshot ?? null;
|
|
214
|
+
if (beforeHooks.length) {
|
|
215
|
+
if (!snapshot) try {
|
|
216
|
+
snapshot = (await (await getCurrentAdapter(adapter)).findMany({
|
|
217
|
+
model,
|
|
218
|
+
where: hookWhere,
|
|
219
|
+
limit: 1
|
|
220
|
+
}))[0] || null;
|
|
221
|
+
} catch {}
|
|
222
|
+
if (snapshot) {
|
|
223
|
+
for (const { source, fn } of beforeHooks) if (await withSpan(`db delete.before ${model}`, {
|
|
224
|
+
[ATTR_HOOK_TYPE]: "delete.before",
|
|
225
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
226
|
+
[ATTR_CONTEXT]: source
|
|
227
|
+
}, () => fn(snapshot, context)) === false) return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const consumed = await consumeFn();
|
|
231
|
+
if (!consumed) return null;
|
|
232
|
+
for (const { source, hooks } of hooksEntries) {
|
|
233
|
+
const toRun = hooks[model]?.delete?.after;
|
|
234
|
+
if (toRun) await queueAfterTransactionHook(async () => {
|
|
235
|
+
await withSpan(`db delete.after ${model}`, {
|
|
236
|
+
[ATTR_HOOK_TYPE]: "delete.after",
|
|
237
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
238
|
+
[ATTR_CONTEXT]: source
|
|
239
|
+
}, () => toRun(consumed, context));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return consumed;
|
|
243
|
+
}
|
|
188
244
|
return {
|
|
189
245
|
createWithHooks,
|
|
190
246
|
updateWithHooks,
|
|
191
247
|
updateManyWithHooks,
|
|
192
248
|
deleteWithHooks,
|
|
193
|
-
deleteManyWithHooks
|
|
249
|
+
deleteManyWithHooks,
|
|
250
|
+
consumeOneWithHooks
|
|
194
251
|
};
|
|
195
252
|
}
|
|
196
253
|
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/integrations/cookie-plugin-guard.ts
|
|
2
|
+
/**
|
|
3
|
+
* Warns when a cookie integration plugin is not effectively last.
|
|
4
|
+
*
|
|
5
|
+
* A plugin is considered misordered when there is at least one other plugin
|
|
6
|
+
* after it in the `plugins` array that declares `hooks.after`, since those
|
|
7
|
+
* hooks can set cookies that this integration will not see.
|
|
8
|
+
*/
|
|
9
|
+
function warnIfCookiePluginNotLast(ctx, pluginId) {
|
|
10
|
+
const plugins = ctx.options.plugins || [];
|
|
11
|
+
if (plugins.length === 0) return;
|
|
12
|
+
const index = plugins.findIndex((p) => p.id === pluginId);
|
|
13
|
+
if (index === -1) return;
|
|
14
|
+
if (!plugins.slice(index + 1).some((p) => p.hooks && Array.isArray(p.hooks.after) && p.hooks.after.length > 0)) return;
|
|
15
|
+
ctx.logger.warn(`[better-auth] Cookie integration plugin "${pluginId}" should be placed last in the plugins array. Plugins with \`hooks.after\` running after it may set cookies that are not forwarded to the framework cookie store. Move your cookie integration plugin to the end of the \`plugins\` array to avoid missing \`Set-Cookie\` headers.`);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { warnIfCookiePluginNotLast };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
|
|
2
2
|
import { setShouldSkipSessionRefresh } from "../api/state/should-session-refresh.mjs";
|
|
3
3
|
import { PACKAGE_VERSION } from "../version.mjs";
|
|
4
|
+
import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
|
|
4
5
|
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
5
6
|
//#region src/integrations/next-js.ts
|
|
6
7
|
function toNextJsHandler(auth) {
|
|
@@ -16,6 +17,7 @@ function toNextJsHandler(auth) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
const nextCookies = () => {
|
|
20
|
+
let hasWarned = false;
|
|
19
21
|
return {
|
|
20
22
|
id: "next-cookies",
|
|
21
23
|
version: PACKAGE_VERSION,
|
|
@@ -25,6 +27,10 @@ const nextCookies = () => {
|
|
|
25
27
|
return ctx.path === "/get-session";
|
|
26
28
|
},
|
|
27
29
|
handler: createAuthMiddleware(async (ctx) => {
|
|
30
|
+
if (!hasWarned) {
|
|
31
|
+
warnIfCookiePluginNotLast(ctx.context, "next-cookies");
|
|
32
|
+
hasWarned = true;
|
|
33
|
+
}
|
|
28
34
|
if ("_flag" in ctx && ctx._flag === "router") return;
|
|
29
35
|
let headersStore;
|
|
30
36
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
|
|
2
2
|
import { PACKAGE_VERSION } from "../version.mjs";
|
|
3
|
+
import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
|
|
3
4
|
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
4
5
|
//#region src/integrations/svelte-kit.ts
|
|
5
6
|
const toSvelteKitHandler = (auth) => {
|
|
@@ -20,6 +21,7 @@ function isAuthPath(url, options) {
|
|
|
20
21
|
return true;
|
|
21
22
|
}
|
|
22
23
|
const sveltekitCookies = (getRequestEvent) => {
|
|
24
|
+
let hasWarned = false;
|
|
23
25
|
return {
|
|
24
26
|
id: "sveltekit-cookies",
|
|
25
27
|
version: PACKAGE_VERSION,
|
|
@@ -28,6 +30,10 @@ const sveltekitCookies = (getRequestEvent) => {
|
|
|
28
30
|
return true;
|
|
29
31
|
},
|
|
30
32
|
handler: createAuthMiddleware(async (ctx) => {
|
|
33
|
+
if (!hasWarned) {
|
|
34
|
+
warnIfCookiePluginNotLast(ctx.context, "sveltekit-cookies");
|
|
35
|
+
hasWarned = true;
|
|
36
|
+
}
|
|
31
37
|
const returned = ctx.context.responseHeaders;
|
|
32
38
|
if ("_flag" in ctx && ctx._flag === "router") return;
|
|
33
39
|
if (returned instanceof Headers) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
|
|
2
2
|
import { PACKAGE_VERSION } from "../version.mjs";
|
|
3
|
+
import { warnIfCookiePluginNotLast } from "./cookie-plugin-guard.mjs";
|
|
3
4
|
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
4
5
|
//#region src/integrations/tanstack-start-solid.ts
|
|
5
6
|
/**
|
|
@@ -20,6 +21,7 @@ import { createAuthMiddleware } from "@better-auth/core/api";
|
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
23
|
const tanstackStartCookies = () => {
|
|
24
|
+
let hasWarned = false;
|
|
23
25
|
return {
|
|
24
26
|
id: "tanstack-start-cookies-solid",
|
|
25
27
|
version: PACKAGE_VERSION,
|
|
@@ -28,6 +30,10 @@ const tanstackStartCookies = () => {
|
|
|
28
30
|
return true;
|
|
29
31
|
},
|
|
30
32
|
handler: createAuthMiddleware(async (ctx) => {
|
|
33
|
+
if (!hasWarned) {
|
|
34
|
+
warnIfCookiePluginNotLast(ctx.context, "tanstack-start-cookies-solid");
|
|
35
|
+
hasWarned = true;
|
|
36
|
+
}
|
|
31
37
|
const returned = ctx.context.responseHeaders;
|
|
32
38
|
if ("_flag" in ctx && ctx._flag === "router") return;
|
|
33
39
|
if (returned instanceof Headers) {
|