better-auth 1.7.5 → 1.7.7
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/rate-limiter/index.mjs +4 -1
- package/dist/api/routes/callback.mjs +16 -64
- package/dist/api/routes/password.mjs +3 -5
- package/dist/api/routes/sign-in.mjs +4 -1
- package/dist/api/routes/sign-up.mjs +3 -10
- package/dist/api/routes/update-user.mjs +7 -20
- package/dist/client/config.mjs +3 -4
- package/dist/client/query-atom.mjs +13 -0
- package/dist/client/query.mjs +37 -27
- package/dist/client/react/react-store.mjs +2 -1
- package/dist/client/session-atom.mjs +2 -1
- package/dist/context/create-context.mjs +3 -2
- package/dist/crypto/purpose.mjs +19 -0
- package/dist/index.mjs +1 -0
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +42 -14
- package/dist/oauth2/link-account.mjs +61 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/admin.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +4 -10
- package/dist/plugins/admin/types.d.mts +4 -1
- package/dist/plugins/captcha/constants.d.mts +1 -0
- package/dist/plugins/captcha/constants.mjs +2 -1
- package/dist/plugins/captcha/index.d.mts +2 -2
- package/dist/plugins/captcha/index.mjs +6 -0
- package/dist/plugins/captcha/types.d.mts +44 -2
- package/dist/plugins/captcha/utils.mjs +11 -1
- package/dist/plugins/captcha/verify-handlers/vercel-botid.mjs +27 -0
- package/dist/plugins/email-otp/routes.mjs +3 -4
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.mjs +23 -4
- package/dist/plugins/oauth-proxy/index.d.mts +5 -6
- package/dist/plugins/oauth-proxy/index.mjs +30 -15
- package/dist/plugins/open-api/index.mjs +1 -0
- package/dist/plugins/organization/client.mjs +2 -1
- package/dist/plugins/phone-number/routes.mjs +5 -4
- package/dist/plugins/username/index.mjs +2 -0
- package/dist/state.mjs +9 -6
- package/dist/utils/hashing.mjs +1 -1
- package/dist/utils/index.mjs +5 -0
- package/dist/utils/middleware-response.mjs +4 -1
- package/dist/utils/password.mjs +16 -2
- package/package.json +9 -8
|
@@ -65,7 +65,10 @@ function rateLimitResponse(retryAfter) {
|
|
|
65
65
|
return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), {
|
|
66
66
|
status: 429,
|
|
67
67
|
statusText: "Too Many Requests",
|
|
68
|
-
headers: {
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-Retry-After": retryAfter.toString()
|
|
71
|
+
}
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
function getRetryAfter(lastRequest, window) {
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
|
-
import { assertValidUserInfo } from "../../utils/validate-user-info.mjs";
|
|
4
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
5
4
|
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
6
5
|
import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
7
|
-
import { getOAuthCallbackPath
|
|
8
|
-
import {
|
|
6
|
+
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
7
|
+
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
9
8
|
import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/state.mjs";
|
|
10
9
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
11
|
-
import { mergeScopes } from "@better-auth/core/oauth2";
|
|
12
10
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
13
11
|
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
14
12
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -149,72 +147,26 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
149
147
|
c.context.logger.error("No callback URL found");
|
|
150
148
|
throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CALLBACK_URL);
|
|
151
149
|
}
|
|
150
|
+
const accountData = {
|
|
151
|
+
...accountKey,
|
|
152
|
+
...tokens,
|
|
153
|
+
scope: tokens.scopes?.join(",")
|
|
154
|
+
};
|
|
152
155
|
if (link) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
method: "oauth",
|
|
163
|
-
oauth: {
|
|
164
|
-
providerId: provider.id,
|
|
165
|
-
profile: providerProfile
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
} catch (e) {
|
|
170
|
-
if (isAPIError(e) && e.body?.code) throw redirectOnError(e.body.code, e.body.message);
|
|
171
|
-
throw e;
|
|
172
|
-
}
|
|
173
|
-
if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
174
|
-
c.context.logger.error("Unable to link account - untrusted provider");
|
|
175
|
-
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
176
|
-
}
|
|
177
|
-
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
|
|
178
|
-
const existingAccount = await c.context.internalAdapter.findAccountByKey(accountKey);
|
|
179
|
-
if (existingAccount) {
|
|
180
|
-
if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
|
|
181
|
-
const mergedScope = mergeScopes(existingAccount.scope, tokens.scopes);
|
|
182
|
-
const updateData = Object.fromEntries(Object.entries({
|
|
183
|
-
providerId: provider.id,
|
|
184
|
-
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
185
|
-
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
186
|
-
idToken: tokens.idToken,
|
|
187
|
-
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
188
|
-
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
189
|
-
scope: mergedScope || void 0
|
|
190
|
-
}).filter(([_, value]) => value !== void 0));
|
|
191
|
-
await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
192
|
-
} else if (!await c.context.internalAdapter.createAccount({
|
|
193
|
-
userId: link.userId,
|
|
194
|
-
...accountKey,
|
|
195
|
-
...tokens,
|
|
196
|
-
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
197
|
-
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
198
|
-
scope: tokens.scopes?.join(",")
|
|
199
|
-
})) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
200
|
-
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
201
|
-
let toRedirectTo;
|
|
202
|
-
try {
|
|
203
|
-
toRedirectTo = callbackURL.toString();
|
|
204
|
-
} catch {
|
|
205
|
-
toRedirectTo = callbackURL;
|
|
206
|
-
}
|
|
207
|
-
throw c.redirect(toRedirectTo);
|
|
156
|
+
const linkResult = await linkOAuthAccount(c, {
|
|
157
|
+
link,
|
|
158
|
+
userInfo,
|
|
159
|
+
account: accountData,
|
|
160
|
+
profile: providerProfile,
|
|
161
|
+
scopes: tokens.scopes
|
|
162
|
+
});
|
|
163
|
+
if (!linkResult.linked) return redirectOnError(linkResult.error.code, linkResult.error.message);
|
|
164
|
+
throw c.redirect(callbackURL);
|
|
208
165
|
}
|
|
209
166
|
if (!userInfo.email) {
|
|
210
167
|
c.context.logger.error(missingEmailLogMessage(provider.id));
|
|
211
168
|
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_FOUND);
|
|
212
169
|
}
|
|
213
|
-
const accountData = {
|
|
214
|
-
...accountKey,
|
|
215
|
-
...tokens,
|
|
216
|
-
scope: tokens.scopes?.join(",")
|
|
217
|
-
};
|
|
218
170
|
let result;
|
|
219
171
|
try {
|
|
220
172
|
result = await handleOAuthUserInfo(c, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { originCheck } from "../middlewares/origin-check.mjs";
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { sensitiveSessionMiddleware } from "./session.mjs";
|
|
4
|
-
import { validatePassword } from "../../utils/password.mjs";
|
|
4
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, validatePassword } from "../../utils/password.mjs";
|
|
5
5
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
6
6
|
import { generateId } from "@better-auth/core/utils/id";
|
|
7
7
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -149,10 +149,8 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
149
149
|
const token = ctx.body.token || ctx.query?.token;
|
|
150
150
|
if (!token) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
151
151
|
const { newPassword } = ctx.body;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
155
|
-
if (newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
152
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
153
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
156
154
|
const id = `reset-password:${token}`;
|
|
157
155
|
const verification = await ctx.context.internalAdapter.consumeVerificationValue(id);
|
|
158
156
|
if (!verification) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
@@ -9,6 +9,8 @@ import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
|
9
9
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
10
10
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
11
11
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
12
|
+
import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
13
|
+
import "../../utils/index.mjs";
|
|
12
14
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
13
15
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
14
16
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -192,7 +194,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
192
194
|
idToken: token
|
|
193
195
|
},
|
|
194
196
|
callbackURL: c.body.callbackURL,
|
|
195
|
-
disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp,
|
|
197
|
+
disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.options?.disableSignUp || provider.disableSignUp,
|
|
196
198
|
source: {
|
|
197
199
|
method: "oauth",
|
|
198
200
|
oauth: {
|
|
@@ -312,6 +314,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
312
314
|
}
|
|
313
315
|
const { email, password } = ctx.body;
|
|
314
316
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
317
|
+
assertPasswordNotTooLong(ctx, password);
|
|
315
318
|
const userRecord = await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase(), { includeAccounts: true });
|
|
316
319
|
const credentialAccount = userRecord?.accounts.find((account) => account.providerId === "credential" && account.accountId === userRecord.user.id);
|
|
317
320
|
if (!userRecord || !credentialAccount) {
|
|
@@ -4,6 +4,7 @@ import { buildSyntheticUserOutput, parseUserInput, parseUserOutput } from "../..
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
8
|
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -149,16 +150,8 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
149
150
|
const { name, email, password, image, callbackURL: _callbackURL, rememberMe, ...rest } = body;
|
|
150
151
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
151
152
|
if (!password || typeof password !== "string") throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
ctx.context.logger.warn("Password is too short");
|
|
155
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
156
|
-
}
|
|
157
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
158
|
-
if (password.length > maxPasswordLength) {
|
|
159
|
-
ctx.context.logger.warn("Password is too long");
|
|
160
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
161
|
-
}
|
|
153
|
+
assertPasswordNotTooShort(ctx, password);
|
|
154
|
+
assertPasswordNotTooLong(ctx, password);
|
|
162
155
|
const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification || ctx.context.options.emailAndPassword.autoSignIn === false;
|
|
163
156
|
const shouldSkipAutoSignIn = ctx.context.options.emailAndPassword.autoSignIn === false || shouldReturnGenericDuplicateResponse;
|
|
164
157
|
const additionalUserFields = parseUserInput(ctx.context.options, rest, "create");
|
|
@@ -4,6 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
8
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
9
10
|
import * as z from "zod";
|
|
@@ -158,16 +159,9 @@ const changePassword = createAuthEndpoint("/change-password", {
|
|
|
158
159
|
}, async (ctx) => {
|
|
159
160
|
const { newPassword, currentPassword, revokeOtherSessions } = ctx.body;
|
|
160
161
|
const session = ctx.context.session;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
165
|
-
}
|
|
166
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
167
|
-
if (newPassword.length > maxPasswordLength) {
|
|
168
|
-
ctx.context.logger.warn("Password is too long");
|
|
169
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
170
|
-
}
|
|
162
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
163
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
164
|
+
assertPasswordNotTooLong(ctx, currentPassword);
|
|
171
165
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
172
166
|
if (!account || !account.password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND);
|
|
173
167
|
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
@@ -203,16 +197,8 @@ newPassword: z.string().meta({ description: "The new password to set is required
|
|
|
203
197
|
}, async (ctx) => {
|
|
204
198
|
const { newPassword } = ctx.body;
|
|
205
199
|
const session = ctx.context.session;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ctx.context.logger.warn("Password is too short");
|
|
209
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
210
|
-
}
|
|
211
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
212
|
-
if (newPassword.length > maxPasswordLength) {
|
|
213
|
-
ctx.context.logger.warn("Password is too long");
|
|
214
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
215
|
-
}
|
|
200
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
201
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
216
202
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
217
203
|
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
218
204
|
if (!account) {
|
|
@@ -295,6 +281,7 @@ const deleteUser = createAuthEndpoint("/delete-user", {
|
|
|
295
281
|
}
|
|
296
282
|
const session = ctx.context.session;
|
|
297
283
|
if (ctx.body.password) {
|
|
284
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
298
285
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
299
286
|
if (!account || !account.password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND);
|
|
300
287
|
if (!await ctx.context.password.verify({
|
package/dist/client/config.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const resolvePublicAuthUrl = (basePath) => {
|
|
|
19
19
|
} catch {}
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
+
const matchesSessionSignal = (path) => path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/revoke-other-sessions" || path === "/change-email" || path === "/change-password";
|
|
22
23
|
const getClientConfig = (options, loadEnv) => {
|
|
23
24
|
const isCredentialsSupported = "credentials" in Request.prototype;
|
|
24
25
|
const baseURL = getBaseURL(options?.baseURL, options?.basePath, void 0, loadEnv) ?? resolvePublicAuthUrl(options?.basePath) ?? "/api/auth";
|
|
@@ -72,9 +73,7 @@ const getClientConfig = (options, loadEnv) => {
|
|
|
72
73
|
};
|
|
73
74
|
const atomListeners = [{
|
|
74
75
|
signal: "$sessionSignal",
|
|
75
|
-
matcher
|
|
76
|
-
return path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/revoke-other-sessions" || path === "/change-email" || path === "/change-password";
|
|
77
|
-
},
|
|
76
|
+
matcher: matchesSessionSignal,
|
|
78
77
|
callback(path) {
|
|
79
78
|
if (path === "/sign-out") broadcastSessionUpdate("signout");
|
|
80
79
|
else if (path === "/update-user" || path === "/update-session") broadcastSessionUpdate("updateUser");
|
|
@@ -110,4 +109,4 @@ const getClientConfig = (options, loadEnv) => {
|
|
|
110
109
|
};
|
|
111
110
|
};
|
|
112
111
|
//#endregion
|
|
113
|
-
export { getClientConfig };
|
|
112
|
+
export { getClientConfig, matchesSessionSignal };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { atom } from "nanostores";
|
|
2
|
+
//#region src/client/query-atom.ts
|
|
3
|
+
const kAuthQuery = Symbol.for("better-auth:auth-query");
|
|
4
|
+
function isAuthQueryStore(store) {
|
|
5
|
+
return kAuthQuery in store && store[kAuthQuery] === true;
|
|
6
|
+
}
|
|
7
|
+
function createAuthQueryAtom(initialValue) {
|
|
8
|
+
const queryAtom = atom(initialValue);
|
|
9
|
+
Object.defineProperty(queryAtom, kAuthQuery, { value: true });
|
|
10
|
+
return queryAtom;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { createAuthQueryAtom, isAuthQueryStore };
|
package/dist/client/query.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { isJsonEqual, withEquality } from "./equality.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { createAuthQueryAtom } from "./query-atom.mjs";
|
|
3
|
+
import { onMount } from "nanostores";
|
|
3
4
|
//#region src/client/query.ts
|
|
4
5
|
const isServer = () => typeof window === "undefined";
|
|
5
6
|
function isAuthQueryStateEqual(a, b) {
|
|
6
7
|
return isJsonEqual(a.data, b.data) && a.error === b.error && a.isPending === b.isPending && a.isRefetching === b.isRefetching && a.refetch === b.refetch;
|
|
7
8
|
}
|
|
8
9
|
const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
9
|
-
const value =
|
|
10
|
+
const value = createAuthQueryAtom({
|
|
10
11
|
data: null,
|
|
11
12
|
error: null,
|
|
12
13
|
isPending: true,
|
|
@@ -14,7 +15,9 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
14
15
|
refetch: (queryParams) => fn(queryParams)
|
|
15
16
|
});
|
|
16
17
|
onMount(value, () => withEquality(value, isAuthQueryStateEqual));
|
|
18
|
+
let latestRequestId = 0;
|
|
17
19
|
const fn = async (queryParams) => {
|
|
20
|
+
const requestId = ++latestRequestId;
|
|
18
21
|
return new Promise((resolve) => {
|
|
19
22
|
const opts = typeof options === "function" ? options({
|
|
20
23
|
data: value.get().data,
|
|
@@ -28,15 +31,17 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
28
31
|
...queryParams?.query
|
|
29
32
|
},
|
|
30
33
|
async onSuccess(context) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
if (requestId === latestRequestId) {
|
|
35
|
+
const current = value.get();
|
|
36
|
+
const stableData = current.data != null && context.data != null && isJsonEqual(current.data, context.data) ? current.data : context.data;
|
|
37
|
+
value.set({
|
|
38
|
+
data: stableData,
|
|
39
|
+
error: null,
|
|
40
|
+
isPending: false,
|
|
41
|
+
isRefetching: false,
|
|
42
|
+
refetch: value.value.refetch
|
|
43
|
+
});
|
|
44
|
+
}
|
|
40
45
|
await opts?.onSuccess?.(context);
|
|
41
46
|
},
|
|
42
47
|
async onError(context) {
|
|
@@ -44,28 +49,33 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
44
49
|
const retryAttempts = typeof request.retry === "number" ? request.retry : request.retry?.attempts;
|
|
45
50
|
const retryAttempt = request.retryAttempt || 0;
|
|
46
51
|
if (retryAttempts && retryAttempt < retryAttempts) return;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (requestId === latestRequestId) {
|
|
53
|
+
const isUnauthorized = context.error.status === 401;
|
|
54
|
+
value.set({
|
|
55
|
+
error: context.error,
|
|
56
|
+
data: isUnauthorized ? null : value.get().data,
|
|
57
|
+
isPending: false,
|
|
58
|
+
isRefetching: false,
|
|
59
|
+
refetch: value.value.refetch
|
|
60
|
+
});
|
|
61
|
+
}
|
|
55
62
|
await opts?.onError?.(context);
|
|
56
63
|
},
|
|
57
64
|
async onRequest(context) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
if (requestId === latestRequestId) {
|
|
66
|
+
const currentValue = value.get();
|
|
67
|
+
value.set({
|
|
68
|
+
isPending: currentValue.data === null,
|
|
69
|
+
data: currentValue.data,
|
|
70
|
+
error: null,
|
|
71
|
+
isRefetching: true,
|
|
72
|
+
refetch: value.value.refetch
|
|
73
|
+
});
|
|
74
|
+
}
|
|
66
75
|
await opts?.onRequest?.(context);
|
|
67
76
|
}
|
|
68
77
|
}).catch((error) => {
|
|
78
|
+
if (requestId !== latestRequestId) return;
|
|
69
79
|
value.set({
|
|
70
80
|
error,
|
|
71
81
|
data: value.get().data,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAuthQueryStore } from "../query-atom.mjs";
|
|
1
2
|
import { listenKeys } from "nanostores";
|
|
2
3
|
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
3
4
|
//#region src/client/react/react-store.ts
|
|
@@ -38,7 +39,7 @@ function useStore(store, options = {}) {
|
|
|
38
39
|
return store.listen(emitChange);
|
|
39
40
|
}, deps);
|
|
40
41
|
const get = () => snapshotRef.current;
|
|
41
|
-
return useSyncExternalStore(subscribe, get, get);
|
|
42
|
+
return useSyncExternalStore(subscribe, get, isAuthQueryStore(store) ? () => store.init : get);
|
|
42
43
|
}
|
|
43
44
|
//#endregion
|
|
44
45
|
export { useStore };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isJsonEqual, withEquality } from "./equality.mjs";
|
|
2
|
+
import { createAuthQueryAtom } from "./query-atom.mjs";
|
|
2
3
|
import { createSessionRefreshManager } from "./session-refresh.mjs";
|
|
3
4
|
import { STORE_UNMOUNT_DELAY, atom, onMount } from "nanostores";
|
|
4
5
|
//#region src/client/session-atom.ts
|
|
@@ -44,7 +45,7 @@ function getSessionAtom($fetch, options) {
|
|
|
44
45
|
freshUntil = 0;
|
|
45
46
|
});
|
|
46
47
|
const refetch = (queryParams) => fetchSession(queryParams);
|
|
47
|
-
const session =
|
|
48
|
+
const session = createAuthQueryAtom({
|
|
48
49
|
data: null,
|
|
49
50
|
error: null,
|
|
50
51
|
isPending: true,
|
|
@@ -15,7 +15,7 @@ import { getAuthTables } from "@better-auth/core/db";
|
|
|
15
15
|
import { createLogger, env, isProduction, isTest } from "@better-auth/core/env";
|
|
16
16
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
17
17
|
import { generateId } from "@better-auth/core/utils/id";
|
|
18
|
-
import { schemaCheckFor } from "@better-auth/core/db/internal";
|
|
18
|
+
import { runtimeSchemaCheckFor, schemaCheckFor } from "@better-auth/core/db/internal";
|
|
19
19
|
import { socialProviders } from "@better-auth/core/social-providers";
|
|
20
20
|
import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
|
|
21
21
|
import { createTelemetry } from "@better-auth/telemetry";
|
|
@@ -228,7 +228,8 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
228
228
|
};
|
|
229
229
|
const initOrPromise = runPluginInit(ctx);
|
|
230
230
|
if (isPromise(initOrPromise)) await initOrPromise;
|
|
231
|
-
ctx.checkSchema =
|
|
231
|
+
ctx.checkSchema = runtimeSchemaCheckFor(ctx.adapter);
|
|
232
|
+
ctx.explicitSchemaCheck = schemaCheckFor(ctx.adapter);
|
|
232
233
|
return ctx;
|
|
233
234
|
}
|
|
234
235
|
//#endregion
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
4
|
+
//#region src/crypto/purpose.ts
|
|
5
|
+
const salt = utf8ToBytes("better-auth:oauth-encryption:v1");
|
|
6
|
+
function deriveSecret(secret, purpose) {
|
|
7
|
+
return bytesToHex(hkdf(sha256, utf8ToBytes(secret), salt, utf8ToBytes(`better-auth:${purpose}:v1`), 32));
|
|
8
|
+
}
|
|
9
|
+
/** Derive an isolated key for one OAuth ciphertext purpose, retaining rotation versions. */
|
|
10
|
+
function derivePurposeKey(secret, purpose) {
|
|
11
|
+
if (typeof secret === "string") return deriveSecret(secret, purpose);
|
|
12
|
+
return {
|
|
13
|
+
keys: new Map(Array.from(secret.keys, ([version, value]) => [version, deriveSecret(value, purpose)])),
|
|
14
|
+
currentVersion: secret.currentVersion,
|
|
15
|
+
...secret.legacySecret && { legacySecret: deriveSecret(secret.legacySecret, purpose) }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { derivePurposeKey };
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProt
|
|
|
2
2
|
import { generateGenericState, parseGenericState } from "./state.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
|
|
4
4
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
5
|
+
import "./utils/index.mjs";
|
|
5
6
|
import { APIError } from "./api/index.mjs";
|
|
6
7
|
import { betterAuth } from "./auth/full.mjs";
|
|
7
8
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
2
|
-
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount } from "./link-account.mjs";
|
|
3
3
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, linkOAuthAccount, parseState, setTokenUtil };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
2
|
-
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount } from "./link-account.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, linkOAuthAccount, parseState, setTokenUtil };
|
|
@@ -2,9 +2,49 @@ import { Account, User } from "../types/models.mjs";
|
|
|
2
2
|
import { GenericEndpointContext, UserProvisioningSource } from "@better-auth/core";
|
|
3
3
|
|
|
4
4
|
//#region src/oauth2/link-account.d.ts
|
|
5
|
+
type OAuthAccountData = Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
|
|
6
|
+
/**
|
|
7
|
+
* Provider profile a freshly linked account may copy onto the local user.
|
|
8
|
+
* `email` and `emailVerified` are identity anchors and are stripped before
|
|
9
|
+
* the remaining fields are written. Provider identity is resolved separately
|
|
10
|
+
* from the raw profile through the provider's account-key contract.
|
|
11
|
+
*/
|
|
12
|
+
type LinkedProviderProfile = {
|
|
13
|
+
name?: string | undefined;
|
|
14
|
+
email?: string | null | undefined;
|
|
15
|
+
emailVerified?: boolean | undefined;
|
|
16
|
+
image?: string | null | undefined;
|
|
17
|
+
};
|
|
18
|
+
interface LinkOAuthAccountOptions {
|
|
19
|
+
link: {
|
|
20
|
+
userId: string;
|
|
21
|
+
email: string;
|
|
22
|
+
};
|
|
23
|
+
userInfo: LinkedProviderProfile;
|
|
24
|
+
account: OAuthAccountData;
|
|
25
|
+
profile: Record<string, unknown>;
|
|
26
|
+
scopes?: string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface LinkOAuthAccountFailure {
|
|
29
|
+
linked: false;
|
|
30
|
+
error: {
|
|
31
|
+
code: string;
|
|
32
|
+
message?: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
type LinkOAuthAccountResult = {
|
|
36
|
+
linked: true;
|
|
37
|
+
} | LinkOAuthAccountFailure;
|
|
38
|
+
declare function linkOAuthAccount(c: GenericEndpointContext, {
|
|
39
|
+
link,
|
|
40
|
+
userInfo,
|
|
41
|
+
account,
|
|
42
|
+
profile,
|
|
43
|
+
scopes
|
|
44
|
+
}: LinkOAuthAccountOptions): Promise<LinkOAuthAccountResult>;
|
|
5
45
|
declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
6
46
|
userInfo: Omit<User, "createdAt" | "updatedAt">;
|
|
7
|
-
account:
|
|
47
|
+
account: OAuthAccountData;
|
|
8
48
|
callbackURL?: string | undefined;
|
|
9
49
|
disableSignUp?: boolean | undefined;
|
|
10
50
|
overrideUserInfo?: boolean | undefined;
|
|
@@ -67,18 +107,6 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
|
67
107
|
password?: string | null | undefined;
|
|
68
108
|
} | null;
|
|
69
109
|
}>;
|
|
70
|
-
/**
|
|
71
|
-
* Provider profile a freshly linked account may copy onto the local user.
|
|
72
|
-
* `email` and `emailVerified` are identity anchors and are stripped before
|
|
73
|
-
* the remaining fields are written. Provider identity is resolved separately
|
|
74
|
-
* from the raw profile through the provider's account-key contract.
|
|
75
|
-
*/
|
|
76
|
-
type LinkedProviderProfile = {
|
|
77
|
-
name?: string | undefined;
|
|
78
|
-
email?: string | null | undefined;
|
|
79
|
-
emailVerified?: boolean | undefined;
|
|
80
|
-
image?: string | null | undefined;
|
|
81
|
-
};
|
|
82
110
|
/**
|
|
83
111
|
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
84
112
|
* copy the freshly linked provider's profile onto the local user, matching the
|
|
@@ -93,4 +121,4 @@ type LinkedProviderProfile = {
|
|
|
93
121
|
*/
|
|
94
122
|
declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
|
|
95
123
|
//#endregion
|
|
96
|
-
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
124
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount };
|
|
@@ -8,7 +8,67 @@ import { createEmailVerificationToken } from "../api/routes/email-verification.m
|
|
|
8
8
|
import { queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
|
|
9
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
10
10
|
import { APIError } from "@better-auth/core/error";
|
|
11
|
+
import { mergeScopes } from "@better-auth/core/oauth2";
|
|
11
12
|
//#region src/oauth2/link-account.ts
|
|
13
|
+
function linkOAuthAccountFailure(code, message) {
|
|
14
|
+
return {
|
|
15
|
+
linked: false,
|
|
16
|
+
error: {
|
|
17
|
+
code,
|
|
18
|
+
message
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function linkOAuthAccount(c, { link, userInfo, account, profile, scopes }) {
|
|
23
|
+
try {
|
|
24
|
+
await assertValidUserInfo(c, {
|
|
25
|
+
user: {
|
|
26
|
+
...userInfo,
|
|
27
|
+
id: link.userId,
|
|
28
|
+
email: userInfo.email ?? void 0
|
|
29
|
+
},
|
|
30
|
+
source: {
|
|
31
|
+
action: "link-account",
|
|
32
|
+
method: "oauth",
|
|
33
|
+
oauth: {
|
|
34
|
+
providerId: account.providerId,
|
|
35
|
+
profile
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!isAPIError(error) || !error.body?.code) throw error;
|
|
41
|
+
return linkOAuthAccountFailure(error.body.code, error.body.message);
|
|
42
|
+
}
|
|
43
|
+
if (!c.context.trustedProviders.includes(account.providerId) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
44
|
+
c.context.logger.error("Unable to link account - untrusted provider");
|
|
45
|
+
return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
46
|
+
}
|
|
47
|
+
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
|
|
48
|
+
const existingAccount = await c.context.internalAdapter.findAccountByKey(account);
|
|
49
|
+
if (existingAccount) {
|
|
50
|
+
if (existingAccount.userId.toString() !== link.userId.toString()) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
|
|
51
|
+
const mergedScope = mergeScopes(existingAccount.scope, scopes);
|
|
52
|
+
const updateData = Object.fromEntries(Object.entries({
|
|
53
|
+
providerId: account.providerId,
|
|
54
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
55
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
56
|
+
idToken: account.idToken,
|
|
57
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
58
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
59
|
+
scope: mergedScope || void 0
|
|
60
|
+
}).filter(([_, value]) => value !== void 0));
|
|
61
|
+
await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
62
|
+
} else if (!await c.context.internalAdapter.createAccount({
|
|
63
|
+
userId: link.userId,
|
|
64
|
+
...account,
|
|
65
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
66
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
67
|
+
scope: scopes?.join(",") ?? account.scope
|
|
68
|
+
})) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
69
|
+
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
70
|
+
return { linked: true };
|
|
71
|
+
}
|
|
12
72
|
async function handleOAuthUserInfo(c, opts) {
|
|
13
73
|
const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
|
|
14
74
|
const source = opts.source ?? {
|
|
@@ -326,4 +386,4 @@ async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
|
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
//#endregion
|
|
329
|
-
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
389
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount };
|
package/dist/package.mjs
CHANGED