better-auth
Version:
The most comprehensive authentication library for TypeScript.
717 lines (711 loc) • 22.7 kB
JavaScript
import { APIError, createRouter } from 'better-call';
export { APIError } from 'better-call';
import { k as createEmailVerificationToken, w as wildcardMatch, u as updateUser, t as toAuthEndpoints, l as originCheckMiddleware, m as error, n as ok, q as accountInfo, x as getAccessToken, y as refreshToken, z as unlinkAccount, A as deleteUserCallback, B as listUserAccounts, C as linkSocialAccount, D as requestPasswordResetCallback, E as requestPasswordReset, F as forgetPasswordCallback, G as deleteUser, I as setPassword, J as changePassword, K as changeEmail, L as sendVerificationEmail, M as verifyEmail, N as resetPassword, O as forgetPassword, P as signInEmail, Q as signOut, R as callbackOAuth, S as signInSocial } from '../shared/better-auth.CewjboYP.mjs';
export { o as originCheck, T as sendVerificationEmailFn } from '../shared/better-auth.CewjboYP.mjs';
import { a as createAuthEndpoint, B as BASE_ERROR_CODES, l as listSessions, b as getSession, r as revokeOtherSessions, d as revokeSessions, e as revokeSession } from '../shared/better-auth.DV5EHeYG.mjs';
export { c as createAuthMiddleware, f as freshSessionMiddleware, g as getSessionFromCtx, h as getSessionQuerySchema, o as optionsMiddleware, i as requestOnlySessionMiddleware, s as sessionMiddleware } from '../shared/better-auth.DV5EHeYG.mjs';
import * as z from 'zod/v4';
import { s as setSessionCookie } from '../shared/better-auth.UfVWArIB.mjs';
import { h as parseUserInput } from '../shared/better-auth.Dcv8PS7T.mjs';
import { b as isDevelopment } from '../shared/better-auth.CMQ3rA-I.mjs';
import { a as logger } from '../shared/better-auth.BjBlybv-.mjs';
import { s as safeJSONParse } from '../shared/better-auth.BZZKN1g7.mjs';
import { g as getIp } from '../shared/better-auth.O2VtDkDK.mjs';
import '../shared/better-auth.CW6D9eSx.mjs';
import '@better-auth/utils/hash';
import '@better-auth/utils/base64';
import '../crypto/index.mjs';
import '@noble/ciphers/chacha.js';
import '@noble/ciphers/utils.js';
import 'jose';
import '@noble/hashes/scrypt.js';
import '@better-auth/utils';
import '@better-auth/utils/hex';
import '@noble/hashes/utils.js';
import '../shared/better-auth.DdzSJf-n.mjs';
import '../shared/better-auth.B4Qoxdgc.mjs';
import '@better-auth/utils/random';
import '@better-fetch/fetch';
import '../shared/better-auth.CuS_eDdK.mjs';
import 'jose/errors';
import '../shared/better-auth.BUPPRXfK.mjs';
import '@better-auth/utils/hmac';
import '@better-auth/utils/binary';
import 'defu';
const signUpEmail = () => createAuthEndpoint(
"/sign-up/email",
{
method: "POST",
body: z.record(z.string(), z.any()),
metadata: {
$Infer: {
body: {}
},
openapi: {
description: "Sign up a user using email and password",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name of the user"
},
email: {
type: "string",
description: "The email of the user"
},
password: {
type: "string",
description: "The password of the user"
},
image: {
type: "string",
description: "The profile image URL of the user"
},
callbackURL: {
type: "string",
description: "The URL to use for email verification callback"
},
rememberMe: {
type: "boolean",
description: "If this is false, the session will not be remembered. Default is `true`."
}
},
required: ["name", "email", "password"]
}
}
}
},
responses: {
"200": {
description: "Successfully created user",
content: {
"application/json": {
schema: {
type: "object",
properties: {
token: {
type: "string",
nullable: true,
description: "Authentication token for the session"
},
user: {
type: "object",
properties: {
id: {
type: "string",
description: "The unique identifier of the user"
},
email: {
type: "string",
format: "email",
description: "The email address of the user"
},
name: {
type: "string",
description: "The name of the user"
},
image: {
type: "string",
format: "uri",
nullable: true,
description: "The profile image URL of the user"
},
emailVerified: {
type: "boolean",
description: "Whether the email has been verified"
},
createdAt: {
type: "string",
format: "date-time",
description: "When the user was created"
},
updatedAt: {
type: "string",
format: "date-time",
description: "When the user was last updated"
}
},
required: [
"id",
"email",
"name",
"emailVerified",
"createdAt",
"updatedAt"
]
}
},
required: ["user"]
// token is optional
}
}
}
},
"422": {
description: "Unprocessable Entity. User already exists or failed to create user.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string"
}
}
}
}
}
}
}
}
}
},
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp) {
throw new APIError("BAD_REQUEST", {
message: "Email and password sign up is not enabled"
});
}
const body = ctx.body;
const {
name,
email,
password,
image,
callbackURL,
rememberMe,
...additionalFields
} = body;
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.INVALID_EMAIL
});
}
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (password.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT
});
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (password.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG
});
}
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
if (dbUser?.user) {
ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`);
throw new APIError("UNPROCESSABLE_ENTITY", {
message: BASE_ERROR_CODES.USER_ALREADY_EXISTS
});
}
const additionalData = parseUserInput(
ctx.context.options,
additionalFields
);
const hash = await ctx.context.password.hash(password);
let createdUser;
try {
createdUser = await ctx.context.internalAdapter.createUser(
{
email: email.toLowerCase(),
name,
image,
...additionalData,
emailVerified: false
},
ctx
);
if (!createdUser) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER
});
}
} catch (e) {
if (isDevelopment) {
ctx.context.logger.error("Failed to create user", e);
}
if (e instanceof APIError) {
throw e;
}
throw new APIError("UNPROCESSABLE_ENTITY", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
details: e
});
}
if (!createdUser) {
throw new APIError("UNPROCESSABLE_ENTITY", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER
});
}
await ctx.context.internalAdapter.linkAccount(
{
userId: createdUser.id,
providerId: "credential",
accountId: createdUser.id,
password: hash
},
ctx
);
if (ctx.context.options.emailVerification?.sendOnSignUp || ctx.context.options.emailAndPassword.requireEmailVerification) {
const token = await createEmailVerificationToken(
ctx.context.secret,
createdUser.email,
void 0,
ctx.context.options.emailVerification?.expiresIn
);
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${body.callbackURL || "/"}`;
await ctx.context.options.emailVerification?.sendVerificationEmail?.(
{
user: createdUser,
url,
token
},
ctx.request
);
}
if (ctx.context.options.emailAndPassword.autoSignIn === false || ctx.context.options.emailAndPassword.requireEmailVerification) {
return ctx.json({
token: null,
user: {
id: createdUser.id,
email: createdUser.email,
name: createdUser.name,
image: createdUser.image,
emailVerified: createdUser.emailVerified,
createdAt: createdUser.createdAt,
updatedAt: createdUser.updatedAt
}
});
}
const session = await ctx.context.internalAdapter.createSession(
createdUser.id,
ctx,
rememberMe === false
);
if (!session) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
});
}
await setSessionCookie(
ctx,
{
session,
user: createdUser
},
rememberMe === false
);
return ctx.json({
token: session.token,
user: {
id: createdUser.id,
email: createdUser.email,
name: createdUser.name,
image: createdUser.image,
emailVerified: createdUser.emailVerified,
createdAt: createdUser.createdAt,
updatedAt: createdUser.updatedAt
}
});
}
);
function shouldRateLimit(max, window, rateLimitData) {
const now = Date.now();
const windowInMs = window * 1e3;
const timeSinceLastRequest = now - rateLimitData.lastRequest;
return timeSinceLastRequest < windowInMs && rateLimitData.count >= max;
}
function rateLimitResponse(retryAfter) {
return new Response(
JSON.stringify({
message: "Too many requests. Please try again later."
}),
{
status: 429,
statusText: "Too Many Requests",
headers: {
"X-Retry-After": retryAfter.toString()
}
}
);
}
function getRetryAfter(lastRequest, window) {
const now = Date.now();
const windowInMs = window * 1e3;
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
}
function createDBStorage(ctx) {
const model = "rateLimit";
const db = ctx.adapter;
return {
get: async (key) => {
const res = await db.findMany({
model,
where: [{ field: "key", value: key }]
});
const data = res[0];
if (typeof data?.lastRequest === "bigint") {
data.lastRequest = Number(data.lastRequest);
}
return data;
},
set: async (key, value, _update) => {
try {
if (_update) {
await db.updateMany({
model,
where: [{ field: "key", value: key }],
update: {
count: value.count,
lastRequest: value.lastRequest
}
});
} else {
await db.create({
model,
data: {
key,
count: value.count,
lastRequest: value.lastRequest
}
});
}
} catch (e) {
ctx.logger.error("Error setting rate limit", e);
}
}
};
}
const memory = /* @__PURE__ */ new Map();
function getRateLimitStorage(ctx) {
if (ctx.options.rateLimit?.customStorage) {
return ctx.options.rateLimit.customStorage;
}
if (ctx.rateLimit.storage === "secondary-storage") {
return {
get: async (key) => {
const data = await ctx.options.secondaryStorage?.get(key);
return data ? safeJSONParse(data) : void 0;
},
set: async (key, value) => {
await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value));
}
};
}
const storage = ctx.rateLimit.storage;
if (storage === "memory") {
return {
async get(key) {
return memory.get(key);
},
async set(key, value, _update) {
memory.set(key, value);
}
};
}
return createDBStorage(ctx);
}
async function onRequestRateLimit(req, ctx) {
if (!ctx.rateLimit.enabled) {
return;
}
const path = new URL(req.url).pathname.replace(
ctx.options.basePath || "/api/auth",
""
);
let window = ctx.rateLimit.window;
let max = ctx.rateLimit.max;
const ip = getIp(req, ctx.options);
if (!ip) {
return;
}
const key = ip + path;
const specialRules = getDefaultSpecialRules();
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
if (specialRule) {
window = specialRule.window;
max = specialRule.max;
}
for (const plugin of ctx.options.plugins || []) {
if (plugin.rateLimit) {
const matchedRule = plugin.rateLimit.find(
(rule) => rule.pathMatcher(path)
);
if (matchedRule) {
window = matchedRule.window;
max = matchedRule.max;
break;
}
}
}
if (ctx.rateLimit.customRules) {
const _path = Object.keys(ctx.rateLimit.customRules).find((p) => {
if (p.includes("*")) {
const isMatch = wildcardMatch(p)(path);
return isMatch;
}
return p === path;
});
if (_path) {
const customRule = ctx.rateLimit.customRules[_path];
const resolved = typeof customRule === "function" ? await customRule(req) : customRule;
if (resolved) {
window = resolved.window;
max = resolved.max;
}
if (resolved === false) {
return;
}
}
}
const storage = getRateLimitStorage(ctx);
const data = await storage.get(key);
const now = Date.now();
if (!data) {
await storage.set(key, {
key,
count: 1,
lastRequest: now
});
} else {
const timeSinceLastRequest = now - data.lastRequest;
if (shouldRateLimit(max, window, data)) {
const retryAfter = getRetryAfter(data.lastRequest, window);
return rateLimitResponse(retryAfter);
} else if (timeSinceLastRequest > window * 1e3) {
await storage.set(
key,
{
...data,
count: 1,
lastRequest: now
},
true
);
} else {
await storage.set(
key,
{
...data,
count: data.count + 1,
lastRequest: now
},
true
);
}
}
}
function getDefaultSpecialRules() {
const specialRules = [
{
pathMatcher(path) {
return path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email");
},
window: 10,
max: 3
}
];
return specialRules;
}
function checkEndpointConflicts(options, logger2) {
const endpointRegistry = /* @__PURE__ */ new Map();
options.plugins?.forEach((plugin) => {
if (plugin.endpoints) {
for (const [key, endpoint] of Object.entries(plugin.endpoints)) {
if (endpoint && "path" in endpoint) {
const path = endpoint.path;
if (!endpointRegistry.has(path)) {
endpointRegistry.set(path, []);
}
endpointRegistry.get(path).push({
pluginId: plugin.id,
endpointKey: key
});
}
}
}
});
const conflicts = [];
for (const [path, entries] of endpointRegistry.entries()) {
if (entries.length > 1) {
const uniquePlugins = [...new Set(entries.map((e) => e.pluginId))];
conflicts.push({
path,
plugins: uniquePlugins
});
}
}
if (conflicts.length > 0) {
const conflictMessages = conflicts.map(
(conflict) => ` - "${conflict.path}" used by plugins: ${conflict.plugins.join(", ")}`
).join("\n");
logger2.error(
`Endpoint path conflicts detected! Multiple plugins are trying to use the same endpoint paths:
${conflictMessages}
To resolve this, you can:
1. Use only one of the conflicting plugins
2. Configure the plugins to use different paths (if supported)
`
);
}
}
function getEndpoints(ctx, options) {
const pluginEndpoints = options.plugins?.reduce(
(acc, plugin) => {
return {
...acc,
...plugin.endpoints
};
},
{}
);
const middlewares = options.plugins?.map(
(plugin) => plugin.middlewares?.map((m) => {
const middleware = (async (context) => {
const authContext = await ctx;
return m.middleware({
...context,
context: {
...authContext,
...context.context
}
});
});
middleware.options = m.middleware.options;
return {
path: m.path,
middleware
};
})
).filter((plugin) => plugin !== void 0).flat() || [];
const baseEndpoints = {
signInSocial,
callbackOAuth,
getSession: getSession(),
signOut,
signUpEmail: signUpEmail(),
signInEmail,
forgetPassword,
resetPassword,
verifyEmail,
sendVerificationEmail,
changeEmail,
changePassword,
setPassword,
updateUser: updateUser(),
deleteUser,
forgetPasswordCallback,
requestPasswordReset,
requestPasswordResetCallback,
listSessions: listSessions(),
revokeSession,
revokeSessions,
revokeOtherSessions,
linkSocialAccount,
listUserAccounts,
deleteUserCallback,
unlinkAccount,
refreshToken,
getAccessToken,
accountInfo
};
const endpoints = {
...baseEndpoints,
...pluginEndpoints,
ok,
error
};
const api = toAuthEndpoints(endpoints, ctx);
return {
api,
middlewares
};
}
const router = (ctx, options) => {
const { api, middlewares } = getEndpoints(ctx, options);
const basePath = new URL(ctx.baseURL).pathname;
return createRouter(api, {
routerContext: ctx,
openapi: {
disabled: true
},
basePath,
routerMiddleware: [
{
path: "/**",
middleware: originCheckMiddleware
},
...middlewares
],
async onRequest(req) {
const disabledPaths = ctx.options.disabledPaths || [];
const path = new URL(req.url).pathname.replace(basePath, "");
if (disabledPaths.includes(path)) {
return new Response("Not Found", { status: 404 });
}
for (const plugin of ctx.options.plugins || []) {
if (plugin.onRequest) {
const response = await plugin.onRequest(req, ctx);
if (response && "response" in response) {
return response.response;
}
}
}
return onRequestRateLimit(req, ctx);
},
async onResponse(res) {
for (const plugin of ctx.options.plugins || []) {
if (plugin.onResponse) {
const response = await plugin.onResponse(res, ctx);
if (response) {
return response.response;
}
}
}
return res;
},
onError(e) {
if (e instanceof APIError && e.status === "FOUND") {
return;
}
if (options.onAPIError?.throw) {
throw e;
}
if (options.onAPIError?.onError) {
options.onAPIError.onError(e, ctx);
return;
}
const optLogLevel = options.logger?.level;
const log = optLogLevel === "error" || optLogLevel === "warn" || optLogLevel === "debug" ? logger : void 0;
if (options.logger?.disabled !== true) {
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
if (e.message.includes("no column") || e.message.includes("column") || e.message.includes("relation") || e.message.includes("table") || e.message.includes("does not exist")) {
ctx.logger?.error(e.message);
return;
}
}
if (e instanceof APIError) {
if (e.status === "INTERNAL_SERVER_ERROR") {
ctx.logger.error(e.status, e);
}
log?.error(e.message);
} else {
ctx.logger?.error(
e && typeof e === "object" && "name" in e ? e.name : "",
e
);
}
}
}
});
};
export { accountInfo, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createEmailVerificationToken, deleteUser, deleteUserCallback, error, forgetPassword, forgetPasswordCallback, getAccessToken, getEndpoints, getSession, linkSocialAccount, listSessions, listUserAccounts, ok, originCheckMiddleware, refreshToken, requestPasswordReset, requestPasswordResetCallback, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, setPassword, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateUser, verifyEmail };