🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

better-auth

Package Overview
Dependencies
Maintainers
1
Versions
915
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

better-auth - npm Package Compare versions

Comparing version
1.6.20
to
1.6.21
+5
-5
dist/api/index.mjs
import { isAPIError } from "../utils/is-api-error.mjs";
import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
import { getIp } from "../utils/get-request-ip.mjs";
import { onRequestRateLimit } from "./rate-limiter/index.mjs";
import { getOAuthState } from "./state/oauth.mjs";
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";

@@ -24,2 +23,3 @@ import { callbackOAuth } from "./routes/callback.mjs";

import { APIError } from "@better-auth/core/error";
import { getIp } from "@better-auth/core/utils/ip";
import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_ROUTE, withSpan } from "@better-auth/core/instrumentation";

@@ -169,2 +169,4 @@ import { normalizePathname } from "@better-auth/core/utils/url";

let currentRequest = req;
const rateLimitResponse = await onRequestRateLimit(currentRequest, ctx);
if (rateLimitResponse) return rateLimitResponse;
for (const plugin of ctx.options.plugins || []) if (plugin.onRequest) {

@@ -178,4 +180,2 @@ const response = await withSpan(`onRequest ${plugin.id}`, {

}
const rateLimitResponse = await onRequestRateLimit(currentRequest, ctx);
if (rateLimitResponse) return rateLimitResponse;
return currentRequest;

@@ -219,2 +219,2 @@ },

//#endregion
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
export { APIError, accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIp, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
import { wildcardMatch } from "../../utils/wildcard.mjs";
import { getIp } from "../../utils/get-request-ip.mjs";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { createRateLimitKey, getIp } from "@better-auth/core/utils/ip";
import { normalizePathname } from "@better-auth/core/utils/url";
import { createRateLimitKey } from "@better-auth/core/utils/ip";
//#region src/api/rate-limiter/index.ts

@@ -285,3 +284,3 @@ const memory = /* @__PURE__ */ new Map();

if (!ip && !ipWarningLogged) {
ctx.logger.warn("Rate limiting could not determine a client IP and is falling back to a single shared per-path bucket. Ensure your runtime forwards a trusted client IP header and configure `advanced.ipAddress.ipAddressHeaders` if needed.");
ctx.logger.warn("Rate limiting could not determine a client IP and is falling back to a single shared per-path bucket. Ensure your runtime forwards a trusted client IP header, then set `advanced.ipAddress.ipAddressHeaders` or `advanced.ipAddress.trustedProxies` so the address can be resolved.");
ipWarningLogged = true;

@@ -288,0 +287,0 @@ }

@@ -7,3 +7,3 @@ import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./account.mjs";

import { requestPasswordReset, requestPasswordResetCallback, resetPassword, verifyPassword } from "./password.mjs";
import { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
import { signInEmail, signInSocial } from "./sign-in.mjs";

@@ -10,0 +10,0 @@ import { signOut } from "./sign-out.mjs";

@@ -67,2 +67,30 @@ import { Prettify as Prettify$1 } from "../../types/helper.mjs";

/**
* Reads the session from the source that can authorize sensitive work.
*
* Stateful deployments must re-read the server-side session store because an
* earlier hook may have populated `ctx.context.session` from cookie cache.
* Stateless deployments keep the signed cookie as the session record.
*/
declare const getAuthoritativeSessionFromCtx: <U extends Record<string, any> = Record<string, any>, S extends Record<string, any> = Record<string, any>>(ctx: GenericEndpointContext) => Promise<{
session: S & {
id: string;
createdAt: Date;
updatedAt: Date;
userId: string;
expiresAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
};
user: U & {
id: string;
createdAt: Date;
updatedAt: Date;
email: string;
emailVerified: boolean;
name: string;
image?: string | null | undefined;
};
} | null>;
/**
* The middleware forces the endpoint to require a valid session.

@@ -94,5 +122,4 @@ */

/**
* This middleware forces the endpoint to require a valid session and ignores cookie cache.
* This middleware forces the endpoint to require a valid authoritative session.
* This should be used for sensitive operations like password changes, account deletion, etc.
* to ensure that revoked sessions cannot be used even if they're still cached in cookies.
*/

@@ -425,2 +452,2 @@ declare const sensitiveSessionMiddleware: (inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{

//#endregion
export { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
export { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };

@@ -302,2 +302,14 @@ import { isAPIError } from "../../utils/is-api-error.mjs";

/**
* Reads the session from the source that can authorize sensitive work.
*
* Stateful deployments must re-read the server-side session store because an
* earlier hook may have populated `ctx.context.session` from cookie cache.
* Stateless deployments keep the signed cookie as the session record.
*/
const getAuthoritativeSessionFromCtx = async (ctx) => {
if (!isStateful(ctx)) return getSessionFromCtx(ctx);
ctx.context.session = null;
return getSessionFromCtx(ctx, { disableCookieCache: true });
};
/**
* The middleware forces the endpoint to require a valid session.

@@ -314,8 +326,7 @@ */

/**
* This middleware forces the endpoint to require a valid session and ignores cookie cache.
* This middleware forces the endpoint to require a valid authoritative session.
* This should be used for sensitive operations like password changes, account deletion, etc.
* to ensure that revoked sessions cannot be used even if they're still cached in cookies.
*/
const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
const session = await getAuthoritativeSessionFromCtx(ctx);
if (!session?.session) throw APIError.from("UNAUTHORIZED", {

@@ -496,2 +507,2 @@ message: "Unauthorized",

//#endregion
export { freshSessionMiddleware, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };
export { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware };

@@ -19,2 +19,3 @@ import { getBaseURL, isDynamicBaseURLConfig } from "../utils/url.mjs";

import { socialProviders } from "@better-auth/core/social-providers";
import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
import { createTelemetry } from "@better-auth/telemetry";

@@ -90,2 +91,7 @@ import defu$1 from "defu";

checkEndpointConflicts(options, logger);
const trustedProxies = options.advanced?.ipAddress?.trustedProxies;
if (trustedProxies && trustedProxies.length > 0) {
const invalid = findInvalidTrustedProxies(trustedProxies);
if (invalid.length > 0) logger.warn(`Ignoring invalid \`advanced.ipAddress.trustedProxies\` entries: ${invalid.join(", ")}. Each entry must be an IP address or CIDR range.`);
}
const cookies = getCookies(options);

@@ -92,0 +98,0 @@ const tables = getAuthTables(options);

@@ -151,2 +151,3 @@ import { getSchema } from "./get-schema.mjs";

for (const [key, value] of Object.entries(betterAuthSchema)) {
if (value.disableMigrations) continue;
const table = tableMetadata.find((t) => t.name === key);

@@ -153,0 +154,0 @@ if (!table) {

@@ -8,4 +8,5 @@ import { BetterAuthOptions } from "@better-auth/core";

order: number;
disableMigrations?: boolean | undefined;
}>;
//#endregion
export { getSchema };

@@ -26,2 +26,3 @@ import { getAuthTables } from "@better-auth/core/db";

};
if (table.disableMigrations) schema[table.modelName].disableMigrations = true;
continue;

@@ -31,3 +32,4 @@ }

fields: actualFields,
order: table.order || Infinity
order: table.order || Infinity,
disableMigrations: table.disableMigrations
};

@@ -34,0 +36,0 @@ }

@@ -6,5 +6,5 @@ import { FieldAttributeToObject, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse } from "./field.mjs";

import { createInternalAdapter } from "./internal-adapter.mjs";
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
import { FieldAttributeToSchema, toZodSchema } from "./to-zod.mjs";
export * from "@better-auth/core/db";
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
import { __exportAll, __reExport } from "../_virtual/_rolldown/runtime.mjs";
import { getSchema } from "./get-schema.mjs";
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
import { convertFromDB, convertToDB } from "./field-converter.mjs";

@@ -22,2 +22,3 @@ import { getWithHooks } from "./with-hooks.mjs";

parseAdditionalUserInput: () => parseAdditionalUserInput,
parseAdditionalUserInputFromProviderProfile: () => parseAdditionalUserInputFromProviderProfile,
parseInputData: () => parseInputData,

@@ -33,2 +34,2 @@ parseSessionInput: () => parseSessionInput,

//#endregion
export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };

@@ -1,2 +0,1 @@

import { getIp } from "../utils/get-request-ip.mjs";
import { getSessionDefaultFields, parseSessionOutput, parseUserOutput } from "./schema.mjs";

@@ -9,2 +8,3 @@ import { getDate } from "../utils/date.mjs";

import { safeJSONParse } from "@better-auth/core/utils/json";
import { getIp } from "@better-auth/core/utils/ip";
import { base64Url } from "@better-auth/utils/base64";

@@ -11,0 +11,0 @@ import { createHash } from "@better-auth/utils/hash";

@@ -27,2 +27,3 @@ import { Account, Session as Session$1, User as User$1 } from "../types/models.mjs";

declare function parseUserInput(options: BetterAuthOptions, user: Record<string, any> | undefined, action: "create" | "update"): Partial<Record<string, any>>;
declare function parseAdditionalUserInputFromProviderProfile(options: BetterAuthOptions, profile: Record<string, unknown> | undefined, action: "create" | "update"): Partial<Record<string, unknown>>;
declare function parseAdditionalUserInput(options: BetterAuthOptions, user?: Record<string, any> | undefined): Partial<Record<string, any>>;

@@ -62,2 +63,2 @@ declare function parseAccountInput(options: BetterAuthOptions, account: Partial<Account>): Partial<Partial<{

//#endregion
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };

@@ -116,2 +116,14 @@ import { getAuthTables } from "@better-auth/core/db";

}
function parseAdditionalUserInputFromProviderProfile(options, profile = {}, action) {
const schema = getFields(options, "user", "input");
const allowedProfileFields = Object.create(null);
for (const key of Object.keys(profile)) {
if (schema[key]?.input === false) continue;
allowedProfileFields[key] = profile[key];
}
return parseInputData(allowedProfileFields, {
fields: schema,
action
});
}
function parseAdditionalUserInput(options, user) {

@@ -150,2 +162,2 @@ const schema = getFields(options, "user", "input");

//#endregion
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
import { isAPIError } from "../utils/is-api-error.mjs";
import { parseAdditionalUserInputFromProviderProfile } from "../db/schema.mjs";
import { setAccountCookie } from "../cookies/session-store.mjs";

@@ -67,5 +68,8 @@ import { redirectOnError } from "./errors.mjs";

if (overrideUserInfo) {
const { id: _, ...restUserInfo } = userInfo;
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "update");
user = await c.context.internalAdapter.updateUser(dbUser.user.id, {
...restUserInfo,
name,
image,
...additionalUserFields,
email: userInfo.email.toLowerCase(),

@@ -82,3 +86,4 @@ emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified

try {
const { id: _, ...restUserInfo } = userInfo;
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "create");
const accountData = {

@@ -95,4 +100,7 @@ accessToken: await setTokenUtil(account.accessToken, c.context),

const { user: createdUser, account: createdAccount } = await c.context.internalAdapter.createOAuthUser({
...restUserInfo,
email: userInfo.email.toLowerCase()
name,
image,
...additionalUserFields,
email: userInfo.email.toLowerCase(),
emailVerified: userInfo.emailVerified
}, accountData);

@@ -158,5 +166,10 @@ user = createdUser;

if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
const { id: _id, email: _email, emailVerified: _emailVerified, ...profile } = userInfo;
try {
return await c.context.internalAdapter.updateUser(userId, profile);
const { id: _id, email: _email, emailVerified: _emailVerified, name, image, ...providerProfile } = userInfo;
const additionalUserFields = parseAdditionalUserInputFromProviderProfile(c.context.options, providerProfile, "update");
return await c.context.internalAdapter.updateUser(userId, {
name,
image,
...additionalUserFields
});
} catch (e) {

@@ -163,0 +176,0 @@ c.context.logger.warn("Could not update user info on account link", e);

//#region package.json
var version = "1.6.20";
var version = "1.6.21";
//#endregion
export { version };
import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
import { getDate } from "../../utils/date.mjs";
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
import { getSessionFromCtx } from "../../api/routes/session.mjs";
import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
import { ADMIN_ERROR_CODES } from "./error-codes.mjs";

@@ -17,3 +17,3 @@ import { hasPermission } from "./has-permission.mjs";

const adminMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
const session = await getAuthoritativeSessionFromCtx(ctx);
if (!session) throw APIError.fromStatus("UNAUTHORIZED");

@@ -150,3 +150,3 @@ return { session };

}, async (ctx) => {
const session = await getSessionFromCtx(ctx);
const session = await getAuthoritativeSessionFromCtx(ctx);
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");

@@ -890,3 +890,3 @@ if (session) {

if (!ctx.body?.permissions) throw new APIError("BAD_REQUEST", { message: "invalid permission check. no permission(s) were passed." });
const session = await getSessionFromCtx(ctx);
const session = await getAuthoritativeSessionFromCtx(ctx);
if (!session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED");

@@ -893,0 +893,0 @@ if (!session && !ctx.body.userId && !ctx.body.role) throw new APIError("BAD_REQUEST", { message: "user id or role is required" });

@@ -1,2 +0,1 @@

import { getIp } from "../../utils/get-request-ip.mjs";
import { PACKAGE_VERSION } from "../../version.mjs";

@@ -10,2 +9,3 @@ import { middlewareResponse } from "../../utils/middleware-response.mjs";

import { hCaptcha } from "./verify-handlers/h-captcha.mjs";
import { getIp } from "@better-auth/core/utils/ip";
//#region src/plugins/captcha/index.ts

@@ -12,0 +12,0 @@ const captcha = (options) => ({

@@ -24,3 +24,3 @@ import { TimeString, ms, sec } from "../../utils/time.mjs";

verificationUri: z.ZodOptional<z.ZodString>;
schema: z.ZodCustom<{
schema: z.ZodOptional<z.ZodCustom<{
deviceCode?: {

@@ -55,3 +55,3 @@ modelName?: string | undefined;

} | undefined;
}>;
}>>;
}, z.core.$strip>;

@@ -404,3 +404,8 @@ type DeviceAuthorizationOptions = z.infer<typeof deviceAuthorizationOptionsSchema>;

userCodeLength: number;
schema: {
generateDeviceCode?: (() => string | Promise<string>) | undefined;
generateUserCode?: (() => string | Promise<string>) | undefined;
validateClient?: ((clientId: string) => boolean | Promise<boolean>) | undefined;
onDeviceAuthRequest?: ((clientId: string, scope: string | undefined) => void | Promise<void>) | undefined;
verificationUri?: string | undefined;
schema?: {
deviceCode?: {

@@ -420,8 +425,3 @@ modelName?: string | undefined;

} | undefined;
};
generateDeviceCode?: (() => string | Promise<string>) | undefined;
generateUserCode?: (() => string | Promise<string>) | undefined;
validateClient?: ((clientId: string) => boolean | Promise<boolean>) | undefined;
onDeviceAuthRequest?: ((clientId: string, scope: string | undefined) => void | Promise<void>) | undefined;
verificationUri?: string | undefined;
} | undefined;
}>;

@@ -428,0 +428,0 @@ };

@@ -28,3 +28,3 @@ import { mergeSchema } from "../../db/schema.mjs";

verificationUri: z.string().optional().describe("The URI where users verify their device code. Can be an absolute URL (https://example.com/device) or relative path (/custom-path). This will be returned as verification_uri in the device code response. If not provided, defaults to /device."),
schema: z.custom(() => true)
schema: z.custom(() => true).optional()
});

@@ -31,0 +31,0 @@ const deviceAuthorization = (options = {}) => {

@@ -25,5 +25,5 @@ import { Awaitable, GenericEndpointContext } from "@better-auth/core";

* so a given token mints at most one session regardless of this value
* (see GHSA-hc7v-rggr-4hvx). The option is kept for source compatibility
* and may be removed in a future major; any value other than `1` is
* ignored and emits a `console.warn` at plugin construction.
* The option is kept for source compatibility and may be removed in a future
* major; any value other than `1` is ignored and emits a `console.warn` at
* plugin construction.
*

@@ -30,0 +30,0 @@ * @default 1

@@ -30,3 +30,3 @@ import { originCheck } from "../../api/middlewares/origin-check.mjs";

};
if (options.allowedAttempts !== void 0 && options.allowedAttempts !== 1) console.warn("[better-auth/magic-link] `allowedAttempts` is ignored: tokens are consumed atomically on the first verification call (GHSA-hc7v-rggr-4hvx). Any value other than `1` has no effect; remove the option to silence this warning.");
if (options.allowedAttempts !== void 0 && options.allowedAttempts !== 1) console.warn("[better-auth/magic-link] `allowedAttempts` is ignored: tokens are consumed atomically on the first verification call. Any value other than `1` has no effect; remove the option to silence this warning.");
async function storeToken(ctx, token) {

@@ -33,0 +33,0 @@ if (opts.storeToken === "hashed") return await defaultKeyHasher(token);

@@ -17,2 +17,11 @@ import { isAPIError } from "../../utils/is-api-error.mjs";

//#region src/plugins/oauth-proxy/index.ts
const consumeOAuthProxyState = async (ctx, state) => {
try {
await parseGenericState(ctx, state, { skipStateCookieCheck: true });
return true;
} catch (e) {
ctx.context.logger.warn("OAuth proxy state missing or invalid", e);
return false;
}
};
const oauthProxyQuerySchema = z.object({

@@ -85,3 +94,3 @@ callbackURL: z.string().meta({ description: "The URL to redirect to after the proxy" }),

}
if (typeof payload.timestamp !== "number" || !payload.userInfo || !payload.account || !payload.callbackURL) {
if (typeof payload.timestamp !== "number" || !payload.userInfo || !payload.account || !payload.state || !payload.callbackURL) {
ctx.context.logger.error("Failed to parse OAuth proxy payload");

@@ -96,7 +105,3 @@ throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");

}
try {
await parseGenericState(ctx, payload.state, { skipStateCookieCheck: true });
} catch (e) {
ctx.context.logger.warn("Failed to clean up OAuth state", e);
}
if (!await consumeOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
let result;

@@ -103,0 +108,0 @@ try {

@@ -7,5 +7,5 @@ import { parseUserOutput } from "../../db/schema.mjs";

import { toBoolean } from "../../utils/boolean.mjs";
import { isGoogleHostedDomainAllowed, verifyGoogleIdToken } from "@better-auth/core/social-providers";
import { createAuthEndpoint } from "@better-auth/core/api";
import * as z from "zod";
import { createRemoteJWKSet, jwtVerify } from "jose";
//#region src/plugins/one-tap/index.ts

@@ -44,10 +44,11 @@ const oneTapCallbackBodySchema = z.object({

if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
let payload;
try {
const { payload: verifiedPayload } = await jwtVerify(idToken, createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs")), {
issuer: ["https://accounts.google.com", "accounts.google.com"],
audience
});
payload = verifiedPayload;
} catch {
const payload = await verifyGoogleIdToken({
token: idToken,
audience
});
if (!payload) throw new APIError("BAD_REQUEST", { message: "invalid id token" });
if (!payload.sub) throw new APIError("BAD_REQUEST", { message: "invalid id token" });
const configuredHostedDomain = googleProvider?.hd;
if (!isGoogleHostedDomainAllowed(configuredHostedDomain, payload.hd)) {
ctx.context.logger.error(`Google One Tap sign-in rejected: id token hosted domain (hd) "${payload.hd ?? "<missing>"}" does not satisfy the configured "hd" option "${configuredHostedDomain}".`);
throw new APIError("BAD_REQUEST", { message: "invalid id token" });

@@ -54,0 +55,0 @@ }

@@ -169,3 +169,7 @@ import { isAPIError } from "../../utils/is-api-error.mjs";

const domain = options.emailDomainName ?? getOrigin(ctx.context.baseURL);
const userEmail = !isAnon && email ? email : `${walletAddress}@${domain}`;
const normalizedEmail = email?.toLowerCase();
let userEmail = `${walletAddress}@${domain}`;
if (!isAnon && normalizedEmail) {
if (!await ctx.context.internalAdapter.findUserByEmail(normalizedEmail)) userEmail = normalizedEmail;
}
const { name, avatar } = await options.ensLookup?.({ walletAddress }) ?? {};

@@ -172,0 +176,0 @@ user = await ctx.context.internalAdapter.createUser({

@@ -164,4 +164,5 @@ import { parseUserOutput } from "../../../db/schema.mjs";

}, async (ctx) => {
const { session, valid } = await verifyTwoFactor(ctx);
const { session, valid, beginAttempt } = await verifyTwoFactor(ctx);
const user = session.user;
const isSignIn = !session.session;
const twoFactor = await ctx.context.adapter.findOne({

@@ -175,7 +176,17 @@ model: twoFactorTable,

if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED);
const validate = await verifyBackupCode({
backupCodes: twoFactor.backupCodes,
code: ctx.body.code
}, ctx.context.secretConfig, opts);
if (!validate.status || !validate.updated) throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE);
const attempt = isSignIn ? await beginAttempt(5) : null;
let validate;
try {
validate = await verifyBackupCode({
backupCodes: twoFactor.backupCodes,
code: ctx.body.code
}, ctx.context.secretConfig, opts);
} catch (error) {
await attempt?.restore();
throw error;
}
if (!validate.status || !validate.updated) {
await attempt?.recordFailure();
throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE);
}
const updatedBackupCodes = await encodeBackupCodes(validate.updated, ctx.context.secretConfig, opts);

@@ -182,0 +193,0 @@ if (!await ctx.context.adapter.incrementOne({

@@ -239,7 +239,13 @@ import { mergeSchema } from "../../db/schema.mjs";

const identifier = `2fa-${generateRandomString(20)}`;
const expiresAt = new Date(Date.now() + maxAge * 1e3);
await ctx.context.internalAdapter.createVerificationValue({
value: data.user.id,
identifier,
expiresAt: new Date(Date.now() + maxAge * 1e3)
expiresAt
});
await ctx.context.internalAdapter.createVerificationValue({
value: "0",
identifier: `2fa-attempts-${identifier}`,
expiresAt
});
await ctx.setSignedCookie(twoFactorCookie.name, identifier, ctx.context.secret, twoFactorCookie.attributes);

@@ -246,0 +252,0 @@ const twoFactorMethods = [];

@@ -125,3 +125,3 @@ import { symmetricDecrypt } from "../../../crypto/index.mjs";

}
const { session, valid, invalid } = await verifyTwoFactor(ctx);
const { session, valid, invalid, beginAttempt } = await verifyTwoFactor(ctx);
const user = session.user;

@@ -138,9 +138,20 @@ const isSignIn = !session.session;

if (isSignIn && twoFactor.verified === false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
if (!await createOTP(await symmetricDecrypt({
key: ctx.context.secretConfig,
data: twoFactor.secret
}), {
period: opts.period,
digits: opts.digits
}).verify(ctx.body.code)) return invalid("INVALID_CODE");
const attempt = isSignIn ? await beginAttempt(5) : null;
let status;
try {
status = await createOTP(await symmetricDecrypt({
key: ctx.context.secretConfig,
data: twoFactor.secret
}), {
period: opts.period,
digits: opts.digits
}).verify(ctx.body.code);
} catch (error) {
await attempt?.restore();
throw error;
}
if (!status) {
await attempt?.recordFailure();
return invalid("INVALID_CODE");
}
if (twoFactor.verified !== true) {

@@ -147,0 +158,0 @@ if (!user.twoFactorEnabled) {

@@ -69,3 +69,24 @@ import { parseUserOutput } from "../../db/schema.mjs";

},
key: signedTwoFactorCookie
key: signedTwoFactorCookie,
beginAttempt: async (allowedAttempts) => {
const identifier = `2fa-attempts-${signedTwoFactorCookie}`;
const consumed = await ctx.context.internalAdapter.consumeVerificationValue(identifier).catch(() => null);
if (!consumed) throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE);
const parsed = Number(consumed.value);
const attempts = Number.isInteger(parsed) && parsed >= 0 ? parsed : allowedAttempts;
if (attempts >= allowedAttempts) {
await ctx.context.internalAdapter.consumeVerificationValue(signedTwoFactorCookie).catch(() => {});
expireCookie(ctx, twoFactorCookie);
throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE);
}
const rearm = (count) => ctx.context.internalAdapter.createVerificationValue({
value: `${count}`,
identifier,
expiresAt: verificationToken.expiresAt
}).catch(() => {});
return {
recordFailure: () => rearm(attempts + 1),
restore: () => rearm(attempts)
};
}
};

@@ -82,3 +103,7 @@ }

session,
key: `${session.user.id}!${session.session.id}`
key: `${session.user.id}!${session.session.id}`,
beginAttempt: async (_allowedAttempts) => ({
recordFailure: async () => {},
restore: async () => {}
})
};

@@ -85,0 +110,0 @@ }

@@ -35,7 +35,13 @@ import { mergeSchema, parseUserOutput } from "../../db/schema.mjs";

const pathsWithHttpHookValidation = ["/sign-up/email", "/update-user"];
const getUsernameToValidate = (username) => options?.validationOrder?.username === "post-normalization" ? normalizer(username) : username;
async function validateUsernameValue(username) {
const usernameToValidate = getUsernameToValidate(username);
if (usernameToValidate.length < minUsernameLength) return USERNAME_ERROR_CODES.USERNAME_TOO_SHORT;
if (usernameToValidate.length > maxUsernameLength) return USERNAME_ERROR_CODES.USERNAME_TOO_LONG;
if (!await validator(usernameToValidate)) return USERNAME_ERROR_CODES.INVALID_USERNAME;
return null;
}
async function validateUsername(username, displayUsername, adapter, currentUserId) {
const usernameToValidate = options?.validationOrder?.username === "post-normalization" ? normalizer(username) : username;
if (usernameToValidate.length < minUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
if (usernameToValidate.length > maxUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
if (!await validator(usernameToValidate)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_USERNAME);
const validationError = await validateUsernameValue(username);
if (validationError) throw APIError.from("BAD_REQUEST", validationError);
const normalizedUsername = normalizer(username);

@@ -251,44 +257,52 @@ const existingUser = await adapter.findOne({

}), options?.schema),
hooks: { before: [{
matcher(context) {
return context.path === "/sign-up/email" || context.path === "/update-user";
hooks: { before: [
{
matcher(context) {
return context.path === "/sign-up/email";
},
handler: createAuthMiddleware(async (ctx) => {
if (typeof ctx.body.displayUsername !== "string" || ctx.body.username !== void 0) return;
if (!await validateUsernameValue(ctx.body.displayUsername)) ctx.body.username = ctx.body.displayUsername;
})
},
handler: createAuthMiddleware(async (ctx) => {
const username = typeof ctx.body.username === "string" && options?.validationOrder?.username === "post-normalization" ? normalizer(ctx.body.username) : ctx.body.username;
if (username !== void 0 && typeof username === "string") {
const minUsernameLength = options?.minUsernameLength || 3;
const maxUsernameLength = options?.maxUsernameLength || 30;
if (username.length < minUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
if (username.length > maxUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_USERNAME);
const normalizedUsername = normalizer(ctx.body.username);
const existingUser = await ctx.context.adapter.findOne({
model: "user",
where: [{
field: "username",
value: normalizedUsername
}]
});
if (ctx.path === "/sign-up/email" && existingUser) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
if (ctx.path === "/update-user" && existingUser) {
const session = await getSessionFromCtx(ctx);
if (!session || existingUser.id !== session.user.id) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
{
matcher(context) {
return context.path === "/sign-up/email" || context.path === "/update-user";
},
handler: createAuthMiddleware(async (ctx) => {
const username = ctx.body.username;
if (username !== void 0 && typeof username === "string") {
const validationError = await validateUsernameValue(username);
if (validationError) throw APIError.from("BAD_REQUEST", validationError);
const normalizedUsername = normalizer(username);
const existingUser = await ctx.context.adapter.findOne({
model: "user",
where: [{
field: "username",
value: normalizedUsername
}]
});
if (ctx.path === "/sign-up/email" && existingUser) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
if (ctx.path === "/update-user" && existingUser) {
const session = await getSessionFromCtx(ctx);
if (!session || existingUser.id !== session.user.id) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
}
}
}
const displayUsername = typeof ctx.body.displayUsername === "string" && options?.validationOrder?.displayUsername === "post-normalization" ? displayUsernameNormalizer(ctx.body.displayUsername) : ctx.body.displayUsername;
if (displayUsername !== void 0 && typeof displayUsername === "string") {
if (options?.displayUsernameValidator) {
if (!await options.displayUsernameValidator(displayUsername)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME);
const displayUsername = typeof ctx.body.displayUsername === "string" && options?.validationOrder?.displayUsername === "post-normalization" ? displayUsernameNormalizer(ctx.body.displayUsername) : ctx.body.displayUsername;
if (displayUsername !== void 0 && typeof displayUsername === "string") {
if (options?.displayUsernameValidator) {
if (!await options.displayUsernameValidator(displayUsername)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME);
}
}
}
})
}, {
matcher(context) {
return context.path === "/sign-up/email";
})
},
handler: createAuthMiddleware(async (ctx) => {
if (ctx.body.username && !ctx.body.displayUsername) ctx.body.displayUsername = ctx.body.username;
if (ctx.body.displayUsername && !ctx.body.username) ctx.body.username = ctx.body.displayUsername;
})
}] },
{
matcher(context) {
return context.path === "/sign-up/email";
},
handler: createAuthMiddleware(async (ctx) => {
if (ctx.body.username && !ctx.body.displayUsername) ctx.body.displayUsername = ctx.body.username;
})
}
] },
options,

@@ -295,0 +309,0 @@ $ERROR_CODES: USERNAME_ERROR_CODES

{
"name": "better-auth",
"version": "1.6.20",
"version": "1.6.21",
"description": "The most comprehensive authentication framework for TypeScript.",

@@ -486,3 +486,3 @@ "type": "module",

"@noble/hashes": "^2.0.1",
"better-call": "1.3.6",
"better-call": "1.3.7",
"defu": "^6.1.4",

@@ -493,9 +493,9 @@ "jose": "^6.1.3",

"zod": "^4.3.6",
"@better-auth/core": "1.6.20",
"@better-auth/drizzle-adapter": "1.6.20",
"@better-auth/kysely-adapter": "1.6.20",
"@better-auth/memory-adapter": "1.6.20",
"@better-auth/mongo-adapter": "1.6.20",
"@better-auth/prisma-adapter": "1.6.20",
"@better-auth/telemetry": "1.6.20"
"@better-auth/core": "1.6.21",
"@better-auth/drizzle-adapter": "1.6.21",
"@better-auth/kysely-adapter": "1.6.21",
"@better-auth/memory-adapter": "1.6.21",
"@better-auth/mongo-adapter": "1.6.21",
"@better-auth/prisma-adapter": "1.6.21",
"@better-auth/telemetry": "1.6.21"
},

@@ -502,0 +502,0 @@ "devDependencies": {

import { BetterAuthOptions } from "@better-auth/core";
//#region src/utils/get-request-ip.d.ts
declare function getIp(req: Request | Headers, options: BetterAuthOptions): string | null;
//#endregion
export { getIp };
import { isDevelopment, isTest } from "@better-auth/core/env";
import { isValidIP, normalizeIP } from "@better-auth/core/utils/ip";
//#region src/utils/get-request-ip.ts
const LOCALHOST_IP = "127.0.0.1";
function getIp(req, options) {
if (options.advanced?.ipAddress?.disableIpTracking) return null;
const headers = "headers" in req ? req.headers : req;
const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"];
for (const key of ipHeaders) {
const value = "get" in headers ? headers.get(key) : headers[key];
if (typeof value === "string") {
const ip = value.split(",")[0].trim();
if (isValidIP(ip)) return normalizeIP(ip, { ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet });
}
}
if (isTest() || isDevelopment()) return LOCALHOST_IP;
return null;
}
//#endregion
export { getIp };

Sorry, the diff of this file is too big to display